go中if err != nil是控制流必需环节而非风格选择,因error是函数返回值且无异常机制,未检查将导致panic或静默错误;必须在依赖调用成功的后续逻辑前检查,错误需包装传递并精准测试。

Go里if err != nil不是风格问题,是控制流事实
Go没有异常机制,err是函数返回值的一部分,不是可选的“提醒”。写if err != nil不是为了“符合规范”,而是因为不检查就继续执行,大概率会panic或产生静默错误。比如json.Unmarshal失败后还去访问解码后的结构体字段,结果全是零值——程序没崩,但逻辑已经错了。
- 所有返回
error的函数调用后,只要后续逻辑依赖该调用成功,就必须检查err - 如果明确知道某次调用不会出错(如
bytes.NewReader([]byte("x"))),可以跳过,但得心里有数,别靠“应该不会错”糊弄自己 - 别把多个
err检查堆在一行:写if err1 != nil || err2 != nil看似省事,实际掩盖了哪个步骤失败,调试时抓瞎
提前返回比嵌套if更安全也更易读
新手常把正常逻辑包在if err == nil { ... }里,形成多层缩进。这不光难看,还会拉长变量作用域、增加误用风险。Go社区默认做法是“错误优先,快速退出”。
- 写成
if err != nil { return err }或if err != nil { return nil, err },干净利落 - 避免
if err != nil { /* 处理错误 */ } else { /* 主逻辑 */ }这种二分结构,else块里的代码离错误检查太远,容易忽略前置条件是否成立 - 如果错误需要包装(比如加上下文),用
fmt.Errorf("read config: %w", err),别用+拼字符串,否则errors.Is()和errors.As()会失效
defer和err一起用时,别让defer覆盖主返回值
常见陷阱:在函数末尾用defer关文件或释放资源,同时又想返回err,结果defer里调用Close()也返回错误,直接把原本的err盖掉了。
-
defer里的Close()必须单独检查,比如if cerr := f.Close(); cerr != nil && err == nil { err = cerr } - 更稳妥的做法是显式关:在关键路径上
defer只做无错操作(如mu.Unlock()),有返回值的操作(如Close())手动调,立刻检查 - 别信
os.File.Close()“一般不会错”的说法——磁盘满、NFS挂掉、权限突变都可能让它返回*os.PathError
测试if err != nil分支不能只靠“造一个nil”
单元测试里模拟错误,很多人随手写mockFunc = func() (int, error) { return 0, nil },再改成return 0, errors.New("boom"),以为覆盖了分支。但真实错误往往带类型、带字段,比如*os.PathError里有Op、Path、Err三个字段,业务逻辑可能根据err.(*os.PathError).Op == "open"做不同处理。
立即学习“go语言免费学习笔记(深入)”;
- 测试具体错误类型时,用
testify/assert.ErrorAs(t, err, &target)或原生errors.As(err, &target) - 别在测试里用
if err != nil { t.Fatal(err) }一棍子打死——你要验证的就是err非空时的行为 - 如果函数里用了
%w包装错误,测试时得用errors.Is(err, fs.ErrNotExist),而不是err == fs.ErrNotExist
真正难的不是写if err != nil,而是判断哪里该检查、检查后怎么传递上下文、以及怎么让错误信息对运维和用户都有意义。这些没法靠格式化工具修好,得在每次return前多问一句:这个err,下游能靠它定位问题吗?










