为什么命名这么难
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
|
但精确不等于啰嗦。theUserManager 和 aPendingResponseBuffer 这样的命名就是矫枉过正——定冠词和冗余后缀不增加任何信息。
核心原则:名字的长度应与作用域成正比。
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 |
isActive、hasPermission |
| 函数用动词 |
名词变动词,意图自明 |
fetchUsers() 而非 users() |
| 类型用领域语言 |
不要用技术术语命名业务概念 |
Order 而非 OrderData |
| 包名要短且通用 |
避免 util/common/helpers |
auth、order、config |
| 避免否定式命名 |
否定式让逻辑变绕 |
isValid 而非 isNotInvalid |
最后一条尤其值得展开。否定式命名在条件判断时会造成双重否定:
1
2
3
4
5
6
7
8
9
|
// ❌ 双重否定,大脑要多绕一圈
if !isNotAvailable {
// 意思是"不是不可用" = "可用"
}
// ✅ 正面命名,直接明了
if isAvailable {
// "可用"
}
|
命名的成本与收益
有人会说:“取个好名字太费时间了。” 但换个角度想:一个函数会被阅读几十次甚至上百次,而命名只发生在创建时。花 30 秒想个好名字,能节省未来几十小时的维护成本。
命名不是文学创作,是工程决策。每一次用 d 代替 requestTimeout,都是在向未来的自己(或接手代码的同事)埋下一颗定时炸弹。
下次写代码前,停一秒,问问自己:这个名字,三个月后我还看得懂吗? 如果犹豫,就改。