请开启 JavaScript
Go开发规范-字节 – 老迟笔记

Go开发规范-字节

一、代码风格

1 代码格式化

(1)统一使用goimports工具对代码进行格式化处理。

该工具不仅能像gofmt一样格式化代码,还会自动处理导入包的排序与依赖清理。在提交代码前,务必运行goimports,以确保代码格式符合团队标准。

(2)单行代码长度严格限制在 120 个字符以内。若代码超出长度,按以下优先级进行换行:

优先在逗号后换行,如:

result := calculate(
    param1,
    param2,
    param3,
)

若逗号后换行不可行,则在运算符前换行,如:

longExpression := veryLongVariable1 +
    veryLongVariable2 -
    veryLongVariable3

对于函数调用的参数列表,若左括号后内容过长,可在左括号后换行并缩进一级,如:

result := complexFunctionCall(
    longParam1,
    longParam2,
    longParam3,
)
2、注释规则
(1)包注释

每个包都必须有清晰的注释说明。包注释不仅要阐述包的功能,还需注明其适用场景与不适用场景。例如:

// Package cache 提供高效的内存缓存机制,支持多种缓存策略,如LRU、LFU等。

// 适用场景:对热点数据频繁读取且数据更新频率较低的场景,如网站配置信息、商品静态信息等。

// 不适用场景:实时性要求极高,数据变化频繁的场景,如股票交易数据、即时聊天消息等。

package cache
(2)函数注释

对于导出函数,必须提供详细的注释。注释内容包括函数的功能描述、参数说明、返回值解释以及使用示例。对于复杂函数,示例尤为重要,能帮助其他开发者快速理解函数的使用方式。例如:

// GetFromCache 根据给定的键从缓存中获取对应的值。
// 参数key:用于在缓存中查找值的唯一键,类型为string。
// 返回值value:从缓存中获取到的值,若未找到则返回nil。
// 返回值ok:表示是否成功从缓存中获取到值,true表示成功,false表示未找到。
// 示例:
//  value, ok := cache.GetFromCache("user:123")
//  if ok {
//      fmt.Println("Value from cache:", value)
//  } else {
//      fmt.Println("Value not found in cache")
//  }
func GetFromCache(key string) (value interface{}, ok bool) {
    // 函数实现逻辑
}

非导出函数若逻辑复杂,同样需要注释说明其设计思路,帮助阅读代码的人理解函数的实现意图。

(3)注释书写规范

注释需使用完整的句子,语言表达清晰、准确,避免使用模糊或容易引起歧义的表述。同时,禁止在代码中使用 “TODO”“FIXME” 等临时性注释,若有未完成的任务或需要修复的问题,应通过项目管理工具(如 JIRA、飞书项目等)进行跟踪记录。

二、命名规范

1、通用命名原则

所有命名必须遵循语义化原则,即通过名称能够清晰地了解其代表的含义。例如,变量名应能准确描述其存储的数据内容,函数名应能准确反映其功能。

避免使用无意义或模糊的缩写,除非是行业内广泛认可的缩写,如HTTPJSONID等。禁止自定义难以理解的缩写,如用usr代替user,用cfg代替config等。

2、包命名

包名统一采用全小写字母,且不包含下划线。包名应尽量简洁,使用单个单词来概括包的主要功能。例如,用于用户认证的包,应命名为auth,而非authentication;用于日志记录的包,应命名为log,而非logging

包名需确保与标准库以及其他已使用的包名无冲突。若包的功能与标准库类似,可添加合适的前缀进行区分,如appio表示应用层面的输入输出操作包,避免与标准库io包冲突。

3、类型与函数命名
(1)接口命名

单方法接口的命名必须以 “er” 结尾,以体现其行为特征。例如,实现读取功能的接口应命名为Reader,实现验证功能的接口应命名为Validator

多方法接口的命名需清晰体现其具体功能,避免使用模糊或无意义的命名。例如,用于处理用户相关业务逻辑的接口,应命名为UserService,而非Userer这种不明确的名称。

(2)函数命名

函数命名应简洁明了,避免冗长且重复的 “动词 + 名词” 组合。若包名已明确体现了函数的功能范畴,函数名可适当简化。例如,在auth包中,实现用户登录功能的函数可直接命名为Login,而非AuthLogin

函数名应准确反映其功能,使用祈使语气的动词开头,如GetUserCreateOrder等,让阅读代码的人能直观地了解函数的作用。

4、变量与常量命名

(1)布尔变量

布尔变量的命名必须以ishascanshould等前缀开头,以明确其逻辑含义。禁止使用否定形式命名,如应使用isValid表示有效性,而不是isNotValid;使用hasPermission表示是否拥有权限,而不是permissionExists这种可能引起歧义的命名。

(2)常量命名

对于枚举常量,采用 “分组前缀 + 状态名” 的命名方式,且前缀需与枚举类型紧密相关。例如,定义 HTTP 方法的枚举常量,应命名为HTTPMethodGetHTTPMethodPostHTTPMethodDelete等,而不是GetMethod这种不规范的命名。

常量命名需全部使用大写字母,单词之间用下划线分隔,以增强可读性。例如,定义最大重试次数的常量,应命名为MAX_RETRIES

三、项目结构

1、基础结构布局

在 Go Modules 的基础上,字节跳动的项目结构遵循清晰的分层与模块化设计理念,结构如下:

project-name/ // 项目根目录,名称通常为项目的唯一标识,如github.com/bytedance/app-name

├── cmd/ # 可执行程序入口目录

│   └── api/ # 例如API服务的入口子目录

│       └── main.go # 该文件仅负责程序的初始化与启动逻辑,不包含具体业务代码

├── internal/ # 存放私有业务模块的目录,按业务领域进行划分

│   ├── user/ # 以用户业务领域为例

│   │   ├── domain/ # 领域模型与核心业务逻辑层

│   │   │   ├── model/ # 存放用户相关的领域实体,如User结构体定义

│   │   │   └── service/ # 实现用户领域的核心业务逻辑,如用户注册、登录逻辑

│   │   ├── infra/ # 基础设施适配层,处理与外部服务的交互

│   │   │   ├── repo/ # 数据访问层实现,如MySQL、MongoDB等数据库操作

│   │   │   └── client/ # 外部服务客户端,如调用第三方支付接口、短信服务接口等

│   │   └── api/ # 接口适配层,处理HTTP、gRPC等接口请求

│   └── common/ # 内部公共组件目录,存放仅在项目内部使用的通用功能代码

│       ├── validator/ #参数校验相关的通用组件

│       └── errno/ # 统一的业务错误码定义与处理组件

├── pkg/ # 跨项目公共组件目录,存放可复用的工具类库等

│   ├── retry/ # 例如重试机制的通用实现包

│   └── metrics/ # 监控指标收集与上报的通用包

├── config/ # 配置文件目录,按不同环境(如dev、test、prod)区分配置文件

├── deploy/ # 部署相关配置目录,包含Dockerfile、Kubernetes配置文件等

├── docs/ # 项目文档目录,包括API文档、技术架构文档、开发指南等

├── test/ # 测试用例目录,包含集成测试、性能测试等测试代码

├── go.mod # Go语言依赖管理文件

└── Makefile # 项目构建脚本文件,用于统一构建、测试、部署等操作
2、目录职责详解
(1)internal/domain/

此目录专注于存放领域核心逻辑代码,与外部基础设施实现完全解耦。领域模型和业务规则在此定义和实现,确保业务逻辑的独立性和可测试性。例如,在电商项目中,订单的创建、修改、查询等核心业务逻辑应在 internal/order/domain/service 目录下实现,而不涉及具体的数据库操作或外部服务调用细节。

(2)internal/infra/

负责将领域层的抽象接口与实际的外部依赖(如数据库、第三方服务)进行适配。遵循依赖倒置原则,领域层依赖抽象接口,而infra层实现这些接口。例如,在internal/user/infra/repo目录下实现 UserRepository 接口,通过 SQL 语句或 ORM 框架完成对用户数据在数据库中的存储和查询操作;在 internal/user/infra/client 目录下实现与第三方短信服务的对接,发送验证码等功能。

(3)pkg/

该目录仅用于存放无业务依赖、高度可复用的工具组件。组件应具备通用性,不涉及任何具体项目的业务逻辑,以避免不同项目间的耦合。例如,pkg/retry包提供通用的重试机制,可应用于不同项目中的网络请求、数据库操作等可能失败的场景;pkg/metrics包提供统一的监控指标收集和上报接口,方便不同项目接入监控系统。

(4)Makefile

项目必须包含 Makefile,且其中需定义标准化的命令,如build用于构建项目,test用于执行测试用例,lint用于代码检查,clean用于清理构建产物等。通过Makefile,团队成员能够统一项目的构建、测试等流程,提高开发效率和项目的可维护性。

四、语法与编码实践

1、变量与常量

(1)变量声明

在局部作用域内,禁止使用 var 关键字声明单个变量,应统一使用短变量声明:=。例如,应使用name := "John",而不是var name string = "John"

对于多个相关变量的声明,若逻辑上紧密关联,可合并声明以提升代码简洁性。例如,name, age := "Alice", 20。但要避免将不相关的变量进行合并声明,以免降低代码可读性。

(2)常量定义

数值常量在定义时必须明确指定类型,以防止隐式类型转换带来的潜在问题。例如,const MaxRetries int = 3,而不是const MaxRetries = 3这种未指定类型的定义方式。

2、函数与方法

(1)函数设计

函数的参数数量原则上不超过 3 个。若参数较多,必须将其封装为结构体,并在结构体内部实现参数校验逻辑。例如:

type OrderQuery struct {​
    UserID string​
    Status string​
    Page   int​
    Limit  int​
}​

func (oq OrderQuery) Validate() error {​
    if oq.UserID == "" {​
        return fmt.Errorf("user ID cannot be empty")​
    }​
    if oq.Page < 1 {​
        return fmt.Errorf("page number must be greater than 0")​
    }​
    if oq.Limit < 1 {​
        return fmt.Errorf("limit number must be greater than 0")​
    }​
    return nil​
}

func ListOrders(query OrderQuery) ([]*Order, error) {​
    if err := query.Validate(); err != nil {​
        return nil, err​
    }​
    // 业务逻辑实现​
}

禁止函数返回[]Terror的简单组合。若函数需要返回切片且可能出现错误,应明确返回([]T, error)形式,并在注释中说明在错误情况下切片的状态(通常为空切片而非nil)。例如:

// GetUsers 获取用户列表,若发生错误,返回空切片和错误信息。​
// 错误情况下,返回的空切片表示未成功获取到任何用户数据。​
func GetUsers() ([]*User, error) {​
    // 函数实现逻辑​
}

(2)方法接收者

方法接收者原则上统一使用指针接收者,以避免在方法调用时发生值拷贝带来的性能开销。但对于不可变的小值类型,如time.Duration,可使用值接收者。

接收者的命名必须为单个字母,且具有一定的语义代表性。例如,对于Order结构体的方法,接收者命名为o *Order;对于User结构体的方法,接收者命名为u *User。禁止使用长命名作为接收者名称,以保持代码的简洁性和可读性。

3、错误处理
(1)错误封装

在 Go 1.13 及以上版本中,必须使用fmt.Errorf("%w", err)的方式对原始错误进行包装,确保错误信息的完整性,禁止丢弃原始错误信息。例如:

func CreateUser(user *User) error {
    if user.Name == "" {
        return fmt.Errorf("user name cannot be empty")
    }
    err := db.Save(user)
    if err != nil {
        return fmt.Errorf("save user to database: %w", err)
    }
    return nil
}

对于业务错误,应定义自定义错误类型,并实现 error 接口。自定义错误类型需包含 Code (错误码)和 Message(错误信息)字段,以便在项目的不同层级对错误进行统一处理,例如映射到 HTTP 状态码等。例如:

type BusinessError struct {​
    Code    int    `json:"code"`​
    Message string `json:"message"`​
}​
​
func (be *BusinessError) Error() string {​
    return fmt.Sprintf("code: %d, message: %s", be.Code, be.Message)​
}
(2)错误判断

使用 errors.Is 函数判断特定错误类型,例如errors.Is(err, sql.ErrNoRows),以确保错误判断的准确性和稳定性。

使用 errors.As 函数进行自定义错误类型的断言,方便在捕获到错误后进行针对性处理。例如:

var be *BusinessError
if errors.As(err, &be) {
    // 处理BusinessError类型的错误
    fmt.Println("Business error:", be.Message)
}

严格禁止直接通过比较错误字符串来判断错误类型,如err.Error() == "not found"。因为错误字符串可能会发生变化,导致错误判断失效。

(3)错误返回

错误信息中严禁包含敏感信息,如数据库密码、用户手机号、身份证号等,以保障系统的安全性。

在顶层函数(如 API 接口处理函数)中,必须对所有可能出现的错误进行处理,禁止直接返回未封装的原始错误。应将原始错误转换为用户友好的信息,例如返回 HTTP 状态码和相应的错误提示信息,提升用户体验和系统的稳定性。

4、并发编程

(1)goroutine 管理

必须使用 sync.WaitGroup 或 context.Context 来管理 goroutine 的生命周期,严禁使用 “裸 goroutine”,防止 goroutine 泄漏,导致资源浪费和潜在的程序崩溃风险。例如,使用 sync.WaitGroup 管理多个任务的并发执行:

func ProcessTasks(tasks []Task) error {
    var wg sync.WaitGroup
    errCh := make(chan error, len(tasks))
    for _, task := range tasks {
        wg.Add(1)
        go func(t Task) {
            defer wg.Done()
            if err := t.Execute(); err != nil {
                errCh <- err
            }
        }(task) // 传递任务副本,避免循环变量引用问题
    }
    // 等待所有goroutine完成
    go func() {
        wg.Wait()
        close(errCh)
    }()
    // 收集错误
    for err := range errCh {
        if err != nil {
            return err
        }
    }
    return nil
}

若使用 context.Context,应在适当的时机(如函数退出、超时等)取消上下文,以确保 goroutine 能及时停止运行,释放资源。

(2)通道使用

在定义通道时,应明确其用途和方向,使用chan<- T表示只写通道,<-chan T表示只读通道,以提升代码的可读性