Go 的包组织、可见性规则与模块系统深入
一、包(Package)基础
1.1 什么是包
// 每个 Go 文件都必须声明自己属于哪个包
package main // main 包是可执行程序的入口
// 同一目录下的所有 .go 文件必须属于同一个包
// 例外:_test.go 文件可以声明 _test 包(用于黑盒测试)
1.2 包的命名规则
// 包名:小写单词,不要下划线或驼峰
package mypackage // ✅
package my_package // ❌
package MyPackage // ❌
// 目录名 = 包名
// src/
// └── utils/ // 目录名 utils
// ├── string.go // package utils
// └── math.go // package utils
1.3 main 包的特殊性
// main 包 + main() 函数 = 可执行程序
package main
import "fmt"
func main() {
fmt.Println("Hello!")
}
二、导入(Import)
2.1 基本导入
import "fmt" // 标准库
import "github.com/gin-gonic/gin" // 第三方包
// 批量导入
import (
"fmt"
"os"
"strings"
)
2.2 导入别名
// 解决包名冲突
import (
"fmt"
myfmt "github.com/other/fmt" // 别名
)
2.3 点导入与下划线导入
// 点导入:将包的导出标识符引入当前命名空间
import . "fmt"
Println("Hello") // 不需要 fmt.Println
// ⚠️ 不推荐使用——降低代码可读性
// 下划线导入:只执行包的 init() 函数
import _ "github.com/go-sql-driver/mysql" // 注册 MySQL 驱动
// 不使用包中的任何标识符,只是让 init() 被调用
三、可见性规则(大小写)
3.1 导出规则
Go 没有 public/private 关键字,通过首字母大小写控制可见性:
package mypackage
// 首字母大写 = 导出(Exported)= 对外可见
type User struct {
Name string // 导出字段
Email string // 导出字段
age int // 未导出字段(包外不可见)
}
func (u *User) GetName() string { // 导出方法
return u.Name
}
func (u *User) validate() error { // 未导出方法(包外不可访问)
if u.Name == "" {
return errors.New("name is required")
}
return nil
}
// 未导出的类型
type internalConfig struct { // 小写开头,包外不可见
debug bool
}
3.2 可见性速查
| 标识符 | 首字母 | 可见范围 |
|---|---|---|
Name | 大写 | 导出 — 任何导入此包的代码都可访问 |
age | 小写 | 未导出 — 仅包内代码可访问 |
GetUser | 大写 | 导出 — 可被外部调用 |
validate | 小写 | 未导出 — 仅包内调用 |
四、包级变量与 init()
4.1 包级变量
package mypackage
// 包级变量:在包内任何地方声明,在 main() 或 import 之前初始化
var config = loadConfig() // 程序启动时执行
// 常量
const Version = "1.0.0"
// init 函数级别的包级变量
var logger = log.New(os.Stderr, "[mypkg] ", log.LstdFlags)
4.2 init() 函数
package mypackage
// init() 在包被导入时自动执行
// 每个包可以有多个 init(),同一个文件内也可以有多个
func init() {
fmt.Println("mypackage initialized")
// 常用于:注册驱动、初始化连接池、验证配置等
}
// init() 的执行顺序
// 1. 首先导入依赖包(递归)
// 2. 依赖包的 init() 先执行
// 3. 最后执行当前包的 init()
4.3 执行顺序
// 完整的程序启动顺序:
// 1. main 包导入的所有包(递归)
// 2. 所有依赖包的包级变量初始化
// 3. 所有依赖包的 init() 执行
// 4. main 包的包级变量初始化
// 5. main 包的 init() 执行
// 6. main() 函数执行
五、Go Modules 深入
5.1 模块路径规范
// go.mod 中声明的模块路径
module github.com/starer-code/myproject
// 规则:
// - 使用代码托管平台的完整路径
// - GitHub: github.com/user/repo
// - GitLab: gitlab.com/user/repo
// - 私有: github.com/company/internal-tool
5.2 内部包(internal)
// Go 强制执行的可见性规则:
// internal/ 目录下的包只能被其父目录及父目录的子目录导入
// 目录结构
myproject/
├── go.mod
├── cmd/
│ └── server/
│ └── main.go // 可以 import "myproject/internal/auth"
├── internal/
│ ├── auth/ // 只能被 myproject 下的代码导入
│ │ └── auth.go
│ └── db/ // 外部项目不能 import "myproject/internal/db"
│ └── db.go
└── pkg/
└── utils/ // 外部可以导入
└── utils.go
5.3 推荐的项目结构
myproject/
├── go.mod
├── go.sum
├── README.md
├── cmd/ # 可执行文件入口
│ └── server/
│ └── main.go
├── internal/ # 私有代码(不可被外部导入)
│ ├── auth/
│ ├── model/
│ └── service/
├── pkg/ # 可被外部导入的公共代码
│ └── utils/
├── api/ # API 定义(protobuf、OpenAPI 等)
├── configs/ # 配置文件
├── scripts/ # 构建/部署脚本
└── test/ # 集成测试
六、循环依赖
6.1 Go 禁止循环导入
// ❌ 编译错误:import cycle not allowed
// package A imports package B
// package B imports package A
// 解决方案:
// 1. 提取公共代码到第三个包 C
// 2. 使用接口解耦(在消费方定义接口)
// 3. 重新设计包结构
6.2 避免循环依赖的技巧
// 技巧1:接口解耦
// package auth 需要调用 package user 的方法
// 在 auth 包中定义接口,在 user 包中实现
// auth/interface.go
type UserFinder interface {
FindByID(id int) (*User, error)
}
// user/user.go(正常导入 auth 不会有循环)
type Service struct {
finder auth.UserFinder // 依赖接口,不依赖具体包
}
// 技巧2:提取公共类型到独立包
// package a 和 package b 都需要 User 结构体
// 提取到 package model 中
七、本节要点速查
| 概念 | 要点 |
|---|---|
| 包名 | 小写单词,目录名 = 包名 |
| 可见性 | 大写开头 = 导出,小写开头 = 未导出 |
| main 包 | 可执行程序入口,必须有 main() |
| init() | 包导入时自动执行,可多个,递归调用 |
| 下划线导入 | import _ "pkg" 仅执行 init() |
| internal | 强制私有,只能被父目录及子目录导入 |
| 循环依赖 | 禁止,用接口解耦或提取公共包 |
| 项目结构 | cmd/ + internal/ + pkg/ 是常见布局 |
| 模块路径 | 使用代码托管平台的完整路径 |
豫公网安备41108202000982号