Skip to content

Commit

Permalink
第七章
Browse files Browse the repository at this point in the history
  • Loading branch information
litianshi committed Jan 11, 2023
1 parent 2762ec9 commit ded5839
Show file tree
Hide file tree
Showing 6 changed files with 144 additions and 144 deletions.
12 changes: 6 additions & 6 deletions chapter/7-error-management/7-2-Ignoring-when-to-wrap-an-error.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ func Foo() error {

```go
if err != nil {
return err
return err
}
```

Expand All @@ -49,11 +49,11 @@ if err != nil {

```go
type BarError struct {
Err error
Err error
}

func (b BarError) Error() string {
return "bar failed:" + b.Err.Error()
return "bar failed:" + b.Err.Error()
}
```

Expand All @@ -73,7 +73,7 @@ if err != nil {

```go
if err != nil {
return fmt.Errorf("bar failed: %w", err)
return fmt.Errorf("bar failed: %w", err)
}
```

Expand All @@ -95,11 +95,11 @@ if err != nil {

![](https://img.exciting.net.cn/44.png)

有关问题根源的信息仍然可用。但是,调用者无法解开此错误并检查源是否为 `bar error`。因此,从某种意义上说,此选项比 `%w` 更具限制性。 但是,既然 `%w` 指令已经发布,我们应该阻止这种情况吗? 不必要。
有关问题根源的信息仍然可用。但是,调用者无法解开此错误并检查源是否为 `bar error`。因此,从某种意义上说,此选项比 `%w` 更具限制性。但是,既然 `%w` 指令已经发布,我们应该阻止这种情况吗?不必要。

包装错误使调用者可以使用源错误。因此,这意味着引入潜在的耦合。例如,假设我们使用包装并且 `Foo` 的调用者检查源错误是否为 `bar error`。现在,如果我们想改变我们的实现并使用另一个会返回另一种错误的函数?它将破坏调用者所做的错误检查。

如果我们想确保我们的客户不依赖我们考虑的东西作为实现细节,那么返回的错误不应该被包装,而是类型转换。在这种情况下,使用 `%v` 而不是 `%w` 可能是要走的路
如果我们想确保我们的客户不依赖我们考虑的东西作为实现细节,那么返回的错误不应该被包装,而是类型转换。在这种情况下,使用 `%v` 来取代可能使用 `%w` 的地方

因此,让我们总结一下我们处理的所有不同选项:

Expand Down
106 changes: 53 additions & 53 deletions chapter/7-error-management/7-3-Comparing-an-error-type-inaccurately.md
Original file line number Diff line number Diff line change
@@ -1,35 +1,35 @@
## 7.3 不准确的错误类型比较
## 7.3 错误类型比较的坑

上一节介绍了一种使用 `%w` 指令包装错误的可能方法。然而,一旦我们开始使用它,改变我们检查特定错误类型的方式也很重要。否则,我们可能会不准确地处理错误
上一节介绍了一种使用 `%w` 指令包装错误的可能方法。然而,一旦我们开始使用它,改变我们检查特定错误类型的方式也很重要。否则,我们可能很难识别错误类型

让我们讨论一个具体的例子。我们将编写一个 HTTP 处理程序以从 ID 返回交易金额。我们的处理程序将解析请求以获取 ID 并从数据库中检索金额。我们的实现可能在两种情况下失败:
让我们讨论一个具体的例子。我们将编写一个 HTTP 处理程序从 ID 返回交易金额。我们的处理程序将解析请求以获取 ID 并从数据库中检索金额。我们的实现可能在两种情况下失败:

* 如果 ID 无效(字符串长度不是五个字符)
* 如果程序数据库失败

在前一种情况下,我们想要返回 StatusBadRequest (400),而在后一种情况下,我们想要 ServiceUnavailable (503)。为此,我们将创建一个 `transientError` 类型来标记错误是暂时的。父处理程序将检查错误类型。如果错误是 `transientError `错误,它将返回 503;否则,400
在前一种情况下,我们想要返回 StatusBadRequest (400),而在后一种情况下,我们想要 ServiceUnavailable (503)。为此,我们将创建一个 `transientError` 类型来标记错误是暂时的。父处理程序将检查错误类型。如果错误是 `transientError` 错误,它将返回 503;否则返回400

让我们首先关注错误类型定义和处理程序将调用的函数:

```go
type transientError struct {
err error
err error
}

func (t transientError) Error() string {
return fmt.Sprintf("transient error: %v", t.err)
return fmt.Sprintf("transient error: %v", t.err)
}

func getTransactionAmount(transactionID string) (float32, error) {
if len(transactionID) != 5 {
return 0, fmt.Errorf("id is invalid: %s", transactionID)
}

amount, err := getTransactionAmountFromDB(transactionID)
if err != nil {
return 0, transientError{err: err}
}
return amount, nil
if len(transactionID) != 5 {
return 0, fmt.Errorf("id is invalid: %s", transactionID)
}

amount, err := getTransactionAmountFromDB(transactionID)
if err != nil {
return 0, transientError{err: err}
}
return amount, nil
}
```

Expand All @@ -39,45 +39,45 @@ func getTransactionAmount(transactionID string) (float32, error) {

```go
func handler(w http.ResponseWriter, r *http.Request) {
transactionID := r.URL.Query().Get("transaction")

amount, err := getTransactionAmount(transactionID)
if err != nil {
switch err := err.(type) {
case transientError:
http.Error(w, err.Error(), http.StatusServiceUnavailable)
default:
http.Error(w, err.Error(), http.StatusBadRequest)
}
return
transactionID := r.URL.Query().Get("transaction")

amount, err := getTransactionAmount(transactionID)
if err != nil {
switch err := err.(type) {
case transientError:
http.Error(w, err.Error(), http.StatusServiceUnavailable)
default:
http.Error(w, err.Error(), http.StatusBadRequest)
}
return
}

// Write response
// Write response
}
```

使用错误类型的 `switch`,我们返回适当的 HTTP 状态代码:400 表示错误请求或 503 表示暂时错误。

此代码完全有效。但是,现在让我们假设我们想要对 `getTransactionAmount` 执行一个小的重构。`TransactionError` 不会由 `getTransactionAmount` 返回,而是由 `getTransactionAmountFromDB`返回。`getTransactionAmount` 现在将使用 `%w` 指令包装此错误:
此代码完全有效。但是,现在让我们假设我们想要对 `getTransactionAmount` 执行一个小的重构。`TransactionError` 不会由 `getTransactionAmount` 返回,而是由 `getTransactionAmountFromDB` 返回。`getTransactionAmount` 现在将使用 `%w` 指令包装此错误:

```go
func getTransactionAmount(transactionID string) (float32, error) {
// Check transaction ID validity

amount, err := getTransactionAmountFromDB(transactionID)
if err != nil {
return 0, fmt.Errorf("failed to get transaction %s: %w",
transactionID, err)
}
return amount, nil
// Check transaction ID validity

amount, err := getTransactionAmountFromDB(transactionID)
if err != nil {
return 0, fmt.Errorf("failed to get transaction %s: %w",
transactionID, err)
}
return amount, nil
}

func getTransactionAmountFromDB(transactionID string) (float32, error) {
// ...
if err != nil {
return 0, transientError{err: err}
}
// ...
// ...
if err != nil {
return 0, transientError{err: err}
}
// ...
}
```

Expand All @@ -99,23 +99,23 @@ func getTransactionAmountFromDB(transactionID string) (float32, error) {

```go
func handler(w http.ResponseWriter, r *http.Request) {
// Get transaction ID

amount, err := getTransactionAmount(transactionID)
if err != nil {
if errors.As(err, &transientError{}) {
http.Error(w, err.Error(), http.StatusServiceUnavailable)
} else {
http.Error(w, err.Error(), http.StatusBadRequest)
}
return
// Get transaction ID

amount, err := getTransactionAmount(transactionID)
if err != nil {
if errors.As(err, &transientError{}) {
http.Error(w, err.Error(), http.StatusServiceUnavailable)
} else {
http.Error(w, err.Error(), http.StatusBadRequest)
}
return
}

// Write response
// Write response
}
```

在这个新版本中,我们去掉了 switch case 类型,现在使用 `errors.As`。这个函数要求第二个参数(目标错误)是一个指针。否则,该函数将编译但在运行时出现恐慌。使用 `errors.As`,无论运行时错误是直接是 `transientError` 类型还是错误包装的 `transientError` ,它都能匹配然后返回 503。
在这个新版本中,我们去掉了 switch case 类型,现在使用 `errors.As`。这个函数要求第二个参数(目标错误)是一个指针。否则,该函数将编译但在运行时出现 panic。使用 `errors.As`,无论运行时错误是直接是 `transientError` 类型还是错误包装的 `transientError`,它都能匹配然后返回 503。

综上所述,如果我们依赖 Go 1.13 的错误包装,我们必须使用 `errors.As` 来检查错误是否为特定类型。这样,无论错误是由我们调用的函数直接返回还是包装在错误中,`error.As` 都将能够递归地解开我们的主要错误并查看其中一个错误是否是特定类型。

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
## 7.4 不准确地比较错误值
## 7.4 错误值比较的坑

本节类似于我们刚刚讨论的那一节,但带有标记错误(错误值)。 首先,我们将定义标记错误传达的信息。 然后,我们将看到如何将错误与值进行比较。
本节类似于我们刚刚讨论的那一节,但带有标记错误(错误值)。首先,我们将定义标记错误传达的信息。然后,我们将看到如何将错误与值进行比较。

标记错误是定义为全局变量的错误:

Expand All @@ -10,7 +10,7 @@ import "errors"
var ErrFoo = errors.New("foo")
```

通常,约定是以 `Err` 开头,后跟错误类型;这里 `ErrFoo`,标记错误传达了 *预期* 的错误。但是我们所说的预期错误是什么意思呢?让我们在 SQL 库的上下文中讨论它。
通常,约定是以 `Err` 开头,后跟错误类型;这里 `ErrFoo`标记错误传达了 *预期* 的错误。但是我们所说的预期错误是什么意思呢?让我们在 SQL 库的上下文中讨论它。

我们想设计一个 `Query` 方法,允许对数据库执行查询。此方法返回行的一部分。找不到行的情况下应该怎么处理呢?这里我们有两个选择:

Expand All @@ -22,7 +22,7 @@ var ErrFoo = errors.New("foo")
如果我们看一下标准库,我们会发现很多标记错误的例子:

* `sql.ErrNoRows`:当查询没有返回任何行时返回(这正是我们的情况)
* `io.EOF`:当没有更多输入时由 `io.Reader` 返回可用的
* `io.EOF`:当没有更多合法的输入时,返回 `io.Reader`

这就是标记错误背后的一般原则。它们传达了客户期望检查的预期错误。因此,作为一般准则:

Expand Down Expand Up @@ -51,11 +51,11 @@ if err != nil {
```go
err := query()
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
// ...
} else {
// ...
}
if errors.Is(err, sql.ErrNoRows) {
// ...
} else {
// ...
}
}
```

Expand Down
74 changes: 37 additions & 37 deletions chapter/7-error-management/7-5-Handling-an-error-twice.md
Original file line number Diff line number Diff line change
@@ -1,36 +1,36 @@
## 7.5 两次处理错误
## 7.5 重复处理了两次错误

多次处理错误是开发人员经常犯的错误,而不是专门在 Go 中。让我们了解为什么这是一个问题以及如何有效地处理错误。

为了说明这个问题,让我们编写一个 `GetRoute` 函数来获取从一对源到一对目标坐标的路线。假设此函数将调用未导出的 `getRoute` 函数,其中包含计算最佳路线的业务逻辑。在调用 `getRoute` 之前,我们必须使用 `validateCoordinates` 验证源坐标和目标坐标。我们还希望记录可能的错误。这是一个可能的实现:

```go
func GetRoute(srcLat, srcLng, dstLat, dstLng float32) (Route, error) {
err := validateCoordinates(srcLat, srcLng)
if err != nil {
log.Println("failed to validate source coordinates")
return Route{}, err
}

err = validateCoordinates(dstLat, dstLng)
if err != nil {
log.Println("failed to validate target coordinates")
return Route{}, err
}

return getRoute(srcLat, srcLng, dstLat, dstLng)
err := validateCoordinates(srcLat, srcLng)
if err != nil {
log.Println("failed to validate source coordinates")
return Route{}, err
}

err = validateCoordinates(dstLat, dstLng)
if err != nil {
log.Println("failed to validate target coordinates")
return Route{}, err
}

return getRoute(srcLat, srcLng, dstLat, dstLng)
}

func validateCoordinates(lat, lng float32) error {
if lat > 90.0 || lat < -90.0 {
log.Printf("invalid latitude: %f", lat)
return fmt.Errorf("invalid latitude: %f", lat)
}
if lng > 180.0 || lng < -180.0 {
log.Printf("invalid longitude: %f", lng)
return fmt.Errorf("invalid longitude: %f", lng)
}
return nil
if lat > 90.0 || lat < -90.0 {
log.Printf("invalid latitude: %f", lat)
return fmt.Errorf("invalid latitude: %f", lat)
}
if lng > 180.0 || lng < -180.0 {
log.Printf("invalid longitude: %f", lng)
return fmt.Errorf("invalid longitude: %f", lng)
}
return nil
}
```

Expand Down Expand Up @@ -79,25 +79,25 @@ func validateCoordinates(lat, lng float32) error {
2021/06/01 20:35:12 invalid latitude: 200.000000
```

这个新的 Go 版本的代码完美吗?并不真地。例如,第一个实现在无效纬度的情况下导致两个日志。不过,我们知道哪个对 `validateCoordinates` 的调用失败了:源坐标或目标坐标。在这里,我们丢失了这些信息。因此,我们需要为错误添加额外的上下文。
这个新的 Go 版本的代码完美吗?还不是最完美。例如,第一个实现在无效纬度的情况下返回两个日志。不过,我们知道哪个对 `validateCoordinates` 的调用失败了:源坐标或目标坐标。在这里,我们丢失了这些信息。因此,我们需要为错误添加额外的上下文。

让我们使用 Go 1.13 错误包装重写最新版本的代码(我们将省略 `validateCoordinates`,因为它将保持不变):

```go
func GetRoute(srcLat, srcLng, dstLat, dstLng float32) (Route, error) {
err := validateCoordinates(srcLat, srcLng)
if err != nil {
return Route{},
fmt.Errorf("failed to validate source coordinates: %w", err)
}

err = validateCoordinates(dstLat, dstLng)
if err != nil {
return Route{},
fmt.Errorf("failed to validate target coordinates: %w", err)
}

return getRoute(srcLat, srcLng, dstLat, dstLng)
err := validateCoordinates(srcLat, srcLng)
if err != nil {
return Route{},
fmt.Errorf("failed to validate source coordinates: %w", err)
}

err = validateCoordinates(dstLat, dstLng)
if err != nil {
return Route{},
fmt.Errorf("failed to validate target coordinates: %w", err)
}

return getRoute(srcLat, srcLng, dstLat, dstLng)
}
```

Expand Down
12 changes: 6 additions & 6 deletions chapter/7-error-management/7-6-Not-handling-a-error.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
## 7.6 不被处理错误
## 7.6 忽略错误,引入的坑

在某些情况下,我们可能希望忽略函数返回的错误。如果我们遇到这种情况,在 Go 中应该只有一种方法。让我们明白为什么
在某些情况下,我们可能希望忽略函数返回的错误。如果我们遇到这种情况,在 Go 中应该只有一种方法。让我们搞明白为什么

我们将考虑以下示例,其中我们调用一个返回单个 `error` 参数的 `notify` 函数。然而,由于我们对这个错误不感兴趣,我们将故意省略任何错误处理:
我们思考以下示例,其中我们调用一个返回单个 `error` 参数的 `notify` 函数。然而,由于我们对这个错误不感兴趣,我们将故意省略任何错误处理:

```go
func f() {
Expand All @@ -17,9 +17,9 @@ func notify() error {

因为我们想忽略错误,所以在这个例子中,我们只是调用了 `notify` 而没有将其输出分配给经典的 `err` 变量。从功能的角度来看,这段代码没有任何问题:它将按预期编译和运行。

但是,从可维护性的角度来看,它可能会导致一些问题。让我们考虑一个新读者到达此代码。这位读者注意到 `notify` 返回一个错误,但它没有由父函数处理。他怎么能猜到是不是故意的?他怎么知道之前的开发者是忘记处理还是故意的?
但是,从可维护性的角度来看,它可能会导致一些问题。让我们考虑一个新读者到达此代码。这位读者注意到 `notify` 返回一个错误,但它没有由上级函数处理。他怎么能猜到是不是故意的?他怎么知道之前的开发者是忘记处理还是故意的?

由于这些原因,当我们想要忽略 Go 中的错误时,只有一种方法可以编写它
由于这些原因,当我们想要忽略 Go 中的错误时,有一种语法糖

```go
_ = notify()
Expand All @@ -42,6 +42,6 @@ _ = notify()
_ = notify()
```

忽略 Go 中的错误应该是例外的。在许多情况下,我们可能仍然倾向于记录它们,即使是 在较低的日志级别。然而,如果我们确定一个错误可以并且应该被忽略,我们必须通过将它分配给空白标识符来明确地做到这一点。这样,未来的读者就会明白这是故意的。
忽略 Go 中的错误应该是例外的。在许多情况下,我们可能仍然倾向于记录它们,即使是在较低的日志级别。然而,如果我们确定一个错误可以并且应该被忽略,我们必须通过将它分配给空白标识符来明确地做到这一点。这样,未来的读者就会明白这是故意的。

现在让我们讨论本章的最后一部分,如何处理 defer 函数返回的错误。
Loading

0 comments on commit ded5839

Please sign in to comment.