⌚ 一块表知道时间,两块表?你完了!——Go 中的「碎裂失败」陷阱
A man with a watch knows what time it is.
A man with two watches is never sure.
—— 古老的程序员谚语(大概)
我们写 Go 的时候,经常忍不住想:“让我再加个返回值,这样调用方更清楚!”
结果呢?——调用方更糊涂了 😅
今天来聊聊一个特别隐蔽却高频踩坑的反模式:用 bool, error 同时表示成功/失败 —— 我们管它叫:
🚨 Splintered Failure Modes(碎裂的失败模式)
🤔 先看一个“看似合理”的函数
func validate(input string) (bool, error) {
if input == "" {
return false, nil // 1️⃣ 输入为空 → 不合法,但没出错?
}
if isCorrupted(input) {
return false, nil // 2️⃣ 数据损坏 → 也不合法,还不报错??
}
if err := checkUpstream(); err != nil {
return false, err // 3️⃣ 真·出错了!但还是 false?
}
return true, nil // 🎉 唯一清爽的路径
}
调用它时,你可能会这么写:
ok, err := validate(userInput)
if !ok {
fmt.Println("校验失败")
return
}
if err != nil {
log.Fatal("系统挂了!")
}
❌ 停! 你已经掉坑里了。
为什么?因为 (bool, error) 的组合,理论上有 4 种状态:
bool |
error |
含义??? |
|---|---|---|
true |
nil |
👍 清晰:成功 |
false |
nil |
❓ 是“输入不合法”?还是“悄悄吞了错误”? |
true |
err |
🤯 矛盾!成功了还报错?(虽然你没写,但调用者不知道) |
false |
err |
🚨 危险!调用者若先看 ok,会把 数据库崩了 当成 用户名太短 😱 |
就像你左手表显示 9:00,右手表显示 10:30 ——
你不敢开会,也不敢睡觉,只能盯着两块表发呆。
这叫 “让非法状态变得可表示” —— 而 Go 的设计哲学恰恰是:
✅ Make illegal states unrepresentable.
(让非法状态无法被表达)
🔧 怎么修?—— 把失败权 统一收归 error
Go 的惯例从来就很简单:
📜 error != nil ⇒ 失败;error == nil ⇒ 成功。
别整花活儿。
我们重构一下:
// 返回值:成功时的数据(不是 flag!),和 error
func validate(input string) (string, error) {
if input == "" {
return "", fmt.Errorf("input cannot be empty")
}
if isCorrupted(input) {
return "", fmt.Errorf("input is corrupted")
}
if err := checkUpstream(); err != nil {
return "", fmt.Errorf("upstream check failed: %w", err) // 包装系统错误
}
return input, nil // ✅ 只要 err == nil,就一定是有效输入!
}
✅ 调用方现在只需关心一件事:
val, err := validate(userInput)
if err != nil {
// 失败!别管是逻辑错还是系统崩——先处理 err
log.Println("校验失败:", err)
return
}
// 👇 到这行?恭喜,val 是干净可用的!
fmt.Println("校验通过:", val)
就像你只戴一块表——时间对不对先不说,至少你知道该信谁 😄
🧩 但!我想区分「用户输错了」和「服务器炸了」怎么办?
好问题!—— 我们不是要把失败分类藏在第二个返回值里,而是:
🎁 把分类信息打包进
error本身!
✅ 方案一:哨兵错误(Sentinel Errors)—— 简单粗暴
var (
ErrEmpty = errors.New("input cannot be empty") // 逻辑错
ErrCorrupted = errors.New("input is corrupted") // 逻辑错
ErrSystem = errors.New("system failure") // 系统错
)
func validate(input string) (string, error) {
if input == "" {
return "", ErrEmpty }
if isCorrupted(input) {
return "", ErrCorrupted }
if err := checkUpstream(); err != nil {
return "", ErrSystem }
return input, nil
}
调用方用 errors.Is 精准打击:
val, err := validate(s)
if err != nil {
switch {
case errors.Is(err, ErrEmpty):
http.Error(w, "别空着啊喂!", 400)
case errors.Is(err, ErrCorrupted):
http.Error(w, "数据有毒,拒收!", 400)
case errors.Is(err, ErrSystem):
http.Error(w, "服务器正在打盹…", 500)
log.Println("🚨 紧急告警:", err)
}
return
}
✅ 方案二:自定义错误类型 —— 携带结构化信息
type ValidationError struct {
Field string
Reason string
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("invalid %s: %s", e.Field, e.Reason)
}
func validate(input string) (string, error) {
if input == "" {
return "", &ValidationError{
"input", "empty"}
}
if isCorrupted(input) {
return "", &ValidationError{
"input", "corrupted"}
}
if err := checkUpstream(); err != nil {
return "", err // 保持原 error(比如 *net.OpError)
}
return input, nil
}
调用方用 errors.As 解包细节:
val, err := validate(s)
if err != nil {
var ve *ValidationError
if errors.As(err, &ve) {
// 是用户问题:温柔提示
fmt.Printf("❌ %s 字段有问题:%s\n", ve.Field, ve.Reason)
return
}
// 否则:是系统问题 → 快跑!
panic(fmt.Sprintf("💥 系统崩了:%v", err))
}
✅ 总结:三句真言
- 别用
bool, error表达成败 —— 它是碎裂的失败,是两块对不上的表; - 让
error独揽失败大权 —— 成功/失败只看它,世界清净; - 复杂分类请塞进
error里 —— 用哨兵 or 自定义类型,既清晰又 Go 风。
记住:
一块表的人,准时上班;
两块表的人,天天迟到。 ⏰