云原生探索系列(十三):Go 语言错误处理

198 阅读8分钟

前言

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. 最后

上面只是理论知识,多看优秀的源码,看看别人是如何进行错误处理的。 在实践中,合理进行错误处理,才可以让错误处理变得更加灵活和高效。