逐步学习Go-错误处理与自定义error

内容纲要

什么是error?

在日常的代码开发中,错误处理和自定义错误都是难免要遇到的。Java中提供了Exception和Error,Python提供了Exception和Error。Go也提供了错误处理:error。比如: Go的文件I/O接口:

func Open(name string) (file *File, err error)

如果os.Open在打开文件时遇到错误,那么返回的err的值为非空。

Go代码使用error值来表示一个异常的状态。

如何处理error?

如果我们调用os.Open返回的err不为空,那么我们应该如何处理?

f, err := os.Open("file.txt")
if err != nil {
    log.Fatal("error occurred when oopen file file.txt", err)
}

error是什么?

在Go中,error也是一种接口类型,这个接口类型定义如下:


type error interface {
    Error() string
}

Go中实现了error接口,其中最通用的error实现是,源代码链接:errors.go


// New returns an error that formats as the given text.
// Each call to New returns a distinct error value even if the text is identical.
func New(text string) error {
    return &errorString{text}
}

// errorString is a trivial implementation of error.
type errorString struct {
    s string
}

func (e *errorString) Error() string {
    return e.s
}

快速创建一个error

根据上面的定义,我们可以使用errors.New("")快速创建一个error。比如:


file, err := os.Open("file.txt")
if err != nil {
    return errors.New("file cannot be open")
}

也可以使用fmt包的Errorf函数,比如:


err := fmt.Errorf("user %q (id %d) not found", name, id)

file

如何自定义error?

error是一个接口类型,根据Go的约定:只要一个类型实现了接口的所有方法,那么这个类型实现了这个接口。

方式1

type NegativeSqrtError float64

func (f NegativeSqrtError) Error() string {
    return fmt.Sprintf("math: square root of negative number %g", float64(f))
}

方式2

type SyntaxError struct {
    msg    string // description of error
    Offset int64  // error occurred after reading Offset bytes
}

func (e *SyntaxError) Error() string { return e.msg }

方式3

实际业务开发中一般我们需要自定义错误,比如:当有错误发生时,我们会返回一个自定的错误,这个错误包含了接口返回时的所需字段信息。

实际业务场景:登录。用户输入手机号和短信验证码登录,如果手机号或者验证码不正确,我们可能会返回自定自定义错误:PhoneOrChaptaIncorrectError,同时我们接口需要返回以下json:

{
    "bizCode":"",
    "msg":"phone or chapta is incorrect"
}

那我们自定义异常可能会这样设计:


type PhoneOrChaptaIncorrectError struct {
    BizCode string
    Msg string
}

func NewPhoneOrChaptaIncorrectError() *PhoneOrChaptaIncorrectError {
    return &PhoneOrChaptaIncorrectError{
        BizCode: "1234",
        Msg: "phone or chapta is incorrect",
    }
}

func (e *PhoneOrChaptaIncorrectError) Error() string {
    return e.Msg
}

或者自定义一个通用异常,然后各种new:


// 定义一个包含业务代码和错误消息的自定义错误类型
type BizError struct {
    BizCode string
    Msg     string
}

// 实现 error 接口的 Error 方法
func (e *BizError) Error() string {
    return fmt.Sprintf("%s: %s", e.BizCode, e.Msg)
}

// 定义一个可以创建具体BizError实例的功能函数
func NewBizError(bizCode, msg string) *BizError {
    return &BizError{
        BizCode: bizCode,
        Msg:     msg,
    }
}

又或者按照Go推荐方式:
先定义一个自己的struct,


type appError struct {
    Error   error
    Message string
    Code    int
}

然后让函数或者方法返回这个struct:


func viewRecord(w http.ResponseWriter, r *http.Request) *appError {
    c := appengine.NewContext(r)
    key := datastore.NewKey(c, "Record", r.FormValue("id"), 0, nil)
    record := new(Record)
    if err := datastore.Get(c, key, record); err != nil {
        return &appError{err, "Record not found", 404}
    }
    if err := viewTemplate.Execute(w, record); err != nil {
        return &appError{err, "Can't display record", 500}
    }
    return nil
}

最后获得自定义的错误消息:


func (fn appHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    if e := fn(w, r); e != nil { // e is *appError, not os.Error.
        c := appengine.NewContext(r)
        c.Errorf("%v", e.Error)
        http.Error(w, e.Message, e.Code)
    }
}

如何判断是哪个error?

我们业务中可能有判断是哪个错误的需求,Go也提供了这样的方法:errors.Is

func Is(err, target error) bool

if _, err := os.Open("non-existing"); err != nil {
        if errors.Is(err, fs.ErrNotExist) {
            fmt.Println("file does not exist")
        } else {
            fmt.Println(err)
        }
    }

什么是error的层次结构

在 Go 1.13 版本中,标准库引入了几个新的错误处理功能,包括 %w 用于 fmt.Errorf 来包装错误,以及 errors.Unwrap 函数用于解开错误层次。


import (
    "errors"
    "fmt"
    "log"
)

// 底层错误
var ErrBase = errors.New("base error")

// 创建错误树的中间层
func middleLayer() error {
    return fmt.Errorf("middle layer: %w", ErrBase)
}

// 创建最顶层的错误
func topLevel() error {
    return fmt.Errorf("top level: %w", middleLayer())
}

func main() {
    err := topLevel()
    if err != nil {
        fmt.Println(err) // 输出完整的错误链条信息

        // 解开错误树以获取原始的错误
        if errors.Is(err, ErrBase) {
            fmt.Println("An error occurred at the base level")
        }

        // 获得错误的直接源头
        wrappedErr := errors.Unwrap(err)
        fmt.Println(wrappedErr) // 输出中间层的错误

        // 递归解开所有层次的错误
        for err != nil {
            fmt.Println(err)
            err = errors.Unwrap(err)
        }
    }
}

总结

  1. Go中的异常处理是返回error接口类型,程序员需要手动判断error是否为空
  2. 快速创建error使用errors.New或者fmt.Errorf(如果你想格式化一些变量)
  3. 自定义异常可以自己实现error接口类型或者使用组合方式实现一个自定义struct
  4. Go的error是有层次结构的,这个类似于异常的继承,Go提供了fmt.Errorf,errors.Is和errors.As, errors.Unwrap来支持层次结构
  5. Go error的设计哲学显式错误检查,有点类似于Java的checked exception。

发表评论

您的电子邮箱地址不会被公开。 必填项已用 * 标注

滚动至顶部