如何使用Golang的`golang.org/x/tools/go/packages`构建多包项目的SSA分析

如何使用Golang的golang.org/x/tools/go/packages构建多包项目的SSA分析

  • 我的项目位于 /home/hbb/projs/zhai/atmodels-server/,项目结构如下:
.
├── Makefile
├── go.mod
├── go.sum
├── go.work
├── go.work.sum
├── atms
│   ├── cmds
│   │   ├── executor.go
│   │   └── root.go
│   ├── main.go
├── pkg
│   ├── app
│   │   ├── appcm
│   │   │   ├── appcm.go
│   ├── errs
│   │   ├── errors.go
│   │   ├── errors_test.go
│   │   └── stack.go
│   ├── go.mod
│   ├── go.sum
  • 如上代码所示,我的 pkg 子文件夹是一个独立的 Go 包;
  • 以下是我使用 golang.org/x/tools/go/packages 运行的代码,我想为我的项目构建调用图:
package main

import (
	"flag"
	"fmt"
	"log"
	"os"
	"strings"

	"golang.org/x/tools/go/callgraph/rta"
	"golang.org/x/tools/go/packages"
	"golang.org/x/tools/go/ssa"
	"golang.org/x/tools/go/ssa/ssautil"
)

// mainPackages returns the main packages to analyze.
// Each resulting package is named "main" and has a main function.
func mainPackages(pkgs []*ssa.Package) ([]*ssa.Package, error) {
	var mains []*ssa.Package
	for _, p := range pkgs {
		if p != nil && p.Pkg.Name() == "main" && !strings.Contains(p.Pkg.Path(), "test") && p.Func("main") != nil {
			mains = append(mains, p)
		}
	}
	if len(mains) == 0 {
		return nil, fmt.Errorf("no main packages")
	}
	return mains, nil
}

func main() {
	flag.Parse()

	// Many tools pass their command-line arguments (after any flags)
	// uninterpreted to packages.Load so that it can interpret them
	// according to the conventions of the underlying build system.
	cfg := &packages.Config{
		Dir:  "/home/hbb/projs/zhai/atmodels-server/",
		Mode: packages.LoadAllSyntax,
	}
	pkgs, err := packages.Load(cfg, flag.Args()...)
	if err != nil {
		fmt.Fprintf(os.Stderr, "load: %v\n", err)
		os.Exit(1)
	}
	if packages.PrintErrors(pkgs) > 0 {
		os.Exit(1)
	}

	// Print the names of the source files
	// for each package listed on the command line.
	for _, pkg := range pkgs {
		fmt.Println(pkg.ID, pkg.GoFiles)
	}

	// Create and build SSA-form program representation.
	prog, _ := ssautil.AllPackages(pkgs, 0)
	prog.Build()

	mains, err := mainPackages(prog.AllPackages())
	if err != nil {
		panic(err)
	}
	var roots []*ssa.Function
	mainPkg := mains[0]
	for _, main := range mains {
		roots = append(roots, main.Func("main"))
	}
	log.Println("roots:", mainPkg, roots)
	graph := rta.Analyze(roots, true).CallGraph
	log.Println("graph:", graph)
}
  • 我的命令是:go run main.go ./...,然后我得到了这个错误:
...  // more pakcages
atmodels-server/atms/tests/https []
atmodels-server/atms/tests/timer []
atmodels-server/atms/utils/retry [/home/hbb/projs/zhai/atmodels-server/atms/utils/retry/retry.go /home/hbb/projs/zhai/atmodels-server/atms/utils/retry/types.go]
atmodels-server/atms/wssvr/contorllers [/home/hbb/projs/zhai/atmodels-server/atms/wssvr/contorllers/types.go]
atmodels-server/atms/wssvr/websocketsvr [/home/hbb/projs/zhai/atmodels-server/atms/wssvr/websocketsvr/server.go]
2024/11/04 22:04:32 roots: package atmodels-server/atms [atmodels-server/atms.main]
panic: T

goroutine 1 [running]:
golang.org/x/tools/go/callgraph/rta.(*rta).addRuntimeType(0xc001243b98, {0x743620?, 0xc00fad05d0?}, 0x0)
        /home/hbb/go/pkg/mod/golang.org/x/tools@v0.26.0/go/callgraph/rta/rta.go:535 +0x8fa
golang.org/x/tools/go/callgraph/rta.(*rta).visitFunc(0xc001243b98, 0xc085f85c00)
        /home/hbb/go/pkg/mod/golang.org/x/tools@v0.26.0/go/callgraph/rta/rta.go:284 +0x14e
golang.org/x/tools/go/callgraph/rta.Analyze({0xc0c25ad6d0, 0x1, 0x2?}, 0x1)
        /home/hbb/go/pkg/mod/golang.org/x/tools@v0.26.0/go/callgraph/rta/rta.go:351 +0x5aa
main.main()
        /home/hbb/tmp/go-ssa/loguse.go:70 +0x5d8
exit status 2
  • 看起来 atmodels-server/pkg 没有被识别并包含在 pkgs 中,那么我该如何解决这个问题呢。

谢谢。


更多关于如何使用Golang的`golang.org/x/tools/go/packages`构建多包项目的SSA分析的实战教程也可以访问 https://www.itying.com/category-94-b0.html

1 回复

更多关于如何使用Golang的`golang.org/x/tools/go/packages`构建多包项目的SSA分析的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


在你的项目中,pkg 是一个独立的 Go 模块(有独立的 go.mod 文件)。packages.Load 默认只加载主模块的包,不会自动包含子模块。你需要显式指定要加载的子模块路径。

以下是修改后的代码,通过 packages.ConfigEnv 字段设置 GOWORK=off 来禁用工作区模式,然后分别加载主模块和子模块:

package main

import (
    "flag"
    "fmt"
    "log"
    "os"
    "strings"

    "golang.org/x/tools/go/callgraph/rta"
    "golang.org/x/tools/go/packages"
    "golang.org/x/tools/go/ssa"
    "golang.org/x/tools/go/ssa/ssautil"
)

func mainPackages(pkgs []*ssa.Package) ([]*ssa.Package, error) {
    var mains []*ssa.Package
    for _, p := range pkgs {
        if p != nil && p.Pkg.Name() == "main" && !strings.Contains(p.Pkg.Path(), "test") && p.Func("main") != nil {
            mains = append(mains, p)
        }
    }
    if len(mains) == 0 {
        return nil, fmt.Errorf("no main packages")
    }
    return mains, nil
}

func main() {
    flag.Parse()
    
    // 禁用工作区模式,确保能加载子模块
    cfg := &packages.Config{
        Dir: "/home/hbb/projs/zhai/atmodels-server/",
        Mode: packages.LoadAllSyntax,
        Env: append(os.Environ(), "GOWORK=off"),
    }
    
    // 加载主模块和子模块的所有包
    patterns := []string{
        "./...",          // 主模块的所有包
        "./pkg/...",      // 子模块的所有包
    }
    
    pkgs, err := packages.Load(cfg, patterns...)
    if err != nil {
        fmt.Fprintf(os.Stderr, "load: %v\n", err)
        os.Exit(1)
    }
    if packages.PrintErrors(pkgs) > 0 {
        os.Exit(1)
    }

    // 打印加载的包信息
    for _, pkg := range pkgs {
        fmt.Printf("Loaded package: %s\n", pkg.PkgPath)
    }

    // 创建SSA程序
    prog, _ := ssautil.AllPackages(pkgs, ssa.InstantiateGenerics)
    prog.Build()

    mains, err := mainPackages(prog.AllPackages())
    if err != nil {
        panic(err)
    }
    
    var roots []*ssa.Function
    for _, main := range mains {
        roots = append(roots, main.Func("main"))
    }
    
    log.Printf("Found %d main packages\n", len(mains))
    
    // 构建调用图
    result := rta.Analyze(roots, true)
    graph := result.CallGraph
    
    // 打印调用图信息
    log.Printf("Call graph has %d nodes\n", graph.Nodes().Len())
}

如果你需要处理泛型,确保使用正确的 ssa.BuilderMode。对于 Go 1.18+,使用 ssa.InstantiateGenerics

prog, _ := ssautil.AllPackages(pkgs, ssa.InstantiateGenerics|ssa.GlobalDebug)

如果仍然遇到 panic: T 错误,这可能是由于泛型类型问题。可以尝试使用不同的 ssa.BuilderMode 组合:

prog, _ := ssautil.AllPackages(pkgs, ssa.InstantiateGenerics|ssa.BareInits)

或者,如果你不需要泛型实例化:

prog, _ := ssautil.AllPackages(pkgs, ssa.BareInits)

要调试具体的类型问题,可以在构建SSA后检查程序:

prog.Build()

// 检查所有包的类型信息
for _, pkg := range prog.AllPackages() {
    if pkg == nil {
        continue
    }
    fmt.Printf("Package: %s\n", pkg.Pkg.Path())
    for _, mem := range pkg.Members {
        if typ, ok := mem.(*ssa.Type); ok {
            fmt.Printf("  Type: %s\n", typ.Name())
        }
    }
}

这样应该能正确加载包含子模块在内的所有包,并为多包项目构建SSA分析和调用图。

回到顶部