我看到很多 golang
社区的开发者,特别是因为它的简单性而被吸引的开发者,对 golang
中的事情应该如何处理做出了一些快速的判断。
其中一件事就是错误处理。由于目前大多数语言的开发者都来自于 OOP
背景,他们习惯于处理异常,或者说"抛出"异常的概念来停止当前的应用程序的流程,而且他们大多认为这也是 golang
的方式,我们必须在出错的情况下停止我们的应用程序的流程。他们错了!
不要滥用你的工具
我见过很多,我以前也是这样做的。每当有意外情况发生时,就用 os.exit(1)
,然后继续前进。好吧,这不是使用go的正确方法!
我明白为什么这被广泛使用,因为大多数早期的 golang
应用程序只是终端工具,而且许多这些工具曾经使用 .sh
可执行文件来构建,我们曾经只是退出1;以表示刚刚发生了一些意外的事情,我们想退出。
我们把这种习惯带到了我们的 golang 简单的终端应用中,然后又带到了复杂的应用中,这只是另一种 Cargo Cult Programming。 我高度鼓励你在不得不这样做的情况下,要非常小心,因为它是:
- 随着你的应用程序的增长,非常难以维护。
- 最重要的是,不可能对这样的代码进行单元测试,这显然表明它的不洁性。
- 以这种方式退出将阻止你的任何延迟操作的执行,你的程序将立即终止,这可能导致资源泄漏。请考虑一下这个例子:
func main() { dbConnection := db.connect("...") defer dbConnection.Close() // this operation won't be executed! entity := Entity{} err := dbConnection.Save(entity) if err != nil { os.Exit(1) } }
考虑传递你的错误
错误只是 golang
中的另一种类型,你必须用它们来控制程序的执行流程。
为了做到这一点,我们必须在整个程序中传播这些错误,直到适当的处理点。
考虑一个管理订单的HTTP API,我们想禁止客户在特定条件下下订单,例如:
package order // package errors var ( UnableToShipToCountry = errors.New("unable to ship order to the requested country") ) type Order struct { // ... order fields } type OrderRepo struct { DB db // ... } func newOrderFromRequest(o OrderRequest) (Order, error) { if o.ShippingAddress.Country != "DE" { return UnableToShipToCountry } // ... the creation logic return Order{...}, nil } func (r *OrderRepo)PlaceOrder(o OrderRequest) error { order, err := newOrderFromRequest(o) if err != nil { // don't handle the error here, its handling may differ return err } // ... db transaction may also return an error return r.db.Save(order) }
在我们的 http package
中:
package http http.HandleFunc("/order", func (w http.ResponseWriter, r *http.Request) { orderRequest := createOrderRequest(r) err := orderRepo.PlaceOrder(orderRequest) if errors.Is(err, order.UnableToShipToCountry) { w.WriteHeader(http.StatusBadRequest) return } if err != nil { // this error in case of DB transaction failure w.WriteHeader(http.StatusInternalServerError) return } // ... w.WriteHeader(http.StatusOK) })
定制你的错误
我们可以创建我们自己的自定义错误值,并在我们的程序中使用它,同时考虑添加一些有用的信息,如错误跟踪这可能会给我们的日志增加一个有益的价值,特别是在调试期间。
type AppErr struct { msg string code int trace string } func (e AppErr) Error() string { return fmt.Sprintf("Msg: %s, code: %d, trace:\n %s", e.msg, e.code, e.trace) } func NewAppErr(msg string, code int) AppErr { stackSlice := make([]byte, 512) s := runtime.Stack(stackSlice, false) return AppErr{msg, code, fmt.Sprintf("\n%s", stackSlice[0:s])} }
而我们在一个包内有这样一个用例:
package admin func A() error { return b() } func b() error { return NewAppErr("error from b function!", 3) }
main.go:
func main() { err := admin.A() fmt.Println(err) }
记录的错误信息将是:
Msg: error from b function!, code: 3, trace: goroutine 1 [running]: ./cmd/app/error.NewAppErr({0x1f42b0, 0x17}, 0x7) ./cmd/app/error/error.go:16 +0x35 ./cmd/app/admin.b(...) ./cmd/app/admin/**admin.go:12** ./cmd/app/admin.A(...) ./cmd/app/admin/**admin.go:8** main.main() ./cmd/app/**main.go:10** +0x8d
你也可以考虑在生产环境中关闭你的跟踪打印,或者通过检查其他配置值。