[golang note] 错误处理
错误处理
• 错误处理的标准模式
√ golang错误处理的标准模式:error接口。
√ golang函数如果要返回错误,规范上是将error作为多返回值中的最后一个,但这并非是强制要求。
▶ error接口
type error interface { Error() string }
▶ 内置的error类型使用
▪ 语法如下
func 函数名(参数列表) (返回值列表, err error) { // 函数体 }
▪ 错误处理
例如我们有一个这样的函数:
func Foo(param int) (n int, err error) { // 函数体 }
调用函数时建议按如下方式处理错误:
n, err := Foo(0) if err != nil { // 错误处理 } else { // 使用返回值n }
▪ 示例如下
package main import ( "errors" "fmt" ) func divide(dividend float64, divisor float64) (result float64, err error) { if divisor == 0 { return -1, errors.New("除数为0") } return dividend / divisor, nil } func main() { result, err := divide(1, 2) if err != nil { fmt.Println(err.Error()) } else { fmt.Println("result =", result) } result, err = divide(1, 0) if err != nil { fmt.Println(err.Error()) } else { fmt.Println("result =", result) } }
▶ 自定义error类型使用
√ golang错误处理支持自定义的error类型,只需要为自定义error类型实现Error接口即可。
▪ 语法如下
type CustomError struct { ... } func (e *CustomError) Error() string { // 函数体 }
▪ 示例如下
package main import ( "fmt" ) type MathError struct { Op string info string } func (e *MathError) Error() string { return "Math operation " + e.Op + " error : " + e.info } func divide(dividend float64, divisor float64) (result float64, err error) { if divisor == 0 { return -1, &MathError{"division", "divisor is zero"} } return dividend / divisor, nil } func main() { result, err := divide(1, 2) if err != nil { fmt.Println(err.Error()) } else { fmt.Println("result =", result) } result, err = divide(1, 0) if err != nil { fmt.Println(err.Error()) } else { fmt.Println("result =", result) } }
▪ 类型转换
√ 如果处理错误时需要获取详细信息,而不仅仅满足于打印一句错误信息,那就需要用到类型转换。
package main import ( "fmt" ) type MathError struct { Op string info string } func (e *MathError) Error() string { return "Math operation " + e.Op + " error : " + e.info } func divide(dividend float64, divisor float64) (result float64, err error) { if divisor == 0 { return -1, &MathError{"division", "divisor is zero"} } return dividend / divisor, nil } func main() { result, err := divide(1, 0) if err != nil { // error类型转换为*MathError指针,因为接口定义传入类型对象为*MathError指针 // 如果接口定义时传入类型对象为MathError,那么这里的写法为err.(MathError) if e, ok := err.(*MathError); ok { fmt.Println(e.info) } } else { fmt.Println("result =", result) } }
资源释放
在c++程序中,经常要注意内存指针、文件句柄、网络套接字等等资源的释放,特别需要注意其释放的时机。而golang使用defer
关键字和背后的内部机制简单地解决了资源释放的问题。
√ defer关键字能保证其后的代码能在函数退出前调用。
√ 一个函数中可以存在多个defer语句,需要注意的是defer语句的调用是遵照先进后出的原则,即最后一个defer语句将最先被执行。
√ 可以在defer后加一个匿名函数来进行复杂的清理工作。
• 简单的清理工作
▶ 语法如下
func 函数名(参数列表) (返回值列表) { ... // 资源申请 defer 清理函数 ... }
▶ 示例如下
package main import ( "io" "os" ) func CopyFile(dst, src string) (w int64, err error) { srcFile, err := os.Open(src) if err != nil { return } defer srcFile.Close() dstFile, err := os.Create(dst) if err != nil { return } defer dstFile.Close() return io.Copy(dstFile, srcFile) } func main() { CopyFile("D:/2.txt", "D:/1.txt") }
▶ 先进后出规则
package main import ( "fmt" ) func Test() { defer fmt.Println(1) defer fmt.Println(2) defer fmt.Println(3) } func main() { Test() }
• 复杂的清理工作
▶ 语法如下
func 函数名(参数列表) (返回值列表) { ... // 资源申请 defer func() { // 复杂的清理工作 } () ... }
▶ 示例如下
package main import ( "fmt" "io" "os" ) func CopyFile(dst, src string) (w int64, err error) { srcFile, err := os.Open(src) if err != nil { return } defer func() { fmt.Println("close file :", src) srcFile.Close() }()
dstFile, err := os.Create(dst) if err != nil { return } defer func() { fmt.Println("close file :", dst) dstFile.Close() }() return io.Copy(dstFile, srcFile) } func main() { CopyFile("D:/2.txt", "D:/1.txt") }
异常处理
一些高级语言中一般提供类似try...catch...finally...的语法,用于捕获异常。golang提供panic和recover两个关键字用于异常处理。
• panic
panic在golang中是一个内置函数,接收一个interface{}类型的值作为参数:
func panic(interface{}) { ... }
当一个函数执行过程中调用panic函数时,函数执行流程将立即终止,但panic之前的defer关键字延迟执行的语句将正常执行,之后该函数将返回到上层调用函数,并逐层向上执行panic流程,直至函数所属的goroutine中所有正在执行函数终止。错误信息将被报告,包括在调用panic()函数时传入的参数。下面用一个示例说明:
package main import ( "fmt" ) func MyFunc1() { defer fmt.Println("MyFunc1 defer 1") panic("MyFunc1 panic test") defer fmt.Println("MyFunc1 defer 2") } func MyFunc2() { defer fmt.Println("MyFunc2 defer 1") MyFunc1() defer fmt.Println("MyFunc2 defer 2") } func main() { MyFunc2() }
程序输出如下:
• recover
recover在golang中是一个内置函数,返回一个interface{}类型的值作为参数:
func recover() interface{} { ... }
panic函数触发后不会立即返回,而是先defer,再返回。如果defer的时候,有办法将panic捕获到,然后及时进行异常处理,并阻止panic传递,那处理机制就完善了。因此golang提供了recover内置函数,用于捕获panic并阻止其向上传递。需要注意的是,recover之后,逻辑并不会恢复到panic处,函数还是会在defer之后返回,但是所属goroutine将不会退出。
▶ 本层函数处理
package main import ( "fmt" ) func MyFunc1() { defer func() { fmt.Println("MyFunc1 defer 1") if r := recover(); r != nil { fmt.Println("Runtime error caught :", r) } }() panic("MyFunc1 panic test") fmt.Println("MyFunc1 defer 2") } func MyFunc2() { defer fmt.Println("MyFunc2 defer 1") MyFunc1() defer fmt.Println("MyFunc2 defer 2") } func main() { MyFunc2() }
程序输出如下:
▶ 上层函数处理
package main import ( "fmt" ) func MyFunc1() { defer fmt.Println("MyFunc1 defer 1") panic("MyFunc1 panic test") fmt.Println("MyFunc1 defer 2") } func MyFunc2() { defer func() { fmt.Println("MyFunc1 defer 2") if r := recover(); r != nil { fmt.Println("Runtime error caught :", r) } }() MyFunc1() defer fmt.Println("MyFunc2 defer 2") } func main() { MyFunc2() }
程序输出如下:
• 模拟try...catch...语法
▶ 语法如下
func Try(f func(), handler func(interface{})) { defer func() { if err := recover(); err != nil { handler(err) } }() f() }
▶ 示例如下
package main import ( "fmt" ) func Try(f func(), handler func(interface{})) { defer func() { if err := recover(); err != nil { handler(err) } }() f() } func main() { Try(func() { panic("main panic") }, func(e interface{}) { fmt.Println(e) }) }