前言
Go 语言的错误处理与许多其他编程语言不同,它没有异常机制(try-catch)来处理错误,而是采用返回错误值的方式。 本文主要阐述 Go 语言的错误处理机制。
1. Go 的错误处理模型
在 Go 中,错误是通过返回一个 error 类型的值来表示的。 error 类型本质上是一个接口,定义如下:
type error interface {
Error() string
}
error 接口有一个方法 Error ,这个方法不接受任何参数,但会返回 string 类型的结果。
2. 生成 error 类型值
通过 errors.new 的方式:
func main() {
err := errors.New("error")
fmt.Println(err) // error
}
通过 fmt.Errorf 的方式(模版化的方式):
func main() {
notFound := fmt.Errorf("error: %v", "Not Found")
fmt.Println(notFound) // error: Not Found
}
fmt.Errorf 这个函数本质就是先调用 fmt.Sprintf 函数得到错误信息,然后在调用 errors.new 函数,所以也可以这样写:
func main() {
notFound := errors.New(fmt.Sprintf("error: %v", "Not Found"))
fmt.Println(notFound) // error: Not Found
}
3. 基本的错误处理
在 Go 中,错误通常作为函数的最后一个返回值返回。调用者需要显式地检查错误,并根据需要进行处理。 如果没有错误,返回的 error 值将为 nil。
示例:
func divide(a, b int) (int, error) {
if b == 0 {
return 0, errors.New("division by zero")
}
return a / b, nil
}
func main() {
result, err := divide(10, 0)
if err != nil {
fmt.Println("Error:", err) // Error: division by zero
return
}
fmt.Println("Result:", result)
}
在这段代码中, divide 函数检查除数是否为零,如果是,则返回一个错误。 调用者需要检查返回的错误值,如果不为 nil ,则表示发生了错误。
4. Go 错误的自定义
通过文章开篇的学习,我们都知道 Go 的 error 类型是一个接口,因此我们可以自定义错误类型,提供更丰富的错误信息。 自定义错误类型可以通过实现 Error() 方法来使其符合 error 接口。
示例
type DivideByZeroError struct {
Msg string
}
func (e *DivideByZeroError) Error() string {
return e.Msg
}
func divide(a, b int) (int, error) {
if b == 0 {
return 0, &DivideByZeroError{Msg: "Cannot divide by zero"}
}
return a / b, nil
}
func main() {
result, err := divide(10, 0)
if err != nil {
if divideErr, ok := err.(*DivideByZeroError); ok {
fmt.Println("Custom Error:", divideErr.Error()) // Custom Error: Cannot divide by zero
} else {
fmt.Println("Error:", err)
}
return
}
fmt.Println("Result:", result)
}
这段代码中,我们定义了一个 DivideByZeroError 自定义错误类型,并在 divide 函数中返回该错误。 主函数中通过类型断言检查错误类型,从而执行更精确的错误处理逻辑。 err.(*DivideByZeroError) 这就是类型断言。
5. 如何判断一个错误具体代表哪一类错误?
5.1 对于类型在已知范围内的错误值,一般使用类型断言或者 switch 语句判断
案例:
func underlyingError(err error) error {
switch err := err.(type) {
case *os.PathError:
return err.Err
case *os.LinkError:
return err.Err
case *os.SyscallError:
return err.Err
case *exec.Error:
return err.Err
}
return err
}
func main() {
r, w, err := os.Pipe()
if err != nil {
fmt.Printf("unexpected error: %s\n", err)
return
}
r.Close()
_, err = w.Write([]byte("hi"))
uError := underlyingError(err)
fmt.Printf("underlying error: %s (type: %T)", uError, uError) // underlying error: broken pipe (type: syscall.Errno)
}
underlyingError 函数的参数是一个 error 类型。
- 如果错误是
*os.PathError、*os.LinkError、*os.SyscallError或*exec.Error类型,它们都有一个Err字段,表示底层的具体错误。 - 如果匹配到其中任何一种类型,就返回它的 Err 字段(即底层错误)。
- 如果错误不匹配这些类型,就返回原始的错误。
os.Pipe():这行代码创建了一个管道,它返回两个文件描述符,r是读取端,w是写入端。如果出现错误,err将被赋值为该错误。r.Close():关闭管道的读取端。这里r被关闭,但没有用来读取数据。关闭读取端通常不会导致错误,但在后续的写入操作中,如果发生了错误,它会被捕获。w.Write([]byte("hi")):在这里尝试向管道的写入端写入数据。由于读取端已经关闭,写入操作通常会出错。underlyingError(err):如果在写入操作时发生错误,err将包含该错误。然后调用underlyingError函数来获取底层的错误类型。如果错误是*os.PathError、*os.LinkError等类型的错误,underlyingError会返回底层的错误;否则,返回原始的错误。
只要类型不相同,我们就可以这样分辨。
5.2 有相应变量且类型相同的错误值,一般直接使用判等操作来判断
像下面这样, os 模块中的源码:
var (
ErrInvalid = errors.New("invalid argument")
ErrPermission = errors.New("permission denied")
ErrExist = errors.New("file already exists")
ErrNotExist = errors.New("file does not exist")
ErrClosed = errors.New("file already closed")
)
示例:
var ErrNotFound = errors.New("not found")
func checkError(err error) {
if err == ErrNotFound {
fmt.Println("错误:未找到")
} else if err != nil {
fmt.Println("未知错误:", err)
} else {
fmt.Println("没有错误")
}
}
func main() {
err := ErrNotFound
checkError(err) // 错误:未找到
err = errors.New("something went wrong")
checkError(err) // 未知错误: something went wrong
}
checkError 函数通过直接使用 == 来判断 err 是否等于预定义的错误 ErrNotFound。
5.3 没有相应变量且类型未知的错误值,只能使用其错误信息的字符串表示形式来判断具体属于哪一个错误
示例:
func checkError(err error) {
if err != nil {
if err.Error() == "not found" {
fmt.Println("错误:未找到")
} else if err.Error() == "invalid input" {
fmt.Println("错误:无效输入")
} else {
fmt.Println("未知错误:", err)
}
} else {
fmt.Println("没有错误")
}
}
func main() {
err1 := errors.New("not found")
checkError(err1) // 错误:未找到
err2 := errors.New("invalid input")
checkError(err2) // 错误:无效输入
err3 := errors.New("something went wrong")
checkError(err3) // 未知错误: something went wrong
}
使用 err.Error() 方法获取错误值的字符串表示,然后通过字符串值来判断具体是哪种错误。 这种方式适用于错误值类型未知或无法直接进行比较的情况。
6. 最后
上面只是理论知识,多看优秀的源码,看看别人是如何进行错误处理的。 在实践中,合理进行错误处理,才可以让错误处理变得更加灵活和高效。