golang中的代码美学

“写代码最重要的是什么?是执行效率?是资源优化?不,是可读性。” -- 出自Robert C. Martin的《代码整洁之道》(Clean Code)一书中

1.前言

最近学习了一些代码优化、提高代码可读性相关的书籍、文档,结合个人的校验,分享此文档给大家。

写出高质量的代码不仅可以提高代码的可维护性和可读性,还可以降低错误发生的概率,减少后期修复的成本。

2.在代码中取名

  • 变量名不要用缩写

  • 尽量能通过名字来知道方法是用于做什么的

  • 像变量添加单位,除非从类型中可得知(数组切片等)

  • 少用utils

3.组合优于继承

“组合优于继承”是一种面向对象的设计原则,它鼓励通过组合(将其他类型作为字段嵌入)来构建复杂功能,而不是通过继承(从基类派生子类)来建立类型的层次关系。

  • 继承 是一种 “is-a” 关系(例如,Dog 是一个 Animal)。它强调代码的垂直扩展,子类自动获得父类的属性和方法,但容易导致脆弱的基类问题和复杂的层次结构。

  • 组合 是一种 “has-a” 或 “uses-a” 关系(例如,Car 有一个 Engine)。它强调代码的水平组装,通过将小型的、独立的单元组合起来,构建更复杂的类型。这带来了更好的灵活性和更低的耦合度。

Go语言从设计上就摒弃了“类”和“继承”的概念,强制使用组合,这是Go语言简洁性和强大并发能力之外的另一大亮点。

//传统继承思维(伪代码):
class Event {
    string ID
    DateTime Timestamp
}

class UserLoginEvent extends Event { // UserLoginEvent 是一个 Event
    string Username
    string IPAddress
}
//Go的组合思维:
type Event struct {
    ID        string
    Timestamp time.Time
}

type UserInfo struct {
    Username string
    IPAddress string
}

// 通过组合构建复杂类型
type UserLoginEvent struct {
    Event    // 嵌入 Event,UserLoginEvent "拥有" 一个 Event
    UserInfo // 嵌入 UserInfo,UserLoginEvent "拥有" 一个 UserInfo
    // 还可以直接添加自己的字段
    LoginMethod string
}

4.代码文档和注释的区别

Code Documentation

How code is used

Code Comments

How code works

代码文档和注释的不同之处在于:代码文档主要是解释代码用法,代码注释主要是解释代码原理\背景。

有像swagger这样的工具会直接从代码文件生成文档,所以代码文档能随着代码一起变化。

在文档中说明类和API的含义是很有帮助的:

  • 利于API的使用方能明白接口的功能、参数的枚举值、返回的示例等

  • 利于后续的开发维护。

所以我们的API文件应该遵守:

  • 每个参数都应加上注释,用于swagger生成代码文档。

  • 状态等字段需要加上枚举值注释

  • 代码需要格式化 goctl api format -dir api/xx

  • api请求参数的必填与非必填应在api文件里设置,而不是在代码里判断

5.注释的使用

很多人在改代码的时候常忘记更新注释

我们有工具来防止代码出现bug,比如测试、编译器检查单元测试用例,但是注释就没有这样的系统工具。所以注释有时并不可信,"注释会说谎,但是代码不会",所以要理解一段代码的含义,看代码就是了。

注释的正确用法:

  • 不应用注释来说明细节,而是用来说明为什么这么做

  • 一些业务的逻辑,可以用注释说明背景

  • 用有意义的命名代替注释

// 糟糕的代码 过多无用的注释
// Process user data and send notification
func p(u *User, n string) error {
    // Validate input
    if u == nil || u.Email == "" {
        return errors.New("invalid user")
    }
    
    // Check if user is active
    if !u.Active {
        return errors.New("user inactive")
    }
    
    // Send notification
    err := s.n.Send(u.Email, n)
    if err != nil {
        return err
    }
    
    return nil
}
// 优雅的代码
func SendNotificationToUser(user *User, message string) error {
    if err := validateUser(user); err != nil {
        return fmt.Errorf("validation failed: %w", err)
    }
    
    if !user.IsActive() {
        return ErrUserInactive
    }
    
    return user.SendNotification(message)
}

func validateUser(user *User) error {
    if user == nil || user.Email == "" {
        return ErrInvalidUser
    }
    return nil
}

6.减少嵌套,让代码扁平化

减少嵌套的方法:

  • 提取函数 (Extraction)

    • 将复杂条件提取为有意义的函数

    • 错误聚合 - 使用工具函数处理多个验证

  • 反转条件 (Inversion)

    • 先处理所有错误情况和边界条件

    • 遇到错误立即返回,不继续嵌套

    • 在循环中跳过不符合条件的项

//糟糕的代码
func FindActiveUsersWithPendingOrders(users []User) []User {
    var result []User
    for i := 0; i < len(users); i++ {
        if users[i].IsActive() {
            orders, err := orderRepo.FindByUserID(users[i].ID)
            if err == nil {
                hasPending := false
                for j := 0; j < len(orders); j++ {
                    if orders[j].Status == OrderStatusPending {
                        hasPending = true
                        break
                    }
                }
                if hasPending {
                    result = append(result, users[i])
                }
            }
        }
    }
    return result
}
//优雅示例 扁平的循环逻辑
func FindActiveUsersWithPendingOrders(users []User) []User {
    var result []User
    
    for _, user := range users {
        //反转条件 
        if !user.IsActive() {
            continue // 跳过不活跃用户
        }
        
        if user.HasPendingOrders() {
            result = append(result, user)
        }
    }
    
    return result
}

// 提取函数
func (u *User) HasPendingOrders() bool {
    orders, err := orderRepo.FindByUserID(u.ID)
    if err != nil {
        return false
    }
    
    for _, order := range orders {
        if order.IsPending() {
            return true
        }
    }
    
    return false
}

func (o *Order) IsPending() bool {
    return o.Status == OrderStatusPending
}

7.常见的设计模式

函数式选项模式(建造者模式变体)

type Server struct {
    host        string
    port        int
    maxConn     int
    timeout     time.Duration
    middleware  []Middleware
}

// 选项函数类型
type ServerOption func(*Server)

// 各种选项函数
func WithHost(host string) ServerOption {
    return func(s *Server) {
        s.host = host
    }
}

func WithPort(port int) ServerOption {
    return func(s *Server) {
        s.port = port
    }
}

func WithMaxConnections(max int) ServerOption {
    return func(s *Server) {
        s.maxConn = max
    }
}

func WithTimeout(timeout time.Duration) ServerOption {
    return func(s *Server) {
        s.timeout = timeout
    }
}

func WithMiddleware(mw ...Middleware) ServerOption {
    return func(s *Server) {
        s.middleware = append(s.middleware, mw...)
    }
}

// 构造函数
func NewServer(opts ...ServerOption) (*Server, error) {
    // 默认配置
    server := &Server{
        host:    "localhost",
        port:    8080,
        maxConn: 100,
        timeout: 30 * time.Second,
    }
    
    // 应用所有选项
    for _, opt := range opts {
        opt(server)
    }
    
    // 验证配置
    if err := server.validate(); err != nil {
        return nil, err
    }
    
    return server, nil
}

func (s *Server) validate() error {
    if s.port < 1 || s.port > 65535 {
        return fmt.Errorf("invalid port: %d", s.port)
    }
    return nil
}

// 使用示例
func main() {
    // 简单的服务器
    basicServer, err := NewServer(
        WithHost("localhost"),
        WithPort(3000),
    )
    
    // 复杂的生产服务器
    productionServer, err := NewServer(
        WithHost("api.company.com"),
        WithPort(8443),
        WithMaxConnections(5000),
        WithTimeout(2 * time.Minute),
        WithMiddleware(
            LoggingMiddleware,
            AuthMiddleware,
            RateLimitMiddleware,
        ),
    )
}

go的函数式选项模式有以下优点:

  1. 配置灵活性 - 可以轻松创建不同配置的对象

  2. 参数验证 - 在构建过程中进行参数验证

  3. 不可变对象 - 一旦构建完成,对象就是不可变的

  4. 流畅接口 - 链式调用让代码更易读

  5. 隐藏复杂性 - 客户端不需要了解对象的复杂构建过程

推荐使用函数式选项模式,因为它最符合Go语言的惯用法,提供了最好的灵活性和可读性。

装饰器模式

// 普通函数的时间装饰器
func MeasureExecutionTime(fn func()) func() {
    return func() {
        start := time.Now()
        fn()
        duration := time.Since(start)
        fmt.Printf("函数执行时间: %v\n", duration)
    }
}

// 使用示例
func main() {
    // 原始函数
    processData := func() {
        time.Sleep(1 * time.Second)
        fmt.Println("数据处理完成")
    }

    // 装饰后的函数
    timedFunction := MeasureExecutionTime(processData)
    
    fmt.Println("执行装饰后的函数:")
    timedFunction()
}
posted @ 2026-06-08 17:40  chenqi1231  阅读(8)  评论(0)    收藏  举报