在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
}
}
因此,最好的做法是在最开始的调用者那里记录日志。