Golang入门笔记-CH11-错误处理

  1. 错误处理
    1. 定义错误
    2. 用 fmt 创建错误对象
  2. 运行时异常和 panic
  3. 从 panic 中恢复
  4. 闭包处理错误

Go 没有像 Java 那样的 try/catch 异常处理机制,而是用 defer/panic/recover 机制来处理异常。

Go 语言的设计者认为 try/catch 机制使用过于泛滥,而且从底层向高层抛出错误太耗费资源,因此他给 Go 语言设计了一种返回值处理错误方式:通过在函数和方法中返回错误对象,这个错误对象一般在多个返回值的最后;如果返回 nil,则表明没有错误,且主调函数应该检查并处理每一个错误。

我们通过调用 pack1 包中的 Func1 函数来了解 Go 语言中的错误处理方式:

Func1返回了两个值,一个 valueerrerr 是错误对象,若 err 不为 nilnil的意思,类似于 Java 中的 null),则进行错误处理,打印出具体错误信息。

1
2
3
4
if value, err := pack1.Func1(param1); err != nil {
fmt.Printf("Error %s in pack1.Func1 with parameter %v", err.Error(), param1)
return // or: return err
}

Go 有一个预先定义的 error 接口类型 :

1
2
3
type error interface { 
Error() string
}

错误处理

定义错误

可以通过 errors 包中 New() 函数传递错误信息,从而自定义错误,如下:

1
err := errors.New("square root of negative number")

我们来看一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
package main

import (
"errors"
"fmt"
)

var errNotFound = errors.New("not found error")

func main() {
fmt.Printf("Error: %v", errNotFound)
}

上述代码运行结果为:

image.png

我们通过测试平方根函数,来了解错误处理机制:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
package main

import (
"errors"
"fmt"
"math"
)

// 自定义错误
var errSqrtNegative = errors.New("square root of negative number")

/*
求平方根:
若传入数据小于 0,返回 0 和错误;否则,返回平方根结果和 nil
*/
func sqrt(x float64) (float64, error) {
if x < 0 {
return 0, errSqrtNegative
}
return math.Sqrt(x), nil
}

func main() {
x, err := sqrt(-1)
fmt.Printf("x: %v\n", x)
if err != nil { // 若错误不为空,进行错误处理
fmt.Printf("error: %v\n", err.Error())
}

x, err = sqrt(9)
fmt.Printf("x: %v\n", x)
if err != nil {
fmt.Printf("error: %v\n", err.Error())
}
}

上述代码运行结果为:

image.png

注意:一般错误信息都会有 Error 前缀,因此错误信息不要以大写字母开头。

自定义错误类型中可以包含错误信息以外的其他信息,我们来看下 os.Open 操作触发的 PathError 错误:

1
2
3
4
5
6
7
8
9
10
// PathError records an error and the operation and file path that caused it.
type PathError struct {
Op string // “open”, “unlink”, etc.
Path string // The associated file.
Err error // Returned by the system call.
}

func (e *PathError) String() string {
return e.Op + " " + e.Path + ": "+ e.Err.Error()
}

如果在一个操作中会发生多种错误,可以用类型断言或者类型判断对错误进行判断:

1
2
3
if err, ok := err.(*os.PathError); ok {
// ...
}

或:

1
2
3
4
5
6
7
8
switch err := err.(type) {
case ParseError:
// ...
case PathError:
// ...
default:
fmt.Printf("not a special error, just %v\n", err)
}

用 fmt 创建错误对象

可以通过 fmt.Errorf() 来创建错误对象,用法类似于 fmt.Printf(),可以用占位符来格式化输出,例如:

1
2
3
if f < 0 {
return 0, fmt.Errorf("math: square root of negative number %g", f)
}

运行时异常和 panic

当发生数组下标越界等运行错误时,会触发 panic,程序会崩溃并抛出一个 runtime.Error 接口类型值。

panic 一般用在错误条件很苛刻不可恢复时,当程序无法继续运行时,比如连接数据库时密码错误,可以使用 panic 函数产生一个中止运行的错误。

panic 可以接收一个任意类型的参数,一般是字符串,当程序执行到 panic 时,会打印出来,同时 panic 之后的语句将不会被执行, 我们来看一个例子:

1
2
3
4
5
6
7
8
9
package main

import "fmt"

func main() {
fmt.Println("Starting the program")
panic("A severe error occurred: stopping the program!")
fmt.Println("Ending the program")
}

上述代码运行结果为:

image.png

不能随意用 panic 来中止程序,必须尽力补救错误以便让程序能够继续正常运行。

从 panic 中恢复

recover 函数可以让程序从 panic 场景下恢复,停止终止过程而恢复正常。

recover 只能在 defer 修饰的函数中使用,可以获取从 panic 中传递过来的错误信息,若是正常调用 recover 只返回 nil

我们来看一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package main

import (
"fmt"
)

func badCall() {
panic("bad end")
}

func test() {
// 用 defer 调用匿名函数
defer func() {
// 用 recover 接收 panic 错误信息
if e := recover(); e != nil {
fmt.Printf("Panicing %s\r\n", e)
}
}()
badCall()
fmt.Printf("After bad call\r\n")
}

func main() {
fmt.Printf("Calling test\r\n")
test()
fmt.Printf("Test completed\r\n")
}

上述代码运行结果为:

image.png

闭包处理错误

每当函数返回错误时,我们都应该去处理,但这样会导致代码重复。结合 defer/panic/recover 机制和闭包我们可以创造一种更优雅的错误处理模式。但是这个模式只适用于函数签名相同的情况,一个很好的例子就是在 Web 应用中的处理函数,它的一般形式如下:

1
2
3
func handler(w http.ResponseWriter, r *http.Request) { 
// ...
}

假设所有函数都是这样的签名:

1
func f(a type1, b type2)

给这个函数类型一个名称:

1
fType1 = func f(a type1, b type2)

在该模式中使用两个帮助函数:

  • check:用来检查是否有错误和 panic 发生。

    1
    2
    3
    4
    5
    func check(err error) { 
    if err != nil {
    panic(err)
    }
    }
  • errorhandler:一个包装函数,接收一个 fType1 类型的函数并返回一个 fType1 类型的函数,在其中包含 defer/recover 机制。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    func errorHandler(fn fType1) fType1 {
    return func(a type1, b type2) {
    defer func() {
    if e, ok := recover().(error); ok {
    log.Printf("run time panic: %v", err)
    }
    }()
    fn(a, b)
    }
    }

当有错误时会 recover 并打印日志,check 函数会在所有的被调函数中调用:

1
2
3
4
5
6
7
8
9
10
func f1(a type1, b type2) {
// ...
f, _, err := handler1() // 调用函数
check(err) // 错误处理
t, err := handler2()
check(err)
_, err2 := handler3()
check(err2)
// ...
}

在这种机制下,所有的错误都会被 recover,且错误处理代码也简化为调用 check(err) 。在这种模式下,不同的错误处理必须对应不同的函数类型;错误处理可能被隐藏在处理包内部。


转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 richffan@outlook.com

文章标题:Golang入门笔记-CH11-错误处理

字数:1.6k

本文作者:Rich Fan

发布时间:2022-10-01, 00:00:00

最后更新:2024-02-27, 08:17:39

原始链接:http://fanrich.github.io/2022/09/30/Golang/Golang%E5%85%A5%E9%97%A8%E7%AC%94%E8%AE%B0-CH11-%E9%94%99%E8%AF%AF%E5%A4%84%E7%90%86/

版权声明: "署名-非商用-相同方式共享 4.0" 转载请保留原文链接及作者。