第31章 包 Package

第31章 包 Package

包(Package),Go语言代码的"集装箱"。它把相关的代码组织在一起,方便复用和管理。

31.1 包的基本概念

31.1.1 为什么需要包

想象一个大型项目:

  • 可能上百个文件
  • 几十个功能模块
  • 多人协作开发

没有包的话,所有代码都在一个文件夹里,会变成"代码地狱"!

包的作用:

  1. 代码组织:按功能划分,结构清晰
  2. 命名空间:不同包的同名类型不会冲突
  3. 访问控制:控制哪些内容对外可见
  4. 复用:一个包可以被多个项目复用

31.1.2 包声明

每个.go文件第一行必须声明自己属于哪个包:

1
2
3
4
5
6
7
8
package main

import "fmt"

// main函数必须属于main包
func main() {
    fmt.Println("Hello, World!")
}

包命名规则:

  • 必须放在源文件顶部
  • 同一个目录的所有.go文件必须属于同一个包
  • 包名通常是小写,简单明了

31.1.3 main 包

main包是程序的入口:

  • 必须有 main() 函数
  • 编译器会把 main 包编译成可执行文件
1
2
3
4
5
6
7
package main

import "fmt"

func main() {
    fmt.Println("这是一个可执行程序!")
}

31.2 包的导入

31.2.1 基本导入

用 import 导入其他包:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
package main

import (
    "fmt"      // 格式化IO
    "math"     // 数学函数
)

func main() {
    fmt.Printf("圆周率 Pi = %.4f\n", math.Pi)   // 3.1416
    fmt.Printf("2的平方根 = %.4f\n", math.Sqrt(2)) // 1.4142
}

31.2.2 别名导入

当包名冲突或太长时,可以起别名:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
package main

import (
    f "fmt"           // fmt起别名f
    s "strings"      // strings起别名s
)

func main() {
    f.Println("别名导入演示")

    text := "hello world"
    f.Println("转大写:", s.ToUpper(text))   // HELLO WORLD
    f.Println("转小写:", s.ToLower(text))   // hello world
    f.Println("重复5次:", s.Repeat("a", 5)) // aaaaa
}

31.2.3 空白导入

用 _ 导入包,只执行包的 init() 函数,不使用包的内容:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
package main

import (
    "fmt"
    _ "unsafe"  // 空白导入,执行unsafe包的初始化代码
)

func main() {
    fmt.Println("空白导入常用于:")
    fmt.Println("1. 注册驱动(如database/sql)")
    fmt.Println("2. 触发初始化代码")
    fmt.Println("3. 导入有副作用的包")
}

空白导入的用途:

  • 导入有副作用的包(如注册驱动)
  • 确保包的初始化代码执行
  • SQLite驱动、图像处理库等

31.3 init 函数

31.3.1 init 自动调用

每个包可以有 init() 函数,在包被导入时自动执行:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
package main

import "fmt"

var initialized bool

// init 在包被导入时自动调用
func init() {
    initialized = true
    fmt.Println("init函数被调用,initialized =", initialized)
}

func main() {
    fmt.Println("main函数开始,initialized =", initialized)
    // init函数被调用,initialized = true
    // main函数开始,initialized = true
}

31.3.2 多个 init 函数

一个文件可以有多个 init(),按出现顺序执行:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
package main

import "fmt"

func init() {
    fmt.Println("第一个init()被调用")
}

func init() {
    fmt.Println("第二个init()被调用")
}

func init() {
    fmt.Println("第三个init()被调用")
}

func main() {
    fmt.Println("main()被调用")
}

执行顺序:

1
2
3
4
第一个init()被调用
第二个init()被调用
第三个init()被调用
main()被调用

31.3.3 init 的执行顺序

包按照依赖顺序初始化:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
package main

import "fmt"

func init() {
    fmt.Println("main包的init")
}

func main() {
    fmt.Println("main函数")
}

31.4 包结构

31.4.1 标准项目结构

一个典型的Go项目结构:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
myproject/
├── cmd/
│   └── myapp/
│       └── main.go          # 程序入口
├── pkg/
│   ├── user/                # 用户模块
│   │   ├── user.go
│   │   └── user_test.go
│   └── order/               # 订单模块
│       ├── order.go
│       └── order_test.go
├── internal/                # 内部使用的包
│   └── config/
│       └── config.go
├── go.mod                   # 模块文件
└── README.md

31.4.2 internal 包

internal 目录是Go的"私有保护"机制:

1
2
3
4
5
6
7
myproject/
├── cmd/
│   └── app/
│       └── main.go
└── internal/
    └── utils/      # 只能被myproject内部的包导入
        └── util.go

其他外部项目无法导入 myproject/internal/utils。

31.4.3 cmd 和 pkg 目录

  • cmd/:应用程序入口
  • pkg/:库代码,可被外部导入

31.5 可见性规则

31.5.1 导出与未导出

Go的访问控制很简单:

  • 首字母大写:导出(exported),其他包可以访问
  • 首字母小写:未导出(unexported),只能在本包内访问
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
package mylib

// Public 类型:其他包可以访问
type Public struct {
    Name string  // 导出字段
    age  int     // 未导出字段
}

// publicFunc:未导出,只能在mylib包内调用
func publicFunc() {}

// PublicFunc:导出,其他包可以调用
func PublicFunc() {}

下面两段代码属于两个不同的包/文件:第一段是 mylib/mylib.go,第二段是另一个模块里的 main.go。它们不能写进同一个文件,放在一起只是为了对照"导出 / 未导出"的可见性。因此第二段直接丢进 Go Playground 会因为找不到 mylib 包而报错,这是预期行为。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// 文件:main.go(与 mylib 不同的包)
package main

import "github.com/yourname/mylib" // 换成你自己的模块路径

func main() {
    // 可以访问首字母大写的导出内容
    p := mylib.Public{Name: "Alice"}
    mylib.PublicFunc()
    _ = p

    // 无法访问未导出的内容:
    // p.age              // 编译错误:age 未导出(cannot refer to unexported field)
    // mylib.publicFunc() // 编译错误:publicFunc 未导出
}

31.6 go mod 依赖管理

31.6.1 初始化模块

go mod init 会在当前目录生成 go.mod,把目录变成一个模块。模块路径通常写仓库地址:

1
2
3
4
5
# 创建新模块
go mod init github.com/username/project

# 已有项目,初始化go.mod
go mod init myproject

生成 go.mod 文件:

1
2
3
4
5
6
7
module github.com/username/project

go 1.21

require (
    github.com/some/pkg v1.2.3
)

31.6.2 添加依赖

go get 会把依赖写进 go.mod,并把精确版本记在 go.sum 里:

1
2
3
4
5
6
7
8
# 添加单个依赖
go get github.com/gin-gonic/gin

# 添加特定版本
go get github.com/gin-gonic/gin@v1.9.0

# 升级到最新版本
go get github.com/gin-gonic/gin@latest

31.6.3 整理依赖

go mod tidy 是最常用的维护命令:它会删掉代码里没用到的依赖,补上代码里用到但 go.mod 里漏掉的依赖:

1
2
# 自动添加缺失的依赖,删除未使用的
go mod tidy

31.7 包的测试

31.7.1 测试文件命名

Go的测试文件有特殊命名规则:

  • 文件名以 _test.go 结尾
  • 放在被测试文件的同一个包目录下
1
2
3
4
5
mypkg/
├── mypkg.go          # 源代码
├── mypkg_test.go     # 测试代码
└── util.go           # 另一个源文件
└── util_test.go      # 对应的测试

31.7.2 编写测试

先看被测试的源文件 mypkg/mypkg.go:

1
2
3
4
package mypkg

// Add 返回两数之和
func Add(a, b int) int { return a + b }

再看同目录下的测试文件 mypkg/mypkg_test.go。测试文件通常和源码放在同一个包里,这样可以直接访问未导出的函数:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
package mypkg

import "testing"

// TestAdd 测试加法函数
func TestAdd(t *testing.T) {
    if got := Add(2, 3); got != 5 {
        t.Errorf("Add(2, 3) = %d, want 5", got)
    }
}

// TestAddNegative 测试负数情况
func TestAddNegative(t *testing.T) {
    if got := Add(-1, -2); got != -3 {
        t.Errorf("Add(-1, -2) = %d, want -3", got)
    }
}

测试函数的签名必须是 func TestXxx(t *testing.T),其中 Xxx 的首字母必须大写,文件名必须以 _test.go 结尾。

31.7.3 运行测试

go test 是最常用的几个开关:-v 显示每个用例、-run 按正则筛选、-cover 输出覆盖率:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# 运行当前目录所有测试
go test -v

# 运行指定测试
go test -v -run TestAdd

# 查看覆盖率
go test -cover

# 生成覆盖率报告
go test -coverprofile=coverage.out
go tool cover -html=coverage.out

31.8 常见问题

31.8.1 包循环依赖

Go不允许循环依赖!

1
2
3
4
5
6
7
// 文件 a/a.go
package a
import "example.com/b"   // a 依赖 b

// 文件 b/b.go
package b
import "example.com/a"   // b 又依赖 a —— 循环依赖,编译直接失败

编译时会报:import cycle not allowed。Go 编译器在解析阶段就会检测到这种环,根本不会开始编译。

解决方案:重构代码,把共同依赖提取到新包。

31.8.2 同名冲突

两个包有相同名称时,用别名解决:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
package main

import (
    myfmt "fmt"    // 给 fmt 起别名,之后要用 myfmt.Printf 调用
    "net/http"     // 正常导入,包名仍然是 http
)

func main() {
    myfmt.Println("使用别名调用 fmt:", http.MethodGet) // 使用别名调用 fmt: GET
}

导入的三种特殊写法值得记住:

1
2
3
4
5
import (
    _ "net/http/pprof"        // 匿名导入:只执行包的 init,不直接使用包名
    . "math"                  // 点导入:把 math 的导出名直接引入当前文件(可读性差,少用)
    myfmt "fmt"               // 别名导入:解决同名冲突或简化长包名
)

本章小结

本章我们学习了包机制:

包的核心概念:

  • 包是Go代码组织的基本单位
  • 每个.go文件必须属于某个包
  • main包是程序入口

包的导入:

  • 基本导入:import "packageName"
  • 别名导入:pkg "path"
  • 空白导入:_ "driver"(只执行init)

可见性规则:

  • 首字母大写 = 导出(其他包可访问)
  • 首字母小写 = 未导出(仅本包可访问)

初始化顺序:

1
import --> 变量初始化 --> init() --> main()

go mod 命令:

命令作用
go mod init初始化模块
go get添加依赖
go mod tidy整理依赖
go mod download下载依赖

测试:

  • 测试文件以 _test.go 结尾
  • 使用 testing 包
  • go test -v 运行测试
最后修改 September 19, 2026: 更新 (3489033b1)