
本文基于 Go 1.25.0 源码进行分析
前言
go list 和 go mod graph 是 go module 中两个常用的诊断与查询命令。
go list 用于查询包或模块的元信息:
go list ./...:列出当前项目中所有的包路径,常用于 CI 中确认哪些包会被编译。myproject/config myproject/api/v1 myproject/internal/servicego list -m all:列出当前模块的完整依赖列表(包括间接依赖),每行输出模块路径和版本号。myproject github.com/gin-gonic/gin v1.9.1 gorm.io/driver/mysql v1.5.7 gorm.io/gorm v1.25.10go 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.go 的 runList 函数。根据 -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从模块缓存中补充Dir、GoMod、Sum、Time等 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
}
- 创建空的
mvs.Graph - 从 root 模块开始,放入并发队列
- 对每个模块:调用
goModSummary读取其 go.mod,提取require列表 - 通过
mg.g.Require(m, summary.require)向图中添加边 - 根据 pruning 策略,决定是否递归加载传递依赖
- 通过
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 遍历每个模块,对每个模块调用回调函数 f。enqueued 保证每个模块版本只被访问一次。注意这里遍历的是图中所有出现过的版本,而不仅仅是被 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 all 和 go mod graph 最终依赖相同,但用途不同:
| 维度 | go list -m all | go mod graph |
|---|---|---|
| 输出内容 | 每个模块的详细信息(版本、目录、校验和等) | 模块间的 require 关系(边) |
| 输出的模块 | 仅 MVS 选定的版本(BuildList) | 图中出现的所有版本(包括未被选中的) |
| 图构建 | expandGraph → readModGraph | LoadModGraph → expandGraph → readModGraph |
| 额外网络请求 | 可选(-u 查更新、-versions 查版本列表) | 无 |
| 典型用途 | 查看实际使用了哪些模块、哪个版本 | 排查版本冲突、分析依赖链路 |
这两条命令在排查依赖问题时经常配合使用:先用 go mod graph | grep <module> 找到依赖链路,再用 go list -m -json <module> 查看具体信息。
7. 源码文件索引
| 文件 | 作用 |
|---|---|
cmd/go/internal/list/list.go | go list 命令主逻辑、输出格式处理 |
cmd/go/internal/modcmd/graph.go | go mod graph 完整实现 |
cmd/go/internal/modload/list.go | ListModules / listModules:模块列表查询 |
cmd/go/internal/modload/build.go | moduleInfo / PackageModuleInfo:模块信息构建 |
cmd/go/internal/modload/buildlist.go | LoadModGraph / readModGraph / ModuleGraph:模块图构建 |
cmd/go/internal/modinfo/info.go | ModulePublic 结构定义 |
cmd/go/internal/mvs/graph.go | mvs.Graph:MVS 增量图结构 |

579

被折叠的 条评论
为什么被折叠?



