Golang惯用代码实践 - 如何应对过度热情的开发者?

Golang惯用代码实践 - 如何应对过度热情的开发者? 我最近有一位新同事审查我的代码,他没有发现错误,而是提出了以下观点:

  • “始终使用 range 循环,永远不要使用类似 C 语言风格的循环——这样更符合 Go 语言的习惯用法。”
  • “每一行(每一行)代码上方都应该有注释来解释其作用——这样人们可以选择只阅读注释而不看代码。”
  • “始终使用命名的返回值,永远不要使用未命名的——这样更符合 Go 语言的习惯用法。”
  • “在单元测试中始终使用 deepEqual,永远不要比较字符串值——当问及原因时,我只得到另一个‘始终使用 deepEqual’的回答。”
  • “永远不要使用基础函数加包装器的形式,直接复制代码——因为这更符合 Go 语言的习惯用法。”
  • “永远不要将一个字符串拆分成两行——这样更容易阅读。”
  • “永远不要返回接口或指针——这样更符合 Go 语言的习惯用法。”
  • “永远不要返回 3 个值,只返回结果和错误——这样更符合 Go 语言的习惯用法。”
  • “为每一个返回的错误始终创建自己的错误实现(结构体)——这样更符合 Go 语言的习惯用法。”
  • “合并请求太大了,不要一次推送半天的工作量。”

这已经失控了,“符合习惯用法”这个词现在被用来证明任何事情,无论其是否有用。当我提出“符合习惯用法”本身不是价值,可维护性才是时,我看到这个人变得很恼火。而且,代码审查不再是为了发现错误,而是为了寻找新的(每天都有前一天没指出的新东西)让代码“更符合习惯用法”的方法。

我该如何与这个人沟通?此外,我感觉这阻碍了我自己的进步,很想知道是否有人有类似的经历。同样对于他来说,花在追求“符合习惯用法”代码上的时间和精力就无法用在其他地方了,追求“符合习惯用法”的代码真的是一项好的投资吗?


更多关于Golang惯用代码实践 - 如何应对过度热情的开发者?的实战教程也可以访问 https://www.itying.com/category-94-b0.html

3 回复

在下次回顾会议中,讨论你的团队相较于工作的其他方面,对地道代码的重视程度。

更多关于Golang惯用代码实践 - 如何应对过度热情的开发者?的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


首先,我建议不要过分关注他的年龄和/或经验,因为这完全与当前讨论的问题无关(你的情况也是如此)。然后,我会专注于最棘手的问题;罗列一堆分歧点无助于建立任何富有成效的对话。

这是一个典型的代码审查文化问题。让我们逐一分析这些观点,并提供Go语言的实际惯用实践。

1. 关于range循环

实际惯用做法:range循环确实是Go的惯用方式,但C风格循环在某些场景下仍然有用。

// 惯用方式 - 遍历切片
for i, v := range items {
    fmt.Printf("index: %d, value: %v\n", i, v)
}

// 但C风格循环在需要控制步长时仍然有效
for i := 0; i < len(items); i += 2 {
    fmt.Printf("item: %v\n", items[i])
}

2. 关于注释

实际惯用做法:Go强调代码自文档化,只在必要时添加注释。

// 不好的做法 - 过度注释
// 增加计数器
counter++

// 好的做法 - 有意义的注释
// 跳过已处理的记录,避免重复计算
if record.Processed {
    continue
}

3. 关于命名返回值

实际惯用做法:命名返回值应仅在提高可读性时使用。

// 未命名返回值 - 简单函数
func Add(a, b int) int {
    return a + b
}

// 命名返回值 - 提高复杂函数的可读性
func ParseConfig(filePath string) (config Config, err error) {
    // 错误处理中可以直接返回
    if filePath == "" {
        err = errors.New("file path is empty")
        return
    }
    // 处理逻辑
    return config, nil
}

4. 关于测试中的deepEqual

实际惯用做法:根据测试需求选择适当的比较方法。

func TestUserCreation(t *testing.T) {
    got := CreateUser("john", "john@example.com")
    want := &User{Name: "john", Email: "john@example.com"}
    
    // 比较特定字段
    if got.Name != want.Name {
        t.Errorf("got name %q, want %q", got.Name, want.Name)
    }
    
    // 或使用cmp.Diff进行详细比较
    if diff := cmp.Diff(want, got); diff != "" {
        t.Errorf("CreateUser() mismatch (-want +got):\n%s", diff)
    }
}

5. 关于代码复用

实际惯用做法:Go鼓励适度的代码复用。

// 好的做法 - 提取通用逻辑
func validateEmail(email string) error {
    if !strings.Contains(email, "@") {
        return errors.New("invalid email format")
    }
    return nil
}

// 在多个地方复用
func CreateUser(email string) error {
    if err := validateEmail(email); err != nil {
        return err
    }
    // 创建用户逻辑
    return nil
}

6. 关于字符串换行

实际惯用做法:长字符串可以换行以提高可读性。

// 允许的换行方式
msg := "这是一个非常长的字符串,为了代码可读性," +
    "我们可以将它分成多行书写,这是Go语言允许的。"

// 或者使用反引号
sqlQuery := `SELECT id, name, email 
FROM users 
WHERE active = true 
ORDER BY created_at DESC`

7. 关于返回接口或指针

实际惯用做法:根据具体需求决定。

// 返回结构体指针 - 当需要修改或避免复制时
func NewUser() *User {
    return &User{}
}

// 返回接口 - 当需要抽象实现时
type Repository interface {
    Find(id int) (User, error)
}

func NewUserRepository() Repository {
    return &userRepository{}
}

8. 关于返回值数量

实际惯用做法:Go支持多返回值,这是其特性之一。

// 三个返回值是常见的模式
func ParseCoordinates(input string) (lat, lon float64, err error) {
    parts := strings.Split(input, ",")
    if len(parts) != 2 {
        return 0, 0, errors.New("invalid format")
    }
    // 解析逻辑
    return lat, lon, nil
}

9. 关于错误处理

实际惯用做法:使用标准错误或自定义错误类型,但不必为每个错误创建结构体。

// 标准错误
var ErrNotFound = errors.New("not found")

// 自定义错误类型 - 当需要携带额外信息时
type ValidationError struct {
    Field   string
    Message string
}

func (e *ValidationError) Error() string {
    return fmt.Sprintf("%s: %s", e.Field, e.Message)
}

沟通建议

在代码审查中,可以引用Go官方文档和社区共识:

  1. 引用Effective Gohttps://golang.org/doc/effective_go
  2. 引用Go Code Review Commentshttps://github.com/golang/go/wiki/CodeReviewComments
  3. 关注可维护性指标:圈复杂度、测试覆盖率、代码重复率
// 展示平衡的做法
// 这个函数展示了Go的惯用写法:
// 1. 清晰的错误处理
// 2. 适当的注释
// 3. 合理的返回值
func ProcessItems(items []Item) (processed int, err error) {
    if len(items) == 0 {
        return 0, errors.New("no items to process")
    }
    
    for _, item := range items {
        if err := validate(item); err != nil {
            // 记录验证失败但不停止处理
            log.Printf("validation failed: %v", err)
            continue
        }
        process(item)
        processed++
    }
    
    return processed, nil
}

代码审查应该关注代码的正确性、可维护性和性能,而不是对"惯用写法"的教条式应用。Go语言的哲学是简单、明确和实用,过度追求形式化的"惯用写法"反而违背了这一哲学。

回到顶部