一、代码风格
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、通用命名原则
所有命名必须遵循语义化原则,即通过名称能够清晰地了解其代表的含义。例如,变量名应能准确描述其存储的数据内容,函数名应能准确反映其功能。
避免使用无意义或模糊的缩写,除非是行业内广泛认可的缩写,如HTTP
、JSON
、ID
等。禁止自定义难以理解的缩写,如用usr
代替user
,用cfg
代替config
等。
2、包命名
包名统一采用全小写字母,且不包含下划线。包名应尽量简洁,使用单个单词来概括包的主要功能。例如,用于用户认证的包,应命名为auth
,而非authentication
;用于日志记录的包,应命名为log
,而非logging
。
包名需确保与标准库以及其他已使用的包名无冲突。若包的功能与标准库类似,可添加合适的前缀进行区分,如appio
表示应用层面的输入输出操作包,避免与标准库io
包冲突。
3、类型与函数命名
(1)接口命名
单方法接口的命名必须以 “er” 结尾,以体现其行为特征。例如,实现读取功能的接口应命名为Reader
,实现验证功能的接口应命名为Validator
。
多方法接口的命名需清晰体现其具体功能,避免使用模糊或无意义的命名。例如,用于处理用户相关业务逻辑的接口,应命名为UserService
,而非Userer
这种不明确的名称。
(2)函数命名
函数命名应简洁明了,避免冗长且重复的 “动词 + 名词” 组合。若包名已明确体现了函数的功能范畴,函数名可适当简化。例如,在auth
包中,实现用户登录功能的函数可直接命名为Login
,而非AuthLogin
。
函数名应准确反映其功能,使用祈使语气的动词开头,如GetUser
、CreateOrder
等,让阅读代码的人能直观地了解函数的作用。
4、变量与常量命名
(1)布尔变量
布尔变量的命名必须以is
、has
、can
、should
等前缀开头,以明确其逻辑含义。禁止使用否定形式命名,如应使用isValid
表示有效性,而不是isNotValid
;使用hasPermission
表示是否拥有权限,而不是permissionExists
这种可能引起歧义的命名。
(2)常量命名
对于枚举常量,采用 “分组前缀 + 状态名” 的命名方式,且前缀需与枚举类型紧密相关。例如,定义 HTTP 方法的枚举常量,应命名为HTTPMethodGet
、HTTPMethodPost
、HTTPMethodDelete
等,而不是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
}
// 业务逻辑实现
}
禁止函数返回[]T
与error
的简单组合。若函数需要返回切片且可能出现错误,应明确返回([]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
表示只读通道,以提升代码的可读性