一、Go 的包系统概述
Go 的包管理相比其他语言更简洁:
| 语言 | 包管理 |
|---|---|
| Java | 目录层级 = 包名,import com.example.utils |
| Node.js | 每个文件一个模块,require('./utils') |
| Go | 一个目录一个包,import "module/utils" |
Go 的核心规则就一条:一个目录下的所有 .go 文件必须属于同一个包。
二、创建包
2.1 包名规则
- 包名 = 目录名(约定,不强制但最好遵守)
- 包名全小写,不用下划线,不用驼峰
package xxx写在每个 .go 文件的第一行
2.2 一个最简单的包
myapp/
├── go.mod ← module myapp
├── main.go ← package main
└── mathutil/ ← 目录
└── mathutil.go ← package mathutil
// mathutil/mathutil.go
package mathutil
func Add(a, b int) int {
return a + b
}
💡 包名不一定要和目录名完全一致,但强烈建议保持一致,否则引用时会造成混乱。
三、导出与私有——大小写决定一切
Go 没有 public / private / protected 关键字。可见性只由首字母大小写决定:
| 首字母 | 可见性 | 示例 |
|---|---|---|
| 大写 | 导出(公开),其他包可以访问 | Add(), Config, ErrNotFound |
| 小写 | 私有,只在当前包内可用 | add(), config, errNotFound |
package mathutil
// Add 公开函数——其他包可以用
func Add(a, b int) int {
return a + b
}
// logCalculation 私有函数——只能在 mathutil 包内用
func logCalculation(name string, a, b, result int) {
println("[" + name + "]", a, b, "=>", result)
}
// Multiply 公开,内部调用私有函数
func Multiply(a, b int) int {
result := a * b
logCalculation("Multiply", a, b, result)
return result
}
// main.go
import "myapp/mathutil"
sum := mathutil.Add(10, 5) // ✅ 公开的,可以访问
mathutil.logCalculation(...) // ❌ 编译错误!私有的,访问不到
这条规则适用于所有命名
package example
var Version = "1.0" // 导出变量
var version = "1.0" // 私有变量
const MaxSize = 1024 // 导出常量
type User struct { ... } // 导出类型
type user struct { ... } // 私有类型
func NewUser() *User { ... } // 导出函数
func newUser() *User { ... } // 私有函数
四、导入
4.1 标准导入
import (
"fmt" // 标准库
"os" // 标准库
"myapp/mathutil" // 项目内的包
"myapp/greet" // 项目内的子目录包
"github.com/xxx/yyy" // 第三方依赖
)
导入路径 = go.mod 中的 module 名 + 子目录路径。
例如 go.mod 中定义的是 module myapp,那么 myapp/mathutil 就对应项目根目录下的 mathutil/ 目录。
4.2 别名导入
当包名冲突或太长时,可以起别名:
import (
"crypto/rand"
mathutil "myapp/mathutil" // 起别名
. "fmt" // 点导入:直接使用函数名(不推荐)
_ "image/png" // 空导入:只跑 init,不使用包内功能
)
mathutil.Add(1, 2) // 用别名调用
Println("hello") // 点导入后可以直接调用(但会让代码难以理解)
4.3 空导入
有时候导入包只是为了执行它的 init() 函数(比如注册数据库驱动),不需要使用包里的任何函数。这时候用 _:
import (
_ "github.com/go-sql-driver/mysql" // 只执行 init(),注册驱动
)
五、init 函数
init() 是 Go 的一个特殊函数:
package mathutil
func init() {
println("[mathutil] 包已初始化")
}
init 的规则
| 规则 | 说明 |
|---|---|
| 自动执行 | 包被导入时自动运行,不需要手动调用 |
| 每个包可以有多个 init | 按文件顺序执行(但建议一个包只写一个) |
| 不能被调用 | mathutil.init() ❌ 不行 |
| 没有参数和返回值 | func init() 是固定的签名 |
执行顺序
[mathutil] 包已初始化 ← 1. 先初始化被依赖的包
[greet] 包已初始化 ← 2. 按导入顺序
[greet/chinese] 包已初始化 ← 3. 子包也是独立的包
[main] main 包已初始化 ← 4. main 包最后执行
main() 函数开始执行 ← 5. 所有 init 跑完才进 main
💡 所有 init 都执行完毕后,才会执行
main()函数。
六、子包(Subpackage)
Go 没有"子包"的概念,greet/chinese 只是一个独立的包 chinese,恰好放在 greet 目录下而已。
greet/ ← package greet(独立的包)
├── greet.go
└── chinese/ ← package chinese(另一个独立的包,和 greet 无关)
└── chinese.go
import (
"myapp/greet" // 导入 greet 包
"myapp/greet/chinese" // 导入 chinese 包(两个包是独立的)
)
greet.Greet("pc") // 用 greet 包
chinese.GoodMorning("pc") // 用 chinese 包
关键理解: chinese 包不能直接访问 greet 包里的私有内容,即使它在目录结构上是"子目录"。它们是两个完全独立的包。
七、项目结构实例
把前面学过的综合起来,一个标准的 Go 项目结构:
myapp/
├── go.mod ← module myapp
├── go.sum ← 依赖锁文件
├── main.go ← package main,入口
├── config/
│ └── config.go ← package config
├── handler/
│ ├── handler.go ← package handler
│ └── user.go ← 同属于 package handler
├── model/
│ └── user.go ← package model
├── service/
│ └── user.go ← package service
└── utils/
└── helper.go ← package utils
// main.go
package main
import (
"myapp/config"
"myapp/handler"
"myapp/model"
"myapp/service"
"myapp/utils"
)
func main() {
// 按需调用各包的公开函数
}
八、循环依赖——红线
Go 禁止循环依赖,这是编译器的红线:
❌ 错误:
package a 引用了 package b
package b 又引用了 package a
编译报错:import cycle not allowed
解决方式:
- 提取公共类型到第三个包
- 接口解耦——让一方依赖接口,另一方实现接口
- 重新设计——通常意味着职责划分不够清晰
九、完整的多包示例
项目结构
gotest/
├── go.mod ← module gotest
├── main.go ← package main
├── mathutil/
│ └── mathutil.go ← package mathutil
└── greet/
├── greet.go ← package greet
└── chinese/
└── chinese.go ← package chinese
mathutil/mathutil.go
package mathutil
func Add(a, b int) int {
return a + b
}
func Sub(a, b int) int {
return a - b
}
func Multiply(a, b int) int {
result := a * b
logCalc("Multiply", a, b, result)
return result
}
// 私有函数,仅在包内可见
func logCalc(name string, a, b, result int) {
println("["+name+"]", a, b, "=>", result)
}
func init() {
println("[mathutil] init")
}
greet/greet.go
package greet
var count int
func Greet(name string) string {
count++
return "你好," + name + "!"
}
func GetCount() int {
return count
}
func init() {
println("[greet] init")
}
greet/chinese/chinese.go
package chinese
func GoodMorning(name string) string {
return "早上好," + name + "!"
}
func init() {
println("[greet/chinese] init")
}
main.go
package main
import (
"fmt"
"gotest/greet"
"gotest/greet/chinese"
"gotest/mathutil"
)
func main() {
fmt.Println(mathutil.Add(10, 5))
fmt.Println(mathutil.Multiply(4, 3))
fmt.Println(greet.Greet("pc"))
fmt.Println(chinese.GoodMorning("pc"))
fmt.Println("问候次数:", greet.GetCount())
}
func init() {
println("[main] init")
}
运行输出
[mathutil] init ← 被依赖的包先 init
[greet] init ← 按导入顺序
[greet/chinese] init ← 子包
[main] init ← main 包最后
[Multiply] 4 3 => 12 ← 然后进入 main()
15
12
你好,pc!
早上好,pc!
问候次数: 1
十、小结
| 知识点 | 一句话 |
|---|---|
| 包名 = 目录名 | 一个目录下的所有 .go 文件必须同属一个包 |
| 大写导出小写私有 | 首字母大写 = 公开,小写 = 私有,没有 public/private 关键字 |
| 导入路径 | module/目录路径 |
| init() | 自动执行,按依赖顺序,全部 init 跑完才进 main |
| 子包 | 目录上看似父子,实际上是两个独立的包 |
| 循环依赖 | 不允许,必须通过接口或公共类型解耦 |
| 包级变量 | 在函数外声明的变量,整个包共享,常用于维护状态 |