error
Contents
错误
程序出现了意外的状况,需要我们额外处理。如果不处理,会破坏程序的正常运行。比如:
- 对 nil map 进行写入操作;对数组进行越界操作。
- 对非 JSON 格式的数据进行 JSON 解析。
处理的方式:
- 独立的异常处理流:exception 对象,通过 try-catch-finally 流程处理;
- 作为正常流程一部分:异常作为普通对象,error 值或者 int ,与正常返回值一起返回,通过条件判断来处理。
panic
panic vs exception same:
- 终止代码的继续执行;
- 展开堆栈,即逐步退出每个函数,直到被 recover 或者 catch 住
- 被 catch 后程序的调用流程都发生改变, 都跳转到 catch 的位置执行 panic 之后的代码则不会执行; differ:
- try catch 更灵活 ,可以针对任一代码块 而 recover 只能在 函数 derfer 中,catch 整个函数
是否 panic 可以实现 exception 的相同功能? 可以,从技术上来说 panic 可以实现和 exception 等价的功能;
和 try .. catch 的比较: 类型用法,理论上可以 用 recover 实现 try catch 的功能;
func hello(){
try {
// do something
} catch (e) {
// handle error
}
}
// 等价于
func hello() {
defer func() {
if err := recover(); err != nil {
// handle error
fmt.Println(debug.Stack())
}
}()
// do something
//
if err != nil {
panic(err)
}
if err2 != nil {
panic(err)
}
}
go 错误处理
错误处理机制:
- 错误作为错误值返回;
- 使用 if err != nil 来 check 每一个错误;
错误处理的问题:
- 没有堆栈信息
- 需要频繁地处理错误,代码变得冗长;
优化措施:
- 集中处理:如果处理方式相同,就放在一个地方处理
- 通过层层传递错误信息,实现类似堆栈功能;并使用 %w 保留原始的错误信息
func main() {
result, err := queryDB()
if err != nil {
// 统一处理
log.Printf("query db error: %v", err)
}
}
func queryDB() (Result, error) {
result, err := queryMySQL()
if err != nil {
return nil, fmt.Errorf("queryDB error, cause: %w", err)
}
// do something
return result, nil
}
func connectMySQL() (*sql.DB, error) {
db, err := sql.Open("mysql", "root:root@tcp(")
if err != nil {
return nil, fmt.Errorf("connectMySQL error, cause: %w", err)
}
return db, nil
}
放弃 exception 的原因: 错误应该作为普通代码流程的一部分:
- 代码应该是显示的: 错误应该被清晰地看到,作为函数签名的一部分,使代码容易理解和维护
- 错误应该立即被处理:减少被忽略的可能性,以及后期集中处理带来的 bug;
panic 使用场景
exception/panic 使用场景: 严重的问题,如果不立即终止 会导致更严重的后果,也就是说会严重破坏数据的正确性:
- 数据丢失,数据损坏;=
- 数据不一致
- 获取到错误的数据 好比心脏病, 你不立即治疗,后面就会造成大面积的问题,无法挽回; unrecoverable error, 不可恢复的错误;
case:
-
逻辑错误:
- 不合适的数据操作: 2. 写入 nil map, 不 panic 数据丢失 3. 数组越界,nil map 访问,不 panic 会获取到错误的数据
- 计算错误:
- 除数为 0,不 panic 会导致程序计算错误
-
关键资源的缺失:
- 初始化失败,不 panic 会导致后续数据的读写错误;
- 运行时候资源缺失,内存资源,文件资源,不 panic 导致数据读写错误;
error As and Is
is : 错误链中是否包含某个错误 as: 错误链中是否包含某个类型的错误,如果包含,将错误转换为该类型
func Is(err error, target error) bool
func As(err error, target interface{}) bool
相同: 对错误进行进一步处理
is 的使用场景:对不同的错误针对性处理,example: 一般调用标准库或者三方库有一个错误列表;
- sql 查询,如果是 not found 错误,可以返回 nil
- 文件操作,如果是文件不存在,读取默认配置
func ReadConfig() (*Config, error) {
f, err := os.Open("config.json")
if errors.Is(err, os.ErrNotExist) {
return defaultConfig, nil
}
....
}
as 使用场景,获取更多的错误信息,再进一步的处理: 一般是转换成自定义错误类型;
- error 转换成自定义的错误类型,获取更多的错误信息,再进一步处理;
type BusinessError struct {
Code int
Msg string
}
func (e *BusinessError) Error() string {
return e.Msg
}
func main() {
err := ErrNotFound
var e ErrorCode
if errors.As(err, &e) {
response := map[string]interface{}{"msg": e.Msg, "code": e.Code}
fmt.Println(response)
}
}
error handle best practice
通常来说
- is : 调用标准库, 三方库, 有会有一些公开的错误实例;
- as: 通常在自己项目中,会将错误都转换成自定义的错误类型,然后通过 as 取出
错误的分类:
- 哨兵错误, sentinel error: 需要特定的处理方式, 如 用户不存在,文件不存在,等等;
- 自定义错误-内部错误: 主要用户日志记录和调试 ;
- 自定义错误-api 错误 : 返回给用户的错误信息;
错误处理建议:
- 构建错误链,基于%w,或者自己实现 wrap 和 unwrap 方法; 将同一个请求或者同一次操作的所有错误都串联起来;
- 将外部的 哨兵错误转换为自定义的哨兵错误,以进行解耦 ;
- 通过自定义内部错误,记录更多的错误信息,方便日志和调试
- 在相同的处理方式,集中在顶层一起处理:如打印日志;
一种实践: 链条上传递都是自定义错误类型,最后统一处理
code example:
// http sever
var NotFound = errors.New("not found")
// 内部错误,用于调试和日志
type InternalError struct {
Code int
Path string
Cause error
Context map[string]interface{}
}
type ApiError struct {
Msg string
Code int
Internal error
}
func (e *MyError) Error() string {
return fmt.Sprintf("path: %s, cause: %v, context: %v", e.Path, e.Cause, e.Context)
}
func (e *MyError) Unwrap() error {
return e.Cause
}
func main() {
http.HandleFunc("/login", login)
http.ListenAndServe(":8080", nil)
}
func handleError(w http.ResponseWriter, err error) {
// Log the full error chain
var errChain string
for err != nil {
if errChain != "" {
errChain += " -> "
}
errChain += err.Error()
err = errors.Unwrap(err)
}
// Log the error chain
fmt.Printf("Error chain: %s\n", errChain)
// Handle specific error types
var apiErr *APIError
if errors.As(err, &apiErr) {
// Return structured error to client
w.WriteHeader(apiErr.Code)
json.NewEncoder(w).Encode(apiErr)
return
}
// Default error response
w.WriteHeader(http.StatusInternalServerError)
json.NewEncoder(w).Encode(map[string]string{
"message": "internal server error",
})
}
func login(w http.ResponseWriter, r *http.Request) {
result , err := queryUser()
if err != nil {
if errors.is(err, NotFound) {
// 返回给用户
handleError(w, &ApiError{Msg: "user not found", Code: 404, Internal: err})
} else {
// 内部错误
handleError(w, &InternalError{Path: "service.login", Cause: err, Context: map[string]interface{}{"userid": userID}})
}
}
// do somethin
}
func queryUser(userID int) (Result, error) {
result, err := queryMySQL(userID)
if err != nil {
if err == sql.ErrNoRows {
return nil, &MyError{Path: "queryDB", Cause: NotFound Context: map[string]interface{}{"userid": userID}}
} else {
return nil, &MyError{Path: "queryDB", Cause: err, code:DBError Context: map[string]interface{}{"userid": userID}}
}
}
// do something
return result, nil
}
实现原理
实现 wrap 和 unrwap 方法:
- wrap: 将其他 error 加入到自身属性中,作为子对象
- unwrap: 将对线返回;
使用 unwrp 一层一层解析出来
func Is(err, target error) bool {
for {
if err == target {
return true
}
if err = Unwrap(err); err == nil {
return false
}
}
}
func As(err error, target interface{}) bool {
for {
if relfect.TypeOf(err) == reflect.TypeOf(target) {
// set target to err
reflect.ValueOf(target).Elem().Set(reflect.ValueOf(err))
return true
}
if err = Unwrap(err); err == nil {
return false
}
}
}
func Unwrap(err error) error {
type wrapper interface {
Unwrap() error
}
if u, ok := err.(wrapper); ok {
return u.Unwrap()
}
return nil
}
// error 实现unwrap
type MyError struct {
msg string
cause error
}
func (e *MyError) Error() string {
return e.msg
}
func (e *MyError) Unwrap() error {
return e.cause
}
Forrest's Blog