在 Go 里写代码,你经常会听到一句话:"Keep it simple, stupid."(保持简单,笨蛋!)但问题是,"简单"(Simple)不等于"容易"(Easy)。写出能跑的代码很容易,但写出几个月后自己还能看懂、别人也敢放心改的代码,就需要点真功夫了。
下面这五个原则,是我用无数个调试到凌晨的夜晚换来的"血泪教训"。它们不是什么高深的理论,而是一套让你和你的代码都更"体面"的日常习惯。下面直接用 Go 的代码来讲。
1. 命名:别让读你代码的人猜哑谜
Go 语言本身很简洁,但这给了我们更大的责任——名字就是最好的注释。
坏味道:
func get(d []string) map[string]string { // 返回用户ID到名字的映射
m := make(map[string]string)
for _, id := range d {
// 从数据库查名字...
m[id] = name
}
return m
}
d 是什么?m 是什么?返回值是什么意思?全靠那行注释撑着。一旦注释和代码不同步,就是一场灾难。
好习惯:
// 不需要注释,名字本身说明一切
func getUserNameMap(userIDs []string) map[string]string {
nameMap := make(map[string]string)
for _, id := range userIDs {
// 从数据库查名字...
nameMap[id] = name
}
return nameMap
}
在 Go 里,好的命名就是最好的文档。days 比 d 好一万倍。如果发现一个变量名或函数名需要额外注释来解释,那通常是名字取得不对。在 Go 的哲学里,省下敲键盘的时间,远不如省下读代码的人理解的时间重要。
2. 函数:只做一件事,并把它做好
这是 Unix 哲学在函数粒度的体现。一个函数如果干了三件不同的事,那它注定难以测试、难以复用、难以理解。
坏味道:
func processOrder(order *Order) error {
if !order.IsValid() {
return errors.New("invalid order")
}
// 计算总额
total := 0.0
for _, item := range order.Items {
total += item.Price * float64(item.Quantity)
}
order.Total = total
// 保存到数据库
if err := db.Save(order); err != nil {
return err
}
// 发送邮件
return email.SendConfirmation(order)
}
这个函数做了三件事:计算、保存、发邮件。如果邮件服务挂了,整个订单流程就失败了,这不合理。
好习惯:
func processOrder(order *Order) error {
if err := validateOrder(order); err != nil {
return err
}
if err := calculateTotal(order); err != nil {
return err
}
if err := saveOrder(order); err != nil {
return err
}
return sendConfirmation(order)
}
func calculateTotal(order *Order) error {
total := 0.0
for _, item := range order.Items {
total += item.Price * float64(item.Quantity)
}
order.Total = total
return nil
}
现在,每个子任务都是独立、可测试的。calculateTotal 可以单独写单元测试,完全不需要数据库或邮件服务的 Mock。
很多 Go 新手喜欢写"大而全"的函数,觉得这样"一气呵成"。但实际上,小而精的函数是代码可读性和可测试性的基石。Go 的测试框架非常轻量,不利用它来为每个独立功能写测试,简直是暴殄天物。
3. 提前返回(Early Return):让你的代码从"山路十八弯"变成"直来直往"
深层的嵌套是认知负担的头号杀手。你在读代码时,大脑需要维护一个"当前在哪个 if 分支里"的栈,层级越深,栈越容易溢出。
坏味道:
func getDiscount(user *User) string {
if user != nil {
if user.IsPremium() {
if user.Age() > 60 {
return "20%"
} else {
return "10%"
}
} else {
return "0%"
}
}
return "0%"
}
三层嵌套,逻辑已经有点绕了。如果再加几个条件,就变成了所谓的"箭头式代码"。
好习惯:
func getDiscount(user *User) string {
if user == nil {
return "0%"
}
if !user.IsPremium() {
return "0%"
}
if user.Age() > 60 {
return "20%"
}
return "10%"
}
主线逻辑是清晰的、平铺的。每个 if 都是一个"卫语句"(Guard Clause),处理完特殊情况就立即返回,让你专注于核心的、"快乐路径"(Happy Path)的逻辑。代码的缩进层次代表了逻辑的嵌套深度,缩进越少,心智负担越小。
在 Go 里,if err != nil { return err } 本身就是一种最典型的"卫语句"模式。把这个模式扩展到业务逻辑的校验和分支处理上,能让整个函数的可读性大幅提升。这是 Go 代码里一种非常"地道"的写法。
4. 注释:是解释"为什么",而不是"是什么"
这是一条有争议的原则,但在 Go 社区尤其重要。注释最大的敌人是代码本身发生变更后,注释却忘了更新。 过时的注释比没有注释更可怕,因为它会误导。
坏味道:
// 检查用户是否有资格获得高级折扣
if user.Points > 1000 && user.Age > 18 && !user.IsBlocked {
// 应用 15% 折扣
price = price * 0.85
}
注释在翻译代码,而不是补充信息。如果业务规则变了,比如积分门槛改成 800,你需要同时改代码和注释,但凡忘了一个,就埋下了隐患。
好习惯:
if user.IsEligibleForPremiumDiscount() {
price = price.ApplyDiscount(0.85)
}
一个命名良好的方法,彻底消灭了注释存在的必要。代码本身就是文档。
在 Go 里,我遵循一个简单原则:注释用来解释"为什么这样设计"(比如这个特殊的业务规则为什么存在,或者这个 workaround 是为了绕过哪个第三方库的 bug),而不是解释"这段代码做了什么"。 公共 API(如包、函数、类型的声明)当然需要注释来生成文档,但那是另一回事。对于实现细节,优先考虑用代码本身来表达意图。
5. 类(Struct):保持小巧和专注
在 Go 里,虽然没有"类"(Class)关键字,但 struct 和它绑定的方法承担了同样的职责。单一职责原则(SRP)同样适用。
坏味道:
type UserService struct {
DB *sql.DB
Cache *redis.Client
Email *email.Sender
}
// ... 包含了注册、登录、改密码、发通知、处理账单等近 2000 行方法
UserService 接管了用户相关的所有事情,它注定会因为任何单一功能的变更而被修改,变成一个"巨无霸",脆弱且难以测试。
好习惯:
type UserRepository struct { DB *sql.DB }
type EmailService struct { Sender *email.Sender }
type UserService struct {
Repo *UserRepository
Email *EmailService
}
每个服务都只关注一个特定的领域,改动成本低,且影响范围可控。如果你要修改邮件逻辑,你不会碰触到 BillingService。
在 Go 项目里,我倾向于按领域边界(如 user、order、payment)来组织包,每个包内的 struct 都承担着该领域内非常聚焦的职责。不要试图在一个 struct 里模拟"整个世界"。Go 的组合(Composition)优于继承(Inheritance)的设计哲学,天然鼓励我们构建小而美、可组合的组件。
总结
这五个原则——好命名、单职函数、提前返回、少写废话注释、小结构体——它们共同指向一个目标:让你的代码更容易被人类理解。
代码首先是写给人看的,其次才是给机器执行的。计算机不在乎你的代码是嵌套了5层还是一层,但你的同事(和未来的你)在乎。
在 Go 的世界里,追求"简洁"和"清晰"不仅仅是风格问题,更是对这门语言设计哲学的尊重。从今天起,有意识地应用这些原则,你的代码会变得更"通情达理",你的调试时间会减少,而你代码的寿命会大大延长。
