xgo

repository·master·Indexed 19 days ago

https://github.com/xhd2015/xgo

An all-in-one test utility for Go that acts as a preprocessor for standard commands like go test and go build. It provides advanced capabilities including monkey patching (mocking), hierarchical stack trace visualization (tracing), and function execution recording.

Tokens
115.2K
Snippets
335
Records
495
Agent score
65%

What's inside xgo

  1. Use the patch package to instrument the Go compiler

    master
    The patch package is designed for instrumenting the Go compiler (go tool compile). It provides the necessary tools to apply modifications or instrumentation to the compiler's behavior.
  2. Use the goja/token package for JavaScript lexical tokens

    master

    The github.com/dop251/goja/token package provides constants representing the lexical tokens of JavaScript (ECMA5). It is useful for writing parsers or tools that need to identify JavaScript syntax elements like keywords, operators, and delimiters.

    import "github.com/dop251/goja/token"
    
    // Example usage of token constants
    var myToken = token.IDENTIFIER
  3. What is xgo and how does it work?

    master

    xgo is a wrapper around the standard go command designed to produce instrumented binaries.

    It achieves this by leveraging the go -toolexec flag, which allows a custom program to intercept and invoke toolchain programs like vet and asm. When you run xgo build, the tool performs the following lifecycle:

    1. Locates your existing GOROOT.
    2. Copies GOROOT to ~/.xgo/go-instruments/GOROOT.
    3. Applies patches to both the compiler and the runtime within that directory.
    4. Builds an instrumented version of the Go compiler.
    5. Executes the build using go build -toolexec exec_tool <package>.
    6. Uses exec_tool to forward compilation commands to the newly built instrumented compiler.
    7. Completes the build process (including linking) to produce an instrumented executable.
    xgo build ./my/example
  4. What is the Patch DSL?

    master

    The Patch DSL is a declarative, AST-aware language used to modify Go source files. It is primarily used by xgo to instrument the Go standard library for features like mocking, tracing, and interception.

    Patch files must have the .xgo.patch extension and reside in a directory structure under patches/<go-version>/src/ that mirrors the GOROOT directory structure. For example, to patch src/runtime/runtime2.go, you would create patches/go1.25/src/runtime/runtime2.go.xgo.patch.

  5. Handle Go 1.25+ overlay restrictions for GOMODCACHE

    master

    Go 1.25+ forbids using the -overlay flag to replace files located under GOMODCACHE. Because xgo runtime packages (like runtime/trace) are often module dependencies in GOMODCACHE, xgo switches to a different mechanism for Go 1.25+:

    1. It copies the xgo runtime to a local directory (.xgo/gen/modules/) outside GOMODCACHE.
    2. It modifies files directly on disk instead of using overlays.
    3. It uses a replace directive in a modified go.mod (via -modfile) to resolve the runtime from the local directory.
    4. It adds blank imports (e.g., import _ "runtime/trace") to ensure runtime packages are compiled.

    Note: This causes runtime/trace to be included in the test binary, which may affect tests asserting on exact functab contents.

  6. How Goroutine-scoped interceptors work

    master

    Unlike traditional monkey patching, xgo provides concurrency-safe mocking. When you call trap.AddInterceptor outside of an init function, the interceptor is applied only to the current Goroutine. This allows you to run tests in parallel without side effects between different test cases, as the interceptor is automatically cleared when the Goroutine exits.

    To manually clean up an interceptor registered after init, use the returned function:

    func main() {
        clear := trap.AddInterceptor(&trap.Interceptor{...})
        defer clear()
        // ...
    }
  7. How xgo handles safe code insertion after declarations

    master

    To avoid inserting code inside raw string literals or separating //go: directives (like //go:embed) from their target declarations, xgo uses a safe insertion logic via GenDeclSafeEnd.

    The Logic:

    1. It checks the next declaration in the file.
    2. If the next declaration does not have a //go: directive, it uses the position of the next declaration (nextDecl.Pos()) as the insertion point. This is more reliable than genDecl.End() when \r bytes are present.
    3. If the next declaration does have a //go: directive, it falls back to the current declaration's end (genDecl.End()) to ensure the directive and its declaration remain together.

    Implementation Details:

    • The insertion uses a newline \n separator rather than a semicolon ; to ensure compatibility with both insertion methods.
    • GenDeclSafeEnd is located in instrument/patch/init.go.
    • safeGenDeclEnd (a wrapper for variable traps) is located in instrument/instrument_var/trap_var.go.
    // GenDeclSafeEnd returns a safe insertion point after a GenDecl.
    // When the next declaration has no go: directive, uses nextDecl.Pos()
    // (computed by the scanner, always correct). Otherwise falls back to
    // genDecl.End() to avoid separating directives from their declarations.
    func GenDeclSafeEnd(file *ast.File, declIndex int) token.Pos {
        if declIndex+1 < len(file.Decls) && !HasGoDirective(file.Decls[declIndex+1]) {
            return file.Decls[declIndex+1].Pos()
        }
        return file.Decls[declIndex].End()
    }
  8. Understand the relationship between function symbols and PC

    master

    A function symbol is essentially a pointer to the function's entry point in the code segment. In Go's runtime, a function symbol is treated as a byte representing the Program Counter (PC), which is a pointer to a read-only part of the binary.

    Key observation: The entryPC (retrieved via runtime.GetcallerFuncPC()) is identical to the function symbol itself.

  9. Compare performance: Native Go vs VSCode Diff

    master

    When choosing between a native Go Myers diff and the VSCode diff implementation, consider the latency trade-off. The VSCode implementation via Node.js IPC is significantly slower than native Go.

    ImplementationLatency
    Myers (Native Go)~1585 ns/op
    VSCode (Node.js IPC)~10.8 ms/op

    Recommendation: Use the VSCode implementation for consistency with Monaco/VS Code editors if the operation is performed in the background. Avoid using it for real-time, high-frequency scenarios due to the ~6842x performance penalty.