错误处理
第 9 课里,strconv.Atoi 把 "42" 变成 42,它的第二个返回值被我们用 _ 丢掉了。那第二个值是一个错误(error),说的是转换成功没有。这回把它留下来:
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:
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:
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}
half(10) 成功,err 是 nil,显示成 <nil>。half(7) 返回 0 和一个错误,错误的内容是 是奇数。第二个 := 没问题:b 是新变量,err 是已有的,只是重新赋值。
带上细节:fmt.Errorf
「余额不够」没说要取多少。fmt.Errorf 和 fmt.Sprintf 一样填格式,只是做出来的是一个错误:
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 假装是一个放文件的文件夹:
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) 会一层一层往里找。直接用 == 只比最外面那一层:
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 方法的类型都是错误。用结构体,就能把细节放在字段里,调用的地方拿到的是数,而不只是一句话:
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 课)。
练一练
写好代码,点检查。提示随便看,实在想不出来再看答案。
可能算不出来的平均数
✓写好 average:返回 nums 的平均数和 nil;如果 nums 是空的,返回 0 和一个错误,内容是 没有数。程序试了两组数。
平均:7.5 出错:没有数
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}
修好它:Atoi 交回两样东西
✓这段程序编译不过。改好它,检查每个年龄:是数字就显示 年龄 30,strconv.Atoi 失败就显示 不是数字 三十。
年龄 30 不是数字 三十
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}
缺货,包一层
✓写好 buy:如果 stock[item] 比 qty 少,返回一个包着 ErrOutOfStock 的错误,内容是 买 3 个 ink:缺货(用真正的数量和商品);不然返回 nil。
好了:pen 抱歉:买 3 个 ink:缺货
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}
这一课学会了
- 可能失败的函数把
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。