Web

Go 的 Package、Module 与测试工具链

随着源文件逐渐增多,程序需要把相关代码组织在一起,并通过测试确保修改没有破坏原有行为。Go 使用 Package 划分代码单元,使用 Module 管理项目和依赖,同时在官方工具链中提供了格式化、静态检查与测试命令。

本文从 Package 和 Module 开始,介绍常用的依赖管理工具,并使用一个简单的加法函数学习单元测试与表格测试。

使用 Package 组织代码

每个 Go 源文件都以 package 声明开始。位于同一目录中的 Go 文件通常属于同一个 Package,并共同组成一个代码单元。

下面的文件声明了一个名为 mathutil 的 Package:

go
package mathutil

func Add(a, b int) int {
	return a + b
}

其他 Package 可以通过 import 引入它,再使用“包名.标识符”的形式访问其中的内容:

go
package main

import (
	"fmt"

	"example.com/calculator/mathutil"
)

func main() {
	result := mathutil.Add(2, 3)
	fmt.Println(result)
}

这里的 example.com/calculator 是 Module 路径,mathutil 是它下面的 Package。Module 会在下一节介绍。

Package 名称与目录

Package 名称通常使用简短的小写单词,并与所在目录名称保持一致。例如,mathutil 目录中的文件通常都声明 package mathutil

可执行程序是一个特殊情况:它使用 package main,并以 main() 函数作为入口。普通 Package 则用于封装可以被其他代码复用的数据和行为。

标识符的可见性

Go 根据标识符首字母的大小写决定它能否被其他 Package 访问:

go
package mathutil

func Add(a, b int) int {
	return a + b
}

func double(value int) int {
	return value * 2
}

Add 以大写字母开头,可以被其他 Package 访问;double 以小写字母开头,只能在 mathutil 内部使用。

这一规则同样适用于类型、变量、常量、方法和 Struct 字段。Go 不使用 publicprivate 关键字,而是通过名称直接表达可见性。

使用 Module 管理项目

Module 是一组相关 Package 的集合,也是 Go 管理依赖版本的基本单位。新项目可以在项目根目录执行以下命令:

bash
go mod init example.com/calculator

这条命令会创建 go.mod 文件,并记录 Module 路径:

go
module example.com/calculator

go 1.25

Module 路径会成为项目内部 Package 的导入路径前缀。例如,mathutil 目录对应的完整导入路径是:

go
import "example.com/calculator/mathutil"

如果 Module 会发布到代码托管平台,通常应使用与仓库地址对应的路径。如果只是本地练习,也可以使用示例路径。

添加与整理依赖

当代码引入第三方 Package 后,可以使用 go get 添加或调整依赖版本:

bash
go get golang.org/x/text

Go 会把需要的版本写入 go.mod,并在 go.sum 中保存依赖内容的校验信息。go.sum 不是依赖清单的替代品,它用于验证下载内容是否一致,通常应和 go.mod 一起保留。

源码发生变化后,可以使用 go mod tidy 让依赖记录与实际导入保持一致:

bash
go mod tidy

这个命令会补齐源码需要的依赖,并移除已经不再使用的依赖记录。添加功能前,可以先查看标准库是否已经提供对应能力,再决定是否引入第三方 Package。

使用官方工具检查代码

Go 工具链为常见的代码检查提供了统一命令。日常开发中最常用的是 go fmtgo vetgo mod tidy

bash
go fmt ./...
go vet ./...
go mod tidy

./... 表示当前 Module 下的所有 Package。

  • go fmt ./... 按 Go 的统一格式修改源文件;
  • go vet ./... 检查编译器不一定会报告的可疑代码;
  • go mod tidy 整理 go.modgo.sum 中的依赖记录。

这些命令关注的问题不同。格式化通过不代表代码逻辑正确,静态检查通过也不能替代测试,因此它们通常需要配合使用。

使用 testing 编写单元测试

Go 标准库提供了 testing Package,并由 go test 负责发现和执行测试。普通单元测试需要满足以下规则:

  1. 测试文件名以 _test.go 结尾;
  2. 测试函数名以 Test 开头;
  3. 测试函数接收一个 *testing.T 参数,并且没有返回值。

为前面的 Add 函数创建测试文件:

go
package mathutil

import "testing"

func TestAdd(t *testing.T) {
	got := Add(2, 3)
	want := 5

	if got != want {
		t.Fatalf("Add(2, 3) = %d, want %d", got, want)
	}
}

测试先调用目标函数得到 got,再将它与期望值 want 比较。结果不一致时,t.Fatalf 会记录错误并立即停止当前测试。

testing.T 还提供了几种常用的失败报告方法:

  • t.Errort.Errorf 报告失败后继续执行当前测试;
  • t.Fatalt.Fatalf 报告失败后立即停止当前测试;
  • 名称带 f 的方法支持 fmt.Printf 风格的格式化参数。

在 Module 根目录运行全部测试:

bash
go test ./...

添加 -v 可以显示每条测试的详细结果:

bash
go test -v ./...

只需要运行名称匹配的测试时,可以使用 -run 参数:

bash
go test -v -run '^TestAdd$' ./...

使用表格测试覆盖多组输入

同一个函数通常需要验证正常值、零值和负数等多种输入。如果为每一组输入重复编写完整测试,准备数据和断言的结构会变得冗余。

表格测试把多组输入与期望结果保存在一个 Slice 中,再用循环执行相同的测试逻辑:

go
package mathutil

import "testing"

func TestAdd(t *testing.T) {
	tests := []struct {
		name string
		a    int
		b    int
		want int
	}{
		{name: "positive numbers", a: 2, b: 3, want: 5},
		{name: "with zero", a: 4, b: 0, want: 4},
		{name: "negative numbers", a: -2, b: -3, want: -5},
	}

	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			got := Add(test.a, test.b)

			if got != test.want {
				t.Errorf(
					"Add(%d, %d) = %d, want %d",
					test.a,
					test.b,
					got,
					test.want,
				)
			}
		})
	}
}

t.Run 会为每组数据创建一个带名称的子测试。某组数据失败时,测试输出会显示对应名称,便于快速定位具体用例。

表格测试适合“输入字段相同、验证逻辑相同”的多组场景。如果不同用例需要完全不同的准备过程和断言方式,拆成独立测试通常更清楚。

检查覆盖率与数据竞争

go test 还可以通过参数执行额外检查。添加 -cover 可以统计测试运行时覆盖到的语句比例:

bash
go test -cover ./...

需要进一步查看哪些代码没有被覆盖时,可以生成覆盖率文件:

bash
go test -coverprofile=coverage.out ./...
go tool cover -html=coverage.out

覆盖率只能说明代码是否在测试中执行过,不能证明断言是否充分。它适合帮助发现遗漏的分支,而不应作为测试质量的唯一标准。

添加 -race 可以在执行测试时检测数据竞争:

bash
go test -race ./...

竞争检测只能发现本次测试实际执行路径中出现的问题,因此结果通过并不表示所有路径都不存在数据竞争。它是一项测试工具检查,不会改变普通单元测试的编写方式。

总结

Package 以目录为基础组织代码,并通过标识符首字母的大小写控制外部可见性。Module 为一组 Package 提供统一的导入路径,并通过 go.modgo.sum 管理依赖。

go fmtgo vetgo mod tidygo test 分别处理格式、静态检查、依赖与测试。编写测试时,可以从单个输入的基础测试开始,再使用表格测试覆盖结构相同的多组用例,并根据需要增加覆盖率和数据竞争检查。