# Go 代码审查指南 基于 Go 官方指南、Effective Go 和社区最佳实践的代码审查清单。 ## 快速审查清单 ### 必查项 - [ ] 错误是否正确处理(不忽略、有上下文) - [ ] goroutine 是否有退出机制(避免泄漏) - [ ] context 是否正确传递和取消 - [ ] 接收器类型选择是否合理(值/指针) - [ ] 是否使用 `gofmt` 格式化代码 ### 高频问题 - [ ] 循环变量捕获问题(Go < 1.22) - [ ] nil 检查是否完整 - [ ] map 是否初始化后使用 - [ ] defer 在循环中的使用 - [ ] 变量遮蔽(shadowing) --- ## 1. 错误处理 ### 1.1 永远不要忽略错误 ```go // ❌ 错误:忽略错误 result, _ := SomeFunction() // ✅ 正确:处理错误 result, err := SomeFunction() if err != nil { return fmt.Errorf("some function failed: %w", err) } ``` ### 1.2 错误包装与上下文 ```go // ❌ 错误:丢失上下文 if err != nil { return err } // ❌ 错误:使用 %v 丢失错误链 if err != nil { return fmt.Errorf("failed: %v", err) } // ✅ 正确:使用 %w 保留错误链 if err != nil { return fmt.Errorf("failed to process user %d: %w", userID, err) } ``` ### 1.3 使用 errors.Is 和 errors.As ```go // ❌ 错误:直接比较(无法处理包装错误) if err == sql.ErrNoRows { // ... } // ✅ 正确:使用 errors.Is(支持错误链) if errors.Is(err, sql.ErrNoRows) { return nil, ErrNotFound } // ✅ 正确:使用 errors.As 提取特定类型 var pathErr *os.PathError if errors.As(err, &pathErr) { log.Printf("path error: %s", pathErr.Path) } ``` ### 1.4 自定义错误类型 ```go // ✅ 推荐:定义 sentinel 错误 var ( ErrNotFound = errors.New("not found") ErrUnauthorized = errors.New("unauthorized") ) // ✅ 推荐:带上下文的自定义错误 type ValidationError struct { Field string Message string } func (e *ValidationError) Error() string { return fmt.Sprintf("validation error on %s: %s", e.Field, e.Message) } ``` ### 1.5 错误处理只做一次 ```go // ❌ 错误:既记录又返回(重复处理) if err != nil { log.Printf("error: %v", err) return err } // ✅ 正确:只返回,让调用者决定 if err != nil { return fmt.Errorf("operation failed: %w", err) } // ✅ 或者:只记录并处理(不返回) if err != nil { log.Printf("non-critical error: %v", err) // 继续执行备用逻辑 } ``` --- ## 2. 并发与 Goroutine ### 2.1 避免 Goroutine 泄漏 ```go // ❌ 错误:goroutine 永远无法退出 func bad() { ch := make(chan int) go func() { val := <-ch // 永远阻塞,无人发送 fmt.Println(val) }() // 函数返回,goroutine 泄漏 } // ✅ 正确:使用 context 或 done channel func good(ctx context.Context) { ch := make(chan int) go func() { select { case val := <-ch: fmt.Println(val) case <-ctx.Done(): return // 优雅退出 } }() } ``` ### 2.2 Channel 使用规范 ```go // ❌ 错误:向 nil channel 发送(永久阻塞) var ch chan int ch <- 1 // 永久阻塞 // ❌ 错误:向已关闭的 channel 发送(panic) close(ch) ch <- 1 // panic! // ✅ 正确:发送方关闭 channel func producer(ch chan<- int) { defer close(ch) // 发送方负责关闭 for i := 0; i < 10; i++ { ch <- i } } // ✅ 正确:接收方检测关闭 for val := range ch { process(val) } // 或者 val, ok := <-ch if !ok { // channel 已关闭 } ``` ### 2.3 使用 sync.WaitGroup ```go // ❌ 错误:Add 在 goroutine 内部 var wg sync.WaitGroup for i := 0; i < 10; i++ { go func() { wg.Add(1) // 竞态条件! defer wg.Done() work() }() } wg.Wait() // ✅ 正确:Add 在 goroutine 启动前 var wg sync.WaitGroup for i := 0; i < 10; i++ { wg.Add(1) go func() { defer wg.Done() work() }() } wg.Wait() ``` ### 2.4 避免在循环中捕获变量(Go < 1.22) ```go // ❌ 错误(Go < 1.22):捕获循环变量 for _, item := range items { go func() { process(item) // 所有 goroutine 可能使用同一个 item }() } // ✅ 正确:传递参数 for _, item := range items { go func(it Item) { process(it) }(item) } // ✅ Go 1.22+:默认行为已修复,每次迭代创建新变量 ``` ### 2.5 Worker Pool 模式 ```go // ✅ 推荐:限制并发数量 func processWithWorkerPool(ctx context.Context, items []Item, workers int) error { jobs := make(chan Item, len(items)) results := make(chan error, len(items)) // 启动 worker for w := 0; w < workers; w++ { go func() { for item := range jobs { results <- process(item) } }() } // 发送任务 for _, item := range items { jobs <- item } close(jobs) // 收集结果 for range items { if err := <-results; err != nil { return err } } return nil } ``` --- ## 3. Context 使用 ### 3.1 Context 作为第一个参数 ```go // ❌ 错误:context 不是第一个参数 func Process(data []byte, ctx context.Context) error // ❌ 错误:context 存储在 struct 中 type Service struct { ctx context.Context // 不要这样做! } // ✅ 正确:context 作为第一个参数,命名为 ctx func Process(ctx context.Context, data []byte) error ``` ### 3.2 传播而非创建新的根 Context ```go // ❌ 错误:在调用链中创建新的根 context func middleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ctx := context.Background() // 丢失了请求的 context! process(ctx) next.ServeHTTP(w, r) }) } // ✅ 正确:从请求中获取并传播 func middleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() ctx = context.WithValue(ctx, key, value) process(ctx) next.ServeHTTP(w, r.WithContext(ctx)) }) } ``` ### 3.3 始终调用 cancel 函数 ```go // ❌ 错误:未调用 cancel ctx, cancel := context.WithTimeout(parentCtx, 5*time.Second) // 缺少 cancel() 调用,可能资源泄漏 // ✅ 正确:使用 defer 确保调用 ctx, cancel := context.WithTimeout(parentCtx, 5*time.Second) defer cancel() // 即使超时也要调用 ``` ### 3.4 响应 Context 取消 ```go // ✅ 推荐:在长时间操作中检查 context func LongRunningTask(ctx context.Context) error { for { select { case <-ctx.Done(): return ctx.Err() // 返回 context.Canceled 或 context.DeadlineExceeded default: // 执行一小部分工作 if err := doChunk(); err != nil { return err } } } } ``` ### 3.5 区分取消原因 ```go // ✅ 根据 ctx.Err() 区分取消原因 if err := ctx.Err(); err != nil { switch { case errors.Is(err, context.Canceled): log.Println("operation was canceled") case errors.Is(err, context.DeadlineExceeded): log.Println("operation timed out") } return err } ``` --- ## 4. 接口设计 ### 4.1 接受接口,返回结构体 ```go // ❌ 不推荐:接受具体类型 func SaveUser(db *sql.DB, user User) error // ✅ 推荐:接受接口(解耦、易测试) type UserStore interface { Save(ctx context.Context, user User) error } func SaveUser(store UserStore, user User) error // ❌ 不推荐:返回接口 func NewUserService() UserServiceInterface // ✅ 推荐:返回具体类型 func NewUserService(store UserStore) *UserService ``` ### 4.2 在消费者处定义接口 ```go // ❌ 不推荐:在实现包中定义接口 // package database type Database interface { Query(ctx context.Context, query string) ([]Row, error) // ... 20 个方法 } // ✅ 推荐:在消费者包中定义所需的最小接口 // package userservice type UserQuerier interface { QueryUsers(ctx context.Context, filter Filter) ([]User, error) } ``` ### 4.3 保持接口小而专注 ```go // ❌ 不推荐:大而全的接口 type Repository interface { GetUser(id int) (*User, error) CreateUser(u *User) error UpdateUser(u *User) error DeleteUser(id int) error GetOrder(id int) (*Order, error) CreateOrder(o *Order) error // ... 更多方法 } // ✅ 推荐:小而专注的接口 type UserReader interface { GetUser(ctx context.Context, id int) (*User, error) } type UserWriter interface { CreateUser(ctx context.Context, u *User) error UpdateUser(ctx context.Context, u *User) error } // 组合接口 type UserRepository interface { UserReader UserWriter } ``` ### 4.4 避免空接口滥用 ```go // ❌ 不推荐:过度使用 interface{} func Process(data interface{}) interface{} // ✅ 推荐:使用泛型(Go 1.18+) func Process[T any](data T) T // ✅ 推荐:定义具体接口 type Processor interface { Process() Result } ``` --- ## 5. 接收器类型选择 ### 5.1 使用指针接收器的情况 ```go // ✅ 需要修改接收器时 func (u *User) SetName(name string) { u.Name = name } // ✅ 接收器包含 sync.Mutex 等同步原语 type SafeCounter struct { mu sync.Mutex count int } func (c *SafeCounter) Inc() { c.mu.Lock() defer c.mu.Unlock() c.count++ } // ✅ 接收器是大型结构体(避免复制开销) type LargeStruct struct { Data [1024]byte // ... } func (l *LargeStruct) Process() { /* ... */ } ``` ### 5.2 使用值接收器的情况 ```go // ✅ 接收器是小型不可变结构体 type Point struct { X, Y float64 } func (p Point) Distance(other Point) float64 { return math.Sqrt(math.Pow(p.X-other.X, 2) + math.Pow(p.Y-other.Y, 2)) } // ✅ 接收器是基本类型的别名 type Counter int func (c Counter) String() string { return fmt.Sprintf("%d", c) } // ✅ 接收器是 map、func、chan(本身是引用类型) type StringSet map[string]struct{} func (s StringSet) Contains(key string) bool { _, ok := s[key] return ok } ``` ### 5.3 一致性原则 ```go // ❌ 不推荐:混合使用接收器类型 func (u User) GetName() string // 值接收器 func (u *User) SetName(n string) // 指针接收器 // ✅ 推荐:如果有任何方法需要指针接收器,全部使用指针 func (u *User) GetName() string { return u.Name } func (u *User) SetName(n string) { u.Name = n } ``` --- ## 6. 性能优化 ### 6.1 预分配 Slice ```go // ❌ 不推荐:动态增长 var result []int for i := 0; i < 10000; i++ { result = append(result, i) // 多次分配和复制 } // ✅ 推荐:预分配已知大小 result := make([]int, 0, 10000) for i := 0; i < 10000; i++ { result = append(result, i) } // ✅ 或者直接初始化 result := make([]int, 10000) for i := 0; i < 10000; i++ { result[i] = i } ``` ### 6.2 避免不必要的堆分配 ```go // ❌ 可能逃逸到堆 func NewUser() *User { return &User{} // 逃逸到堆 } // ✅ 考虑返回值(如果适用) func NewUser() User { return User{} // 可能在栈上分配 } // 检查逃逸分析 // go build -gcflags '-m -m' ./... ``` ### 6.3 使用 sync.Pool 复用对象 ```go // ✅ 推荐:高频创建/销毁的对象使用 sync.Pool var bufferPool = sync.Pool{ New: func() interface{} { return new(bytes.Buffer) }, } func ProcessData(data []byte) string { buf := bufferPool.Get().(*bytes.Buffer) defer func() { buf.Reset() bufferPool.Put(buf) }() buf.Write(data) return buf.String() } ``` ### 6.4 字符串拼接优化 ```go // ❌ 不推荐:循环中使用 + 拼接 var result string for _, s := range strings { result += s // 每次创建新字符串 } // ✅ 推荐:使用 strings.Builder var builder strings.Builder for _, s := range strings { builder.WriteString(s) } result := builder.String() // ✅ 或者使用 strings.Join result := strings.Join(strings, "") ``` ### 6.5 避免 interface{} 转换开销 ```go // ❌ 热路径中使用 interface{} func process(data interface{}) { switch v := data.(type) { // 类型断言有开销 case int: // ... } } // ✅ 热路径中使用泛型或具体类型 func process[T int | int64 | float64](data T) { // 编译时确定类型,无运行时开销 } ``` --- ## 7. 测试 ### 7.1 表驱动测试 ```go // ✅ 推荐:表驱动测试 func TestAdd(t *testing.T) { tests := []struct { name string a, b int expected int }{ {"positive numbers", 1, 2, 3}, {"with zero", 0, 5, 5}, {"negative numbers", -1, -2, -3}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := Add(tt.a, tt.b) if result != tt.expected { t.Errorf("Add(%d, %d) = %d; want %d", tt.a, tt.b, result, tt.expected) } }) } } ``` ### 7.2 并行测试 ```go // ✅ 推荐:独立测试用例并行执行 func TestParallel(t *testing.T) { tests := []struct { name string input string }{ {"test1", "input1"}, {"test2", "input2"}, } for _, tt := range tests { tt := tt // Go < 1.22 需要复制 t.Run(tt.name, func(t *testing.T) { t.Parallel() // 标记为可并行 result := Process(tt.input) // assertions... }) } } ``` ### 7.3 使用接口进行 Mock ```go // ✅ 定义接口以便测试 type EmailSender interface { Send(to, subject, body string) error } // 生产实现 type SMTPSender struct { /* ... */ } // 测试 Mock type MockEmailSender struct { SendFunc func(to, subject, body string) error } func (m *MockEmailSender) Send(to, subject, body string) error { return m.SendFunc(to, subject, body) } func TestUserRegistration(t *testing.T) { mock := &MockEmailSender{ SendFunc: func(to, subject, body string) error { if to != "test@example.com" { t.Errorf("unexpected recipient: %s", to) } return nil }, } service := NewUserService(mock) // test... } ``` ### 7.4 测试辅助函数 ```go // ✅ 使用 t.Helper() 标记辅助函数 func assertEqual(t *testing.T, got, want interface{}) { t.Helper() // 错误报告时显示调用者位置 if got != want { t.Errorf("got %v, want %v", got, want) } } // ✅ 使用 t.Cleanup() 清理资源 func TestWithTempFile(t *testing.T) { f, err := os.CreateTemp("", "test") if err != nil { t.Fatal(err) } t.Cleanup(func() { os.Remove(f.Name()) }) // test... } ``` --- ## 8. 常见陷阱 ### 8.1 Nil Slice vs Empty Slice ```go var nilSlice []int // nil, len=0, cap=0 emptySlice := []int{} // not nil, len=0, cap=0 made := make([]int, 0) // not nil, len=0, cap=0 // ✅ JSON 编码差异 json.Marshal(nilSlice) // null json.Marshal(emptySlice) // [] // ✅ 推荐:需要空数组 JSON 时显式初始化 if slice == nil { slice = []int{} } ``` ### 8.2 Map 初始化 ```go // ❌ 错误:未初始化的 map var m map[string]int m["key"] = 1 // panic: assignment to entry in nil map // ✅ 正确:使用 make 初始化 m := make(map[string]int) m["key"] = 1 // ✅ 或者使用字面量 m := map[string]int{} ``` ### 8.3 Defer 在循环中 ```go // ❌ 潜在问题:defer 在函数结束时才执行 func processFiles(files []string) error { for _, file := range files { f, err := os.Open(file) if err != nil { return err } defer f.Close() // 所有文件在函数结束时才关闭! // process... } return nil } // ✅ 正确:使用闭包或提取函数 func processFiles(files []string) error { for _, file := range files { if err := processFile(file); err != nil { return err } } return nil } func processFile(file string) error { f, err := os.Open(file) if err != nil { return err } defer f.Close() // process... return nil } ``` ### 8.4 Slice 底层数组共享 ```go // ❌ 潜在问题:切片共享底层数组 original := []int{1, 2, 3, 4, 5} slice := original[1:3] // [2, 3] slice[0] = 100 // 修改了 original! // original 变成 [1, 100, 3, 4, 5] // ✅ 正确:需要独立副本时显式复制 slice := make([]int, 2) copy(slice, original[1:3]) slice[0] = 100 // 不影响 original ``` ### 8.5 字符串子串内存泄漏 ```go // ❌ 潜在问题:子串持有整个底层数组 func getPrefix(s string) string { return s[:10] // 仍引用整个 s 的底层数组 } // ✅ 正确:创建独立副本(Go 1.18+) func getPrefix(s string) string { return strings.Clone(s[:10]) } // ✅ Go 1.18 之前 func getPrefix(s string) string { return string([]byte(s[:10])) } ``` ### 8.6 Interface Nil 陷阱 ```go // ❌ 陷阱:interface 的 nil 判断 type MyError struct{} func (e *MyError) Error() string { return "error" } func returnsError() error { var e *MyError = nil return e // 返回的 error 不是 nil! } func main() { err := returnsError() if err != nil { // true! interface{type: *MyError, value: nil} fmt.Println("error:", err) } } // ✅ 正确:显式返回 nil func returnsError() error { var e *MyError = nil if e == nil { return nil // 显式返回 nil } return e } ``` ### 8.7 Time 比较 ```go // ❌ 不推荐:直接使用 == 比较 time.Time if t1 == t2 { // 可能因为单调时钟差异而失败 // ... } // ✅ 推荐:使用 Equal 方法 if t1.Equal(t2) { // ... } // ✅ 比较时间范围 if t1.Before(t2) || t1.After(t2) { // ... } ``` --- ## 9. 代码组织 ### 9.1 包命名 ```go // ❌ 不推荐 package common // 过于宽泛 package utils // 过于宽泛 package helpers // 过于宽泛 package models // 按类型分组 // ✅ 推荐:按功能命名 package user // 用户相关功能 package order // 订单相关功能 package postgres // PostgreSQL 实现 ``` ### 9.2 避免循环依赖 ```go // ❌ 循环依赖 // package a imports package b // package b imports package a // ✅ 解决方案1:提取共享类型到独立包 // package types (共享类型) // package a imports types // package b imports types // ✅ 解决方案2:使用接口解耦 // package a 定义接口 // package b 实现接口 ``` ### 9.3 导出标识符规范 ```go // ✅ 只导出必要的标识符 type UserService struct { db *sql.DB // 私有 } func (s *UserService) GetUser(id int) (*User, error) // 公开 func (s *UserService) validate(u *User) error // 私有 // ✅ 内部包限制访问 // internal/database/... 只能被同项目代码导入 ``` --- ## 10. 工具与检查 ### 10.1 必须使用的工具 ```bash # 格式化(必须) gofmt -w . goimports -w . # 静态分析 go vet ./... # 竞态检测 go test -race ./... # 逃逸分析 go build -gcflags '-m -m' ./... ``` ### 10.2 推荐的 Linter ```bash # golangci-lint(集成多个 linter) golangci-lint run # 常用检查项 # - errcheck: 检查未处理的错误 # - gosec: 安全检查 # - ineffassign: 无效赋值 # - staticcheck: 静态分析 # - unused: 未使用的代码 ``` ### 10.3 Benchmark 测试 ```go // ✅ 性能基准测试 func BenchmarkProcess(b *testing.B) { data := prepareData() b.ResetTimer() // 重置计时器 for i := 0; i < b.N; i++ { Process(data) } } // 运行 benchmark // go test -bench=. -benchmem ./... ``` --- ## 参考资源 - [Effective Go](https://go.dev/doc/effective_go) - [Go Code Review Comments](https://go.dev/wiki/CodeReviewComments) - [Go Common Mistakes](https://go.dev/wiki/CommonMistakes) - [100 Go Mistakes](https://100go.co/) - [Go Proverbs](https://go-proverbs.github.io/) - [Uber Go Style Guide](https://github.com/uber-go/guide/blob/master/style.md)