Go代码审查:检查什么才能让代码不炸裂
\n\n代码审查不仅仅是Jira里的一个勾选标记,也不是在拉取请求评论中与同事争吵的理由。这可能是避免在生产环境中自食其果的最诚实的方式。而当谈到Go——这种因简洁而备受喜爱、又因“泛型在哪里?”(剧透:它们已经有了)而遭人憎恨的语言时——审查就成了一门艺术。在本文中,我们将详细分析在Go代码审查中具体要关注什么,让你的代码不仅能运行,还能赏心悦目。开始吧!
\n\n1. 错误处理:不要忽略,不要害怕
\n\nGo没有异常。这里一切都很诚实:如果函数可能失败,它会返回error。新手最常见的问题是通过下划线忽略错误。请记住:“永远不要忽略错误”——这不仅仅是一句口头禅,而是一条法则。
代码中要检查什么:
\n- \n
- 检查 err != nil —— 只要函数返回错误,就必须进行检查。即使你确信“这里不会出错”。 \n
- 记录日志或向上传递 —— 错误要么附带上下文记录日志,要么向上返回。忽略错误是不好的做法。 \n
- 与哨兵错误比较 —— 使用
errors.Is()和errors.As(),而不是直接使用==比较。 \n
// 不好:忽略错误\nfile, _ := os.Open("config.yaml")\n\n// 好:检查并记录日志\nfile, err := os.Open("config.yaml")\nif err != nil {\n log.Printf("无法打开配置文件: %v", err)\n return err\n}\ndefer file.Close()\n\n\n2. 协程与并发:混乱中的秩序
\n\n协程是Go的超能力。但能力越大,责任越大。在审查中,我们经常看到协程泄漏、数据竞争和死锁。没有并发检查的Go代码审查,就像开着跑车却不系安全带。
\n\n协程检查清单:
\n- \n
- 启动协程 —— 始终控制其生命周期。使用
sync.WaitGroup或上下文。 \n - 数据竞争 —— 检查对共享数据的访问是否受到互斥锁或通道的保护。运行
go test -race。 \n - 协程中的恐慌 —— 如果协程发生恐慌,整个程序都会崩溃。在生产代码中务必使用
recover。 \n
// 不好:不受控制的协程\ngo func() {\n // 可能崩溃并杀死整个进程\n doWork()\n}()\n\n// 好:使用WaitGroup和recover\nvar wg sync.WaitGroup\nwg.Add(1)\ngo func() {\n defer wg.Done()\n defer func() {\n if r := recover(); r != nil {\n log.Printf("协程崩溃: %v", r)\n }\n }()\n doWork()\n}()\nwg.Wait()\n\n\n3. 接口:小而精
\n\n在Go中,接口是契约。但与Java不同,它们不必是庞大的。Go社区有一个原则:“接口越小越好”。一个方法——完美。两个——正常。三个——已经可疑了。
\n\n要检查什么:
\n- \n
- 接口大小 —— 如果超过3-4个方法,考虑拆分。 \n
- 接受接口,返回结构体 —— Go的经典智慧。它使代码灵活且可测试。 \n
- 不要预先定义接口 —— 让消费者而不是生产者来创建它们。 \n
// 不好:庞大的接口\ntype Storage interface {\n Save(data []byte) error\n Load(id string) ([]byte, error)\n Delete(id string) error\n List() ([]string, error)\n Connect() error\n Close() error\n}\n\n// 好:小接口\ntype Saver interface {\n Save(data []byte) error\n}\ntype Loader interface {\n Load(id string) ([]byte, error)\n}\n