Contents

error

错误

程序出现了意外的状况,需要我们额外处理。如果不处理,会破坏程序的正常运行。比如:

  1. 对 nil map 进行写入操作;对数组进行越界操作。
  2. 对非 JSON 格式的数据进行 JSON 解析。

处理的方式:

  1. 独立的异常处理流:exception 对象,通过 try-catch-finally 流程处理;
  2. 作为正常流程一部分:异常作为普通对象,error 值或者 int ,与正常返回值一起返回,通过条件判断来处理。

panic

panic vs exception same:

  1. 终止代码的继续执行;
  2. 展开堆栈,即逐步退出每个函数,直到被 recover 或者 catch 住
  3. 被 catch 后程序的调用流程都发生改变, 都跳转到 catch 的位置执行 panic 之后的代码则不会执行; differ:
  4. 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 使用场景: 严重的问题,如果不立即终止 会导致更严重的后果,也就是说会严重破坏数据的正确性:

  1. 数据丢失,数据损坏;=
  2. 数据不一致
  3. 获取到错误的数据 好比心脏病, 你不立即治疗,后面就会造成大面积的问题,无法挽回; unrecoverable error, 不可恢复的错误;

case:

  1. 逻辑错误:

    1. 不合适的数据操作: 2. 写入 nil map, 不 panic 数据丢失 3. 数组越界,nil map 访问,不 panic 会获取到错误的数据
    2. 计算错误:
      1. 除数为 0,不 panic 会导致程序计算错误
  2. 关键资源的缺失:

    1. 初始化失败,不 panic 会导致后续数据的读写错误;
    2. 运行时候资源缺失,内存资源,文件资源,不 panic 导致数据读写错误;

error As and Is

is : 错误链中是否包含某个错误 as: 错误链中是否包含某个类型的错误,如果包含,将错误转换为该类型

func Is(err error, target error) bool
func As(err error, target interface{}) bool

相同: 对错误进行进一步处理

is 的使用场景:对不同的错误针对性处理,example: 一般调用标准库或者三方库有一个错误列表;

  1. sql 查询,如果是 not found 错误,可以返回 nil
  2. 文件操作,如果是文件不存在,读取默认配置
func ReadConfig() (*Config, error) {
  f, err := os.Open("config.json")
  if errors.Is(err, os.ErrNotExist) {
    return defaultConfig, nil
  }
  ....
}

as 使用场景,获取更多的错误信息,再进一步的处理: 一般是转换成自定义错误类型;

  1. 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

通常来说

  1. is : 调用标准库, 三方库, 有会有一些公开的错误实例;
  2. as: 通常在自己项目中,会将错误都转换成自定义的错误类型,然后通过 as 取出

错误的分类:

  1. 哨兵错误, sentinel error: 需要特定的处理方式, 如 用户不存在,文件不存在,等等;
  2. 自定义错误-内部错误: 主要用户日志记录和调试 ;
  3. 自定义错误-api 错误 : 返回给用户的错误信息;

错误处理建议:

  1. 构建错误链,基于%w,或者自己实现 wrap 和 unwrap 方法; 将同一个请求或者同一次操作的所有错误都串联起来;
  2. 将外部的 哨兵错误转换为自定义的哨兵错误,以进行解耦 ;
  3. 通过自定义内部错误,记录更多的错误信息,方便日志和调试
  4. 在相同的处理方式,集中在顶层一起处理:如打印日志;

一种实践: 链条上传递都是自定义错误类型,最后统一处理

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 方法:

  1. wrap: 将其他 error 加入到自身属性中,作为子对象
  2. 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
}