Go 包与模块管理

AI 辅助生成本文由 AI 起草并经人工核查,示例均标明软件版本;发现错误请以官方文档为准。

Go 1.24

Go 的「包」是编译单元,「模块」是版本单元。两者概念不同,混在一起就会遇到 package xxx is not in std 这类报错。

环境准备

go version   # go1.24.x

module 与 package

  • module:一组包的集合,由根目录的 go.mod 定义,有统一的导入路径前缀与版本。
  • package:同一目录下的 .go 文件,同目录只能有一个包名。
mkdir hello && cd hello
go mod init example.com/hello

go.mod:

module example.com/hello

go 1.24

导入路径 = 模块路径 + 相对目录:

hello/
  go.mod                  module example.com/hello
  main.go                 package main
  greet/
    greet.go              package greet
  internal/
    store/
      store.go            package store
// main.go
package main

import (
    "fmt"

    "example.com/hello/greet"
    "example.com/hello/internal/store"
)

func main() {
    fmt.Println(greet.Hello("世界"))
    _ = store.New()
}

导出规则

标识符首字母大写才导出(对其他包可见),小写只在包内可见。没有 public / private 关键字。

package greet

// 导出
func Hello(name string) string { return "你好," + name }

// 包内可见
func normalize(name string) string { return name }

internal/ 是编译器强制的边界:只有 internal 的父目录及其子目录内的包能导入它。上面的例子中,example.com/hello/internal/store 只能被 example.com/hello/... 导入,外部模块引用会直接编译失败。这是发布库时保护实现的推荐做法。

包名约定

  • 包名小写、单数、无下划线:http、strings、user。
  • 不要用 util、common、helpers 这种大杂烩名字,它们会无限膨胀。
  • 调用处读起来自然:user.New() 优于 users.UserNew()。
  • 目录名与包名通常一致;不一致时以 package 声明为准(internal/store 里写 package storage 也合法,但会让人困惑)。

依赖管理

go get github.com/google/uuid@latest     # 加依赖(会写 go.mod / go.sum)
go get github.com/google/uuid@v1.6.0     # 固定版本
go mod tidy                              # 清理未使用依赖、补全缺失依赖
go mod download                          # 预下载到本地缓存
go list -m all                           # 列出所有依赖
go mod graph | grep uuid                 # 查依赖来源

go.sum 记录每个模块的校验和,必须提交;go.mod 里的 require 是最小版本选择(MVS)的结果:

require (
    github.com/google/uuid v1.6.0
    golang.org/x/sync v0.10.0 // indirect
)

// indirect 表示不是直接导入,而是依赖的依赖。不要手改这些行,用 go mod tidy 维护。

版本选择规则

Go 采用最小版本选择:多个依赖要求同一模块的不同版本时,选满足全部要求的最高最小版本。这带来两个后果:

  • 升级某个依赖可能顺带提升其他包的版本(看 go mod tidy 后的 diff)。
  • 想强制某个版本,用 go get module@version 显式提升,而不是 replace。

replace 的用途

replace 把某个模块指向本地路径或另一个版本,适合本地联调:

replace example.com/lib => ../lib

注意:

  • replace 不传递:你的库用 replace,引入你的项目不会继承。
  • 提交前想清楚是否需要保留;临时调试完记得删。
  • 不能用 replace 绕开版本冲突,那只是把问题藏起来。

vendor 目录

go mod vendor     # 把依赖复制到 vendor/
go build -mod=vendor

适合无法访问代理的构建环境,或需要审计全部依赖源码的团队。代价是仓库变大、升级要重新 vendor。有 vendor/ 时 Go 默认用它,别让它和 go.mod 不一致。

工作区(多模块)

一个仓库里有多个 module 时,用 go.work 把它们的本地路径连起来:

go work init ./api ./lib
go work use ./tools

go.work 通常不提交(每人本地路径可能不同),CI 里直接按 go.mod 构建。

循环依赖

Go 不允许包级循环导入,编译直接报 import cycle not allowed。常见解法:

  1. 抽公共包:把双方都依赖的类型放到第三个包(如 model)。
  2. 依赖接口而非实现:调用方在自己包里定义接口,实现方实现它。
  3. 合并:确实是同一职责,就合成一个包。
// package a 不再导入 b,只声明需要的接口
package a

type Fetcher interface {
    Fetch(id string) (string, error)
}

func Run(f Fetcher) error { _, err := f.Fetch("1"); return err }

常见错误与注意事项

1. 目录里混了两个包名

store/
  a.go   package store
  b.go   package storage

编译报 found packages store and storage。同目录只能一个包名。

2. main 包不在根目录

go run ./cmd/server 而不是 go run .;一个模块可以有多个 main 包,分别放在 cmd/<name>/ 下。

3. 导入路径写成本地路径

import "./greet" 在 module 模式下无效。用完整模块路径。

4. 忘了 go mod tidy

删除代码后依赖仍留在 go.mod,或者新增 import 后构建报缺少模块。

5. 依赖私有仓库

需要配 GOPRIVATE 与凭据:

go env -w GOPRIVATE=git.example.com/*

否则会尝试走公共代理并失败。

6. 用包级变量做「全局配置」

测试之间互相污染。需要共享状态时显式传参或用结构体,别用可变包级变量。

7. 版本号写 v0 却当稳定版用

v0.x 与 v1+ 的兼容性承诺不同,对外发布的库在接口稳定后再打 v1.0.0。

小结

  • go.mod 定义模块,目录定义包,导入路径 = 模块路径 + 相对路径。
  • 大写导出,internal/ 由编译器强制隔离。
  • go.sum 必须提交;go mod tidy 是日常维护命令。
  • 版本用 go get module@vX.Y.Z 显式控制,replace 只用于临时联调。
  • 循环依赖靠抽公共类型或依赖接口解决。

下一步