Contents

命名是编程中最难的事:从变量到模块的命名实战

为什么命名这么难

Phil Karlton(Netscape 首席工程师)有一句名言:

计算机科学只有两件难事:缓存失效和命名。

缓存失效的难度你大概有体会——但命名?不就是给变量起个名字吗?

看这行代码:

1
d := t2.Sub(t1).Milliseconds()

你知道 d 是什么吗?延迟?持续时间?距离?不读上下文根本猜不到。再看看这个函数调用:

1
result := processData(data, true, 2, false)

processData 做了什么?true 控制什么?2 是什么含义?false 又是干嘛的?这类代码在每个项目里都存在,每次维护都要当考古学家。

命名的本质是在有限的字符里压缩最多的语义。好名字让代码自文档化,坏名字让代码变成密码本。下面从四个层次拆解命名这件事。

第一层:变量命名——精确胜过简短

变量命名最常见的错误是过度缩写。早期编程语言有变量名长度限制,缩写是不得已。现在没有这个限制,但缩写习惯留了下来。

1
2
3
4
5
6
7
8
9
// 坏名字:猜不出含义
var usrMgr *Client
var d time.Duration
var buf []byte

// 好名字:语义自解释
var userManager *Client
var requestTimeout time.Duration
var pendingResponses []byte

但精确不等于啰嗦。theUserManageraPendingResponseBuffer 这样的命名就是矫枉过正——定冠词和冗余后缀不增加任何信息。

核心原则:名字的长度应与作用域成正比。

1
2
3
4
5
6
7
8
// 循环内局部变量,一两个字母完全合理
for i, v := range items {
    fmt.Printf("%d: %v\n", i, v)
}

// 跨函数/包的变量,必须语义完整
var maxRetryAttempts = 3  // ✅ 好
var max = 3               // ❌ 作用域大但名字太短

一个容易忽略的细节:布尔变量应该能直接用于 if 语句且读起来通顺。

1
2
3
4
5
6
7
8
9
// ✅ 读起来像自然语言
if isActive { ... }
if hasPermission { ... }
if canRetry { ... }

// ❌ 需要绕弯才能理解
if flag { ... }       // 什么 flag?
if status { ... }     // status 本身不是布尔值
if check { ... }      // check 是动词还是名词?

技巧:布尔变量用 is/has/can/should 前缀,让 if 语句读起来像一个完整句子。

第二层:函数命名——动词驱动,说清楚做什么

函数名应该是一个动词短语,回答"做什么"而不是"是什么"。

1
2
3
4
5
6
7
// ❌ 名词命名,看不出是函数还是变量
func userList() []User { ... }
func orderCount() int { ... }

// ✅ 动词开头,意图清晰
func fetchUsers() []User { ... }
func countPendingOrders() int { ... }

Go 礄言约定 receiver 方法名可以短一些,因为已经有类型上下文。但导出函数必须有完整语义。

1
2
3
4
5
6
7
// ✅ receiver 方法可以简洁
func (u *User) Save() error { ... }
func (u *User) IsActive() bool { ... }

// ✅ 包级函数需要完整描述
func SaveUser(u *User) error { ... }       // 包名不提供语义时
func ValidateEmailFormat(email string) error { ... }

一个实用的模式:用"检查-动作"命名对来替代注释

1
2
3
4
5
6
7
// ❌ 需要注释解释
// 检查余额是否足够
func CheckOrder(o *Order) bool { ... }

// ✅ 函数名即文档
func IsBalanceSufficient(o *Order) bool { ... }
func DeductBalance(o *Order) error { ... }

当函数名需要超过 5 个词才能说清楚时,往往意味着函数做了太多事。这时候该考虑拆分而非加长名字。

第三层:类型命名——用领域语言建模

类型名决定了开发者对这个概念的心智模型。用错类型名,整个代码库的对话都会变得拧巴。

1
2
3
4
5
6
7
8
9
// ❌ 用技术术语命名业务概念
type UserRecord struct { ... }    // "Record" 是数据库术语
type OrderData struct { ... }      // "Data" 没有语义
type PaymentInfo struct { ... }   // "Info" 太泛

// ✅ 用领域语言命名
type User struct { ... }
type Order struct { ... }
type Payment struct { ... }

在领域驱动设计(DDD)中,类型名应该直接来自业务专家的词汇。如果产品经理说"订阅计划",你的类型就该叫 SubscriptionPlan,而不是 SubPlanCfg

接口命名要特别小心。Go 礄言约定单方法接口加 -er 后缀,但多方法接口不要乱加。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// ✅ Go 惯例:单方法接口加 -er
type Reader interface {
    Read(p []byte) (n int, err error)
}

type Logger interface {
    Log(msg string)
}

// ❌ 多方法接口硬加 -er 很别扭
type UserValidator interface {        // ✅ 直接用职责命名
    Validate(u *User) error
    ValidateEmail(email string) error
}

第四层:模块/包命名——最难的一层

包名是代码库的顶层组织架构,改包名意味着所有 import 路径都要变。所以包名必须短、通用、无歧义

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// ❌ 坏包名
import "myapp/util"           // util 是什么都有用的垃圾桶
import "myapp/common"         // common 比 util 更糟糕
import "myapp/helpers"       // helpers 意味着没有真正的架构
import "myapp/user_service"   // 包名不该有下划线和 service 后缀

// ✅ 好包名
import "myapp/auth"           // 认证相关
import "myapp/order"           // 订单领域
import "myapp/middleware"      // HTTP 中间件

Go 的包名设计哲学是:包名 + 导出符号 = 完整语义

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// ✅ 包名和函数名配合
import "myapp/http"
client := http.GetClient()     // 读作 "http.GetClient"

import "myapp/timeutil"
dur := timeutil.ParseDuration()  // 读作 "timeutil.ParseDuration"

// ❌ 包名和函数名重复
import "myapp/user"
user.GetUser()     // "user.GetUser" 重复了

Python 和 Java 也有类似原则:模块/类名应该与其内部成员配合读通。

1
2
3
4
5
# ❌ 模块名和函数名重复
from user_service import get_user_service

# ✅ 配合阅读
from user import get_profile  # "user.get_profile"

六条命名原则总结

经过四个层次的拆解,我提炼出六条可直接落地的原则:

原则 要点 示例
名字长度与作用域成正比 局部变量可以短,全局/导出必须长 i vs maxRetryAttempts
布尔值用前缀 is/has/can/should isActivehasPermission
函数用动词 名词变动词,意图自明 fetchUsers() 而非 users()
类型用领域语言 不要用技术术语命名业务概念 Order 而非 OrderData
包名要短且通用 避免 util/common/helpers authorderconfig
避免否定式命名 否定式让逻辑变绕 isValid 而非 isNotInvalid

最后一条尤其值得展开。否定式命名在条件判断时会造成双重否定:

1
2
3
4
5
6
7
8
9
// ❌ 双重否定,大脑要多绕一圈
if !isNotAvailable {
    // 意思是"不是不可用" = "可用"
}

// ✅ 正面命名,直接明了
if isAvailable {
    // "可用"
}

命名的成本与收益

有人会说:“取个好名字太费时间了。” 但换个角度想:一个函数会被阅读几十次甚至上百次,而命名只发生在创建时。花 30 秒想个好名字,能节省未来几十小时的维护成本。

命名不是文学创作,是工程决策。每一次用 d 代替 requestTimeout,都是在向未来的自己(或接手代码的同事)埋下一颗定时炸弹。

下次写代码前,停一秒,问问自己:这个名字,三个月后我还看得懂吗? 如果犹豫,就改。