
前言
上一篇我们搞清楚了 import path 是怎么被解析成 module + 路径 的。这一篇我们来聊个更实际的问题:Go 编译器到底去磁盘的哪个角落实打实地把代码读出来的?
你可能会觉得:"不就是下载到 $GOPATH/pkg/mod 嘛"。
没错!但是这只是其中的一个环节,Go 找包其实严格遵守一套顺序:Main Module → Replace → Vendor(如果有) → Module Cache。
1. 第一顺位:Main Module(亲儿子优先)
不管你引用的包叫什么名字,Go 永远最先检查:这玩意儿是不是当前项目(Main Module)里的?
这也是为什么你可以在项目里创建一个叫 github.com/gin-gonic/gin 的 go.mod,然后在代码里 import "github.com/gin-gonic/gin",Go 会毫不犹豫地通过编译,并使用你本地目录下的代码,完全无视真正的 GitHub 仓库。
判定逻辑:
- 读取当前项目根目录下的
go.mod,找到module声明(比如module example.com/myproject)。 - 检查你的
import path是不是以这个module path开头的。 - 如果是,直接去当前项目的对应子目录找。找到了就用,找不到就报错(它不会因为你本地没找到,就自作聪明去网上再找一遍)。
例子:看起来像"第三方",但仍然命中 Main Module(前缀匹配)
go.mod:
go
module github.com/acme/demo
目录结构:
text
demo/
go.mod
lib/log/log.go
main.go
main.go:
go
package main
import "github.com/acme/demo/lib/log"
func main() {
log.Info("hi")
}
虽然长得像 GitHub 路径,但它其实就是你的 Main Module,Go 会直接从本地 ./lib/log 读代码。
2. 第二顺位:Replace(最高级指令)
如果 Main Module 里没这号"人",Go 就会去看 go.mod 里的 replace 指令。
replace 是 Go Module 里的"红头文件",它的效力高于一切版本规则和远程仓库。
go
replace github.com/user/project => ../local-project
只要你的 import path 命中了 replace 箭头左边的 module,Go 就会立即、强制转向箭头右边的路径去查找。
- 本地路径 :如果右边是本地目录(如
../local-project),Go 直接读那个目录,不走网络,不看版本,不进缓存。 - 远程路径 :如果右边是另一个 module(如
github.com/fork/project),Go 会转而去下载那个 module 的特定版本。
常见坑点:
replace 仅在 Main Module 的 go.mod 里生效。如果你引用的第三方包 A 里也写了 replace,那是无效的。Go 只听当前项目(主项目)的指挥。
3. 特殊分支:Vendor(替补席)
这里有个特殊情况。如果你的项目里有一个 vendor 目录,并且你(或者 IDE 默认)开启了 -mod=vendor 模式,查找顺序会在这里发生分叉。
在 vendor 模式下,Go 完全无视 Module Cache 和 GOPROXY。
它只会做一件事:去当前项目的 vendor/ 目录下找对应的包。如果 vendor 里没有,它就直接报错,哪怕你本地缓存里有,哪怕网上有,它都不看。
这就是为什么有时候你明明 go get 了新版本,但代码死活不更新------快去看看是不是不知不觉启用了 Vendor 模式,而 vendor 目录还没同步更新(需要跑 go mod vendor)。
4. 第四顺位:Module Cache(常规武器)
如果以上都没命中(不是自己人、没被替换、没开 vendor),Go 终于要走常规流程了:去 Module Cache 找。
也就是我们要找那个大家最熟悉的目录:
$GOPATH/pkg/mod
Go 会根据 go.mod(或者 go.sum 锁定的版本)里要求的版本号,去缓存目录下拼出一个路径。
例如 import "github.com/gin-gonic/gin",版本是 v1.9.0,Go 就会去:
$GOPATH/pkg/mod/github.com/gin-gonic/gin@v1.9.0
注意,这里的目录名是带版本号后缀的。这意味着 v1.9.0 和 v1.9.1 在磁盘上是两个完全独立的目录,互不干扰。这也解决了 GOPATH 时代"多版本共存"的千古难题。
5. 最后的兜底:GOPROXY(没有才去下载)
如果 Module Cache 里也没有这个包(比如你是第一次引入,或者刚清了缓存),Go 才会真正发起网络请求。
但它不是直接去 GitHub 也就是 Direct 模式(除非你设了 GOPRIVATE),而是去 GOPROXY 询问。
流程如下:
- 请求 GOPROXY(默认
proxy.golang.org)。 - 下载该版本的
.zip包和.mod文件。 - 解压 到
$GOPATH/pkg/mod/cache/download暂存。 - 校验 (对比
go.sum或 Checksum Database)。 - 搬运 并解压到
$GOPATH/pkg/mod/...下的最终目录。 - 编译器读取最终目录。
总结
当你写下一行 import 时,Go 编译器的流程:
- 是标准库吗?
- 是 ->
$GOROOT/src-> 结束。 - 否 -> 继续。
- 是 ->
- 是 Main Module 里的包吗? (看
go.mod的 module 声明)- 是 -> 项目根目录/子目录 -> 结束。
- 否 -> 继续。
- 有
replace吗?- 有 -> 听
replace的话,去指定目录 -> 结束。 - 否 -> 继续。
- 有 -> 听
- 开了
-mod=vendor吗?- 是 -> 去
vendor/找 -> 找到用,找不到报错 -> 结束。 - 否 -> 继续。
- 是 -> 去
- Module Cache 里有缓存吗? (
$GOPATH/pkg/mod)- 有 -> 直接用 -> 结束。
- 否 -> 去 GOPROXY 下载 -> 存入 Cache -> 用 -> 结束。
是
否
是
否
有
否
是
否
有
否
import path
是标准库吗?
GOROOT/src
结束
是 Main Module 里的包吗?
项目根目录/子目录
有 replace 吗?
按 replace 指向目录查找
开了 -mod=vendor 吗?
vendor/ 目录
Module Cache 里有缓存吗?
GOPATH/pkg/mod
去 GOPROXY 下载
存入 Cache
看懂这个顺序,下次再遇到"包找不到"或者"修改不生效",你就知道该去哪个环节找原因了。