“可对值进行编程,而错误是值,因此可对错误进行编程.错误不像异常,因为错误没有什么特别之处,而未处理的异常可能导致程序崩溃.”–Rob Pike
错误的分类和处理机制
广义错误(errors) 非期望的行为
- 未捕获错误(untrapped error 异常)
未知错误,未定义的错误类型 --> 段错误
- 可捕获错误(trappend error)
已知错误,预定义的错误类型
- 编译错误(compile errors)
编译器捕获 --> 语法错误
- 运行时错误(runtime errors)
运行时捕获 --> 解引用空指针,数组越界访问
- 逻辑错误(logic errors)
人工(测试/错误处理代码/代码审查)捕获
- 编译错误(compile errors)
C 是非类型安全的语言.段错误(Segmentation Fault)在编译期和运行期都没捕获处理,由操作系统进行异常处理,属于未捕获错误.
Go 是类型安全的语言.不会出现编译期和运行期都无法捕获的错误,所以 go 的错误是可捕获的错误.编译错误可以靠编译器发现,因此, go 要处理的错误就只有两种:
- 运行时错误:由运行时捕获并抛出 panic.处理方式:
- 被 recover 捕获,被处理或继续抛出 panic.
- 未被 recover 捕获,程序崩溃.
- 逻辑错误:非预期的行为(结果),不会引发运行时错误.处理方式:
- 无法判定错误严重性,希望调用者决定,返回 error.
- 判定错误严重,希望程序停止运行(崩溃),主动抛出 panic.
错误的表示
- 返回 error
- 抛出 panic
错误的处理
- 控制流处理机制( if/else/return )
- 异常处理机制( panic/recover )
error
错误原因的种类
- 0 ,总是成功,不返回或返回对应的值.(v = fnxxx(…))
- 1 ,错误只有一种情况,用 bool 类型返回值, ture 表示正确, false 表示错误.(v,ok = fnxxx(…))
- n ,错误有多种的情况,用 error 类型返回值, nil 表示正确,其他不同的值表示不同的错误.(ret,err = fnxxx(…))
错误处理跟在函数调用之后,正确的代码由可能就不要缩进,使代码平坦.
v,ok = fnxxx()
if !ok{
return;
}
fnyyy() //不用缩进,代码平坦
发生错误时,尽可能返回个有用的值而非一定是 nil,因为用户可能这么用
v,_= fnxxx()
fmt.Printf("...",v) //不作判断直接使用 v
尽量不定义新的 error 类型.没有特殊性需要通过新的 error 类型去传达,只要返回通用的 error 类型.内置的 errors 包可以构建一个 error 变量.
import errors
fnxxx()error{
//...
return errors.New("error ...")
}
更好用 fmt.Errorf ,封装了 errors.New.
package fmt
import "errors"
func Errorf(format string, a ...interface{}) error {
return errors.New(Sprintf(format, a...))
}
nil 用作返回值的含意
- 零值,表示返回值是 nil, retval = nil
- 零值,表示无错误, error = nil
- 占位符,表示不想返回值.
panic
panic 表示程序遇到严重错误.
发生 panic 时,程序的行为
- 程序正常执行终止,进入 panic 状态.
- 从栈顶到栈底倒序执行 defer 函数.
- 如果有 defer 函数调用了 recover 函数, 那么 panic 被捕获, panic 状态解除, defer 所在函数正常返回(程序进入正常状态).
- 否则直到 main 函数,然后异常退出.
- 打印栈跟踪信息,之后清理栈.
遇到不可能的情况, panic 是最好的选择
switch s := suit(drawCard()); s {
case "Spades":
// ...
case "Hearts":
// ...
case "Diamonds":
// ...
case "Clubs":
// ...
default:
panic(fmt.Sprintf("invalid suit %v", s))
}
假设这段代码是程序的主要逻辑. suit() 的返回值有且仅有四种(Spades,Hearts,Diamonds,Clubs),这是已知条件,是 suit() 给出的契约,可以从 suit() 源码或 API 手册确认.现在进入default 分支,出现了第五种值. 处理方式一,打印日志,继续运行,带着错误继续运行产生更多的错误,继续运行也没意义了,还不如停止抛出 panic.处理方式二,返回错误值给上层调用者处理,上层代码是根据 suit() 的契约,带着预期使用 suit(), 现在这一种超预期的结果要它处理,那潜藏的成千上万种超预期的结果是否也要它处理.所以返回错误值,调用者也处理不了,还是原地抛出 panic.
Must 开头的函数一般要求正确的输入,如果输入不正确引发 panic
package regexp
func MustCompile(str string) *Regexp {
regexp, error := Compile(str)
if error != nil {
panic(`regexp: Compile(` + quote(str) + `): ` + error.Error())
}
return regexp
}
//使用场景
var httpSchemeRE = regexp.MustCompile(`^https?:`)//"http:"或"https"
这里直接用 `^https?:` 调用 Compile() 也能得到想要的结果.但是接口不匹配,只想要一个值但 Compile() 却返回两个值,所以需要一个包装函数获得希望的接口方便使用,即 MustCompile() 函数. 这种情况下最终传给 Compile() 是 `^https?:` ,结果是可预期的(regexp = xxx , error = nil),如果得到非预期的结果(nil != nil)抛出 panic 可以快速发现低级错误( `^https?:` 拼写错误)或严重错误( Compile() 函数有问题).
panic 函数的参数 interface{} 类型,可以传给它任何值.对应的处理函数是 recover,如果它捕获了 panic,那么返回值是传给 panic 函数的值.如果没捕获 panic,那么返回值是 nil.尽量用 error 接口的值传给 panic 函数,以备 recover 函数捕获后分析.
defer func(){
if err := recover(); err != nil{
}
}
panic(errors.New("some errors"))