Kodewick
开始学 →
第 14 课,共 24 课目录

错误处理

第 14 课,共 24 课 · 第 4 部分 · 出错和函数 · 约 20 分钟

第 9 课里,strconv.Atoi 把 "42" 变成 42,它的第二个返回值被我们用 _ 丢掉了。那第二个值是一个错误(error),说的是转换成功没有。这回把它留下来:

prog.go
1package main2 3import (4	"fmt"5	"strconv"6)7 8func main() {9	n, err := strconv.Atoi("42")10	fmt.Println(n, err)11	n, err = strconv.Atoi("4x2")12	fmt.Println(n, err)13}
输出
42 <nil>
0 strconv.Atoi: parsing "4x2": invalid syntax

成功时 err 是 nil,fmt 显示成 <nil>:没出错。失败时 n 是 0,err 说明出了什么问题:strconv.Atoi: parsing "4x2": invalid syntax(解析 "4x2":格式不对)。

if err != nil

Go 没有那种自己从函数里跳出来的「异常」(panic 是例外,第 15 课)。可能失败的函数把错误放在最后一个返回值里交回来,调用的地方马上检查。error 是一个接口,只有一个方法:

type error interface { Error() string }

错误是 nil 就是「没出错」,不是 nil 就是出了问题;fmt 显示它时调用的就是 Error()。以后你会一遍又一遍地写 if err != nil:

prog.go
1package main2 3import (4	"fmt"5	"strconv"6)7 8func main() {9	inputs := []string{"120", "35", "abc", "8"}10	total := 011	for _, s := range inputs {12		n, err := strconv.Atoi(s)13		if err != nil {14			fmt.Printf("跳过:%v\n", err)15			continue16		}17		total += n18	}19	fmt.Printf("合计:%d\n", total)20}
输出
跳过:strconv.Atoi: parsing "abc": invalid syntax
合计:163

不是数字的那一项说一声就跳过,其余的照样加起来,是 163。常见的写法:先在 if 里把错误处理掉,正常的路往下走,不用多缩进一层。

自己的错误:errors.New

自己写的函数也可以返回错误。errors.New 用一句话做出一个错误。错误放在最后返回;一切顺利就返回 nil:

prog.go
1package main2 3import (4	"errors"5	"fmt"6)7 8func withdraw(balance, amount int) (int, error) {9	if amount <= 0 {10		return balance, errors.New("金额必须大于 0")11	}12	if amount > balance {13		return balance, errors.New("余额不够")14	}15	return balance - amount, nil16}17 18func main() {19	balance := 10020	for _, amount := range []int{30, 200, -5, 50} {21		newBalance, err := withdraw(balance, amount)22		if err != nil {23			fmt.Printf("取 %d 失败:%v\n", amount, err)24			continue25		}26		balance = newBalance27		fmt.Printf("取了 %d,余额 %d\n", amount, balance)28	}29}
输出
取了 30,余额 70
取 200 失败:余额不够
取 -5 失败:金额必须大于 0
取了 50,余额 20

Go 的习惯:英文的错误信息小写开头、末尾不加句号,因为它常常被拼到一句更长的话中间。

先猜一猜

这段程序显示什么?

1package main2 3import (4	"errors"5	"fmt"6)7 8func half(n int) (int, error) {9	if n%2 != 0 {10		return 0, errors.New("是奇数")11	}12	return n / 2, nil13}14 15func main() {16	a, err := half(10)17	fmt.Println(a, err)18	b, err := half(7)19	fmt.Println(b, err)20}

带上细节:fmt.Errorf

「余额不够」没说要取多少。fmt.Errorf 和 fmt.Sprintf 一样填格式,只是做出来的是一个错误:

prog.go
1package main2 3import "fmt"4 5func withdraw(balance, amount int) (int, error) {6	if amount > balance {7		return balance, fmt.Errorf("取 %d:余额只有 %d", amount, balance)8	}9	return balance - amount, nil10}11 12func main() {13	_, err := withdraw(80, 120)14	fmt.Println(err)15	left, err := withdraw(80, 30)16	fmt.Println(left, err)17}
输出
取 120:余额只有 80
50 <nil>

用 %w 包一层,用 errors.Is 认出来

程序常常要知道出的是哪一种错,光有一句话不够。常见的办法:在所有函数外面放一个变量(整个程序都能用),把这种错误做好一次,以后拿它来比。下面用一个 map 假装是一个放文件的文件夹:

prog.go
1package main2 3import (4	"errors"5	"fmt"6)7 8var ErrNotFound = errors.New("找不到")9 10var files = map[string]string{11	"notes.txt": "买牛奶",12	"todo.txt":  "给银行打电话",13}14 15func readFile(name string) (string, error) {16	text, ok := files[name]17	if !ok {18		return "", fmt.Errorf("读 %s:%w", name, ErrNotFound)19	}20	return text, nil21}22 23func main() {24	for _, name := range []string{"notes.txt", "photo.jpg"} {25		text, err := readFile(name)26		if errors.Is(err, ErrNotFound) {27			fmt.Printf("没有这个文件:%v\n", err)28			continue29		}30		fmt.Printf("%s 写着:%s\n", name, text)31	}32}
输出
notes.txt 写着:买牛奶
没有这个文件:读 photo.jpg:找不到

fmt.Errorf 里的 %w 把一个错误包起来:新错误的话里带着 ErrNotFound 的话,里面还留着 ErrNotFound 本身。errors.Is(err, ErrNotFound) 会一层一层往里找。直接用 == 只比最外面那一层:

prog.go
1package main2 3import (4	"errors"5	"fmt"6)7 8var ErrNotFound = errors.New("找不到")9 10func main() {11	err := fmt.Errorf("读 photo.jpg:%w", ErrNotFound)12	fmt.Println(err == ErrNotFound)13	fmt.Println(errors.Is(err, ErrNotFound))14	fmt.Println(errors.Unwrap(err) == ErrNotFound)15}
输出
false
true
true

errors.Unwrap 剥掉一层。把 %w 换成 %v,显示出来的话一模一样,可是里面什么也没留下,errors.Is 就是 false 了。

自己的错误类型

任何有 Error() string 方法的类型都是错误。用结构体,就能把细节放在字段里,调用的地方拿到的是数,而不只是一句话:

prog.go
1package main2 3import (4	"errors"5	"fmt"6)7 8type BalanceError struct {9	Amount, Balance int10}11 12func (e *BalanceError) Error() string {13	return fmt.Sprintf("取 %d:还差 %d", e.Amount, e.Amount-e.Balance)14}15 16func withdraw(balance, amount int) (int, error) {17	if amount > balance {18		return balance, &BalanceError{Amount: amount, Balance: balance}19	}20	return balance - amount, nil21}22 23func main() {24	_, err := withdraw(50, 80)25	err = fmt.Errorf("交房租:%w", err)26	fmt.Println(err)27	var be *BalanceError28	if errors.As(err, &be) {29		fmt.Println("差了", be.Amount-be.Balance)30	}31}
输出
交房租:取 80:还差 30
差了 30

errors.As 是 errors.Is 的「类型断言」版:它一层一层找类型是 *BalanceError 的错误,找到了就放进 be。所以要给它 &be,也就是要它填的那个变量的地址(第 11 课)。

练一练

写好代码,点检查。提示随便看,实在想不出来再看答案。

练习 1

可能算不出来的平均数

写好 average:返回 nums 的平均数和 nil;如果 nums 是空的,返回 0 和一个错误,内容是 没有数。程序试了两组数。

要显示
平均:7.5
出错:没有数
exercise1.go
1package main2 3import (4	"errors"5	"fmt"6)7 8func average(nums []int) (float64, error) {9	return 0, nil10}11 12func main() {13	for _, nums := range [][]int{{6, 9, 7, 8}, {}} {14		avg, err := average(nums)15		if err != nil {16			fmt.Println("出错:" + err.Error())17			continue18		}19		fmt.Printf("平均:%v\n", avg)20	}21}
输出
点「运行」或按 Ctrl+Enter
练习 2

修好它:Atoi 交回两样东西

这段程序编译不过。改好它,检查每个年龄:是数字就显示 年龄 30,strconv.Atoi 失败就显示 不是数字 三十。

要显示
年龄 30
不是数字 三十
exercise2.go
1package main2 3import (4	"fmt"5	"strconv"6)7 8func main() {9	for _, s := range []string{"30", "三十"} {10		n := strconv.Atoi(s)11		fmt.Println("年龄", n)12	}13}
输出
点「运行」或按 Ctrl+Enter
练习 3

缺货,包一层

写好 buy:如果 stock[item] 比 qty 少,返回一个包着 ErrOutOfStock 的错误,内容是 买 3 个 ink:缺货(用真正的数量和商品);不然返回 nil。

要显示
好了:pen
抱歉:买 3 个 ink:缺货
exercise3.go
1package main2 3import (4	"errors"5	"fmt"6)7 8var ErrOutOfStock = errors.New("缺货")9 10var stock = map[string]int{"pen": 5, "ink": 1}11 12func buy(item string, qty int) error {13	return nil14}15 16func main() {17	for _, item := range []string{"pen", "ink"} {18		err := buy(item, 3)19		if errors.Is(err, ErrOutOfStock) {20			fmt.Println("抱歉:" + err.Error())21		} else {22			fmt.Println("好了:" + item)23		}24	}25}
输出
点「运行」或按 Ctrl+Enter

这一课学会了

  • 可能失败的函数把 error 放在最后一个返回值;nil 表示成功。
  • error 是一个接口:Error() string。调用完马上 if err != nil 检查。
  • strconv.Atoi 返回数和错误;不是数字时数是 0。
  • errors.New("…") 做一个简单的错误;fmt.Errorf 像 Sprintf 一样带上细节。
  • %w 把一个错误包进另一个;errors.Is 一层层往里找,errors.Unwrap 剥掉一层。
  • 有 Error() 方法的自己的类型就是错误;errors.As(err, &target) 找到它并填进 target。
这一课学完了吗?点一下标记。进度存在这个浏览器里,随时可以撤销。