如何使用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
更多关于如何使用Golang的`golang.org/x/tools/go/packages`构建多包项目的SSA分析的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html
在你的项目中,pkg 是一个独立的 Go 模块(有独立的 go.mod 文件)。packages.Load 默认只加载主模块的包,不会自动包含子模块。你需要显式指定要加载的子模块路径。
以下是修改后的代码,通过 packages.Config 的 Env 字段设置 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分析和调用图。

