diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..ffae34df1d2a162c6459d92cc8143a00fe6d489c Binary files /dev/null and b/.DS_Store differ diff --git a/chap-23-errors/.DS_Store b/chap-23-errors/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..5008ddfcf53c02e82d7eee2e57c38e5672ef89f6 Binary files /dev/null and b/chap-23-errors/.DS_Store differ diff --git a/chap-23-errors/errors.md b/chap-23-errors/errors.md new file mode 100644 index 0000000000000000000000000000000000000000..15f41f890ee5df85a799460667f329c5d6be9b1d --- /dev/null +++ b/chap-23-errors/errors.md @@ -0,0 +1,1533 @@ +# 第二十三章:错误 + + + +![](imgs/errors.2a0610d5.jpg) + +---- + + + +## 1 你将在本章学到什么 + +* 什么是错误 +* 如何创建一个错误 +* 如何处理错误 + +## 2 涵盖的技术概念 + +* 错误 +* 错误接口 +* 哨兵错误 +* 黑盒错误 +* 解包错误 + +## 3 错误涵盖四个不同的概念 + +​ IEEE 词汇表 [@institute1990ieee]()列出了术语“错误”的四种不同含义: + +1. 计算/观察/测量值与真实/指定/理论值之间存在的差异。 + + 1. 例如:程序计算两个对象之间的距离。 程序输出30m,但实际距离为28m,因此程序产生了一个错误。 + +2. 程序中的步骤或数据定义不正确。 通常称为“**故障**”。 + + 1. 例如:定义一个函数将字符串作为输入,并将其转换为浮点数。 但是字符串格式错误,导致该函数无法执行转换 + +3. 偏离预期的错误结果。 通常称为“**失败**”。 + + 1. 例如:有一个用来计算特定预订的总价的函数。 当函数运行时,价格是145.99`$`,但是当客户端用计算器计算时,结果是189`$`。 函数给出的结果明显是错误的 + 2. 这很接近于上面第一个对于错误的定义 + +4. 产生错误结果的人为行为。 通常称为“**错误**” + + 1. 例如:要求数据库管理员在生产数据库中插入一行。 他最终删除了整个数据库。 (跑路...) + + 作为程序员,我们需要防止程序中潜在的**错误**和**失败**。 同时我们还可以通过提供更多的文档来避免**错误**。 + +## 4 示例:一个可能失败的程序 + +### 4.1 上下文:存储配置 + +我们的酒店管理软件需要访问一些配置值:酒店名称、国家增值税税率、其数据库系统的用户名和密码等。 + +一种简单的解决方案是将这些配置值直接写入程序。 这种方式虽然可行,但并不推荐; 我们不能将密码存储到程序编译后的二进制文件中,也不能存储增值税率。 如果密码更改或增值税率更改,我们需要重新编译它......除此之外,这是一个安全问题; 密码将直接写入二进制文件中,使其可读。 + +在这个例子中,我们将采用下面这种解决方案来加载文件的配置。 让我们看看如何实现它。 + + + +### 4.2 方案实现 + +首先,先定义一个 go.mod(用 go mod init 初始化): + +```go +module "maximilien-andile.com/errors/error-example" + +go 1.16 +``` + +然后我们建一个配置包: + +```go +// errors/example-failure/config/main.go +package config + +import ( + "fmt" + "io/ioutil" +) + +func load() []byte { + data, err := ioutil.ReadFile("/tmp/myHotelApp/config.txt") + fmt.Println(err) + return data +} + +func Print() { + fmt.Println(string(load())) +} +``` + +我们在这个包中有一个私有的函数:`load`。 它将读取一个文件。 它使用 `ioutil` 包中的 `ReadFile` 函数。 我们将加载的文件由 `path` 标识: `"/tmp/myHotelApp/config.txt"` + +该函数由公共的函数 `Print` 调用。 这将打印从文件中加载的数据。 + +`main`函数将调用 `config.Print`: + +```go +// errors/example-failure/main.go +package main + +import "maximilien-andile.com/errors/error-example/config" + +func main() { + config.Print() +} +``` + +### 4.3 构建并运行 + +```go +$ go build main.go +./main +open /tmp/myHotelApp/config.txt: no such file or directory +``` + +Go 告诉我们它无法打开文件,因为它不存在。 显示的文本不在我们的程序中; 它包含在变量 `err` 中。 对 `ioutil.ReadFile` 的调用返回两个结果: + +```go +dat, err := ioutil.ReadFile("/tmp/myHotelApp/config.txt") +fmt.Println(err) +``` + +让我们检查 ReadFile 的签名! 为此,我们有两个选择 + +* 如果你使用IDE,则可以通过单击跳转到函数定义 + + * 这是函数的签名 + + ```go + func ReadFile(filename string) ([]byte, error) + ``` + +* 如果你没有使用任何IDE,你也可以通过在线文档找到它 + + * 进入 **pkg.go.dev** 这个网站, 并搜索 **ioutil** + + ![](imgs/go_package_discovery.63cc3a84.png) + +我们注意到函数返回两个结果: + +* 一个字节类型的`slice` +* 一个错误 + +函数文档详细说明了该函数的工作原理: + +“`ReadFile` 读取以 `filename` 命名的文件并返回内容。 成功的调用返回 `err == nil`,而不是 `err == EOF`。 因为 ReadFile 读取整个文件,它不会将 Read 中的 EOF 视为要报告的错误。” + +* 该函数将尝试读取整个文件 +* 在成功的情况下,错误的值为`nil` +* 当有错误时,第二个结果的值不为 `nil` + +### 4.4 修复错误 + +让我们在请求的位置创建一个文件来测试我们程序的行为。 为此,我们将使用命令行创建一个文件: + +```shell +$ mkdir /tmp/myHotelApp +$ echo "hello" > /tmp/myHotelApp/config.txt +``` + +第一个命令将创建目录 `/tmp/myHotelApp`。 第二个命令将 `hello` 写入文件 `/tmp/myHotelApp/config.txt` 。注意,我们将删除文件中已有的内容。 (附加内容使用 `>>` 而不是 `>`)。 + +让我们再次运行程序: + +```shell +$ ./main + +hello +``` + +这次 `err` 打印的值为 ``。 程序读取文件成功! 内容打印在了屏幕上。 + +## 5 函数和方法可能在失败时返回错误接口 + +通常,函数和方法都会有两个返回值: + +* 一个是我们期待函数产生的返回值 + +* 另一个则是`error`类型的返回值 + + * 例如:ioutil.ReadFile 的第一个返回是包含文件内容的字节类型`slice`。 第二个则是错误 + + ```go + func ReadFile(filename string) ([]byte, error) + ``` + + * 例如: `fmt.Println` 返回写入的字节数和错误 + + ```go + func Println(a ...interface{}) (n int, err error) + ``` + +当错误等于 `nil` 时,则认为函数执行成功。 + +当错误不是 `nil` 时,说明**出了点问题**,我们可以丢弃结果。 + +一些函数/方法只返回一个类型为 `error`接口的元素: + +```go +csvWriter := csv.NewWriter(csvFile) +err = csvWriter.Write(invoiceData) +``` + +### 5.0.0.1 错误接口 + +这是错误接口在标准库中的定义: + +```go +type error interface { + Error() string +} +``` + +标准库中返回的所有错误都实现了此接口。 打印错误时`Error`会被执行。 + +### 5.1 Load 函数签名的修改 + +这是`load`函数的新版本: + +```go +// errors/example-fix/config/main.go +package config + +import ( + "io/ioutil" +) + +func Load() (string, error) { + data, err := ioutil.ReadFile("/tmp/myHotelApp/config.txt") + if err != nil { + return "", err + } + return string(data), nil +} +``` + +与之前的有何不同? + +* 该函数现在是**公开**的。 +* 出参更改。 现在返回一个**字符串**和一个**错误**。 +* 当我们调用 `ioutil.ReadFile` 时,我们初始化了两个局部变量:`data` 和 `err` +* 然后我们测试变量`err`是否为nil。 为什么 ? + * 因为 `ioutil.ReadFile` 的文档告诉我们“成功调用返回 `err == nil`”。 + * 因此,当 `err` 不等于 `nil` 时;肯定就有问题了。 + * 我们不会试图了解出了什么问题。 我们只返回两个结果:空字符串和错误。 + * 调用者需要**处理错误**。 +* 当没有错误时,我们返回文件中检索到的数据和 `nil`。 + * 为什么我们需要返回 `nil` (作为第二个结果)? + * 因为调用者需要知道是否发生了错误。 我们通过将第二个结果参数赋值为 `nil` 来发出信号。 + +### 5.2 发生错误时通知函数调用者 + +```go +// errors/example-fix/main.go +package main + +import ( + "fmt" + "log" + + "maximilien-andile.com/errors/errorExampleFixed/config" +) + +func main() { + confData, err := config.Load() + if err != nil { + log.Fatalf("Impossible to load application config because: %s", err) + } + fmt.Println(confData) +} +``` + +#### 5.2.1 代码说明 + +#### 5.2.2 程序测试 + +首先测试一下新的`load`函数: + +```shell +$ go build main.go +$./main +hello +``` + +接着我们试着让它崩溃! 为此,我们需要将配置文件删除: + +```shell +$ rm /tmp/myHotelApp/config.txt +$ ./main +2020/03/07 18:13:21 Impossible to load application config because open /tmp/myHotelApp/config.txt: no such file or directory +``` + +最后我们再检查一下程序返回的退出码: + +```shell +$ echo $? +1 +``` + +退出码为1。 + +##### 5.2.2.1 关于退出码 + +在基于 UNIX 和 Windows 的系统中,程序在停止时返回**退出码**。 该码用于警告启动程序的人(或机器)是否出现问题。 按照惯例,返回 0 作为退出码的程序被认为已成功执行任务并退出。 当程序返回 0 以外的其他退出码时,表明程序执行存在问题。 + +某些程序会在出现特定错误时返回特定的退出码。 在他们的文档中可以找到退出码及其含义之间的映射。 + +在 Go 中,我们可以通过以下调用强制操作系统退出: + +```go +os.Exit(1) +``` + +我们可以把值更改为 0 到 125 之间的任一个整数,以使得程序可移植到所有系统。 + +```go +os.Exit(120) +``` + +## 6 如何创建独立的错误 + +你可以使用多种方式来创建错误: + +### 6.1 errors.New + +标准库中的`errors`包提供了错误接口的实现。 你可以很轻松地在应用程序中创建错误。 让我们举个例子: + +```go +// errors/example-fix-2/config/config.go +package config + +import ( + "errors" + "io/ioutil" +) + +const fileHeader = "APPCONF" + +func Load() (string, error) { + data, err := ioutil.ReadFile("/tmp/myHotelApp/config.txt") + if err != nil { + return "", err + } + conf := string(data) + if conf[0:7] != fileHeader { + return "", errors.New("the config file do not begin by accepted header") + } + return conf, nil +} +``` + +在这里,我们给`load`增加另一个实现用来测试。 此时文件必须以特定字符开头。 我们取前七个字符(通过对 conf 变量进行切片:我们从索引 0 取到索引 7 )。 + +当文件的起始不是 const `fileHeader` 的值时。 我们返回一个空字符串以及一个使用`error`包中的函数 `New` 构建的新错误。 让我们测试一下我们的错误 + +```shell +$ go run main.go +2020/03/06 19:14:07 Impossible to load application config because the config file does not begin by accepted header +exit status 1 +``` + +### 6.2 fmt.Errorf + +包 `fmt` 允许您使用 `fmt.Errorf` 创建错误。 这里有个例子: + +```go +func Load() (string, error) { + //... + if conf[0:7] != fileHeader { + return "", fmt.Errorf("the config file do not begin by accepted header") + } + //.. +} +``` + +在内部 `fmt.Errorf` 将调用 `errors.New`! + +### 6.3 Sentinel error + +哨兵错误在标准库中很常见。 它们是`error`类型的预定义变量。 包中的函数和方法会返回它们。 如果返回这样的错误,调用者将能够做出相应处理(请参阅下一节以了解如何处理哨兵错误) + +```go +// errors/sentinel/config/config.go +package config + +import ( + "errors" + "io/ioutil" +) + +var ErrNoConfigFile = errors.New("no config file at the specified location: /tmp/myHotelApp/config.txt") + +func Load() (string, error) { + data, err := ioutil.ReadFile("/tmp/myHotelApp/config.txt") + if err != nil { + return "", ErrNoConfigFile + } + return string(data), nil +} +``` + +这是 `rsa` 包中的一个示例: + +```go +var ErrMessageTooLong = errors.New("crypto/rsa: message too long for RSA public key size") +``` + +`io`包中也有类似例子: + +```go +var ErrShortWrite = errors.New("short write") +var ErrShortBuffer = errors.New("short buffer") +var EOF = errors.New("EOF") +var ErrNoProgress = errors.New("multiple Read calls return no data or error") +``` + +### 6.4 实现错误接口的自定义类型 + +```go +type HeaderError struct { + FaultyHeader string +} + +func (e *HeaderError) Error() string { + return fmt.Sprintf("Bad header. Provided %s, expected : APPCONF", e.FaultyHeader) +} +``` + +`HeaderError` 类型实现了`error`接口。 我们可以在我们的函数中使用它: + +```go +func Load() (string, error) { + //... + if conf[0:7] != fileHeader { + return "", &HeaderError{FaultyHeader:conf[0:7]} + } + //.. +} +``` + +## 7 如何将一个错误包装成另一个错误? + +### 7.1 包装是什么? + +由于另一个函数调用,函数或方法可能会遇到错误。 这里有个例子: + +```go +// errors/wrapping/useCase/main.go + + +func transferFileContents(filename string) error { + contents, err := ioutil.ReadFile(filename) + if err != nil { + return err + } + err = ioutil.WriteFile("/tmp/filecontents", contents, 0644) + if err != nil { + return err + } + return nil +} +``` + +`transferFileContents` 中有两个函数调用:`ioutil.ReadFile` 和 `ioutil.WriteFile`。 这两个函数都会返回错误。 当出现错误时,函数直接返回`ioutil`包函数返回的错误。 当发生错误时,调用者将在没有额外上下文的情况下收到如下错误: + +```shell +open /tmp/myHotelApp/config.txt: no such file or directory +``` + +我们可以做的是将这个错误包装到一个新的错误中以向其添加上下文: + +```shell +during the transfer of file context, something went wrong: open /tmp/myHotelApp/config.txt: no such file or directory +``` + +### 7.2 with fmt.Errorf + +要包装错误,你可以使用以下语法: + +```go +// errors/wrapping/fmt/main.go +package main + +// ... + +func transferFileContents(filename string) error { + contents, err := ioutil.ReadFile(filename) + if err != nil { + return fmt.Errorf("during file transfer impossible to open source file: %w", err) + } + err = ioutil.WriteFile("/tmp/filecontents", contents, 0644) + if err != nil { + return fmt.Errorf("during file transfer impossible to write source file: %w", err) + } + return nil +} +``` + +要将错误包装到新错误中,我们使用 `%w`。 + +让我们创建一个使用此函数的简单应用程序: + +```go +// errors/wrapping/fmt/main.go +package main + +import ( + "fmt" + "io/ioutil" + "log" +) + +func main() { + err := transferFileContents("/my/imaginary/file") + if err != nil { + log.Printf("error occured: %s", err) + } +} +``` + +当返回错误时,错误信息变成了: + +```shell +2020/03/22 19:09:13 error occurred: during file transfer impossible to open source file: open /my/imaginary/file: no such file or directory +``` + +取代了之前的: + +```shell +2020/03/22 19:10:18 error occured: open /my/imaginary/file: no such file or directory +``` + +### 7.3 使用自定义错误类型和 Unwrap 方法 + +当你定义自定义错误类型时,你可以实现 `Unwrap` 方法: + +```go +type ReadingError struct { + IOError error + Filename string +} + +func (e *ReadingError) Error() string { + return fmt.Sprintf("an error occured while attempting to read the file %s", e.Filename) +} + +func (e *ReadingError) Unwrap() error { + return e.IOError +} +``` + +* `Unwrap` 方法返回底层错误 + + + +## 8 如何处理错误? + +### 8.1 不要忽略错误! + +你可以忽略错误。 但你不应该这么做! 如果出现错误,你应该修改程序的流程。 + +```go +// 别这么做 +func main() { + transferFileContents("/my/imaginary/file") + log.Println("tranfer done") +} + +func transferFileContents(filename string) error { + //... +} +``` + +`transferFileContents` 函数返回一个错误。 在`main`函数中,我们忽略了该错误。 当传输失败时,我们却仍然打印传输已完成的日志。 + +### 8.2 选项 1:将它们视为不透明的 + +在某些 Go 程序中,错误被视为不透明信号。 调用者不会通过查看错误来找出原因。 他们只会测试 `err` 是否不为`nil`: + +```go +func main() { + err := transferFileContents("/my/imaginary/file") + if err != nil { + log.Fatalf("transfer impossible caused by: %s", err) + } + log.Println("tranfer done") +} +``` + +用户将收到错误警告。 + +### 8.3 选项 2:根据错误调整程序流程 + +#### 8.3.1 通过简单的比较检测是否存在哨兵错误 + +调用者可以检测到函数返回的哨兵错误。 这有一个非常常见的示例:读取 CSV 文件。 这是我们将阅读的 CSV 文件: + +```shell +John,Doe,256 +Diffie,Lock,257 +``` + +每行代表一条记录。 记录的值用逗号分隔(CSV 表示逗号分隔值)。 代码如下: + +```go +// errors/handling/detect/sentinelIs/main.go +package main + +import ( + "encoding/csv" + "errors" + "fmt" + "io" + "log" + "os" +) + +func main() { + file, err := os.Open("test.csv") + defer file.Close() + if err != nil { + log.Printf("impossible to open file %s", err) + return + } + + r := csv.NewReader(file) + for { + record, err := r.Read() + if errors.Is(err, io.EOF) { + break + } + if err != nil { + log.Fatal(err) + } + fmt.Println(record) + } +} +``` + +该程序将读取一个 CSV 文件并遍历它。 它使用 CSV 阅读器。 Reader 有一个 `Read` 方法可以读取一行。 为了读取文件中的所有数据,我们添加了一个 for 循环。 + +这是 `Read` 函数的文档: + +![](imgs/csv_read_documentation.70d0e630.png) + +当到达文件末尾时, `Read` 将返回 `nil` 和哨兵错误 `io.EOF` 。 当 `err` 等于 `io.EOF` 时,我们用 `break` 停止循环。 这就是我们两次检查 `err` 值的原因: + +```go +if err == io.EOF { + break +} +if err != nil { + log.Printf(err) + return +} +``` + +在第一种情况下,文件已经读取完毕,此时我们退出循环; 而在第二种情况下,会产生某些错误,此时我们希望程序立即停止。 + +#### 8.3.2 通过errors.Is检测是否存在哨兵错误 + +使用错误包中的函数 Is,可以检测到是否发生了特定错误。 它能检测到被包装的错误,这是函数的签名: + +```go +func Is(err, target error) bool +``` + +两个入参:`err` 和`error`接口类型 `target` 。 `target` 参数是你想在错误链中发现的错误,err 是你的实际错误。 这里有一个示例用法: + +```go +r := csv.NewReader(file) +for { + record, err := r.Read() + // 使用Is函数代替简单的 == 比较 + if errors.Is(err, io.EOF) { + break + } + if err != nil { + log.Fatal(err) + } + fmt.Println(record) +} +``` + +在这个片段中,我们可以用`==`替换 `errors.Is(err, io.EOF)` 因为 `io.EOF` 是由 `r.Read()` 直接返回的。 下一个示例将演示函数 `Is` 能够检测嵌套错误链中的哨兵错误: + +```go +// errors/handling/detect/sentinelIsChain/main.go +package main + +import ( + "errors" + "fmt" +) + +func main() { + err := foo() + if errors.Is(err, errSentinel) { + fmt.Println("errSentinel detected in the error chain with errors.Is") + } + if err == errSentinel { + fmt.Println("errSentinel detected in the error chain by ==") + } +} + +var errSentinel = errors.New("test") + +func foo() error { + return fmt.Errorf("error : %w", bar()) +} + +func bar() error { + return errSentinel +} +``` + +下面是程序的执行结果: + +```shell +errSentinel detected in the error chain with errors.Is +``` + +在 `main` 函数中,我们调用了 `foo` 函数,它将返回一个错误,该错误包含了 `bar` 的结果。 函数 `bar` 返回哨兵错误 `errSentinel`。 + +#### 8.3.3 通过errors.As检测错误类型 + +函数 `errors.As` 可用于检测错误链中是否返回了 X 类型的错误。 让我们看看这个函数的签名: + +```go +func As(err error, target interface{}) bool +``` + +该函数有两个入参: + +* `error` 接口类型的参数err +* 空接口类型参数`target`,注意这个参数 + * (A) 不能为空 + * (B) 必须是一个指针 + * (C) 必须是一个接口或者是`error`接口的实现 + +这里有一个示例: + +```go +// errors/handling/detect/type/main.go +package main + +import ( + "errors" + "fmt" + "io/ioutil" + "log" +) + +func main() { + err := transferFileContents("/my/imaginary/file") + var readingError *ReadingError + if errors.As(err, &readingError) { + log.Fatalf("error of reading occured: %s: %s", readingError, readingError.Unwrap()) + } + var writingError *WritingError + if errors.As(err, &writingError) { + log.Fatalf("error of writing occured: %s", err) + } + log.Println("transfer done") +} +``` + +`readingError` 是`ReadingError`类型的变量。`ReadingError` 是指针类型。 它表示所有指向 `ReadingError` 类型变量的指针。 我们用两个参数调用`errors.As`:`err` 和 `&readingError`。 运算符 `&` 表示我们获取变量 `readingError` 的地址。 如果我们直接给 `errors.As` 变量 `readingError` 呢? 为什么不这么做? 让我们详细说明原因: + +* `readingError` 在声明之后的值是什么? + * 是`nil`。 它违反了规则(A)。 +* `readingError` 是什么类型? + * `*ReadingError` +* `&readingError` 是什么? + * 它是指向值 `readingError` 的指针,该值为`nil`。 + * 它不是一个空指针。 它指向一个值,该值为 `nil` ,但指针本身不为`nil` + * 因此 `&readingError` 不是 `nil` (A) 而是一个指针 (B)。 +* `&readingError` 是接口吗? + * 不是 +* `&readingError` 实现了`error` 接口吗? + * 是的!规则C得到了验证。 + + + +## 9 一点建议 + +错误是编程的一部分。 你的程序必须处理可能发生的所有错误。 你必须考虑最坏的情况。 问问自己,自己的程序可能出现什么问题? 什么技术可能导致恶意用户使你的程序崩溃? + +### 9.1 为错误添加上下文 + +创建错误时,你应该向用户提供足够多的信息(也应向负责维护应用程序的团队提供)。 没有上下文的错误更难理解,也更难找到错误的源头。 + +### 9.2 不要忽略错误 + +这可能很明显,但仍然有许多开发人员犯了这个错误。 应该处理出现的错误: + +* 返回给调用者 +* 或处理(代码里实现了某种自动修复机制) + +### 9.3 谨慎使用fatal + +当调用 `log.Fatal` 或 `log.Fatalf` 时,会隐含地强制程序突然退出(使用 `os.Exit(1)`)。“程序会因此立即终止,导致 `deffer`修饰的函数不会运行。” (os/proc.go)。 `deffer`函数通常用于清理逻辑(例如关闭文件描述符)。 因此,不运行它们可能会阻止程序执行清理逻辑。 + +### 9.4 创建容错程序 + +硬件工程师经常使用术语“容错”。 大多数硬件组件旨在优雅地处理故障并从中恢复。 软件工程师还应该构建他们的程序来容忍错误。 尽管失败(可能是**暂时**或**永久**的),程序应该依旧能够实现其目的。 + +对于 Go 程序员来说意味着什么,我们可以采用哪些技术来改进我们的程序? + +#### 9.4.0.1 从错误中恢复 + +如果发生错误,请检查它是否是**可恢复**的。 + +例如,你正在构建一个调用 Web 服务的程序。 在程序执行期间,调用失败。 失败的根源是网络(您的服务器已与互联网断开连接)。 此错误是可恢复的,这意味着你可以从错误中恢复,因为网络将在某个时候再次可用。 + +如果你的 Web 服务调用成功,但网络返回 HTTP 400 错误(“错误请求”),则该错误不可恢复。 显然,你在开发 Web 服务客户端时犯了一个错误。 需要人工干预来解决这个问题。 + +#### 9.4.0.2 给程序留有后备选项 + +后备选项是“如果首选选项不可用,则是一种应急选项”(维基百科)。 例如,在我们的程序中,网络不可达或返回了错误。 此时我们应该考虑后备选项。 + +根据错误是否是可恢复的,后备选项也将会有所不同。 + +如果你遇到网络故障,你可以实现重试机制,而不是直接返回错误。 你将按可配置的次数重试 Web 服务。 + + + +## 10 应用 + +### 10.1 问题 + +在现有系统中,一组酒店生成的发票是简单的文本文件。 这些文件存储在存档室中的硬盘驱动器中。 新软件需要加载所有这些发票才能运行。 + +在下图中,你能看到一个示例发票。 + +![](imgs/invoice_example.86becaf5.png) + +客户端表示: + +* 发票使用CSV编码 +* 发票已于 2003 年 7 月至 2020 年 7 月发出 +* 金额格式为: + * 以美元计 + * 全部乘以100。 + * 例如:文件中的 $13.54 将是 1354 +* 它们都存储在一个目录中。 +* 在这个目录中,它们是子目录。 +* 每个月的发票都会有一个单独的子目录存放。 +* 每个子目录都按照相同的模式命名: + * 月 - 年 + * 例如:`October - 2008` + +你被要求构建一个程序,该程序将: + +* 读取所有的发票文件 +* 计算: + * 酒店收取的增值税总额 + * 客户支付的总金额(VAT Inc.) + * 发票总金额 不含增值税 + * 这些金额必须每个月都提供 + +程序必须是容错的。 也许有些发票存在坏数据...要进行此练习,你可以下载发票文件夹。 它位于这里:. + +要下载的文件是 zip。 在下图中,你可以看到列出了所有子目录的截图。 + +![](imgs/data-folder-application.01f7df84.png) + +在下图中,你可以看到其中一个子目录的内容。 + +![](imgs/data_subdirectory_contents.a6e54823.png) + +### 10.2 提示/建议 + +* 你需要找到一种方法来计算 2003 年 7 月到 2020 年之间每个月的三个指标。 +* for循环可能能帮到你 +* 目录名称有规律; 你可以在你的代码中使用它... +* 您将需要使用包中的函数: + * fmt + * github.com/Rhymond/go-money + * log + * os + * time + * encoding/csv +* 鼓励你使用外部包 **github.com/Rhymond/go-money** + * 使用此包,可以帮助你处理货币计算 +* 在对真正的实现进行编码之前,可以先编写伪代码(用英文编写的程序执行的每个步骤的描述) + +### 10.3 解决方案 + +#### 10.3.1 构思 + +我们需要遍历所有目录。 每个目录都包含特定于一个月的数据。 我们可以预测他们的名字。 有了目录的名称,我们就可以打开它并读取其中包含的所有文件。 + +我们需要为每个文件解析其内容并检索增值税和不含增值税的部分。 总计金额只是简单地增值税 + 不含增值税部分。 因此没有必要获取全部的文件数据。 + +每个月之后,我们打印该月的总计金额,然后我们需要重新初始化计数器! + +#### 10.3.2 伪代码 + +* For each month between July 2007 and July 2020 + * **totalVat** = 0 + * **totalVatExc** = 0 + * **folderName** = month-year + * open **folderName** + * k = 0 + * For each file in directory + * k++ + * **fileName** = invoice-k + * Open and read fileName + * Parse contents + * **totalVat** = **totalVat** + **vat** extracted from invoice + * **totalVatExc** = **totalVatExc** + **vatExc** extracted from invoice + * Print **totalVat** $ **totalVatExc** $ for month-year + +注意:我强烈建议你到此为止,接下来尝试自己实现具体功能。 + +你可能无法完全做到,没关系! 你试一试! 之后,仔细阅读详细的解决方案(这不是唯一有效的解决方案)。 + +#### 10.3.3 实现 + +* 首先,我们通过创建一个新目录和一个 go.mod 文件来创建项目。 + +```go +module maximilien-andile.com/errors/application + +go 1.13 +``` + +可以根据需要调整模块路径。 你可以手动或使用以下命令创建 go.mod 文件: + +```shell +go mod init maximilien-andile.com/errors/application +``` + +* 然后创建我们的目录结构(见下图 )。 + * 我们创建一个 cmd 目录。 它将包含我们所有的主要文件。 每个主文件将存储在一个子目录中。 + * 目前,我们有一个应用程序。 我们创建一个名为“compute”的目录。 + * 在这个目录中,我们创建了一个空白文件 compute.go。 (这将是我们的应用程序文件)。 + +![](imgs/errors_application_structure.a820de8e.png) + +* 接下来,我们编写compute.go文件的主结构: + +```go +package main + + +func main() { + +} +``` + +Compute.go 源文件属于 `main`包。 在这个源文件中,我们定义了一个 `main` 函数。 + +* 我们需要随着时间的推移进行迭代。 为此,我们将使用 for 循环。 for 循环需要一个开始日期和一个结束日期。 Go 有一个标准的时间值包:`time`! + +```go +package main + +import "time" + +const startDate = "2003-07-01" +const endDate = "2020-07-01" + + + +func main() { + start, err := time.Parse("2006-01-02", startDate) + if err != nil { + log.Fatalf("impossible to parse start date %s", err) + } + end, err := time.Parse("2006-01-02", endDate) + if err != nil { + log.Fatalf("impossible to parse end date %s", err) + } +} +``` + +这里我们使用了`time`包的 `Parse` 函数。 我们来看看关于它的文档(在 https://pkg.go.dev/time 上) + +![](imgs/parse_function_doc.62ee9779.png) + +`Parse` 函数有两个入参,一个格式化时间所需的格式(译者注:类似于`java`中的`yyyy-MM-dd`,但是这里是用`2020-10-01`这种格式)和要被格式化的时间的值。 两者都是字符串。 格式指示 Go 如何格式化输入时间(第二个参数)。 + +格式不是`YYYY-MM-DD`(一些有经验的程序员更习惯这种布局)而是`2006-01-02`。 时间参考:Mon Jan 2 15:04:05 -0700 MST 2006 用于定义格式。 + +在我们的示例中,我们将时间字符串`“2003-07-01”`和`“2020-07-01”`解析为两个常量。 这里的格式是`“2006-01-02”`。 + +请注意,解析操作可能会失败,这就是 `time.Parse` 作为第二个参数返回错误的原因。 当发生错误时,我们调用 `log.Fatalf`。 它将记录到标准输出并执行和退出程序。 + +该函数返回一个 `time.Time` 类型的值 + +* 接着我们构造循环 + +```go +for d := start; d.Unix() < end.Unix(); d = d.AddDate(0, 1, 0) { + +} +``` + +* 这个循环将从 2003 年 7 月到 2020 年 7 月的每个月迭代。让我们详细说明它是如何构建的: + * Init 语句:我们创建变量 d。 它的值用“start”初始化。 + * 条件:我们比较 d 和 end。 比较是在 UNIX 之间进行的,即自 1970年 1 月以来的秒数 + * 只要不满足条件,循环就终止(Go 不会执行 post 语句) + * Post 语句:我们调用 `d.AddDate(0, 1, 0)` 这将增加一个月到 d 并返回新的日期。 由于赋值,d 的值将被这个新日期覆盖。 +* 下一步是编写 for 循环的主体: + +```go +const baseDirPath = "/my/base/dir" + +for d := start; d.Unix() < end.Unix(); d = d.AddDate(0, 1, 0) { + + monthDirPath := fmt.Sprintf("%s/%s-%d", baseDirPath, d.Month(), d.Year()) + + dir, err := os.Open(monthDirPath) + if err != nil { + log.Fatalf("failed to open directory %s: %s", monthDirPath, err) + } + + defer dir.Close() + + list, err := dir.Readdirnames(0) + if err != nil { + log.Fatalf("failed to read all files in directory %s: %s", monthDirPath, err) + } + + // 遍历list中的每一个文件名 + for _, name := range list { + + // TODO : 读取文件并提取数据 + } +} +``` + +以下是程序的逐步操作: + +1. 使用月份目录的路径初始化名为 `monthDirPath` 的局部变量。 请注意, `baseDirectory` 是一个常量。 它是数据目录的完整路径。 + 1. 例如:d 等于 2019 年 7 月,则 `dirPath` 等于 `/my/base/dir/July-2019` +2. 使用 `os.Open(monthDirPath)` 打开目录。 + 1. `os.Open`有两个返回值 + 1. 一个指向`os.File`结构体的指针 + 2. 一个错误 + 2. 我们通过断言 `err` 为`nil`来处理错误。 如果 `err` 不是 `nil`,我们调用 `log.Fatalf` +3. 然后我们告诉 go 调用 `dir.Close` 当(a)周围的函数(即 main)返回或(b)相应的 goroutine panic 时。 + 1. 这是一个deffer声明 +4. 然后程序将获取根目录中的**文件**(或**目录**)列表(感谢对 `dir.Readdirnames(0)` 的调用) + 1. `Readdirnames`函数的文档描述:(n 是函数的输入参数)“如果 n <= 0,`Readdirnames` 返回单个切片中目录中的所有名称。 在这种情况下,如果 `Readdirnames` 成功(一直读取到目录末尾),它将返回切片和 nil 错误。 如果在目录结束之前遇到错误,`Readdirnames` 将返回读取到该点的名称和非零错误。” + 2. 通过将输入参数设置为 0,我们可以确保获得目录中所有的文件名。 如果发生错误,我们调用 `log.Fatalf` +5. 然后我们将用 for 循环 `for _, name := range list` 迭代包含所有文件名的切片 + 1. 这里我们使用了range语句 + 2. range 语句将在每个循环中为两个变量 _ 和 name 分配一个新值 + 3. 第一个变量被命名为 _ 因为我们对它不感兴趣。 它是切片中的元素索引。 + +让我们详细说明我们将在这个循环中放入的内容: + +```go +for _, name := range list { + // 构建目录路径 + filePath := fmt.Sprintf("%s/%s", monthDirPath, name) + // 从目录中提取数据 + vatExc, vat, err := invoice.ReadFromFile(filePath) + if err != nil { + log.Fatalf("failed to parse invoice %s: %s", filePath, err) + } + //... +} +``` + +1. 文件路径`filePath`是由 `fmt.Sprintf` 生成的。 + 1. 例如:`“/my/base/dir/April-2005/invoice-1”` +2. 然后我们从发票包中调用 `ReadFromFile` 方法。 发票处理逻辑比较复杂; 我们已将所有逻辑放在发票包中。 (为了让代码更容易理解) + +让我们看看发票包: + +```go +package invoice + +//... + +func readCSV(filename string) ([]string, error) { + invoiceCSV, err := os.Open(filename) + defer invoiceCSV.Close() + if err != nil { + return nil, err + } + reader := csv.NewReader(invoiceCSV) + record, err := reader.Read() + if err == io.EOF { + return nil, errors.New("file is empty") + } + if err != nil { + return nil, err + } + return record, nil +} +``` + +我们将剖析的第一个函数是 `readCSV`。 此函数返回一段字符串(代表包含在 CSV 文件中的数据)和错误。 请注意,此函数是私有的。 + + 1. 通过`os.Open`打开文件 + 2. 我们不会忘记对 Close 的延迟调用(在 invoiceCSV 上定义的方法) + 3. 接着我们读取CSV文件 + 1. 第一步是创建一个 CSV 阅读器: `csv.NewReader(invoiceCSV)` + 2. 这是 `NewReader` 的签名: `func NewReader(r io.Reader) *Reader` + 3. 它需要一个接口 `io.Reader` 作为输入。 (`os.Open` 的第一个结果是一个 `io.Reader`(它实现了必要的方法)) + 4. `reader`有一个方法 `Read` 。 从 `csv` 包文档中,它将读取第一个 CSV 记录。 + 5. 读取返回一个错误,存在两种情况 + 1. 当`reader`没有数据可供阅读时。 在这种情况下 `err` 等于 `io.EOF` + 2. 当读取过程中出了问题 + 6. 在我们的错误处理中,我们区分这两种情况来为函数调用者添加信息。 + 4. 当没有遇到错误时,变量`record`与值 `nil` 一起返回。 因此,调用者知道调用成功了。 + +这是`invoice`包的唯一公共功能: + +```go +package invoice + +import ( + "encoding/csv" + "errors" + "io" + "os" + "strconv" + + "github.com/Rhymond/go-money" +) + +func ReadFromFile(filename string) (*money.Money, *money.Money, error) { + record, err := readCSV(filename) + if err != nil { + return nil, nil, err + } + // 记录: 发票编号 [0326582789 Unicornquiver 126730 25346 152076 5] + vatExcConverted, err := strconv.Atoi(record[2]) + if err != nil { + return nil, nil, err + } + vatExc := money.New(int64(vatExcConverted), "USD") + vatConverted, err := strconv.Atoi(record[3]) + if err != nil { + return nil, nil, err + } + vat := money.New(int64(vatConverted), "USD") + return vatExc, vat, nil +} +``` + +来看一下导入部分。 我们使用了货币模块包。 这个包的路径是`github.com/Rhymond/go-money`。 该代码可在 Github https://github.com/Rhymond/go-money 上免费获得。 作者 (Rhymond) 在 MIT 许可下发表了它。 + +该包将允许您操纵金额。 + +要在我们的项目中使用它,我们需要告诉 Go 在本地获取代码的副本: + +```shell +go get github.com/Rhymond/go-money +``` + +它会: + + 1. 下载包代码 + 2. 修改 `go.mod`(和 `go.sum`)文件以添加依赖项: + +```go +module maximilien-andile.com/errors/application + +go 1.13 + +require ( + github.com/Rhymond/go-money v1.0.1 +) +``` + +* 该函数采用单个参数:文件名。 +* 它返回三个结果: `*money.Money, *money.Money, error` + +现在让我们增强公共的 `ReadFromFile` 函数。 此函数的目标是解析发票以返回两个值:(1) 不含增值税的金额和 (2) 增值税金额。 + +* 这里的第一个动作是调用 `readCSV` 函数:`record, err := readCSV(filename)`。 请注意,与往常一样,错误值 (`err`) 受到控制。 + +* **(A)** 让我们解释一下代码:`vatExcConverted, err := strconv.Atoi(record[2])` + + `record[2]` 将从`record`切片中提取索引 2 处的值(第三个位置,记住切片的索引从 0 开始) + + `record[2]` 是 `string` 类型(记住 `record` 是 `[]string` 类型) + + * 我们想将其转换为整数(以使用货币库)。 为此,我们使用 `strconv.Atoi` + + `vatExcConverted` 将是 **`int`** 类型。 + +* **(B)** 然后我们创建一个指向新的 `money.Money` 结构的指针。 为此,我们使用语句:`vatExc := money.New(int64(vatExcConverted), "USD")`。 它将创建和 `vatExcConverted` 相同数量的货币(以美元为单位)。 + +* **重要提示**:`money.Newt` 需要一个 `int64`; 这就是我们转换它的原因。 另一个重要的点是将金额视为整数,这意味着它们都被乘以 100。$15.26 必须乘以 100(=1526)才能通过`money.New`生成。 我们不需要进行转换,因为在我们的数据集中,金额已经乘以 100。 + +* 我们重复 **(A)** 和 **(B)** 以获得增值税金额。 + +* 返回这两个金额。 请注意,我们返回**指针**。 + +我们完成了`invoice`包。 这是完整的代码: + +```go +// errors/application/solution/invoice/invoice.go +package invoice + +import ( + "encoding/csv" + "errors" + "io" + "os" + "strconv" + + "github.com/Rhymond/go-money" +) + +// ReadFromFile will open the file specified in parameter +// ReadFromFile 将打开参数中指定的文件 +// it will parse the csv data contained inside +// 它将解析包含在其中的 csv 数据 +// Example file content : 2360520710,Winghickory,72224,14445,86669,8 +// 示例文件内容:2360520710,Winghickory,72224,14445,86669,8 +// The column 2 and 3 will be interpreted as dollar amount multiplied by 100 +// 第 2 列和第 3 列将被解释为美元金额乘以 100 +func ReadFromFile(filename string) (*money.Money, *money.Money, error) { + record, err := readCSV(filename) + if err != nil { + return nil, nil, err + } + // record: invoice number [0326582789 Unicornquiver 126730 25346 152076 5] + // 记录: 发票编号 [0326582789 Unicornquiver 126730 25346 152076 5] + vatExcConverted, err := strconv.Atoi(record[2]) + if err != nil { + return nil, nil, err + } + vatExc := money.New(int64(vatExcConverted), "USD") + vatConverted, err := strconv.Atoi(record[3]) + if err != nil { + return nil, nil, err + } + vat := money.New(int64(vatConverted), "USD") + return vatExc, vat, nil +} + +func readCSV(filename string) ([]string, error) { + invoiceCSV, err := os.Open(filename) + defer invoiceCSV.Close() + if err != nil { + return nil, err + } + reader := csv.NewReader(invoiceCSV) + record, err := reader.Read() + if err == io.EOF { + return nil, errors.New("file is empty") + } + if err != nil { + return nil, err + } + return record, nil +} +``` + +这是`main`包的完整代码: + +```go +// errors/application/solution/cmd/compute/compute.go +package main + +import ( + "fmt" + "log" + "os" + "time" + + "github.com/Rhymond/go-money" + "maximilien-andile.com/errors/application/invoice" +) + +const startDate = "2003-07-01" +const endDate = "2020-07-01" +const baseDirPath = "/Users/maximilienandile/Documents/DEV/goBook/errors/application/data" + +var totalVat *money.Money +var totalVatExc *money.Money + +func init() { + totalVat = money.New(0, "USD") + totalVatExc = money.New(0, "USD") +} + +func main() { + start, err := time.Parse("2006-01-02", startDate) + if err != nil { + log.Fatalf("impossible to parse start date %s", err) + } + end, err := time.Parse("2006-01-02", endDate) + if err != nil { + log.Fatalf("impossible to parse end date %s", err) + } + // from start to end date add 1 month at each iteration + // 从 start 到 end 每轮循环增加一个月 + for d := start; d.Unix() < end.Unix(); d = d.AddDate(0, 1, 0) { + // create a var that will contain the name of the dir to open + // 创建一个包含要打开的目录名称的变量 + monthDirPath := fmt.Sprintf("%s/%s-%d", baseDirPath, d.Month(), d.Year()) + // open the directory + // 打开目录 + dir, err := os.Open(monthDirPath) + if err != nil { + log.Fatalf("failed to open directory %s: %s", monthDirPath, err) + } + // defer the closing of the dir + // 延迟关闭目录 + defer dir.Close() + // read all files and folder into the dir + // 读取所有文件并读入目录 + list, err := dir.Readdirnames(0) + if err != nil { + log.Fatalf("failed to read all files in directory %s: %s", monthDirPath, err) + } + // iterate for each filename in list + // 遍历list中的文件名 + for _, name := range list { + // construct dir path + // 构建目录路径 + filePath := fmt.Sprintf("%s/%s", monthDirPath, name) + // extract data from dir + // 从目录中提取数据 + vatExc, vat, err := invoice.ReadFromFile(filePath) + if err != nil { + log.Fatalf("failed to parse invoice %s: %s", filePath, err) + } + totalVat, err = totalVat.Add(vat) + if err != nil { + log.Fatalf("impossible to add VAT to counter") + } + totalVatExc, err = totalVatExc.Add(vatExc) + if err != nil { + log.Fatalf("impossible to add VAT to counter") + } + } + } + + log.Println("total VAT", totalVat.Display()) + log.Println("total VAT Exc", totalVatExc.Display()) + +} +``` + +```go +vatExc, vat, err := invoice.ReadFromFile(filePath) +if err != nil { + log.Fatalf("failed to parse invoice %s: %s", filePath, err) +} +totalVat, err = totalVat.Add(vat) +if err != nil { + log.Fatalf("impossible to add VAT to counter") +} +totalVatExc, err = totalVatExc.Add(vatExc) +if err != nil { + log.Fatalf("impossible to add VAT to counter") +} +``` + +我们从发票文件中提取两个金额,然后将它们添加到我们的全局金额(`totalVat` 和 `totalVatExc`)。 为了进行添加,我们使用方便的方法 `Add`。 请注意,添加可能会失败。 错误由通用方法处理。 + +最后两个语句将打印总金额: + +```go +log.Println("total VAT", totalVat.Display()) +log.Println("total VAT Exc", totalVatExc.Display()) +``` + +##### 10.3.3.1 log.Fatalf和延迟函数 + +调用 `log.Fatal` 时,不会运行延迟函数。 在我们的例子中,延迟函数将关闭打开的文件。 为读取文件而打开的文件描述符不会被我们的程序显式关闭。 在基于 UNIX 的系统上,文件描述符将在调用 `Exit` 时被操作系统关闭(内部 `log.Fatal` 调用 `os.Exit(1)`)。 + +### 10.4 奖金问题 + +1. 我们忘记打印一个指标了。 再次阅读要求以发现被我们漏掉的指标并修复程序。 +2. 在这个程序中,一旦发票格式错误,程序就会崩溃。 我们可能想避免这种情况。 你觉得该怎么做? + +### 10.5 奖金问题解答 + +1. 缺少的金额是包含增值税的总额。 我们可以选择从发票中提取(修改发票包)。 但是有一个更快的方法:我们只需要添加 `totalVat` 和 `totalVatExc`。 我们需要创建一个类型为 `*money.Money` 的新变量 `totalVatInc`。 我们在 `init` 函数中对其进行初始化。 + +```go +// errors/application/bonus1/cmd/compute/compute.go +package main + +//... + +var totalVat *money.Money +var totalVatExc *money.Money +var totalVatInc *money.Money + +func init() { + totalVat = money.New(0, "USD") + totalVatExc = money.New(0, "USD") + totalVatInc = money.New(0, "USD") +} + +func main(){ + //... + log.Println("total VAT", totalVat.Display()) + log.Println("total VAT Exc", totalVatExc.Display()) + // Total VAT included = VAT + Total VAT Exc + totalVatInc, err = totalVat.Add(totalVatExc) + if err != nil { + log.Fatalf("impossible to add total VAT + Total VAT Exc %s", err) + } + log.Println("total VAT Inc", totalVatInc.Display()) +} +``` + +2. 当我们无法读取发票时,程序就会停止。 我们的数据源可能有几张坏发票。 当一张坏发票无法处理时,我们可以简单地记录一个错误并继续处理其他发票。 我们使用 continue 进入 for 循环中的下一次迭代。 + +```go +// errors/application/bonus2/cmd/compute/compute.go + +// ... + +vatExc, vat, err := invoice.ReadFromFile(filePath) +if err != nil { + log.Printf("failed to parse invoice %s: %s", filePath, err) + continue +} +``` + + + +## 11 自我测试 + +### 11.1 题目 + +1. `error` 是一个具体类型,它是标准库的一部分。 对还是错? +2. `Unwrap` 方法的目的是什么? +3. 如何用 `fmt.Errorf` **包装**错误? +4. 什么是“哨兵错误”? +5. 如何创建一个新的错误? +6. 如何检查函数是否执行失败? + +### 11.2 解答 + +1. `error` 是一个具体类型,它是标准库的一部分。 对还是错? + + 1. 错误,错误是标准库中定义的接口 + 2. 它有一个名为 `Error` 的方法,它返回一个字符串 + + ```go + type error interface { + Error() string + } + ``` + +2. `Unwrap` 方法的目的是什么? + + 1. `Unwrap` 方法返回一个“包装”到另一个错误中的底层错误。 + 2. 基础错误是包含在另一个错误中的错误。 + +3. 如何用 `fmt.Errorf` **包装**错误? + + 1. 使用格式化 `%w` (w = wrap) + 2. 示例:`return nil, fmt.Errorf("impossible to read data:%w",err)` + +4. 什么是“哨兵错误”? + + 1. “哨兵错误”是由包公开的变量 + + 2. 这个变量有`error`接口类型 + + 3. 该包将在特定情况下返回此标记错误 + + 4. 调用者可以在收到哨兵错误时触发特定行为 + + 5. 例如: + + `io`包中的`var EOF = errors.New("EOF")` + + `cmdflag`包中的`var ErrFlagTerminator = errors.New("flag terminator")` + +5. 如何创建一个新的错误? + + 1. 创建实现`error`接口的类型,创建该类型的变量并获取其地址。 + 2. 或者调用`errors.New` + 3. 再或者调用`fmt.Errorf` + 4. 最后两个解决方案更容易。 + +6. 如何检查函数是否执行失败? + + 1. 函数应该返回一个错误(通常是第二个结果) + 2. 检查该错误是否不等于 `nil` + 3. 如果不等于 `nil`,则函数失败 + 4. 示例: + + ```go + vatExc, vat, err := invoice.ReadFromFile(filePath) + if err != nil { + log.Printf("failed to parse invoice %s: %s", filePath, err) + continue + } + ``` + + + +## 12 关键要点 + +* 可能失败的函数和方法应该返回类型为 `error`接口的元素 +* `errors.New` 和 `fmt.Errorf` 是创建错误的两个便捷函数 +* 当被调用的函数返回错误时,调用者必须**检查是否发生了错误** + * 当错误结果与 `nil` 不同时,代表错误发生了,可以丢弃其结果 + +```go +vatExc, vat, err := invoice.ReadFromFile(filePath) +if err != nil { + log.Printf("failed to parse invoice %s: %w", filePath, err) + continue +} +``` + +* 一个错误可能包含一个(或多个)潜在错误 + * 通常我们称之为一个错误`wrap`了另一个错误 +* 要包装错误,请使用格式化 `%w` 和 `fmt.Errorf` +* 当你返回一个哨兵错误时,你允许你的方法的调用者在发生这样的错误时实现不同的逻辑 +* 在程序中的某个点收到的错误可能包含多个包装错误 +* `errors.Is` 是一个有用的函数,用于测试错误链中是否存在特定错误 + + + +1. 许可证有不同的形式:MIT、Apache 2.0、ISC、BSD、GPLv2、GPLv3、AGPLv3 等。 这是一个复杂的主题。 你可以访问此网站以了解有关许可证的更多信息:https://opensource.guide/legal/,其中介绍了不同的许可证 + +## 参考书目 + +* [institute1990ieee] Electrical, Institute of, and Electronics Engineers. 1990. “IEEE Standard Glossary of Software Engineering Terminology: Approved September 28, 1990, IEEE Standards Board.” In. Inst. of Electrical; Electronics Engineers. + diff --git a/chap-23-errors/imgs/csv_read_documentation.70d0e630.png b/chap-23-errors/imgs/csv_read_documentation.70d0e630.png new file mode 100644 index 0000000000000000000000000000000000000000..b800086c569fa3c179ba5ca85b9f09e7db38f40f Binary files /dev/null and b/chap-23-errors/imgs/csv_read_documentation.70d0e630.png differ diff --git a/chap-23-errors/imgs/data-folder-application.01f7df84.png b/chap-23-errors/imgs/data-folder-application.01f7df84.png new file mode 100644 index 0000000000000000000000000000000000000000..03b6ea4631882252f11c5617e46d868b4351fdab Binary files /dev/null and b/chap-23-errors/imgs/data-folder-application.01f7df84.png differ diff --git a/chap-23-errors/imgs/data_subdirectory_contents.a6e54823.png b/chap-23-errors/imgs/data_subdirectory_contents.a6e54823.png new file mode 100644 index 0000000000000000000000000000000000000000..cee2b7f41e1392993d5aae1b042676176353c661 Binary files /dev/null and b/chap-23-errors/imgs/data_subdirectory_contents.a6e54823.png differ diff --git a/chap-23-errors/imgs/errors.2a0610d5.jpg b/chap-23-errors/imgs/errors.2a0610d5.jpg new file mode 100644 index 0000000000000000000000000000000000000000..8f1387b558b71af8565285e6953d4d93b487d0c2 Binary files /dev/null and b/chap-23-errors/imgs/errors.2a0610d5.jpg differ diff --git a/chap-23-errors/imgs/errors_application_structure.a820de8e.png b/chap-23-errors/imgs/errors_application_structure.a820de8e.png new file mode 100644 index 0000000000000000000000000000000000000000..e180b8131025cdd4363b3a15fe7e8f75b2ea08eb Binary files /dev/null and b/chap-23-errors/imgs/errors_application_structure.a820de8e.png differ diff --git a/chap-23-errors/imgs/go_package_discovery.63cc3a84.png b/chap-23-errors/imgs/go_package_discovery.63cc3a84.png new file mode 100644 index 0000000000000000000000000000000000000000..44ea0a93e70b8cccd15a44603a586b461ad73173 Binary files /dev/null and b/chap-23-errors/imgs/go_package_discovery.63cc3a84.png differ diff --git a/chap-23-errors/imgs/invoice_example.86becaf5.png b/chap-23-errors/imgs/invoice_example.86becaf5.png new file mode 100644 index 0000000000000000000000000000000000000000..e6d61b22040534e3843eeae8cb1ee770e9216ab2 Binary files /dev/null and b/chap-23-errors/imgs/invoice_example.86becaf5.png differ diff --git a/chap-23-errors/imgs/parse_function_doc.62ee9779.png b/chap-23-errors/imgs/parse_function_doc.62ee9779.png new file mode 100644 index 0000000000000000000000000000000000000000..273e72fd411beff2978b92bd99d1551dafecdba9 Binary files /dev/null and b/chap-23-errors/imgs/parse_function_doc.62ee9779.png differ