轨道
/
Go
Go
/
练习
/
农场
农场

农场

学习练习

简介

错误接口

在 Go 中,错误处理不是通过异常来完成的。 相反,错误是普通_值_,它们的类型实现了内置的error接口。 error接口非常精简。 它只包含一个方法Error(),该方法以字符串的形式返回错误信息。

type error interface {
  Error() string
}

每当你定义的函数在执行过程中可能发生需要传递给调用方的错误时,都需要把error列为返回类型之一。 如果函数有多个返回值,按照惯例,error总是最后一个。

func DoSomething() (int, error) {
  // ...
}

创建并返回一个简单的错误

你不必总是自己去实现错误接口。 要创建一个简单的错误,可以使用标准库包errors中的errors.New()函数。 你唯一需要传入的是字符串形式的错误信息,errors.New()会负责创建一个包含你的信息并实现error接口的值。

如果函数返回错误,最好为所有其他返回值都返回零值:

func DoSomething() (SomeStruct, int, error) {
  // ...
  return SomeStruct{}, 0, errors.New("failed to calculate result")
}
Caution

你不应假设所有函数在出现错误时都会把其他返回值设为零值。 最佳做法是假设一旦发生错误,使用任何其他返回值都是不安全的。 唯一的例外是文档明确指出在出错时其他返回值仍然有意义的情况。

如果你想在多个地方使用这样一个简单的错误,应该为这个错误声明一个变量,而不是内联使用errors.New。 按照惯例,变量名应以Err或err开头(取决于它是否被导出)。 这些错误变量通常被称为_哨兵错误_。

import "errors"

var ErrNotFound = errors.New("resource was not found")

func DoSomething() error {
  // ...
  return ErrNotFound
}

让函数返回nil作为错误,以表示函数执行期间没有发生错误:

func Foo() (int, error) {
  return 10, nil
}

错误检查

如果你调用一个返回错误的函数,通常会把错误值存入一个名为err的变量。 在使用函数的实际结果之前,你需要先检查没有错误。

为了避免嵌套代码的“正常路径”,应该先处理错误情况。 我们可以用==和!=把错误与nil进行比较,当err不为nil时,就知道发生了错误。

func processUserFile() error {
	file, err := os.Open("./users.csv")
	if err != nil {
		return err
	}

	// do something with file
}

大多数情况下,错误会根据上面的示例沿着函数调用栈向上返回。 处理错误的另一种方式是把错误记录下来,然后继续执行其他操作。 好的做法是要么返回错误,要么记录错误,绝不要两者都做。

由于 Go 中大多数函数都把错误作为返回值之一,你会在 Go 代码里到处看到/用到if err != nil这种模式。

自定义错误类型

如果你希望错误包含的信息不止错误信息字符串,可以创建一个自定义错误类型。 如前所述,任何实现了error接口的东西(即拥有Error() string方法)都可以在 Go 中充当错误。

通常会用结构体来创建自定义错误类型。 按照惯例,自定义错误类型的名称应以Error结尾。 另外,最好用指针接收者来实现Error() string方法,想了解其中的理由,可以看这条 Stackoverflow 评论。 注意,这意味着你需要返回指向自定义错误的指针,否则它不会被算作error,因为非指针值没有提供Error() string方法。

type MyCustomError struct {
  message string
  details string
}

func (e *MyCustomError) Error() string {
  return fmt.Sprintf("%s, details: %s", e.message, e.details)
}

func someFunction() error {
  // ...
  return &MyCustomError{
    message: "...",
    details: "...",
  }
}

说明

你期盼已久的那一天终于到来了,现在你自豪地成为了阿尔卑斯山上一座美丽农场的主人。

你依然不喜欢一大早爬起来喂牛。作为一名出色的工程师,你造了一台投喂机,取名FEED-M-ALL。

完成这个项目还差最后一步:一段计算每头牛应得饲料量的代码。重要的是每头牛分到的量要一样,你得避免它们起冲突。牛可是很敏感的。

幸运的是,你不需要自己推导所有计算饲料量的公式。你用了一个在网上找到的神秘外部库。据说它能让牛过得最开心。这个库导出了一个类型,实现了下面这个接口。你接下来自己写的代码就要依赖它。

type FodderCalculator interface {
	FodderAmount(int) (float64, error)
	FatteningFactor() (float64, error)
}

在编写代码的过程中,你会逐步改进错误处理,让它更健壮,日后在日常农场生活中使用时也更容易调试。

1. 平均分配食物

首先,你专心写出计算每头牛饲料量所需的代码。

实现一个DivideFood函数,它接受一个FodderCalculator和一个作为整数的奶牛数量作为实参。 在这个任务中,你可以假设传入的奶牛数量总是大于 0。 该函数应返回每头牛的食物量,类型为float64;如果出错,则返回错误。

要进行计算,你首先需要获取所有奶牛所需饲料的总量。这通过调用FodderAmount方法并传入奶牛数量来完成。此外,你还需要一个用来乘以这个总量的系数。调用FatteningFactor方法即可得到这个系数。有了这两个值和奶牛数量,你现在就能算出每头牛的食物量(类型为float64)。这正是DivideFood函数应当返回的结果。

如果你调用的某个方法返回了错误,执行就应停止,并把这个错误原样从DivideFood函数返回。

// For this example, we assume FodderAmount returns 50
// and FatteningFactor returns 1.5.
DivideFood(fodderCalculator, 5)
// => 15 <nil>

// Now assuming FodderAmount returns an error with message "something went wrong".
DivideFood(fodderCalculator, 5)
// => 0 "something went wrong"

2. 检查奶牛数量

在做上面第一个任务时,你发现所用的外部库并没有想象中那么高质量。比如,它无法正确处理无效输入。你希望在自己的代码里加上对输入值的检查,来绕过这个限制。

编写一个ValidateInputAndDivideFood函数,它的签名与上面的DivideFood相同。

  • 如果传入的奶牛数量大于 0,函数应调用DivideFood并返回该调用的结果。
  • 如果奶牛数量小于或等于 0,函数应返回一个错误,消息为"invalid number of cows"。
ValidateInputAndDivideFood(fodderCalculator, 5)
// => 15 <nil>

ValidateInputAndDivideFood(fodderCalculator, -2)
// => 0 "invalid number of cows"

3. 改进错误处理

在把数量传下去之前先做检查是个好办法,但你对那条不够具体的错误消息不太满意。你决定做得更好:创建一个名为InvalidCowsError的自定义错误类型。

这个自定义错误应保存奶牛数量(int)和一条自定义消息(string),并且Error方法应按以下格式序列化这些数据:

{number of cows} cows are invalid: {custom message}

有了这个自定义错误,实现一个ValidateNumberOfCows函数,它接受整数形式的奶牛数量,并返回一个错误(或 nil)。

  • 如果奶牛数量小于 0,函数返回一个InvalidCowsError,其自定义消息设为"there are no negative cows"。
  • 如果奶牛数量等于 0,函数返回一个InvalidCowsError,其自定义消息设为"no cows don't need food"。
  • 否则,函数返回nil,表示验证成功。
err := ValidateNumberOfCows(-5)
err.Error()
// => "-5 cows are invalid: there are no negative cows"

辛苦搭建好这个验证函数之后,你发现天色已晚,于是离开书桌去欣赏山间的日落。真正把这个新的验证函数加进代码这件事,你决定留到改天再做。

通过 GitHub 编辑 链接将在新窗口或新标签页中打开
Go Exercism

准备好开始 农场 了吗?

注册 Exercism,借助 34 个概念166 个练习 和真人导师指导,学习并掌握 Go,全部免费。