原创分享 Go 中没有 try/catch,该如何处理错误

yudotyang · 2021年03月10日 · 226 次阅读

今天给大家讲讲在 Go 中是如何处理错误的。

在 Go 语言中,没有像其他语言那样提供 try/catch 方法来处理错误。然而,Go 中是将错误作为函数返回值来返回给调用者的。下面详细讲解 Go 语言的错误处理方法。

在 Go 中,当程序遇到错误时,不像其他语言那样会终止运行。而是将错误作为是一个普通的值从函数中返回,让调用者根据函数的返回值来进行处理。由源码可知,error 是 Go 中一个内建的数据类型,默认值是 nil。 一般作为函数返回值列表的最后一个返回,由调用者检查是否是 nil。类似这样:

val, err := myFunction(args...)
if err != nil {
    //处理错误
}else {
    //运行正常的代码
}

让我们来看下 error 在源码中的定义:

type error interface {
    Error() string
}

原来,error 实际上就是一个 interface 类型,并定义了一个返回字符串的 Error 方法。即所有实现了 Error 方法的类型都可以作为错误类型。 我们自定义一个错误类型:

package main
import "fmt"

type MyError struct{}

func (myErr *MyError) Error() string {
    return "Something unexpected happed!"
}

func main() {
    myErr := &MyError()

    fmt.Println(myErr)
}

以上代码中,我们定义了一个 MyError 结构体,实现了 Error 方法,这样 MyError 就成了一个错误类型。

但是,这样每次我们都需要创建一个结构体,并且实现 Error 方法,是不是有点复杂?Go 的开发者们为了避免复杂的创建, 在 errors 包中提供了 New 函数来快速创建错误类型 。如下:

package main

import (
    "fmt"
  "errors"
)

func main() {
  //这里通过调用New函数快速创建了一个myErr类型
  myErr := errors.New("Someting unexpected happend!")

  fmt.Println(myErr)
}

再来看下 errors 包中 New 方法的源码定义,很简单,就是定义了一个 errorString 结构体,实现了 Error 方法。整个包的代码如下:

package errors

func New(text string) error {
    return &errorString{text}
}

type errorString struct {
    s string
}

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

error 信息的实际应用

在真实的项目中,我们不仅仅只需要一个字符串信息,而是需要更多的信息来帮助我们程序发生了什么。

下面以 HTTP 请求返回错误(状态码非 200)为例来来讲解。当我们处理 HTTP 请求时,需要知道 HTTP 的状态码是什么以及如何处理。 如下:

type ErrorCodeHandle struct {
    StatusCode int
    Method string
    Handler func(context.Context)
}

func (err *ErrorCodeHandle) Error() string {
    return fmt.Sprintf("Something went wrong with the method-%v request. Server returned StatusCode-%v.", err.Method, err.StatusCode)
}

func GetUserEmail(userId int) (string, error) {

    //请求失败
    return "", &ErrorCodeHandle{404, "GET"}
}

func main() {
    if email, err := GetUserEmail(1); err != nil {
        fmt.Println(err)

    //这里使用了类型断言,因为凡是实现了Error方法的struct都属于error类型
        if errVal := err.(ErrorCodeHandle); errVal.Status == 404 {
            fmt.Println("Not Found")
            err.Handle(context.Background())
        }else {
            //没有错误,函数调用成功
            fmt.Println("User email is:", email)
        }
    }
}

让我们来解析下上述代码: ++ 我们自定义了 ErrorCodeHandle 类型,并实现了 Error 方法 ++ GetUserEmail 模拟返回 404 错误 ++ 在 main 函数中,调用 GetUserEmail 函数,并对 err 进行了类型断言,判断是否是 ErrorCodeHandle 类型,以便进一步获取该结构体中的属性

当函数返回的错误属于不同的错误类型时,可以使用 switch.. case 语句进行判断。如下:

type NetworkErr struct {}

func (e *NetworkError) Error() string {
    return "A network connection was aborted"
}

type FileSaveFailedError struct {}

func (e *FileSaveFailedError) Error() string {
    return "The request file could not be saved"
}

func saveFileToRemote() error {
    result := 2 //模拟保存操作的结果
    if result == 1 {
        return &NetworkError{}
    }else if result == 2 {
        return &FileSaveFailedError{}
    }else {
        return nil
    }
}

func main() {
    //检查错误类型:err.(type)
    switch err := saveFileToRemote(); err.(type) {
        case nil:
            fmt.Println("File successfully saved")
        case *NetworkError:
            fmt.Println("Network Error:", err)
        case *FileSaveFailedError:
            fmt.Println("File save Error:", err)
    }
}

好了,现在来总结一下: ++ error 是一个接口类型,默认值是 nil, ++ 一般作为函数的最后一个返回值返回,由调用者处理错误 ++ 在调用者中判断错误的时候,需要用类型断言判断 error 的类型,再做后续处理。因为凡是实现了该接口中 Error 方法的类型都可以作为自定义的错误类型。 ++ 在实现了 error 接口的数据类型中,可以自定义上下文信息,以帮助调用者获取更多的信息 ++ 因为是数据类型,所以可以自定义方法来获取想要的错误信息,而非直接调用类型属性

一些建议

  1. 对错误进行处理 有一种方式可以忽略错误,就是用下划线接收返回值。

    val, _ := someFunctionWhichCanReturnAnError()
    

    像上面代码就忽略了错误。即使没有获取错误或者错误不重要,这将对后续代码导致级联的影响。所以,强烈建议在可能的情况下都要处理错误。

  2. 不要单纯将错误返回,添加上下文信息

    func someFunction() error {
    val, err := someFunctionWhichCanReturnAnError()
    
    if err != nil {
        return err
    }
    //处理其他逻辑
    }
    

    以上代码中,在遇到错误时就是简单的把错误返回了,这导致调用者不知道该错误来源于哪里。因此,较好的方式是将该错误进一步封装,添加更多的上下文信息。例如可以使用 errors 包中的 Wrap 方法来给错误增加上说明。

  3. 避免重复处理错误 当处理日志的时候,可能会把日志记录到日志文件汇总。 以下代码就是重复记录了 2 次日志。

func someFunction() error {
    if err != nil {
        //记录err日志
        return err
    }
}

func someOtherFunction() error {
    val, err := someFunction()
    if err != nil {
        //记录err日志
        return err
    }
}

因此,最好的做法是在最开始的调用者那里记录日志

大家好,我是于洋,公众号 Go 学堂,谢谢大家支持。

更多原创文章干货分享,请关注公众号
  • 加微信实战群请加微信(注明:实战群):gocnio
暂无回复。
需要 登录 后方可回复, 如果你还没有账号请点击这里 注册