纯函数必须满足:不读写全局变量、不调用非确定性函数(如time.Now、rand.*、os.Getenv)、不执行I/O、不修改入参;测试需表驱动、显式覆盖边界值、用errors.Is判错、禁用mock接口与context。

纯函数本身不依赖外部状态、无副作用、输入决定输出,天然适合单元测试——但前提是它真的“纯”。很多 Go 函数看似简单,实则暗藏 time.Now()、rand.Intn()、全局变量或隐式调用标准库 I/O,一测就飘。
怎么判断一个 Go 函数是不是真纯函数
它必须同时满足:不读写全局变量、不调用 time.Now() / rand.* / os.Getenv() 等非确定性函数、不执行 I/O(如 http.Get、os.ReadFile)、不修改入参(尤其指针或 slice 底层数据)。
- 常见错误现象:
ValidatePhone("13800138000")在本地跑 10 次都通过,CI 上偶发失败——大概率内部用了regexp.Compile但没缓存,编译时触发了不可控的 panic - 更隐蔽的是:函数里调用了另一个包的
log.Printf或metrics.Inc,虽不改变返回值,但让测试无法干净断言行为 - 结构体方法即使只读字段,若字段是
*sync.Mutex或含 channel,也破坏纯度——因为可能隐式触发 runtime 调度或死锁
纯函数测试必须用表驱动 + 结构体切片
把测试数据组织成 []struct{input string; expected bool} 是底线。散落在 if/else 或多个 t.Run 里,会导致新增 case 时漏覆盖边界值,比如空字符串、超长数字、UTF-8 边界字符。
- 示例中
{"", false}和{"+8613800138000", true}必须显式列出,不能靠“应该会进 else 分支”来假设 - 避免在测试里拼接 input:
input := "138" + "00138000"—— 这会让模糊测试(fuzz test)无法识别原始语义 - 如果函数返回 error,每个 case 必须声明
expectedErr error,而不是只判err != nil
error 判断必须用 errors.Is,不是字符串匹配
Go 的 error 是值,不是字符串。用 err.Error() == "invalid format" 会崩在多语言环境、日志修饰、甚至只是加了个前缀空格。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
立即学习“go语言免费学习笔记(深入)”;
- 正确方式:定义包级错误变量
var ErrInvalidFormat = errors.New("invalid format"),函数内用return ErrInvalidFormat - 测试中写
if !errors.Is(err, ErrInvalidFormat) { t.Fatalf("expected %v, got %v", ErrInvalidFormat, err) } - 如果函数可能返回多种 error(如
ErrNotFound、ErrTooManyRequests),每个都要有独立测试 case,不能只测“有 error”
别在纯函数里留 mock 接口入口
纯函数不该为“可测性”预留接口参数。例如写成 func ParseTime(s string, nowFunc func() time.Time) (time.Time, error) 是错的——这已经不是纯函数,而是半纯半 impure 的混合体。
- 真正做法:把时间依赖抽到上层,让纯函数只接收
time.Time或时间戳 int64;需要“当前时间”的逻辑由调用方传入,测试时直接塞固定值 - 同理,不要给
json.Marshal或strings.TrimSpace加回调参数——它们本就是纯的,mock 只会增加调用方负担、掩盖真实行为 - 如果函数签名开始出现
ctx context.Context或logger Logger,说明它已越界,该拆分出副作用部分了
纯函数的可测性陷阱不在语法,而在“你以为它纯,其实它偷偷摸摸干了别的事”。每次加新测试前,先扫一眼函数体里有没有任何带括号的非局部调用——那是最可靠的纯度检查器。

















