Go中没有try/catch,该如何处理错误?

2023-01-31 15:13:43 浏览数 (1)

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

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

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

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

代码语言:javascript复制
type error interface {
  Error() string
}

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

代码语言:javascript复制
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函数来快速创建错误类型。如下:

代码语言:javascript复制
package main

import (
  "fmt"
  "errors"
)

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

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

代码语言:javascript复制
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的状态码是什么以及如何处理。如下:

代码语言:javascript复制
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语句进行判断。如下:

代码语言:javascript复制
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. 对错误进行处理

有一种方式可以忽略错误,就是用下划线接收返回值。

代码语言:javascript复制
val, _ := someFunctionWhichCanReturnAnError()

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

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

代码语言:javascript复制
func someFunction() error {
  val, err := someFunctionWhichCanReturnAnError()
  
  if err != nil {
    return err
  }
  //处理其他逻辑
}

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

3. 避免重复处理错误

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

代码语言:javascript复制
func someFunction() error {
  if err != nil {
    //记录err日志
    return err
  }
}


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

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

0 人点赞