命名在 Go 中不仅是风格偏好,更是接口契约的一部分:标识符首字母大写即为导出标识符,小写即为包级私有,该机制由编译器强制执行。标识符一经发布,即成为包对外接口的组成部分,后续更名的成本由所有调用方承担。因此,评审中遇到 GetName()、MAX_RETRIES 这类命名,应要求更名。
Go Proverbs 中的 "Clear is better than clever" 在命名上的依据是一个基本事实:调用点永远带有包名。读者是在 http.Client、json.Decoder 这类调用位置阅读标识符的,本文的大部分规则——避免与包名重复、New 与 NewTypeName 的选择——都可以由这一点推导出来。
从其他语言迁移到 Go 的开发者常带入原有习惯:常量用 ALL_CAPS,变量用 snake_case,getter 一律加 Get 前缀,工具函数集中放入 util。这类代码可以通过编译,但会在评审中暴露对 Go 惯例的不熟悉。
标识符
所有标识符应使用 MixedCaps,禁止使用下划线。 原因在于:Go 的导出机制依赖首字母大小写,工具链同样默认这一惯例。例外只有三处:测试子用例名、生成代码、cgo 互操作。
// ✅ 合规
MaxPacketSize
userCount
parseHTTPResponse
// ❌ 违规
MAX_PACKET_SIZE // C/Python 风格常量
max_packet_size // snake_case
kMaxBufferSize // 匈牙利命名
缩写大小写惯例:缩写要么全大写,要么全小写,不得混用。Url、HttpServer、userId 均不符合规范;导出标识符中的缩写整体大写,url 应写作 URL,grpc 应写作 GRPC。正确写法是 URL、HTTPServer、xmlParser、userID、newHTTPSURL。
标识符长度应与作用域成比例:几行循环内的 i 是合格的,包级标识符则应完整描述其含义。惯用单字母名称包括:i/j/k 循环下标、n 数量、v 值、k 键、r 为 io.Reader、w 为 io.Writer、b 字节缓冲、t 为 *testing.T;ctx 与 err 的拼法固定,不应另创拼法。
另有三条基础规则:
- 名字应描述内容,而非类型。
userSlice、countInt把实现细节编码进了名字,类型一变,名字就与实际不符;users、count描述的才是变量承载的内容。 - 同一概念在全库只应使用一个名称。
user不应交替写作account、person,userID不应写作uid或userId——命名一致才能保证可搜索性。 - 避免与包名重复。调用点已经带有包名,标识符再重复包名只会增加阅读成本:
http.Client而非http.HTTPClient,user.New()而非user.NewUser();在dbpool包内写type DBPool struct{},其中的DB是冗余的。
包名和文件名
包名应小写、使用单个词、采用单数形式,不含下划线与 MixedCaps。可参照标准库:net/url 而非 net/urls,go/token 而非 go/tokens;允许使用数字,oauth2、k8s 均为合法先例。
util、helper、common、base、model 这类包名不传达任何用途,还容易与常见导入名冲突,评审时应一律要求更名。当开发者想向 util 中添加函数时,通常说明该函数属于一个更具体的包。
目录名应与包名一致,并去掉连字符:user-service/ 对应 package userservice,rate-limit/ 对应 package ratelimit。几个特殊目录有工具链语义:cmd/ 下各子目录均为 package main,internal/ 限制可见性,testdata/ 与 vendor/ 被工具链特殊对待。
文件名应小写,单词间用下划线分隔:user_handler.go、http_client_test.go。这是下划线在 Go 文件命名中的正常用法。_test.go、_linux.go、_amd64.go 等后缀携带构建约束,不得随意使用。
import 别名仅在命名冲突时使用,且应使用能说明用途的短名:
// ✅ 冲突时才用别名,名字描述用途
import (
"crypto/rand"
mrand "math/rand"
)
// ❌ 无意义别名
import f "fmt"
无意义的别名会迫使读者推断其指向。
函数和方法
返回值的函数应使用名词,说明其返回内容;执行动作的应使用动词,说明其行为。UserName() 与 WriteFile() 的区别即在于此。在此基础上有几条具体规则。
值 getter 不加 Get 前缀,方法名就是字段名的大写形式。 这是 Go 与多数语言差异最大的命名约定,也是迁移过程中最常见的错误之一:
// ✅
func (u *User) Name() string { return u.name }
func (u *User) SetName(n string) { u.name = n }
// ❌
func (u *User) GetName() string { return u.name }
user.Name() 读起来直接,GetName() 中的 Get 是冗余。反之,昂贵或阻塞的操作应使用 Fetch、Compute,向调用方表明该操作存在显著开销。
返回 bool 的谓词是这条规则的例外,保留 Is/Has/Can 前缀:IsHealthy() bool 明确表示一次判断,不带前缀的 Healthy() bool 无法看出返回的是什么。标准库先例:reflect.Type.IsVariadic()、net.IP.IsLoopback()。注意,返回富类型时可以裸用名词,Healthy() (HealthStatus, error) 语义不同,是合法的。
构造函数:包内只有一个主类型时使用 New(),ring.New() 无歧义;存在多个可构造类型时使用 NewTypeName(),http 包类型较多,因此是 http.NewRequest、http.NewServeMux。判断标准仍是调用点:名字与包名组合后是否出现冗余重复。
接收者应是类型名的 1~2 字母缩写,并在该类型所有方法间保持一致:func (s *Server) Start() error 与 func (s *Server) Stop() error 共用同一个 s。不得使用 this 或 self:Go 方法出现频率高,短接收者可减少视觉干扰。
接口:单方法接口用"方法名 + -er 后缀"命名,Reader、Closer、Stringer 是标准库给出的模板;多方法接口用名词(Handler)或组合单方法接口(ReadWriteCloser)。实现惯用方法时必须严格匹配标准接口的签名——Read 必须匹配 io.Reader,不得另造 ReadData;字符串化方法命名为 String,而非 ToString。Read、Write、Close、String、Error、Len、ServeHTTP 各有对应接口,遵循这些签名即可获得生态兼容性。
接受格式串与可变参数的函数应以 f 结尾:Errorf、Wrapf、Logf。f 表明该函数支持 fmt 格式化语法;缺少该后缀时,WrapError(err, "...%d", n) 的签名无法体现格式化行为。
后缀本身也是行为信号:Must 表示失败即 panic,In 表示就地修改,WithContext 表示 context 变体。MustParse()、SortIn()、FetchWithContext() 的行为可从名字直接推断。
类型、常量与错误
结构体用 MixedCaps 名词命名。Struct、Object、Data 这类后缀不携带信息,Request、FileHeader 自带语义。
常量用 MixedCaps,名字应说明角色而不是取值。ALL_CAPS 在 Go 中不符合规范——大小写已被语言用于表达可见性,全大写意味着强制导出。此外,值可能变更而角色不变:DefaultPort 在端口改为 9090 后仍然正确,Port8080 则立即与实际值不符。规范写法是 const MaxRetries = 3、const defaultTimeout = 30 * time.Second。
枚举值应以类型名为前缀,并且必须保护零值。 前缀(StatusReady)避免跨类型冲突,调用点也更易读;零值保护防止一类隐蔽的静默错误——var s Status 的默认值是 0,若 0 对应 StatusReady,未初始化的值就无法与被显式赋值的状态区分,这是常见错误。
type Status int
// ✅ 零值是哨兵
const (
StatusUnknown Status = iota
StatusReady
StatusRunning
)
// ❌ 危险:var s Status 会静默等于 StatusReady
const (
StatusReady Status = iota
StatusRunning
)
哨兵错误变量用 Err 前缀,自定义错误类型用 Error 后缀:ErrNotFound、PathError。哨兵错误的字符串应带包名前缀,这样错误经多层包装后仍可识别来源,便于日志排查:errors.New("mypackage: not found");不带前缀的 "not found" 无法定位来源。
错误字符串应全小写——包括缩写——且不以标点结尾。 错误经常被拼接进上下文,如 fmt.Errorf("decoding config: %w", err);句中出现大写缩写会破坏一致性,"invalid message id" 是符合惯例的形式。
测试
测试函数命名为 Test 加被测对象名:TestParseToken、TestServer_Handle(方法测试)。下划线在标识符中少数合法场景之一就是测试子用例,例如 TestParseToken_InvalidInput。
表驱动用例的 name 应使用全小写描述性短语:"valid id"、"empty input",失败输出因此易于阅读。字段使用 input/expected 标注数据流向。测试辅助函数在失败时应直接 panic,并以 must 为前缀命名:mustLoadFixture()。
格式化交给机器
缩进、空格、导入排序,应一律交给 gofmt 与 goimports 处理。gofmt 的输出是唯一标准,评审中不应出现格式类意见;goimports 在此之上自动增删导入并按规范分组,要求更严格的团队可再加 gofumpt。部分命名问题机器也能检查,revive、errname、predeclared 等规则见静态检查规范。
行长没有硬性上限,但超过约 120 字符必须换行,并在语义边界断行;4 个以上参数应一个一行。 当参数多到需要刻意排版时,更合适的解法通常是合并为 options 结构体,而非排版调整。
// ✅ 每个参数独占一行
handleUsers(
w,
r,
serviceName,
cfg,
logger,
)
代码组织
错误与边界情况应最先处理(早返回),使主路径保持最低缩进。 读者按顺序检查 if err != nil 的守卫后,其余代码即为主路径:
// ✅ 早返回
func process(data []byte) (*Result, error) {
if len(data) == 0 {
return nil, errors.New("empty data")
}
parsed, err := parse(data)
if err != nil {
return nil, fmt.Errorf("parsing: %w", err)
}
return transform(parsed), nil
}
if 分支以 return/break/continue 结尾时,else 必须删除;简单赋值应写成"先默认值、后覆盖"。同一变量的多重比较写成 switch 比 if-else 链清晰,默认行为也更易识别。
if 条件超过 3 个操作数时,应提取为命名布尔变量——连续的 || 运算符会把业务逻辑掩盖在符号之下。例外:最后一个条件涉及昂贵计算时保持内联,以利用短路求值。
// ✅ 自解释
isAdmin := user.Role == RoleAdmin
isOwner := resource.OwnerID == user.ID
if isAdmin || isOwner || expensiveCheck(user, resource) {
allow()
}
仅在 if 中使用的变量,作用域应收进 if 语句:if err := validate(input); err != nil { return err }——err 的生命周期与用途一致。
非零值用 := 声明,零值初始化用 var:var count int 表示"从零开始、稍后赋值",name := "default" 表示"声明即有值"。
slice 与 map 必须显式初始化,不得向调用方返回 nil。写入 nil map 会 panic;nil slice 序列化为 JSON null,而 []User{} 序列化为 [],对 API 消费者而言二者行为不同。容量已知时应预分配 make([]T, 0, n);对常见的 10 条量级数据,不应预分配 1000。
复合字面量必须写字段名。 位置写法在类型增删字段时会静默错位,&http.Server{Addr: ":8080"} 不会产生歧义。
函数应保持短小专注,参数不超过 4 个,context.Context 必须是第一个参数,参数顺序固定为 ctx → 输入 → 输出目的地;超出时应合并为 options 结构体。
注释
打破规范时,应在代码旁留一行注释说明原因。每条规范都允许例外,但例外必须有依据——该注释面向评审者与后续维护者。doc comment 怎么写、何时该写,见文档写作规范,本文不展开。
速查清单
评审时可作为参照:
- 标识符应全部使用 MixedCaps;下划线仅允许出现在文件名和测试子用例中
- 缩写不得混写:
URL、userID、HTTPServer,不使用userId - 名字长短应与作用域成比例;描述内容,不描述类型
- 同一概念全库同名;名字不与包名重复
- 包名应小写、单词、单数;不使用
util/helper/common/base/model - 目录名应与包名一致,并去掉连字符:
rate-limit/→ratelimit - import 别名仅在冲突时使用,且名称应说明用途
- getter 不带
Get;bool 谓词保留Is/Has/Can;昂贵操作命名为Fetch/Compute - 单一主类型用
New(),多个类型用NewTypeName() - 接收者用一两个字母,全部方法保持一致;不使用
this/self - 接口:单方法用"方法名 +
-er",多方法用名词;String而非ToString - 格式串函数以
f结尾;Must表示 panic,In表示就地修改,WithContext表示携带 ctx - 常量描述角色而非取值;枚举加类型前缀,零值用哨兵
- 哨兵错误用
Err前缀、字符串带包名;错误串全小写,结尾不带标点 - 测试名
Test+ 被测对象;表驱动name全小写;helper 加must前缀 - 格式以 gofmt/goimports 为准;超过 120 字符或 4 个以上参数即换行
- 先处理错误再写主路径;if 以 return 结尾即删除 else;3 个以上条件提取布尔变量
var表示零值,:=表示有值;slice/map 显式初始化;字面量写明字段名- ctx 永远是第一个参数;参数超过 4 个合并为 options
- 允许例外,但必须在代码旁留一行注释说明原因
包与目录的组织不止于命名,更完整的目录结构话题见Go 项目布局。