17 go list / go mod graph 背后做了什么?

在这里插入图片描述

本文基于 Go 1.25.0 源码进行分析

前言

go listgo mod graph 是 go module 中两个常用的诊断与查询命令。

go list 用于查询包或模块的元信息:

  • go list ./...:列出当前项目中所有的包路径,常用于 CI 中确认哪些包会被编译。
    myproject/config
    myproject/api/v1
    myproject/internal/service
    
  • go list -m all:列出当前模块的完整依赖列表(包括间接依赖),每行输出模块路径和版本号。
    myproject
    github.com/gin-gonic/gin v1.9.1
    gorm.io/driver/mysql v1.5.7
    gorm.io/gorm v1.25.10
    
  • go list -m -u all:在上面的基础上,额外标记每个依赖是否有可用的新版本,方便判断哪些依赖需要升级。
    myproject
    github.com/gin-gonic/gin v1.9.1 [v1.10.0]
    gorm.io/driver/mysql v1.5.7 [v1.6.0]
    gorm.io/gorm v1.25.10
    

go mod graph 用于输出模块之间的依赖关系图。每行输出一条边,格式为 A B,表示模块 A 依赖模块 B。适合配合工具(如 modgraphviz)做可视化分析,也可以用来排查间接依赖是怎么被引入的。

myproject gorm.io/driver/mysql@v1.5.7
myproject github.com/gin-gonic/gin@v1.9.1
gorm.io/driver/mysql@v1.5.7 gorm.io/gorm@v1.25.10

简单来说:

  • go list:我依赖了什么。
  • go mod graph:依赖之间的关系是什么。

1. go list:两种模式,-m 分支

go list 命令的入口在 cmd/go/internal/list/list.gorunList 函数。根据 -m 标志,走不同的分支:

// src/cmd/go/internal/list/list.go
func runList(ctx context.Context, cmd *base.Command, args []string) {
	modload.InitWorkfile()
	// ...
	modload.Init()

	if *listM {
		// 模块模式:查询模块信息
		// ...
		mods, err := modload.ListModules(ctx, args, mode, *listReuse)
		// ...
		for _, m := range mods {
			do(m)
		}
		return
	}

	// 包模式:查询包信息
	pkgs := load.PackagesAndErrors(ctx, pkgOpts, args)
	// ...
}

2. 包模式(默认)

不带 -m 时,go list 走包模式。调用链:

runList
  └─ load.PackagesAndErrors(ctx, pkgOpts, args)
       └─ 内部调用 modload.LoadPackages
            └─ 解析 import,定位包所在模块,加载 go.mod

最终为每个匹配的包输出一个 PackagePublic 结构。这个结构定义在 list.go 的命令帮助文档中,也是 go list -json 输出的 JSON schema:

// src/cmd/go/internal/list/list.go(帮助文本中描述的结构)
type Package struct {
    Dir        string   // 包源码目录
    ImportPath string   // import 路径
    Name       string   // 包名
    Module     *Module  // 所属模块信息(关键字段)
    GoFiles    []string // .go 源文件
    Imports    []string // 依赖的包
    Deps       []string // 传递依赖的所有包
    // ...
}

其中 Module 字段将包与模块关联起来。这个字段通过 modload.PackageModuleInfo 填充:

// src/cmd/go/internal/modload/build.go
func PackageModuleInfo(ctx context.Context, pkgpath string) *modinfo.ModulePublic {
	if isStandardImportPath(pkgpath) || !Enabled() {
		return nil
	}
	m, ok := findModule(loaded, pkgpath)
	if !ok {
		return nil
	}

	rs := LoadModFile(ctx)
	return moduleInfo(ctx, rs, m, 0, nil)
}

标准库包返回 nil,第三方包通过 findModule 从已加载的包缓存中找到其所属模块,再调用 moduleInfo 构建完整的模块信息。

2.1 默认输出格式

不加任何格式标志时,默认模板是 {{.ImportPath}},即只输出包的 import 路径:

// src/cmd/go/internal/list/list.go
if *listFmt == "" {
	if *listM {
		*listFmt = "{{.String}}"
	} else {
		*listFmt = "{{.ImportPath}}"
	}
}

2.2 -json 输出与字段过滤

-json 模式通过反射实现字段过滤。当使用 -json=Field1,Field2 时,只输出指定字段:

// src/cmd/go/internal/list/list.go
if listJson {
	do = func(x any) {
		if !listJsonFields.needAll() {
			v := reflect.New(reflect.TypeOf(x).Elem()).Elem()
			v.Set(reflect.ValueOf(x).Elem())
			for i := 0; i < v.NumField(); i++ {
				if !listJsonFields.needAny(v.Type().Field(i).Name) {
					v.Field(i).SetZero()
				}
			}
			x = v.Interface()
		}
		b, err := json.MarshalIndent(x, "", "\t")
		// ...
	}
}

遍历结构体的每个字段,不在请求列表中的字段直接 SetZero,然后 JSON 序列化时 omitempty 会自动省略零值。


3. 模块(module)模式(-m

-m 时,go list 走模块模式。此时不加载任何包,只查询模块信息。

3.1 -m 对标志的约束

进入模块模式后,与包相关的标志全部禁用:

// src/cmd/go/internal/list/list.go
if *listM {
	if *listCompiled {
		base.Fatalf("go list -compiled cannot be used with -m")
	}
	if *listDeps {
		base.Fatalf("go list -deps cannot be used with -m")
	}
	if *listExport {
		base.Fatalf("go list -export cannot be used with -m")
	}
	if *listFind {
		base.Fatalf("go list -find cannot be used with -m")
	}
	if *listTest {
		base.Fatalf("go list -test cannot be used with -m")
	}
	// ...
}

3.2 核心调用链

runList (-m)
  └─ modload.LoadModFile(ctx)         // 加载 go.mod
  └─ modload.ListModules(ctx, args, mode, reuse)
       └─ listModules(ctx, rs, args, mode, reuse)
            ├─ 无参数:返回主模块信息
            ├─ "all" / "...":expandGraph → 遍历 BuildList
            └─ path@version:queryReuse → 查询特定版本

3.3 ListModules:入口函数

// src/cmd/go/internal/modload/list.go
func ListModules(ctx context.Context, args []string, mode ListMode, reuseFile string) ([]*modinfo.ModulePublic, error) {
	// ... 解析 reuse 文件 ...

	rs, mods, err := listModules(ctx, LoadModFile(ctx), args, mode, reuse)

	// 并发填充额外信息
	sem := make(chan token, runtime.GOMAXPROCS(0))
	if mode != 0 {
		for _, m := range mods {
			add := func(m *modinfo.ModulePublic) {
				sem <- token{}
				go func() {
					if mode&ListU != 0 {
						addUpdate(ctx, m)
					}
					if mode&ListVersions != 0 {
						addVersions(ctx, m, mode&ListRetractedVersions != 0)
					}
					if mode&ListRetracted != 0 {
						addRetraction(ctx, m)
					}
					if mode&ListDeprecated != 0 {
						addDeprecation(ctx, m)
					}
					<-sem
				}()
			}
			add(m)
			if m.Replace != nil {
				add(m.Replace)
			}
		}
	}
	// ...
}

先通过 listModules 获取基础模块列表,再根据 mode 标志并发填充 Update / Versions / Retracted / Deprecated 等额外信息。信号量 sem 控制并发度为 GOMAXPROCS

3.4 listModules:参数解析与模块图

// src/cmd/go/internal/modload/list.go
func listModules(ctx context.Context, rs *Requirements, args []string, mode ListMode, reuse map[module.Version]*modinfo.ModulePublic) (_ *Requirements, mods []*modinfo.ModulePublic, mgErr error) {
	if len(args) == 0 {
		// 无参数:返回主模块
		var ms []*modinfo.ModulePublic
		for _, m := range MainModules.Versions() {
			ms = append(ms, moduleInfo(ctx, rs, m, mode, reuse))
		}
		return rs, ms, nil
	}

	needFullGraph := false
	for _, arg := range args {
		if arg == "all" || strings.Contains(arg, "...") {
			needFullGraph = true
			// ...
		}
	}

	var mg *ModuleGraph
	if needFullGraph {
		rs, mg, mgErr = expandGraph(ctx, rs)
	}

	// 逐个处理参数
	for _, arg := range args {
		if path, vers, found := strings.Cut(arg, "@"); found {
			// path@version 形式:查询特定版本
			info, err := queryReuse(ctx, path, vers, current, allowed, reuse)
			mod := moduleInfo(ctx, noRS, module.Version{Path: path, Version: info.Version}, mode, reuse)
			mods = append(mods, mod)
			continue
		}

		// 模块路径或模式匹配
		if arg == "all" {
			match = func(p string) bool { return !gover.IsToolchain(p) }
		}
		// 遍历 BuildList 进行匹配
		for _, m := range mg.BuildList() {
			if match(m.Path) {
				matches = append(matches, m)
			}
		}
		// 并发获取模块信息
		q := par.NewQueue(runtime.GOMAXPROCS(0))
		for i, m := range matches {
			q.Add(func() {
				fetchedMods[i] = moduleInfo(ctx, rs, m, mode, reuse)
			})
		}
		<-q.Idle()
		mods = append(mods, fetchedMods...)
	}
	return rs, mods, mgErr
}
  • 无参数:直接返回主模块信息
  • all / ...:需要完整模块图(expandGraph),遍历 BuildList 匹配
  • path@version:通过 queryReuse 查询特定版本,不需要完整模块图

3.5 ModulePublic:模块信息结构

go list -m -json 输出的 JSON 对应 ModulePublic 结构:

// src/cmd/go/internal/modinfo/info.go
type ModulePublic struct {
	Path       string           // 模块路径
	Version    string           // 版本号
	Query      string           // 版本查询字符串
	Versions   []string         // 可用版本列表(-versions)
	Replace    *ModulePublic    // 被替换为(replace 指令)
	Time       *time.Time       // 版本发布时间
	Update     *ModulePublic    // 可用更新(-u)
	Main       bool             // 是否为主模块
	Indirect   bool             // 是否为间接依赖
	Dir        string           // 本地目录
	GoMod      string           // go.mod 文件路径
	GoVersion  string           // 要求的 Go 版本
	Retracted  []string         // 撤回信息(-retracted)
	Deprecated string           // 弃用信息(-u)
	Error      *ModuleError     // 错误
	Sum        string           // 校验和
	GoModSum   string           // go.mod 校验和
}

3.6 moduleInfo:填充模块详情

每个模块的详细信息由 moduleInfo 函数构建:

// src/cmd/go/internal/modload/build.go
func moduleInfo(ctx context.Context, rs *Requirements, m module.Version, mode ListMode, reuse map[module.Version]*modinfo.ModulePublic) *modinfo.ModulePublic {
	// 主模块:直接从内存获取
	if m.Version == "" && MainModules.Contains(m.Path) {
		info := &modinfo.ModulePublic{
			Path: m.Path, Main: true,
		}
		info.GoVersion = rawGoVersion[m]
		info.Dir = MainModules.ModRoot(m)
		info.GoMod = modFilePath(modRoot)
		return info
	}

	// 第三方模块:从模块缓存补充信息
	info := &modinfo.ModulePublic{
		Path:     m.Path,
		Version:  m.Version,
		Indirect: rs != nil && !rs.direct[m.Path],
	}

	completeFromModCache := func(m *modinfo.ModulePublic) {
		// 复用检查
		if old := reuse[mod]; old != nil {
			if err := checkReuse(ctx, mod, old.Origin); err == nil {
				*m = *old
				return
			}
		}
		// 查询版本信息(Time 等)
		if q, err := Query(ctx, m.Path, m.Version, "", nil); err == nil {
			m.Version = q.Version
			m.Time = &q.Time
		}
		// 补充 GoMod、Dir、Sum 等
		// ...
	}

	// 处理 replace
	r := Replacement(m)
	if r.Path == "" {
		completeFromModCache(info)
		return info
	}
	info.Replace = &modinfo.ModulePublic{Path: r.Path, Version: r.Version}
	completeFromModCache(info.Replace)
	info.Dir = info.Replace.Dir
	info.GoMod = info.Replace.GoMod
	return info
}
  • 主模块信息从内存直接获取,不走缓存
  • 第三方模块通过 completeFromModCache 从模块缓存中补充 DirGoModSumTime
  • Indirect 字段通过 rs.direct 判断——如果模块路径不在 direct 集合中,就是间接依赖
  • replace 的模块会额外构建 Replace 子结构

3.7 -u 标志:查询可用更新

-u 触发 addUpdate,对每个模块执行一次 Query(ctx, m.Path, "upgrade", m.Version, CheckAllowed)

// src/cmd/go/internal/modload/build.go
func addUpdate(ctx context.Context, m *modinfo.ModulePublic) {
	if m.Version == "" {
		return
	}
	info, err := Query(ctx, m.Path, "upgrade", m.Version, CheckAllowed)
	// ...
	if gover.ModCompare(m.Path, info.Version, m.Version) > 0 {
		m.Update = &modinfo.ModulePublic{
			Path:    m.Path,
			Version: info.Version,
			Time:    &info.Time,
		}
	}
}

"upgrade" 作为查询参数,如果查到比当前更高的版本,就填入 Update 字段。这就是为什么 go list -m -u all 会比 go list -m all 慢很多——它要为每个模块向 GOPROXY 发起一次版本查询请求。


4. go mod graph:打印模块依赖图

4.1 完整源码

go mod graph 的实现非常精简,整个 runGraph 函数只有 40 行:

// src/cmd/go/internal/modcmd/graph.go
func runGraph(ctx context.Context, cmd *base.Command, args []string) {
	modload.InitWorkfile()

	if len(args) > 0 {
		base.Fatalf("go: 'go mod graph' accepts no arguments")
	}
	modload.ForceUseModules = true
	modload.RootMode = modload.NeedRoot

	goVersion := graphGo.String()
	if goVersion != "" && gover.Compare(gover.Local(), goVersion) < 0 {
		toolchain.SwitchOrFatal(ctx, &gover.TooNewError{
			What:      "-go flag",
			GoVersion: goVersion,
		})
	}

	mg, err := modload.LoadModGraph(ctx, goVersion)
	if err != nil {
		base.Fatal(err)
	}

	w := bufio.NewWriter(os.Stdout)
	defer w.Flush()

	format := func(m module.Version) {
		w.WriteString(m.Path)
		if m.Version != "" {
			w.WriteString("@")
			w.WriteString(m.Version)
		}
	}

	mg.WalkBreadthFirst(func(m module.Version) {
		reqs, _ := mg.RequiredBy(m)
		for _, r := range reqs {
			format(m)
			w.WriteByte(' ')
			format(r)
			w.WriteByte('\n')
		}
	})
}

LoadModGraph 构建模块图,WalkBreadthFirst 遍历输出。

4.2 输出格式

每行两个字段,空格分隔:

主模块 依赖A@v1.0.0
依赖A@v1.0.0 依赖B@v0.3.0
依赖A@v1.0.0 依赖C@v1.2.0
依赖B@v0.3.0 依赖D@v0.1.0
  • 左侧是模块,右侧是它的一个 require 依赖
  • 主模块没有 @version 后缀(因为 m.Version == ""
  • 其他模块都带 @version

format 函数可见:

format := func(m module.Version) {
	w.WriteString(m.Path)
	if m.Version != "" {
		w.WriteString("@")
		w.WriteString(m.Version)
	}
}

4.3 LoadModGraph:加载模块图

// src/cmd/go/internal/modload/buildlist.go
func LoadModGraph(ctx context.Context, goVersion string) (*ModuleGraph, error) {
	rs, err := loadModFile(ctx, nil)
	if err != nil {
		return nil, err
	}

	if goVersion != "" {
		// -go 标志:用指定版本的语义加载图
		pruning := pruningForGoVersion(goVersion)
		if pruning == unpruned && rs.pruning != unpruned {
			rs = newRequirements(unpruned, rs.rootModules, rs.direct)
		}
		return rs.Graph(ctx)
	}

	rs, mg, err := expandGraph(ctx, rs)
	if err != nil {
		return nil, err
	}
	requirements = rs
	return mg, nil
}

-go 标志的作用:Go 1.17 引入了 pruned module graph,不同 Go 版本对模块图的加载策略不同。-go=1.16 会以 unpruned 模式加载完整传递依赖,-go=1.17+ 会启用 pruned 模式只加载直接依赖的 go.mod。

4.4 expandGraph:展开完整模块图

// src/cmd/go/internal/modload/buildlist.go
func expandGraph(ctx context.Context, rs *Requirements) (*Requirements, *ModuleGraph, error) {
	mg, mgErr := rs.Graph(ctx)
	if mgErr != nil {
		return rs, mg, mgErr
	}

	if !mg.allRootsSelected() {
		// root 版本与图中选定的版本不一致,需要更新
		// ...
	}
	return rs, mg, nil
}

rs.Graph(ctx) 是真正构建图的地方,它调用 readModGraph

4.5 readModGraph:并发构建依赖图

这是整个模块图构建的核心:

// src/cmd/go/internal/modload/buildlist.go
func readModGraph(ctx context.Context, pruning modPruning, roots []module.Version, unprune map[module.Version]bool) (*ModuleGraph, error) {
	var (
		mu       sync.Mutex
		hasError bool
		mg       = &ModuleGraph{
			g: mvs.NewGraph(cmpVersion, graphRoots),
		}
	)

	loadQueue = par.NewQueue(runtime.GOMAXPROCS(0))

	loadOne := func(m module.Version) (*modFileSummary, error) {
		return mg.loadCache.Do(m, func() (*modFileSummary, error) {
			summary, err := goModSummary(m)
			mu.Lock()
			if err == nil {
				mg.g.Require(m, summary.require)
			}
			mu.Unlock()
			return summary, err
		})
	}

	var enqueue func(m module.Version, pruning modPruning)
	enqueue = func(m module.Version, pruning modPruning) {
		if m.Version == "none" {
			return
		}
		if _, dup := loading.LoadOrStore(dedupKey{m, pruning}, nil); dup {
			return
		}

		loadQueue.Add(func() {
			summary, err := loadOne(m)
			if err != nil {
				return
			}
			for _, r := range summary.require {
				if pruning != pruned || summary.pruning == unpruned || unprune[r] {
					enqueue(r, nextPruning)
				}
			}
		})
	}

	for _, m := range roots {
		enqueue(m, pruning)
	}
	<-loadQueue.Idle()

	return mg, nil
}
  1. 创建空的 mvs.Graph
  2. 从 root 模块开始,放入并发队列
  3. 对每个模块:调用 goModSummary 读取其 go.mod,提取 require 列表
  4. 通过 mg.g.Require(m, summary.require) 向图中添加边
  5. 根据 pruning 策略,决定是否递归加载传递依赖
  6. 通过 sync.Map 去重,避免循环依赖导致无限加载

并发度由 par.NewQueue(runtime.GOMAXPROCS(0)) 控制,最多同时加载 GOMAXPROCS 个模块的 go.mod。

4.6 mvs.Graph:图的数据结构

// src/cmd/go/internal/mvs/graph.go
type Graph struct {
	cmp      func(p, v1, v2 string) int
	roots    []module.Version
	required map[module.Version][]module.Version // 核心:模块 → 依赖列表
	isRoot   map[module.Version]bool
	selected map[string]string                   // 模块路径 → 选定版本
}

required 是一个邻接表,key 是模块,value 是它的 require 列表。selected 记录每个模块路径最终选定的版本(MVS 的结果)。

添加依赖时,自动维护 selected

// src/cmd/go/internal/mvs/graph.go
func (g *Graph) Require(m module.Version, reqs []module.Version) {
	g.required[m] = reqs
	for _, dep := range reqs {
		if _, ok := g.isRoot[dep]; !ok {
			g.isRoot[dep] = false
		}
		if g.cmp(dep.Path, g.Selected(dep.Path), dep.Version) < 0 {
			g.selected[dep.Path] = dep.Version
		}
	}
}

每次添加新的 require 关系时,如果依赖版本比当前选定的更高,就更新 selected。这就是 MVS 算法的增量实现——每个模块的选定版本始终是所有 require 中的最高版本。

4.7 WalkBreadthFirst:广度优先遍历

go mod graph 的输出顺序由 BFS 决定:

// src/cmd/go/internal/mvs/graph.go
func (g *Graph) WalkBreadthFirst(f func(m module.Version)) {
	var queue []module.Version
	enqueued := make(map[module.Version]bool)
	for _, m := range g.roots {
		if m.Version != "none" {
			queue = append(queue, m)
			enqueued[m] = true
		}
	}

	for len(queue) > 0 {
		m := queue[0]
		queue = queue[1:]
		f(m)
		reqs, _ := g.RequiredBy(m)
		for _, r := range reqs {
			if !enqueued[r] && r.Version != "none" {
				queue = append(queue, r)
				enqueued[r] = true
			}
		}
	}
}

从 root 开始,BFS 遍历每个模块,对每个模块调用回调函数 fenqueued 保证每个模块版本只被访问一次。注意这里遍历的是图中所有出现过的版本,而不仅仅是被 MVS 选中的版本。


5. ModuleGraph vs mvs.Graph

层级类型职责
上层modload.ModuleGraph管理加载缓存、提供 BuildList 等高层方法
底层mvs.Graph纯粹的图结构 + MVS 版本选择
// src/cmd/go/internal/modload/buildlist.go
type ModuleGraph struct {
	g         *mvs.Graph
	loadCache par.ErrCache[module.Version, *modFileSummary]
	buildListOnce sync.Once
	buildList     []module.Version
}

ModuleGraph 包装了 mvs.Graph,加上了加载缓存(loadCache,避免重复读取同一模块的 go.mod)和 BuildList 的懒计算。

BuildList 返回 MVS 选定的最终版本列表:

// src/cmd/go/internal/modload/buildlist.go
func (mg *ModuleGraph) BuildList() []module.Version {
	mg.buildListOnce.Do(func() {
		mg.buildList = slices.Clip(mg.g.BuildList())
	})
	return mg.buildList
}

6. 两个命令的关系

go list -m allgo mod graph 最终依赖相同,但用途不同:

维度go list -m allgo mod graph
输出内容每个模块的详细信息(版本、目录、校验和等)模块间的 require 关系(边)
输出的模块仅 MVS 选定的版本(BuildList图中出现的所有版本(包括未被选中的)
图构建expandGraphreadModGraphLoadModGraphexpandGraphreadModGraph
额外网络请求可选(-u 查更新、-versions 查版本列表)
典型用途查看实际使用了哪些模块、哪个版本排查版本冲突、分析依赖链路

这两条命令在排查依赖问题时经常配合使用:先用 go mod graph | grep <module> 找到依赖链路,再用 go list -m -json <module> 查看具体信息。


7. 源码文件索引

文件作用
cmd/go/internal/list/list.gogo list 命令主逻辑、输出格式处理
cmd/go/internal/modcmd/graph.gogo mod graph 完整实现
cmd/go/internal/modload/list.goListModules / listModules:模块列表查询
cmd/go/internal/modload/build.gomoduleInfo / PackageModuleInfo:模块信息构建
cmd/go/internal/modload/buildlist.goLoadModGraph / readModGraph / ModuleGraph:模块图构建
cmd/go/internal/modinfo/info.goModulePublic 结构定义
cmd/go/internal/mvs/graph.gomvs.Graph:MVS 增量图结构
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值