错误error
# 错误处理
在实际工程项目中,希望通过程序的错误信息快速定位问题,但是又不喜欢错误处理代理写的冗余而又啰嗦.Go
语言没有提供像Java、C#
语言中的try...catch
异常处理方式,而是通过函数返回值逐层往上抛.这种设计,鼓励工程师在代码中显式的检查错误,而非忽略错误,好处就是避免漏掉本应处理的错误.但是带来一个弊端,让代码啰嗦.
# 1.1 什么是错误
什么是错误?
错误指的是可能出现问题的地方出现了问题.比如打开一个文件时失败,这种情况在人们的意料之中.
而异常指的是不应该出现问题的地方出现了问题.比如引用了空指针,这种情况在人们的意料之外.可见,错误是业务过程的一部分,而一异常不是.
Go
中的错误也是一种类型.错误用内置的error
类型表示.就像其他类型的,如int
,float64
.错误值可以存储在变量中,从函数中返回,等等.
# 1.2 演示错误
示例如: 程序尝试打开一个不存在的文件.
示例代码:
package main
import (
"fmt"
"os"
)
func main() {
f,err := os.Open("/test.txt")
if err != nil {
fmt.Println(err)
return
}
//根据f进行文件的读写
fmt.Println(f.Name(),"opend successfully")
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
在
os
包中有打开文件的功能函数func Open(name string)(file *File,err error)
如果文件已经成功打开,那么
Open
函数将返回文件处理.如果在打开文件时出现错误,将返回一个非nil
错误.
如果一个函数或方法返回一个错误,那么按照惯例,它必须是函数返回的最后一个值.因此,Open
函数返回的值是最后一个值.
处理错误的管用方法是将返回的错误与nil
进行比较.nil
值表示没有发生错误,而非nil
值表示出现错误.在示例中,检查错误是否为nil
.如果它不是nil
,只需要打印错误并从主函数返回.
运行结果:
open /test.txt: No such file or directory
等到一个错误,说明该文件不存在
# 1.3 错误类型表示
Go
语言通过内置的错误接口提供了非常简单的错误处理机制.
再深入一点,看看如何定义错误类型的构建.错误是一个带有以下定义的接口类型.
type error interface {
Error() string
}
2
3
它包含一个带有Error()
字符串的方法.任何实现这个接口的类型都可以作为一个错误使用.这个方法提供了对错误的描述.
当打印错误时,fmt.Println
函数在内部调用Error()
方法来获取错误的描述.这就是错误描述是如何字啊一行中打印出来的.
从错误中提取更多信息的不同方法
既然知道错误是一种接口类型,那么如何提取更多关于错误的信息.
在上面示例中,仅仅是打印了错误的描述.如果想要的是导致错误的文件的实际路径.一种可能的方法是解析错误字符串.如下程序的输出.
open /test.txt: No such file or directory
可以解析这个错误消息并从中获取文件路径"/test.txt".但这是一个糟糕的方法.在最新版本的语言中,错误描述可以随时更改,代码将会中断.
# 是否有办法可靠地获取文件名?
1.断言底层结构类型并从结构字段获取更多信息
如果仔细阅读打开函数的文档,可看到它返回的是PathError
类型的错误.PathError
是一个struct
类型,它在标准库中的实现如下
type PathError struct {
Op string
Path string
Err error
}
func (e *PathError) Error() string { return e.Op + " " + e.Path : ": " + e.Err.Error()}
2
3
4
5
6
7
从上面的代码中,可以理解PathError
通过声明Error() string
方法实现了错误接口.该方法连接操作、路径和实际错误并返回它.这丫就得到了错误信息,
open /test.txt: No such file or directory
PathError
结构的路径字段包含导致错误的文件路径.修改上面写的程序,并打印出路径.
修改代码:
package main
import (
"fmt"
"os"
)
func main(){
f,err := os.Open("/test.txt")
if err,ok := err.(*os.PathError);ok {
fmt.Println("File at path",err.Path,"failed to open")
return
}
fmt.Println(f.Name(),"opened successfully")
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
在上面的程序中,使用类型断言获得错误接口的基本值.然后用错误来打印路径这个程序输出.
File at path /test.txt failed to open
2.断言底层结构类型,并使用方法获取更多信息
获得更多信息的第二种方法是断言底层类型,并通过调用struct
类型的方法获取更多信息.
示例代码:
type DNSError struct {
...
}
func (e *DNSError) Error() string {
...
}
func (e *DNSError) Timeout() bool {
...
}
func (e *DNSError) Temporary() bool {
...
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
从上面的代码中可以看到,DNSError struct
有两个方法Timeout() bool
和Temporary() bool
,他们返回一个布尔值,表示错误是由于超时还是临时的.
编写一个断言*DNSError
类型的程序,并调用这些方法来确定错误是临时的还是超时的.
package main
import (
"fmt"
"net"
)
func main() {
addr,err := net.LookupHost("golangbot123.com")
if err,ok := err.(*net.DNSError);ok {
if err.Timeout() {
fmt.Println("operation timed out")
} else if err.Temporary() {
fmt.Println("temporary error")
} else {
fmt.Println("generic error: ",err)
}
return
}
fmt.Println(addr)
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
在上面的程序中,正在尝试获取一个无效域名的ip
地址,这是一个无效的域名.golangbot123.com
.通过声明它来输入*net.DNSError
来获得错误的潜在价值.
例子中,错误既不是展示的,也不是由于超时,因此程序会打印出来.
generic error: lookup golangbot123.com: no such host
如果错误是临时的或超时的,那么相应的if
语句就会执行,可以适当地处理它.
3.直接比较
获得更多关于错误的详细信息的第三种方法是直接与类型错误的变量进行比较.通过一个例子来理解这个问题.
filepath
包的Glob
函数用于返回与模式匹配的所有文件的名称.当模式出现错误时,该函数将返回一个错误ErrBadPattern
在filepath
包中定义了ErrBadPattern
,如下所述:
var ErrBadPattern = errors.New("syntax error in pattern")
errors.New()
用于创建新的错误.
当模式出现错误时,由Glob
函数返回ErrBadPattern
.
示例检验错误:
package main
import (
"fmt"
"path/filepath"
)
func main() {
files,error := filepath.Glob("[")
if error != nil && error == filepath.ErrBadPattern {
fmt.Println(error)
return
}
fmt.Println("matched files",files)
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
运行结果:
syntax error in pattern
不要忽略错误
永远不要忽略一个错误.忽略错误会招致麻烦.例如示例列出了与模式匹配的所有文件的名称,二忽略了错误处理代码.
package main
import (
"fmt"
"path/filepath"
)
func main(){
files,_ := filepath.Glob("[")
fmt.Println("matched files",files)
}
2
3
4
5
6
7
8
9
10
11
从前面的例子中已经知道模式是无效的.忽略了Glob
函数返回的错误,方法是使用行号中的空白表示符.
matched files []
由于忽略了这个错误,输出看起来好像没有文件匹配这个模式,但是实际上本身是畸形的.所以不要忽略错误.
# 1.4 自定义错误
创建自定义错误可以使用errors
包下的New()
函数,以及fmt
包下的:Errorf()
函数.
//errors包:
func New(text string) error {}
//fmt包:
func Errorf(format string, a ...interface{}) error {}
2
3
4
5
在使用New()
函数创建自定义错误之前,了解它是如何实现的.下面提供了错误包中的新功能的实现.
// Package errors implements functions to manipulate errors.
package errors
// New returns an error that formats as the given text.
func New(text string) error {
return &errorString{text}
}
// errorString is a trivial implementation of error.
type errorString struct {
s string
}
func (e *errorString) Error() string {
return e.s
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
既然知道了New()
函数是如何工作的,那么就在自己的程序中使用它来创建一个自定义错误.
创建一个简单的程序,计算一个圆的面积,如果半径为负,将返回一个错误.
package main
import (
"errors"
"fmt"
"math"
)
func circleArea(radius float64) (float64, error) {
if radius < 0 {
return 0, errors.New("Area calculation failed, radius is less than zero")
}
return math.Pi * radius * radius, nil
}
func main() {
radius := -20.0
area, err := circleArea(radius)
if err != nil {
fmt.Println(err)
return
}
fmt.Printf("Area of circle %0.2f", area)
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
运行结果:
Area calculation failed, radius is less than zero
使用Errorf
向错误添加更多信息
上面的程序运行得很好,但是如果打印出导致错误的实际半径,那就不好了.这就是fmt
包的Errorf
函数的用武之地.这个函数根据一个格式说明器格式化错误,并返回一个字符串作为值来满足错误.
使用Errorf
函数,修改程序.
package main
import (
"fmt"
"math"
)
func circleArea(radius float64) (float64, error) {
if radius < 0 {
return 0, fmt.Errorf("Area calculation failed, radius %0.2f is less than zero", radius)
}
return math.Pi * radius * radius, nil
}
func main() {
radius := -20.0
area, err := circleArea(radius)
if err != nil {
fmt.Println(err)
return
}
fmt.Printf("Area of circle %0.2f", area)
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
运行结果:
Area calculation failed, radius -20.00 is less than zero
使用struct
类型和字段提供关于错误的更多信息
还可以使用将错误接口实现为错误的struct
类型.这提供了更多的错误处理的灵活性.在示例中,如果想要访问导致错误的半径,那么现在唯一的方法是解析错误描述区域计算失败,半径-20.00
小于零.这不是一种正确的方法,因为如果描述发生了变化,代码就会中断.
使用在前面的教程中解释的标准库的策略,在“断言底层结构类型并从struct
字段获取更多信息”,并使用struct
字段来提供对导致错误的半径的访问.创建一个实现错误接口的struct
类型,并使用它的字段来提供关于错误的更多信息.
第一步是创建一个struct
类型来表示错误.错误类型的命名约定是,名称应该以文本Error
结束.把struct
类型命名为areaError
type areaError struct {
err string
radius float64
}
2
3
4
上面的struct
类型有一个字段半径,它存储了为错误负责的半径的值,并且错误字段存储了实际的错误消息.
下一步,是实现error
接口
func (e *areaError) Error() string {
return fmt.Sprintf("radius %0.2f: %s", e.radius, e.err)
}
2
3
在上面的代码片段中,使用一个指针接收器区域错误来实现错误接口的Error() string
方法.这个方法打印出半径和错误描述.
package main
import (
"fmt"
"math"
)
type areaError struct {
err string
radius float64
}
func (e *areaError) Error() string {
return fmt.Sprintf("radius %0.2f: %s", e.radius, e.err)
}
func circleArea(radius float64) (float64, error) {
if radius < 0 {
return 0, &areaError{"radius is negative", radius}
}
return math.Pi * radius * radius, nil
}
func main() {
radius := -20.0
area, err := circleArea(radius)
if err != nil {
if err, ok := err.(*areaError); ok {
fmt.Printf("Radius %0.2f is less than zero", err.radius)
return
}
fmt.Println(err)
return
}
fmt.Printf("Area of circle %0.2f", area)
}
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
36
程序输出:
Radius -20.00 is less than zero
使用结构类型的方法提供关于错误的更多信息
编写一个程序来计算矩形的面积.如果长度或宽度小于0
,这个程序将输出一个错误.
第一步是创建一个结构来表示错误.
type areaError struct {
err string //error description
length float64 //length which caused the error
width float64 //width which caused the error
}
2
3
4
5
上面的错误结构类型包含一个错误描述字段,以及导致错误的长度和宽度.
现在有了错误类型,现在实现错误接口,并在错误类型上添加一些方法来提供关于错误的更多信息.
func (e *areaError) Error() string {
return e.err
}
func (e *areaError) lengthNegative() bool {
return e.length < 0
}
func (e *areaError) widthNegative() bool {
return e.width < 0
}
2
3
4
5
6
7
8
9
10
11
在上面的代码片段中,返回Error() string
方法的错误描述.当长度小于0
时,lengthNegative() bool
方法返回true
;当宽度小于0
时,widthNegative() bool
方法返回true
.这两种方法提供了更多关于误差的信息,在这种情况下,面积计算是否失败,因为长度是负的,还是宽度为负的.因此,使用了struct
错误类型的方法来提供更多关于错误的信息.
下一步是写出面积计算函数.
func rectArea(length, width float64) (float64, error) {
err := ""
if length < 0 {
err += "length is less than zero"
}
if width < 0 {
if err == "" {
err = "width is less than zero"
} else {
err += ", width is less than zero"
}
}
if err != "" {
return 0, &areaError{err, length, width}
}
return length * width, nil
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
上面的rectArea
函数检查长度或宽度是否小于0
,如果它返回一个错误消息,则返回矩形的面积为nil
.
主函数:
func main() {
length, width := -5.0, -9.0
area, err := rectArea(length, width)
if err != nil {
if err, ok := err.(*areaError); ok {
if err.lengthNegative() {
fmt.Printf("error: length %0.2f is less than zero\n", err.length)
}
if err.widthNegative() {
fmt.Printf("error: width %0.2f is less than zero\n", err.width)
}
}
fmt.Println(err)
return
}
fmt.Println("area of rect", area)
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
运行结果:
error: length -5.00 is less than zero
error: width -9.00 is less than zero
2
# 1.5 painc()和recover()
Golang
中引入两个内置函数panic
和recover
来触发和终止异常处理流程,同时引入关键字defer
来延迟执行defer
后面的函数. 一直等到包含defer
语句的函数执行完毕时,延迟函数(defer后的函数)
才会被执行,而不管包含defer
语句的函数是通过return
的正常结束,还是由于panic
导致的异常结束.可以在一个函数中执行多条defer
语句,它们的执行顺序与声明顺序相反. 当程序运行时,如果遇到引用空指针、下标越界或显式调用panic
函数等情况,则先触发panic
函数的执行,然后调用延迟函数.调用者继续传递panic
,因此该过程一直在调用栈中重复发生:函数停止执行,调用延迟执行函数等.如果一路在延迟函数中没有recover
函数的调用,则会到达该协程的起点,该协程结束,然后终止其他所有协程,包括主协程(类似于C
语言中的主线程,该协程ID
为1
).
# panic:
- 1、内建函数
- 2、假如函数
F
中书写了panic
语句,会终止其后要执行的代码,在panic
所在函数F
内如果存在要执行的defer
函数列表,按照defer
的逆序执行 - 3、返回函数
F
的调用者G
,在G
中,调用函数F
语句之后的代码不会执行,假如函数G
中存在要执行的defer
函数列表,按照defer
的逆序执行,这里的defer
有点类似try-catch-finally
中的finally
- 4、直到
goroutine
整个退出,并报告错误
# recover:
- 1、内建函数
- 2、用来控制一个
goroutine
的panicking
行为,捕获panic
,从而影响应用的行为 - 3、一般的调用建议
- a). 在
defer
函数中,通过recever
来终止一个gojroutine
的panicking
过程,从而恢复正常代码的执行 - b). 可以获取通过
panic
传递的`error
- a). 在
简单来讲:go
中可以抛出一个panic
的异常,然后在defer
中通过recover
捕获这个异常,然后正常处理.
错误和异常从Golang
机制上讲,就是error
和panic
的区别.很多其他语言也一样,比如C++/Java,
没有error
但有errno
,没有panic
但有throw
.
Golang
错误和异常是可以互相转换的:
- 1.错误转异常,比如程序逻辑上尝试请求某个
URL
,最多尝试三次,尝试三次的过程中请求失败是错误,尝试完第三次还不成功的话,失败就被提升为异常了. - 2.异常转错误,比如
panic
触发的异常被recover
恢复后,将返回值中error
类型的变量进行赋值,以便上层函数继续走错误处理流程.
什么情况下用错误表达,什么情况下用异常表达,就得有一套规则,否则很容易出现一切皆错误或一切皆异常的情况.
以下给出异常处理的作用域(场景):
- 1.空指针引用
- 2.下标越界
- 3.除数为
0
- 4.不应该出现的分支,比如
default
- 5.输入不应该引起函数错误
其他场景使用错误处理,这使得函数接口很精炼.对于异常,可以选择在一个合适的上游去recover
,并打印堆栈信息,使得部署后的程序不会终止.
说明: Golang
错误处理方式一直是很多人诟病的地方,有些人吐槽说一半的代码都是"if err != nil { / 打印 && 错误处理 / }"
,严重影响正常的处理逻辑.当区分错误和异常,根据规则设计函数,就会大大提高可读性和可维护性.
# 1.6 错误处理的正确姿势
姿势一:失败的原因只有一个时,不使用error
案例:
func (self *AgentContext) CheckHostType(host_type string) error {
switch host_type {
case "virtual_machine":
return nil
case "bare_metal":
return nil
}
return errors.New("CheckHostType ERROR:" + host_type)
}
2
3
4
5
6
7
8
9
可以看出,该函数失败的原因只有一个,所以返回值的类型应该为bool
,而不是error
,重构一下代码:
func (self *AgentContext) IsValidHostType(hostType string) bool {
return hostType == "virtual_machine" || hostType == "bare_metal"
}
2
3
说明:大多数情况,导致失败的原因不止一种,尤其是对I/O
操作而言,用户需要了解更多的错误信息,这时的返回值类型不再是简单的bool
,而是error
.
姿势二:没有失败时,不使用error
error
在Golang
中是如此的流行,以至于很多人设计函数时不管三七二十一都使用error
,即使没有一个失败原因. 如下示例代码:
func (self *CniParam) setTenantId() error {
self.TenantId = self.PodNs
return nil
}
2
3
4
对于上面的函数设计,就会有下面的调用代码:
err := self.setTenantId()
if err != nil {
// log
// free resource
return errors.New(...)
}
2
3
4
5
6
根据正确姿势,重构一下代码:
func (self *CniParam) setTenantId() {
self.TenantId = self.PodNs
}
2
3
于是调用代码变为:
self.setTenantId()
姿势三:error
应放在返回值类型列表的最后
对于返回值类型error
,用来传递错误信息,在Golang
中通常放在最后一个.
resp, err := http.Get(url)
if err != nil {
return nill, err
}
2
3
4
`bool 作为返回值类型时也一样.
value, ok := cache.Lookup(key)
if !ok {
// ...cache[key] does not exist…
}
2
3
4
姿势四:错误值统一定义,而不是跟着感觉走
很多人写代码时,到处return errors.New(value)
,而错误value
在表达同一个含义时也可能形式不同,比如“记录不存在”的错误value
可能为:
- 1."record is not existed."
- 2."record is not exist!"
- 3."###record is not existed!!!"
- 4....
这使得相同的错误value
撒在一大片代码里,当上层函数要对特定错误value
进行统一处理时,需要漫游所有下层代码,以保证错误value
统一,不幸的是有时会有漏网之鱼,而且这种方式严重阻碍了错误value
的重构.
于是,可以参考C/C++
的错误码定义文件,在Golang
的每个包中增加一个错误对象定义文件,如下所示:
var ERR_EOF = errors.New("EOF")
var ERR_CLOSED_PIPE = errors.New("io: read/write on closed pipe")
var ERR_NO_PROGRESS = errors.New("multiple Read calls return no data or error")
var ERR_SHORT_BUFFER = errors.New("short buffer")
var ERR_SHORT_WRITE = errors.New("short write")
var ERR_UNEXPECTED_EOF = errors.New("unexpected EOF")
2
3
4
5
6
姿势五:错误逐层传递时,层层都加日志
层层都加日志非常方便故障定位.
说明:至于通过测试来发现故障,而不是日志,目前很多团队还很难做到.如果你或你的团队能做到,那么请忽略这个姿势.
姿势六:错误处理使用defer
般通过判断error
的值来处理错误,如果当前操作失败,需要将本函数中已经create
的资源destroy
掉,示例代码如下:
func deferDemo() error {
err := createResource1()
if err != nil {
return ERR_CREATE_RESOURCE1_FAILED
}
err = createResource2()
if err != nil {
destroyResource1()
return ERR_CREATE_RESOURCE2_FAILED
}
err = createResource3()
if err != nil {
destroyResource1()
destroyResource2()
return ERR_CREATE_RESOURCE3_FAILED
}
err = createResource4()
if err != nil {
destroyResource1()
destroyResource2()
destroyResource3()
return ERR_CREATE_RESOURCE4_FAILED
}
return nil
}
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
当Golang
的代码执行时,如果遇到defer
的闭包调用,则压入堆栈.当函数返回时,会按照后进先出的顺序调用闭包. 对于闭包的参数是值传递,而对于外部变量却是引用传递,所以闭包中的外部变量err
的值就变成外部函数返回时最新的err
值. 根据这个结论,重构上面的示例代码:
func deferDemo() error {
err := createResource1()
if err != nil {
return ERR_CREATE_RESOURCE1_FAILED
}
defer func() {
if err != nil {
destroyResource1()
}
}()
err = createResource2()
if err != nil {
return ERR_CREATE_RESOURCE2_FAILED
}
defer func() {
if err != nil {
destroyResource2()
}
}()
err = createResource3()
if err != nil {
return ERR_CREATE_RESOURCE3_FAILED
}
defer func() {
if err != nil {
destroyResource3()
}
}()
err = createResource4()
if err != nil {
return ERR_CREATE_RESOURCE4_FAILED
}
return nil
}
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
36
姿势七:当尝试几次可以避免失败时,不要立即返回错误
如果错误的发生是偶然性的,或由不可预知的问题导致.一个明智的选择是重新尝试失败的操作,有时第二次或第三次尝试时会成功.在重试时,需要限制重试的时间间隔或重试的次数,防止无限制的重试.
两个案例:
- 平时上网时,尝试请求某个
URL
,有时第一次没有响应,当再次刷新时,就有了惊喜. - 团队的一个
QA
曾经建议当Neutron
的attach
操作失败时,最好尝试三次,这在当时的环境下验证果然是有效的.
姿势八:当上层函数不关心错误时,建议不返回error
对于一些资源清理相关的函数(destroy/delete/clear
),如果子函数出错,打印日志即可,而无需将错误进一步反馈到上层函数,因为一般情况下,上层函数是不关心执行结果的,或者即使关心也无能为力,于是建议将相关函数设计为不返回error
.
姿势九:当发生错误时,不忽略有用的返回值
通常,当函数返回non-nil
的error
时,其他的返回值是未定义的(undefined
),这些未定义的返回值应该被忽略.然而,有少部分函数在发生错误时,仍然会返回一些有用的返回值.比如,当读取文件发生错误时,Read
函数会返回可以读取的字节数以及错误信息.对于这种情况,应该将读取到的字符串和错误信息一起打印出来.
说明:对函数的返回值要有清晰的说明,以便于其他人使用.
# 1.7 异常处理的正确姿势
姿势一:在程序开发阶段,坚持速错
速错,简单来讲就是“让它挂”,只有挂了你才会第一时间知道错误.在早期开发以及任何发布阶段之前,最简单的同时也可能是最好的方法是调用panic
函数来中断程序的执行以强制发生错误,使得该错误不会被忽略,因而能够被尽快修复.
姿势二:在程序部署后,应恢复异常避免程序终止
在Golang
中,某个Goroutine
如果panic
了,并且没有recover
,那么整个Golang
进程就会异常退出.所以,一旦Golang
程序部署后,在任何情况下发生的异常都不应该导致程序异常退出,在上层函数中加一个延迟执行的recover
调用来达到这个目的,并且是否进行recover
需要根据环境变量或配置文件来定,默认需要recover
. 这个姿势类似于C
语言中的断言,但还是有区别:一般在Release
版本中,断言被定义为空而失效,但需要有if
校验存在进行异常保护,尽管契约式设计中不建议这样做.在Golang
中,recover
完全可以终止异常展开过程,省时省力.
在调用recover
的延迟函数中以最合理的方式响应该异常:
- 1.打印堆栈的异常调用信息和关键的业务信息,以便这些问题保留可见;
- 2.将异常转换为错误,以便调用者让程序恢复到健康状态并继续安全运行.
一个简单的例子:
func funcA() error {
defer func() {
if p := recover(); p != nil {
fmt.Printf("panic recover! p: %v", p)
debug.PrintStack()
}
}()
return funcB()
}
func funcB() error {
// simulation
panic("foo")
return errors.New("success")
}
func test() {
err := funcA()
if err == nil {
fmt.Printf("err is nil\\n")
} else {
fmt.Printf("err is %v\\n", err)
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
期望test
函数的输出是:
err is foo
实际上test
函数的输出是:
err is nil
原因是panic
异常处理机制不会自动将错误信息传递给error
,所以要在funcA
函数中进行显式的传递,代码如下所示:
func funcA() (err error) {
defer func() {
if p := recover(); p != nil {
fmt.Println("panic recover! p:", p)
str, ok := p.(string)
if ok {
err = errors.New(str)
} else {
err = errors.New("panic")
}
debug.PrintStack()
}
}()
return funcB()
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
姿势三:对于不应该出现的分支,使用异常处理
当某些不应该发生的场景发生时,就应该调用panic
函数来触发异常.比如,当程序到达了某条逻辑上不可能到达的路径:
switch s := suit(drawCard()); s {
case "Spades":
// ...
case "Hearts":
// ...
case "Diamonds":
// ...
case "Clubs":
// ...
default:
panic(fmt.Sprintf("invalid suit %v", s))
}
2
3
4
5
6
7
8
9
10
11
12
姿势四:针对入参不应该有问题的函数,使用panic
设计
入参不应该有问题一般指的是硬编码,先看这两个函数(Compile和MustCompile)
,其中MustCompile
函数是对Compile
函数的包装:
func MustCompile(str string) *Regexp {
regexp, error := Compile(str)
if error != nil {
panic(`regexp: Compile(` + quote(str) + `): ` + error.Error())
}
return regexp
}
2
3
4
5
6
7
所以,对于同时支持用户输入场景和硬编码场景的情况,一般支持硬编码场景的函数是对支持用户输入场景函数的包装. 对于只支持硬编码单一场景的情况,函数设计时直接使用panic
,即返回值类型列表中不会有error
,这使得函数的调用处理非常方便(没有了乏味的"if err != nil {/ 打印 && 错误处理 /}"
代码块).
# 错误处理方法包
- https://golang.google.cn/pkg/os/
- https://golang.google.cn/pkg/errors/
- https://golang.google.cn/pkg/fmt/#Errorf
- 01
- AWS NAT-NetWork-Firwalld配置(一)04-09
- 02
- AWS NAT-NetWork-Firwalld配置(二)04-09
- 03
- kubernetes部署minio对象存储01-18