解决 Go 依赖冲突


1. 构建测试环境

如下图所示:

go_mod_graph.png

其中,go.timd.cn/c 从 v0.0.1 升级到 v0.0.2 时,存在破坏性更新,导致 go.timd.cn/a@v0.0.1 无法与 go.timd.cn/b@v0.0.2 一起工作。

go.timd.cn/c 的项目结构如下:

.
├── c.go
├── go.mod
└── pkg
    └── print.go

v0.0.1 版本 go.mod 的内容如下:

module go.timd.cn/c

go 1.22.6

v0.0.1 版本 c.go 的内容如下:

package c

func C() {
    println("[go.timd.cn/c] C called")
}

v0.0.1 版本 pkg/print.go 的内容如下:

package pkg

func Print() {
    println("[go.timd.cn/c/pkg] Print called")
}

v0.0.2 版本删除 pkg 包。

go.timd.cn/b 的项目结构如下:

.
├── b.go
├── go.mod
└── go.sum

v0.0.1 版本 go.mod 的内容如下:

module go.timd.cn/b

go 1.22.6

require go.timd.cn/c v0.0.1

v0.0.1 版本 b.go 的内容如下:

package b

import (
    "go.timd.cn/c"
    "go.timd.cn/c/pkg"
)

func B() {
    println("go.timd.cn/b started")
    c.C()
    pkg.Print()
    println("go.timd.cn/b ended")
}

v0.0.2 版本 go.mod 的内容如下:

module go.timd.cn/b

go 1.22.6

require go.timd.cn/c v0.0.2

v0.0.2 版本 b.go 的内容如下:

package b

import (
    "go.timd.cn/c"
)

func B() {
    println("go.timd.cn/b started")
    c.C()
    println("go.timd.cn/b ended")
}

go.timd.cn/a 的项目结构如下:

.
├── a.go
├── go.mod
└── go.sum

v0.0.1 版本 go.mod 的内容如下:

module go.timd.cn/a

go 1.22.6

require go.timd.cn/b v0.0.1

require go.timd.cn/c v0.0.1

v0.0.1 版本 a.go 的内容如下:

package a

import (
    "go.timd.cn/b"
    "go.timd.cn/c"
    "go.timd.cn/c/pkg"
)

func A() {
    println("go.timd.cn/a started")
    b.B()
    c.C()
    pkg.Print()
    println("go.timd.cn/a ended")
}

1.1. 创建 test 项目

mkdir test
cd test/
go mod init test

go.mod 的内容如下:

module test

go 1.22.6

require go.timd.cn/a v0.0.1

require go.timd.cn/b v0.0.2

require go.timd.cn/c v0.0.2 // indirect

test.go 的内容如下:

package main

import (
    "go.timd.cn/a"
    "go.timd.cn/b"
    "log"
)

func main() {
    log.Println("test started")
    a.A()
    b.B()
    log.Println("test ended")
}

执行 go mod tidy,得到类似下面的错误:

go: finding module for package go.timd.cn/c/pkg
go: test imports
    go.timd.cn/a imports
    go.timd.cn/c/pkg: module go.timd.cn/c@latest found (v0.0.2), but does not contain package go.timd.cn/c/pkg

2. 查看依赖图

go mod graph

Graph 以文本形式打印应用 replace 后的模块依赖图。在输出中,每行有两个空白分隔的字段:模块及其一个依赖。除 main 模块没有 @version 外,每个模块被标识为 path@version 形式的字符串。

可以使用如下脚本将依赖图转换成图片格式:

需要安装 GraphViz:apt install -y graphviz

#!/bin/bash

go_mod_graph=$(go mod graph)

# 将输出转换为 Graphviz 格式
echo "digraph G {" > graphviz_input.dot
echo "  rankdir=LR;" >> graphviz_input.dot
echo "  node [shape=box];" >> graphviz_input.dot

# 遍历每一行,添加节点和边
while read -r line; do
    # 移除行尾的换行符
    line=$(echo "$line" | tr -d '\r')
    # 将依赖关系添加到文件中
    echo "  \"$line\";" >> graphviz_input.dot
done <<< "$go_mod_graph"

echo "}" >> graphviz_input.dot

dot -Tpng graphviz_input.dot -o go_mod_graph.png
rm graphviz_input.dot

列出具名包:

go list -json -m [all | 模块名]

对于第 1 节的示例,执行 go list -json -m go.timd.cn/c 输出:

{
    "Path": "go.timd.cn/c",
    "Version": "v0.0.2",
    "Time": "2024-09-11T06:31:54Z",
    "Indirect": true,
    "Dir": "/root/go/pkg/mod/go.timd.cn/c@v0.0.2",
    "GoMod": "/root/go/pkg/mod/cache/download/go.timd.cn/c/@v/v0.0.2.mod",
    "GoVersion": "1.22.6"
}

可见 MVS 算法最终选择的是 go.timd.cn/c@v0.0.2,而 go.timd.cn/a@v0.0.1 使用 go.timd.cn/c@v0.0.2 模块中已经移除的包 go.timd.cn/c/pkg 导致 go mod tidy 报错。


3. 解决方法

3.1. 尝试升级 go.timd.cn/a

建议。

3.2. 降级 go.timd.cn/b

建议。

3.3. FORK

不建议。

如果想在 test 模块中,同时使用 go.timd.cn/c 的 v0.0.1 和 v0.0.2 版本,并且不改变 go.timd.cn/a,那么可以 FROK 不向前兼容的模块:go.timd.cn/b@v0.0.2、go.timd.cn/c@v0.0.2。

# 为避免干扰,清理缓存
go clean -cache
go clean -modcache

# 创建 b-fork 和 c-fork 存储库
git init --bare ~git/git-repository/b-fork.git
git init --bare ~git/git-repository/c-fork.git
sudo chown -R git:git ~git/git-repository/

# 创建 Workspace
mkdir workspace
cd workspace/
git clone git@127.0.0.1:/home/git/git-repository/b.git/ b-fork
git clone git@127.0.0.1:/home/git/git-repository/c.git/ c-fork

进入 c-fork 目录:

git remote remove origin
git remote add origin git@127.0.0.1:/home/git/git-repository/c-fork.git/
git checkout -b fork-v0.0.2 v0.0.2
git branch

将 go.mod 修改为:

module go.timd.cn/c-fork

go 1.22.6

提交:

git commit -a -m "v0.0.2"
git push -u origin fork-v0.0.2
git tag -d v0.0.2
git tag v0.0.2
git push origin v0.0.2

进入 b-fork 目录:

git remote remove origin
git remote add origin git@127.0.0.1:/home/git/git-repository/b-fork.git/
git checkout -b fork-v0.0.2 v0.0.2
git branch

将 b.go 修改为:

package b

import (
    c "go.timd.cn/c-fork"
)

func B() {
    println("go.timd.cn/b started")
    c.C()
    println("go.timd.cn/b ended")
}

将 go.mod 修改为:

module go.timd.cn/b-fork

go 1.22.6

require go.timd.cn/c-fork v0.0.2

提交:

go mod tidy
go mod download
git commit -a -m "v0.0.2"
git push -u origin fork-v0.0.2
git tag -d v0.0.2
git tag v0.0.2
git push origin v0.0.2

回到 workspace 目录,初始化 test:

mkdir test
cd test/
go mod init test
go get go.timd.cn/a@v0.0.1
go get go.timd.cn/b-fork@v0.0.2

test.go 的内容如下:

package main

import (
        "go.timd.cn/a"
        b "go.timd.cn/b-fork"
        "log"
)

func main() {
        log.Println("test started")
        a.A()
        b.B()
        log.Println("test ended")
}

运行:

go mod tidy
go mod download

回到 workspace 目录:

go work init b-fork/ c-fork/ test/
go run ./test

逐渐修改 b-fork、c-fork 使其可以正常工作,然后提交、打标签。具体流程,可参考 http://timd.cn/go-workspace/ 中的 1.4.1 和 1.4.2 小节。