Golang: Language: Error Handling

 17th December 2021 at 6:29pm

Go 错误处理机制有两种:

  • 绝大多数函数返回 errors
  • 部分无法恢复的情况,比如访问数组不存在的 index,会抛出一个运行时异常,被称为 panic

Errors

Go 中有一种 pattern,一个函数返回两个值,分别是操作结果和 err 对象,比如 os.Open()

func Open(name string) (file *File, err error)

使用时:

f, err := os.Open("filename.ext")
if err != nil {
    log.Fatal(err)
}

自定义错误

基于字符串、不带其他数据,同时不使用类型来区分错误时:

// simple string-based error
err1 := errors.New("math: square root of negative number")

// with formatting
err2 := fmt.Errorf("math: square root of negative number %g", x)

带其他数据或者希望使用类型来区分错误时,可以自定义一个 struct,只要实现了 Go 预先定义的 error 接口:

type error interface {
    Error() string
}

比如:

type SyntaxError struct {
    Line int
    Col  int
}

func (e *SyntaxError) Error() string {
    return fmt.Sprintf("%d:%d: syntax error", e.Line, e.Col)
}
type InternalError struct {
    Path string
}

func (e *InternalError) Error() string {
    return fmt.Sprintf("parse %v: internal error", e.Path)
}

假如不带其他数据,可以定义一个 int 型错误,比如:

type keyIncorrectError int

func (ki keyIncorrectError) Error() string {
	return "openpgp: incorrect key"
}

// 因为每次的 Error() string 都是一样的,出现这种错误时,该包返回一个固定的
// error 对象(ErrKeyIncorrect)即可。该包函数的调用方判断 err 是否等于 
// ErrKeyIncorrect 即可,而不用 switch / case 来判断 error 类型。
var ErrKeyIncorrect error = keyIncorrectError(0)

使用时用 switch / case 语句来判断是哪种错误:

if err := Foo(); err != nil {
    switch e := err.(type) {
    case *SyntaxError:
        // Do something interesting with e.Line and e.Col.
    case *InternalError:
        // Abort and file an issue.
    default:
        log.Println(e)
    }
}

Panic

Panic 类似于 C++、Java 或 Python 的异常。抛出一个异常,用 panic() 函数:

func foo() int {
    panic("something wrong happened")
    return 0
}

panic 是针对 goroutine 的。一旦抛出异常,正常的执行流被中断。如果没有调用 recover(),程序会 crash 并且打印出堆栈,像这样:

goroutine 11 [running]:
testing.tRunner.func1(0xc420092690)
    /usr/local/go/src/testing/testing.go:711 +0x2d2
panic(0x53f820, 0x594da0)
    /usr/local/go/src/runtime/panic.go:491 +0x283
github.com/yourbasic/bit.(*Set).Max(0xc42000a940, 0x0)
    ../src/github.com/bit/set_math_bits.go:137 +0x89
github.com/yourbasic/bit.TestMax(0xc420092690)
    ../src/github.com/bit/set_test.go:165 +0x337
testing.tRunner(0xc420092690, 0x57f5e8)
    /usr/local/go/src/testing/testing.go:746 +0xd0
created by testing.(*T).Run
    /usr/local/go/src/testing/testing.go:789 +0x2de

GOTRACEBACK 变量可以控制输出信息的详细程度。

子协程的 panic 并不会被父协程 recover

这个需要特别注意

func say(s string) {
    fmt.Println(s)
    time.Sleep(1000 * time.Millisecond)
    panic("saying panic!")
}

func main() {
    defer func(){
        if p:= recover();p!=nil{
            err := fmt.Errorf("internal error:%v", p)
            fmt.Println(err)
        }
    }()
    go say("world")
    say("hello")
}

虽然 main() 中有 recover(),但是程序仍然会 panic 退出。

panic 后的恢复

Go 中唯一能停止 panic 将程序 crash 的方式是,在 defer 语句 中调用 recover() 函数:

func main() {
    n := foo()
    fmt.Println("main received", n)
}

func foo() int {
    defer func() {
        if err := recover(); err != nil {
            fmt.Println(err)
        }
    }()
    m := 1
    panic("foo: fail")
    m = 2
    return m
}
foo: fail
main received 0

也可以使用命名返回值(named return value),使得 defer 语句中可以指定返回值。如果没有指定返回值,程序继续执行时就像当前函数返回了空的返回值一样返回。例子:

func main() {
    n := foo()
    fmt.Println("main received", n)
}

func foo() (m int) {
    defer func() {
        if err := recover(); err != nil {
            fmt.Println(err)
            m = 2
        }
    }()
    m = 1
    panic("foo: fail")
    m = 3
    return m
}
foo: fail
main received 2

参考