# Go 中的错误处理


## Go 错误基本概念

在 Go 中，错误被视为一种值，并且被设计成普通的接口，而不是像其他语言中的异常。这种设计鼓励程序员明确地处理每一个可能的错误，并使代码更加清晰和预测。

### error 接口

`error` 是 Go 中的一个基本接口，用于表示一个错误条件，其定义如下：

```go
type error interface {
    Error() string
}
```

只要一个类型提供了这个 `Error() string` 方法，那么它就实现了 `error` 接口，可以被视为一个错误。这种方法允许开发者创建各种错误类型，只要它们都实现了该方法。

### 内建的错误实现

Go 提供了一些内建的工具来创建和操作错误。

- **errors.New**: 这是创建新错误的最简单方法。给定一个字符串描述，它会返回一个新的错误，这个错误会返回给定的字符串作为它的 `Error()` 方法的输出。

  ```go
  err := errors.New("my error description")
  ```

- **fmt.Errorf**: 这是一个更灵活的创建错误的方式，允许你像使用 `fmt.Printf` 那样格式化错误消息。
  ```go
  val := 42
  err := fmt.Errorf("my error with value: %d", val)
  ```

这些内建的方法为错误处理提供了基础，但 Go 的生态系统还提供了更多的工具和模式来帮助开发者更好地处理、传播和记录错误。

## 错误处理模式

在 Go 中，错误处理是编写健壮和可维护代码的核心组成部分。Go 不使用传统的异常处理机制，而是选择明确地返回错误作为函数的返回值。这种方法鼓励开发者更加明确地处理错误，但也产生了几种常见的错误处理模式。

### 传统的 "if err != nil" 检查

这是 Go 中最基本也是最常见的错误处理模式。当函数返回一个错误时，你会立即检查这个错误，然后决定如何处理它。

```go
file, err := os.Open("filename.txt")
if err != nil {
    log.Fatalf("failed opening file: %s", err)
}
defer file.Close()
```

这种方法的优势在于它很直接、明确。你总是知道在什么情况下会检查错误，以及当发生错误时会发生什么。

### 使用 defer 进行错误处理

`defer` 语句允许你延迟函数的执行，通常用于资源的清理操作，如文件关闭、解锁资源等。与错误处理结合，你可以确保在函数退出时始终执行某些操作，无论函数是正常退出还是因为错误而返回。

一个常见的例子是确保已打开的文件在函数结束时关闭：

```go
file, err := os.Open("filename.txt")
if err != nil {
    log.Fatalf("failed opening file: %s", err)
}
defer file.Close()
```

此外，有些库提供了支持错误处理的延迟函数，这允许你在 `defer` 语句中处理错误。

### 使用多值返回来同时返回结果和错误

Go 支持多值返回，这使得函数可以同时返回结果和错误。这是 Go 错误处理的核心模式，因为它允许调用者明确地处理错误，同时还可以接收函数的结果。

例如，一个函数可能会这样定义和调用：

```go
func divide(a, b float64) (float64, error) {
    if b == 0 {
        return 0, errors.New("division by zero")
    }
    return a / b, nil
}

result, err := divide(5, 0)
if err != nil {
    log.Println("Error:", err)
    return
}
fmt.Println("Result:", result)
```

这种方法的优势是它强制调用者处理错误，因为他们必须接收和检查返回的错误值。

## 常见的错误类型

### 哨兵错误（Sentinel Errors）

哨兵错误是预先声明的特定错误，通常用作特定函数调用失败时的返回值。它们是已命名的值，全局可见，并由包的使用者直接检查。

例如，当你到达文件或输入流的末尾时，Go 的 `io` 包会返回 `io.EOF` 错误。哨兵错误是明确的，调用者可以使用 `if err == io.EOF` 来检查它。

优点:

- 易于使用，因为它是一个已知的值。

缺点:

- 过度使用可能导致 API 与具体错误值紧密耦合，限制了将来的变更。
- 可能导致错误处理代码过于分散。

以下是 Go 标准库中的一些哨兵错误类型的示例：

- [io.EOF](https://pkg.go.dev/io#pkg-variables): 这可能是最知名的哨兵错误。当在输入结束时尝试从 io.Reader 读取数据时，它表示没有更多的数据可读。
- [os.ErrExist and os.ErrNotExist](https://pkg.go.dev/os#pkg-variables): 当文件或目录存在或不存在时，这些错误会被返回，例如在尝试创建一个已存在的文件或查找一个不存在的文件时。
- [io.ErrShortWrite](https://pkg.go.dev/io#ErrShortWrite): 当只有部分数据被写入 io.Writer 时，返回此错误。
- [io.ErrUnexpectedEOF](https://pkg.go.dev/io#ErrUnexpectedEOF): 当在读取更多的数据之前输入结束时，返回此错误。

### 自定义错误类型（Custom Error Types）

自定义错误类型是一种特殊的错误，它们是实现了 `error` 接口的自定义类型。与哨兵错误不同，这些错误类型可以携带更多的上下文信息。

例如，你可以定义一个表示网络错误的类型，它包含一个状态码和一个错误消息：

```go
type NetworkError struct {
    Code    int
    Message string
}

func (e *NetworkError) Error() string {
    return fmt.Sprintf("Network Error [%d]: %s", e.Code, e.Message)
}
```

优点:

- 可以提供关于错误的更多上下文。
- 更加灵活，易于修改和扩展。

缺点:

- 需要更多的代码来定义和处理。

以下是 Go 标准库中的一些自定义错误类型的示例：

- [net.DNSError](https://pkg.go.dev/net#DNSError): 当查找域名时，此错误表示 DNS 查询的问题。

  ```go
  type DNSError struct {
      Name    string
      Server  string
      IsTimeout bool
      // ... 其他字段
  }

  func (e *DNSError) Error() string { /*...*/ }
  ```

- [os.SyscallError](https://pkg.go.dev/os#SyscallError): 当系统调用失败时，此错误提供有关调用和原因的详细信息。

  ```go
  type SyscallError struct {
      Syscall string
      Err     error
  }

  func (e *SyscallError) Error() string { return e.Syscall + ": " + e.Err.Error() }
  ```

  [os.SyscallError 源码链接](https://golang.org/pkg/os/#SyscallError)

- [json.SyntaxError](https://pkg.go.dev/encoding/json#SyntaxError): 此错误表示在解析 JSON 时发生的语法问题。

  ```go
  type SyntaxError struct {
      Offset int64
      // contains filtered or unexported fields
  }

  func (e *SyntaxError) Error() string { /*...*/ }
  ```

这些自定义错误类型允许库提供关于发生的错误更多的上下文。调用者可以通过类型断言来检查和处理这些特定类型的错误，并获取错误的额外字段或方法。

## 错误包装与展开

错误包装与展开是 Go 1.13 中引入的特性，使得开发者可以更灵活地处理、检查和传递错误信息。

Wrap errors 主要用于以下几个目的：

- **记录错误**：确保错误信息被记录到日志中。
- **确保应用完整性**：在处理错误的同时，确保应用程序的完整性达到 100%。
- **防止错误重复报告**：避免在多处重复报告同一错误。

### 使用 `%w` 进行错误包装

在 Go 中，你可能想要添加上下文到一个错误中，使其更具有描述性。为了实现这个目的，你可以使用 `fmt.Errorf` 函数中的 `%w` 格式符来“包装”一个错误。

示例：

```go
func someFunction() error {
    if err := anotherFunction(); err != nil {
        return fmt.Errorf("an error occurred: %w", err)
    }
    return nil
}
```

在上面的例子中，如果 `anotherFunction()` 返回一个错误，那么我们会使用 `%w` 将这个错误“包装”在一个新的错误中，并添加一些额外的上下文。

#### 正确的使用 wrap 的时机

错误包装（error wrapping）是指将错误包装在一个容器中，该容器还会提供原始错误。这在 Golang 中被称为 **error chaining**。错误包装通常用于以下两种情况：

- 添加额外的上下文信息到错误中
- 将错误标记为特定的错误

```go
// Custom error type
type MyError struct {
	message string
}

func (e *MyError) Error() string {
	return e.message
}

func returnDirectError() error {
	return errors.New("this is a direct error")
}

func returnCustomError() error {
	return &MyError{message: "this is a custom error"}
}

func returnFmtErrorWithW(err error) error {
	return fmt.Errorf("this is a wrapped error: %w", err)
}

func returnFmtErrorWithV(err error) error {
	return fmt.Errorf("this is a non-wrapped error: %v", err)
}
```

| Option                 | Extra context | Marking an error | Source error available |
| ---------------------- | ------------- | ---------------- | ---------------------- |
| **直接返回错误**       | ❌            | ❌               | ✅                     |
| **自定义错误类型**     | ✅            | ✅               | ✅                     |
| **fmt.Errorf with %w** | ✅            | ❌               | ✅                     |
| **fmt.Errorf with %v** | ✅            | ❌               | ❌                     |

1. **Option**：列出了不同的错误处理和创建方法。
2. **Extra context**：描述了是否可以为错误添加额外的上下文。例如，给出详细的错误信息或额外的状态数据。
3. **Marking an error**：描述了是否可以将错误标记为某种特定类型。这在后续处理中可能非常有用，因为它允许我们基于错误的类型或标记采取特定的行动。
4. **Source error available**：描述了原始错误是否可用，即是否可以从包装或标记的错误中提取原始错误。

### 使用 `errors.Is` 和 `errors.As` 进行错误展开和检查

1. **errors.Is**

这个函数允许你检查一个错误值是否与一个特定的错误相匹配，即使它可能已经被包装在其他错误中。

示例：

```go
var myErr = errors.New("my error")

err := fmt.Errorf("an error occurred: %w", myErr)
if errors.Is(err, myErr) {
    fmt.Println("The error is myErr!")
}
```

在上面的例子中，即使我们的 `err` 是一个包装了 `myErr` 的新错误，`errors.Is` 也能正确地检测出其原始错误是 `myErr`。

2. **errors.As**

这个函数用于检查包装的错误是否可以赋值给一个特定的错误类型。它是类型安全的，并且在错误链中查找匹配的错误。

示例：

```go
type MyCustomError struct {
    Msg string
}

func (e *MyCustomError) Error() string {
    return e.Msg
}

func main() {
    var customErr = &MyCustomError{"this is a custom error"}
    err := fmt.Errorf("an error occurred: %w", customErr)

    var target *MyCustomError
    if errors.As(err, &target) {
        fmt.Println("The error is of type MyCustomError:", target.Msg)
    }
}
```

在上面的例子中，我们首先定义了一个自定义错误类型 `MyCustomError`。然后我们使用 `%w` 创建了一个包装了这个自定义错误的新错误。最后，我们使用 `errors.As` 来检查这个包装的错误是否为 `MyCustomError` 类型，并成功地获取了它的原始值。

### pkg/errors（Public archive）

`pkg/errors` 是 Go 社区中一个非常受欢迎的错误处理包。尽管 Go 1.13 中对标准库的 `errors` 包进行了扩展，加入了类似的功能，但 `pkg/errors` 仍然有其特色和用途。以下是对 `pkg/errors` 的简要介绍：

**主要功能**：

1. **创建新的错误**：

   - `errors.New`：基础的创建新错误的函数，类似于标准库中的同名函数。
   - `errors.Errorf`：带有格式化功能的创建错误的函数，可以像 `fmt.Errorf` 那样使用。

2. **错误包装（Wrap errors）**：

   - `errors.Wrap` 和 `errors.Wrapf`：这两个函数都可以将现有的错误包装起来，同时添加描述信息。最重要的是，它们还会记录当前的堆栈信息，这对于后期调试是非常有价值的。

3. **获取错误原因和堆栈信息**：
   - `errors.Cause`：这个函数可以从包装过的错误中提取出原始的错误（或称为“根错误”）。
   - 使用 `%+v` 与 `fmt` 函数（如 `fmt.Println`）一同使用，可以打印出错误的详细堆栈信息。

**使用场景与建议**：

- 当你希望除了错误消息外，还保留错误发生的上下文和堆栈信息时，使用 `pkg/errors` 会非常有帮助。

- 在应用代码的更低层（如数据访问层或第三方服务接口）使用 `errors.Wrap` 或 `errors.Wrapf` 来包装返回的错误。这样，在更上层，你可以利用堆栈信息更容易地识别和定位错误发生的位置。

- 在应用的顶层或主函数中，你可以使用 `errors.Cause` 来检查错误是否匹配预期的 sentinel errors 或特定的错误类型。

**最佳实践**：

1. **使用基本错误构造**：在应用的代码中，如果需要构造新的错误，使用 `errors.New` 或 `errors.Errorf`。
2. **在跨包调用时直接返回错误**：当你的函数或方法调用其他包中的函数或方法时，如果出现错误，通常应直接返回该错误，而不是进行额外的包装。
3. **保存堆栈信息**：当你需要与其他库或标准库协作时，考虑使用 `errors.Wrap` 或 `errors.Wrapf` 来保存关于错误来源的堆栈信息。
4. **避免冗余的错误记录**：不要在产生错误的每个地方都进行日志记录。相反，将错误信息直接返回，并在更上层进行处理和记录。
5. **记录堆栈详情**：在程序的主函数或工作的 goroutine 的入口点，使用 `%+v` 格式描述符来记录完整的错误堆栈详情。
6. **获取并比较根错误**：当需要检查一个特定的基础错误时，使用 `errors.Cause` 来获取最底层的原因，并与已知的 sentinel error 进行比较。

## Panic and Recover

在 Go 语言中，`panic` 和 `recover` 是两个用于处理运行时错误和异常情况的内建函数。它们为开发者提供了一种处理非预期错误的机制，但在常规的错误处理中，我们更推荐使用 `error` 类型。下面来详细介绍这两个函数：

### panic

`panic` 是一个内建函数，它中止当前函数的执行，并开始在当前的 Goroutine 中进行 panic。当函数 F 调用 `panic` 时，F 的执行会立即停止，同时 F 中的任何 `defer` 函数都会执行。然后 F 返回到调用它的函数，在那里也执行相同的过程，直到当前的 Goroutine 中所有函数都返回，此时程序会终止。除非此 panic 被 recover 捕获，否则程序将显示 panic 信息并退出。

示例：

```go
func main() {
    testPanic()
    fmt.Println("This will not be printed!")
}

func testPanic() {
    defer fmt.Println("Defer in testPanic")
    panic("Panic in testPanic")
    fmt.Println("This will not be executed!")
}
```

#### 何时应用 panic

在 Go 语言中，`panic` 是用来处理非常特殊的错误情况的，它不同于常规的错误处理方式，如通过 `error` 类型来返回错误。以下是对您提到的两种情境的进一步讨论：

1. **程序性错误 (Programmer Errors)**: 不想让进程继续执行

   - 这些是由于编程错误引起的，而不是预期的运行时错误。例如，传递一个无效的 HTTP 状态码或尝试注册一个已经存在的数据库驱动。
   - 这些错误通常是由于程序员的疏忽，或者在某些情况下，是对 API 的误用。
   - 对于这类错误，你期望调用者立即知道并修复错误，而不是运行时尝试恢复或忽略它。这就是为什么使用 `panic` 的原因，因为它会导致程序崩溃（除非使用 `recover` 捕获）。

2. **强依赖 (Critical Dependencies)**: 程序启动
   - 这些是程序必须要满足的条件，如果不满足，程序就不能正确运行。例如，如果一个 web 服务启动时不能连接到其数据库，那么它可能会 `panic`，因为它不能在没有数据库的情况下正常工作。
   - 在这种情况下，程序无法继续执行，并且最好停止运行，而不是尝试继续在一个已知不稳定或不可靠的状态下运行。
   -

### recover

`recover` 也是一个内建函数，它可以让你从 panic 的状态中恢复。如果与一个 `defer` 语句一同使用，可以捕获到 `panic` 的输入参数，从而允许程序从 panic 中恢复。

`recover` 只有在与 `defer` 一同使用时才有效。在正常的执行过程中调用 `recover`，它将返回 `nil`。但如果当前的 Goroutine 由于 `panic` 而崩溃，调用 `recover` 将捕获 `panic` 的输入参数，并恢复正常的执行。

示例：

```go
func main() {
    handlePanic()
    fmt.Println("Program recovered and continues to execute!")
}

func handlePanic() {
    defer func() {
        if r := recover(); r != nil {
            fmt.Println("Recovered from panic:", r)
        }
    }()
    panic("A severe error occurred!")
    fmt.Println("This will not be executed!")
}
```

尽管 `panic` 和 `recover` 为处理异常情况提供了一种机制，但在 Go 社区中，我们推荐使用 `error` 类型来处理错误，并保留 `panic` 用于真正的异常情况，例如程序的不可恢复的状态。

> [!question] 为什么 `recover` 只有在与 `defer` 一同使用时才有效？
>
> `recover()` 函数设计的初衷是为了从 `panic` 中恢复正常执行，并给予程序一个处理错误或清理资源的机会。它的工作方式与 `panic` 有关，只有当程序处于 `panic` 状态时，`recover()` 才能捕获到该 `panic`。
>
> 为什么 `recover` 只在 `defer` 中有效有以下原因：
>
> 1. **执行流程**：当一个函数中出现 `panic`，该函数的执行会立即停止，并运行任何 `defer` 语句。如果没有在 `defer` 中调用 `recover()`，那么 `panic` 会继续在调用堆栈中传播，直到程序终止或者在某个 `defer` 中调用了 `recover()`。
>
> 2. **栈展开**：当 `panic` 发生时，Go 会开始展开栈，即它会停止当前函数的执行并开始执行该函数的所有 `defer` 语句。如果在这个过程中没有调用 `recover()`，`panic` 会继续向上在调用堆栈中展开。
>
> 3. **恢复的时机**：由于 `recover` 是为了捕获和处理 `panic` 而设计的，所以它只在 `panic` 进行时有意义。在普通函数中调用 `recover()` 会返回 `nil`，因为没有 `panic` 可以恢复。而 `defer` 保证了其内部的代码在函数退出或发生 `panic` 时都会执行，因此它为 `recover` 提供了正确的时机来捕获 `panic`。
>
> 为了简单总结，`recover` 之所以只在 `defer` 中有效，是因为 `defer` 提供了在 `panic` 时执行代码的机制，这使得 `recover` 有机会在正确的时机捕获和处理 `panic`。

## 忽略错误

当我们谈论“不处理错误”时，我们实际上是指在程序中遇到错误后，我们选择不对其采取任何措施。这在 Go 中并不是推荐的做法，因为 Go 鼓励明确地处理所有可能出现的错误。但在某些情况下，开发人员可能会选择不处理错误，原因可能有以下几点：

1. **预期的错误**：在某些情况下，错误可能是预期之内的，开发者可能已经知道这种错误会发生，并且知道它对程序的运行没有实质性的影响。
2. **日志记录而不是处理**：在某些情况下，开发人员可能选择记录错误而不是处理它，这样他们可以后续分析它，而不是立即对其采取措施。
3. **忽略不重要的错误**：有时，某些错误可能不会对程序的核心功能产生影响，因此可以被安全地忽略。
4. **疏忽**：这并不是一个好的理由，但有时开发人员可能会不小心遗漏错误处理。

忽略错误的示例：

```go
  // 安全地忽略错误并添加为什么忽略的注释
  _ = err
```

## 总结

**`panic` 与 `recover` 使用指南：**

1. 限制使用：在绝大多数场景中，建议使用 `error` 类型，而不是 `panic` 和 `recover` 来处理错误。
2. 异常情况：只有在程序无法继续运行的场景下才使用 `panic`，如初始化失败或不应出现的错误。
3. 不替代常规错误：如文件未找到时，不应使用 `panic`，而应返回一个 `error`。
4. 中间件和框架：在可能的顶部位置使用 `recover` 来捕捉 `panic`，以确保整体程序稳定性。
5. `defer` 中的 `recover`：仅在 `defer` 中可以捕获 `panic`。
6. 提供详尽信息：捕获 `panic` 后，记录详细信息，如堆栈信息。
7. 资源清理：使用 `defer` 确保资源，如文件、网络连接等，被正确释放。
8. 重新抛出 `panic`：有时可能需要重新抛出，但需明确原因并理解可能的后果。
9. 避免在库中使用：编写供他人使用的库时，尽量不要使用 `panic`。
10. 文档说明：确保在文档中明确标明什么情况下会出现 `panic`。

**Go 错误处理最佳实践：**

1. 限制哨兵错误：过多的哨兵错误可能导致代码紧耦合。如需使用，务必清晰定义。
2. 提供明确上下文：使用错误包装为错误提供明确上下文，方便错误追踪。
3. 返回不透明错误：隐藏实现细节，返回简单的 `error`，增强 API 的灵活性。
4. 使用 `error`：常规错误处理优先选择 `error`，而不是 `panic` 或 `recover`。
5. 返回具体错误类型：提供详细的错误信息，帮助调用者更准确地处理错误。
6. 遵循标准接口：无论使用哪个库，都应遵循标准的 `error` 接口。
7. 明确处理：不应忽略函数返回的错误。如需忽略，使用空标识符明确表示。

总的来说，正确和一致地处理错误是保证 Go 程序健壮性和可维护性的关键。遵循这些最佳实践可以帮助你构建更加健壮、可靠和易于维护的 Go 程序。

## 参考

- https://go.dev/doc/effective_go#errors
- https://go.dev/blog/error-handling-and-go
- https://go.dev/blog/go1.13-errors
- https://go.dev/blog/defer-panic-and-recover
- https://go.dev/doc/tutorial/handle-errors
- https://go.googlesource.com/proposal/+/master/design/29934-error-values.md
- https://go.dev/blog/defer-panic-and-recover

