如果你写过真正的 Go 代码,你一定感受过那种“痛”。
你从一个简洁优雅的函数开始,然后加上错误处理……突然间,你的代码一半变成了这样:
result, err := doSomething()if err !=nil{return err}
一遍又一遍,反复出现。
它不是“坏设计”——确实能用——但它真的很繁琐。多年来,Go 对错误处理的冗长性一直被诟病。但转折来了:
Go 已经悄悄地在修复这个问题。不是靠什么炫酷新语法,也没有破坏兼容性,而是通过一系列细致而稳妥的改进。现在的体验,确实好多了。
曾经的“痛”
你可能写过这样的代码:
func processFile(filename string) error { file, err := os.Open(filename)if err !=nil{return fmt.Errorf("can't open file: %w", err)} defer file.Close() data, err := io.ReadAll(file)if err !=nil{return fmt.Errorf("can't read file: %w", err)} processed, err := transform(data)if err !=nil{return fmt.Errorf("transform failed: %w", err)} err = saveToDatabase(processed)if err !=nil{return fmt.Errorf("save failed: %w", err)}returnnil}
实际业务逻辑很少,大多数都是 iferr!=nil
的模板代码。
改变一切的修复:多错误支持
在 Go 1.20 中引入了一个小但关键的特性:多错误合并(multi-error)。
在此之前,如果在清理阶段多个操作失败(例如关闭资源),你只能返回一个错误,其它错误就“消失”了。
现在你可以这样写:
func cleanup() error {var problems []errorif err := closeDatabase(); err !=nil{ problems = append(problems, err)}if err := closeCache(); err !=nil{ problems = append(problems, err)}if err := closeFiles(); err !=nil{ problems = append(problems, err)}return errors.Join(problems...)}
这意味着:当 cleanup 失败时,你能一次性看到所有出错信息。
而且最棒的是:你依然可以使用 errors.Is()
来精确判断具体错误:
err := cleanup()if errors.Is(err, sql.ErrConnDone){ log.Println("数据库关闭出错")}
即使这个数据库错误是五个错误中的一个,也能准确识别。
错误包装: %w
终于有用了
早在 Go 1.13, fmt.Errorf
就支持了 %w
用于包装原始错误,但很多开发者要么用错,要么干脆不用。
对比:
// 不好 — 丢失上下文return fmt.Errorf("something broke")// 正确 — 保留原始错误链return fmt.Errorf("something broke: %w", err)
好处是:你能得到一整条错误链,方便追踪问题发生在哪一步。
例如:
func getUser(userID string) error { user, err := fetchFromDB(userID)if err !=nil{return fmt.Errorf("getting user %s: %w", userID, err)}returnnil}func fetchFromDB(id string) error { row := db.QueryRow("SELECT * FROM users WHERE id = ?", id)if err := row.Scan(&user); err !=nil{return fmt.Errorf("database query failed: %w", err)}returnnil}
如果出错,输出会是:
getting user 123: database query failed: sql:no rows in result set
你能清晰地看到问题出在哪里。
自定义错误也更强大了
你可以自定义错误类型,并配合 errors.As
精确识别错误来源:
type ValidationErrorstruct{FieldstringValueinterface{}Messagestring}func (e ValidationError)Error()string{return fmt.Sprintf("field '%s' is wrong: %s", e.Field, e.Message)}
使用方式:
func checkUser(user User) error {var problems []errorif user.Email==""{ problems = append(problems,ValidationError{"email", user.Email,"email can't be empty"})}if user.Age<0{ problems = append(problems,ValidationError{"age", user.Age,"age can't be negative"})}return errors.Join(problems...)}
错误处理:
if err := checkUser(user); err !=nil{var ve ValidationErrorif errors.As(err,&ve){return badRequest(ve.Error())}return serverError(err)}
你可以优雅地区分用户错误与系统错误。
现代 Go 应用中的错误分层
现在的大型 Go 应用通常按以下方式处理错误:
┌─────────────────┐│WebHandler│←转换为 HTTP 响应├─────────────────┤日志记录,输出 JSON│• HTTP codes ││•Logging││• JSON output │└─────────┬───────┘│┌─────────▼───────┐│BusinessLogic│←加上下文,聚合错误├─────────────────┤│•Validation││•Rules│└─────────┬───────┘│┌─────────▼───────┐│DataLayer│←数据库、文件、网络 I/O├─────────────────┤│•Queries││•Filesystem││•Network I/O │└─────────────────┘
每一层都加上恰当的上下文,避免彼此干扰。
附加技巧:简洁错误处理模式
你可以用 Result
模板减少重复代码:
type Result[T any]struct{Value TError error}func Try[T any](value T, err error)Result[T]{returnResult[T]{Value: value,Error: err}}func (r Result[T])Must() T {if r.Error!=nil{ panic(r.Error)}return r.Value}
然后你可以这样写:
data :=Try(os.ReadFile("config.json")).Must()
用于脚本或工具型程序,比 iferr!=nil
简洁很多。
工具链也变聪明了
不仅是语言本身,Go 的生态也在进步:
go vet
会检查未处理的错误编辑器能实时标红错误
静态分析工具(如
staticcheck
)提供更好建议
就算你写错了,也能第一时间被提醒。
Go 团队是怎么说的?
Go 团队明确表示:不会引入 try/catch 或新语法糖。
他们选择了一条更成熟的道路:在不增加复杂性的前提下优化现有模型。
那么……这些改进有意义吗?
当然有。
2025 年写 Go,你会发现:
模板代码更少
错误信息更丰富
测试更容易
问题定位更清晰
虽然是细微的改进,却彻底改变了使用体验。
真相:
Go 没有放弃错误处理模式。它只是打磨得更顺手了。
没有戏剧性的变化,没有破坏性的升级,只是每年一点一点变得更好。
iferr!=nil
还在 —— 也将一直在。但现在,它配备了一整套现代工具,让你愿意、甚至享受与错误打交道的过程。
曾经最令人头疼的部分,如今变得“正常”了 —— 而这正是理想状态。