V
Vel·ToolKit
简洁 · 高效 · 即开即用
ZH
第 16 章 / 共 20 章

包与模块

导入、可见性、init、go mod、版本管理

每个目录是一个包;目录下所有 .go 文件 package 名必须一致。可执行程序的入口包必须叫 main,包含 func main()。库包名通常和目录名相同。

myapp/
├── go.mod
├── main.go               // package main
└── internal/
    └── store/
        ├── store.go      // package store
        └── store_test.go // package store

可见性

标识符首字母大写则导出(包外可见),小写则只在包内可用——结构体字段也遵守这个规则。包是 Go 的最小封装单位,没有 public/private 关键字。

// store/user.go
package store

func parse(s string) {}      // 包内可用
func Parse(s string) {}      // 导出

type User struct {
    ID   int     // 包外可读写
    name string // 包内私有
}

导入

import (
    "fmt"                           // 标准库
    "example.com/myapp/internal/db" // 同一模块内
    "github.com/google/uuid"        // 第三方

    alias "encoding/json"            // 起别名(含义冲突时)
    . "math"                         // 点导入,把符号引到当前作用域(除测试外不推荐)
    _ "github.com/lib/pq"            // 空白导入,只触发 init
)

init 函数与执行顺序

每个文件可定义任意个 init(),按文件名顺序执行;同一包的 init 在 main() 前跑。包之间的 init 顺序由依赖关系决定:被依赖的包先初始化。空白导入 `_ "github.com/lib/pq"` 就是用它的副作用——驱动注册。

package main

import "fmt"

type Driver interface {
    Name() string
}

type MemoryDriver struct{}

func (MemoryDriver) Name() string { return "memory" }

var registry = map[string]Driver{}

func Register(name string, d Driver) { registry[name] = d }

func init() {
    Register("memory", MemoryDriver{})
}

func main() {
    fmt.Println(registry["memory"].Name())
}

Go Modules

$ go mod init example.com/myapp       # 新建模块(example.com/myapp 是 import 路径前缀)
$ go get github.com/google/uuid       # 加最新依赖
$ go get github.com/google/uuid@v1.6.0 # 锁版本
$ go get -u ./...                      # 升级当前模块所有依赖
$ go mod tidy                          # 同步 go.mod / go.sum,删未用、加未列
$ go mod download                      # 拉到本地缓存
$ go mod why github.com/x/y            # 解释为什么依赖了它
$ go mod graph                         # 依赖关系图

go.mod 结构

module example.com/myapp

go 1.22

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

replace example.com/lib => ../lib       // 本地调试用
exclude github.com/bad/pkg v1.2.3       // 排除某版本

语义化版本与 /v2

Go Modules 严格遵循 SemVer:vMAJOR.MINOR.PATCH,且 v2 及以上的大版本必须把模块路径改成 module example.com/lib/v2,并以 v2 子目录或主分支形式发布。导入时也写 example.com/lib/v2,否则就是不同模块。

internal 与目录布局

internal 目录下的包只能被同一模块内引入——这是 Go 编译器强制的访问控制。常见“标准布局”:

myapp/
├── cmd/
│   └── api/main.go        // 可执行入口
├── internal/              // 仅本模块可用
│   ├── http/
│   ├── service/
│   └── store/
├── pkg/                   // 对外可复用的工具库(可选)
├── api/                   // proto / openapi
├── go.mod
└── go.sum

GOPROXY 与私有模块

Go 默认通过 GOPROXY 代理下载依赖。国内常配置 goproxy.cn;公司私有仓库需要把对应路径加到 GOPRIVATE,让 go 命令跳过代理与校验和数据库。

$ go env -w GOPROXY=https://goproxy.cn,direct
$ go env -w GOSUMDB=sum.golang.google.cn
$ go env -w GOPRIVATE=*.corp.example.com,git.internal.io

workspace(多模块开发)

Go 1.18+ 提供 go.work 让你同时开发多个本地模块而不用每个都改 replace。

$ go work init ./api ./lib
$ go work use ./tools         # 再加一个