错误处理在每一个语言中都是一项重要内容。众所周知,一般写程序时遇到的分为异常与错误两种,Golang中也不例外。Golang遵循『少便是多』的设计哲学,错误处理也力求简洁明了,在错误处理上采用了相似c语言的错误处理方案,另外在错误以外也有异常的概念,Golang中引入两个内置函数panic和recover来触发和终止异常处理流程。程序员
错误指的是可能出现问题的地方出现了问题,好比打开一个文件时可能失败,这种状况在人们的意料之中 ;而异常指的是不该该出现问题的地方出现了问题,好比引用了空指针,这种状况在人们的意料以外。可见, 错误是业务逻辑的一部分,而异常不是 。golang
咱们知道在C语言里面是经过返回-1或者NULL之类的信息来表示错误,可是对于使用者来讲,不查看相应的API说明文档,根本搞不清楚这个返回值究竟表明什么意思,好比返回0是成功仍是失败?针对这样状况Golang中引入error接口类型做为错误处理的标准模式,若是函数要返回错误,则返回值类型列表中确定包含error;Golang中引入两个内置函数panic和recover来触发和终止异常处理流程,同时引入关键字defer来延迟执行defer后面的函数。一直等到包含defer语句的函数执行完毕时,延迟函数(defer后的函数)才会被执行,而无论包含defer语句的函数是经过return的正常结束,仍是因为panic致使的异常结束。你能够在一个函数中执行多条defer语句,它们的执行顺序与声明顺序相反。web
程序运行时若出现了空指针引用、数组下标越界等异常状况,则会触发Golang中panic函数的执行,程序会中断运行,并当即执行在该goroutine中被延迟的函数,若是不作捕获,程序会崩溃。数据库
错误和异常从Golang机制上讲,就是error和panic的区别。不少其余语言也同样,好比C++/Java,没有error但有errno,没有panic但有throw,但panic的适用场景有一些不一样。因为panic会引发程序的崩溃,所以panic通常用于严重错误。编程
咱们编写一个简单的程序,该程序试图打开一个不存在的文件:json
package main import ( "fmt" "os" ) func main() { f, err := os.Open("/test.txt") if err != nil { fmt.Println("error:",err) return } fmt.Println(f.Name(), "open successfully") }
能够看到咱们的程序调用了os包的Open方法,该方法定义以下:数组
// Open opens the named file for reading. If successful, methods on // the returned file can be used for reading; the associated file // descriptor has mode O_RDONLY. // If there is an error, it will be of type *PathError. func Open(name string) (*File, error) { return OpenFile(name, O_RDONLY, 0) }
参考注释能够知道若是这个方法正常返回的话会返回一个可读的文件句柄和一个值为 nil 的错误,若是该方法未能成功打开文件会返回一个*PathError类型的错误。服务器
若是一个函数 或方法 返回了错误,按照Go的惯例,错误会做为最后一个值返回。因而 Open 函数也是将 err 做为最后一个返回值返回。框架
在Go语言中,处理错误时一般都是将返回的错误与 nil 比较。nil 值表示了没有错误发生,而非 nil 值表示出现了错误。因而有个咱们上面那行代码:函数
if err != nil { fmt.Println("error:",err) return }
若是你阅读任何一个Go语言的工程,会发现相似这样的代码随处可见,Go语言就是用这种简单的形式处理代码中出现的错误。
咱们在playground中执行,发现结果显示
error: open /test.txt: No such file or directory
能够发现咱们有效的检测并处理了程序中打开一个不存在文件所致使的错误,在示例中咱们仅仅是输出该错误并返回。
上面提到Open方法出现错误会返回一个*PathError类型的错误,这个类型具体是什么状况呢?别急,咱们先来看一下Go中错误具体是怎么实现的。
Go中返回的error类型到底是什么呢?看源码发现error类型是一个很是简单的接口类型,具体以下
// The error built-in interface type is the conventional interface for // representing an error condition, with the nil value representing no error. type error interface { Error() string }
error 有了一个签名为 Error() string 的方法。全部实现该接口的类型均可以看成一个错误类型。Error() 方法给出了错误的描述。
fmt.Println 在打印错误时,会在内部调用 Error() string 方法来获得该错误的描述。上一节示例中的错误描述就是这样打印出的。
如今咱们回到刚才代码里的*PathError类型,首先显而易见os.Open方法返回的错误是一个error类型,故咱们能够知道PathError类型必定实现了error类型,也就是说实现了Error方法。如今咱们看下具体实现
type PathError struct { Op string Path string Err error } func (e *PathError) Error() string { return e.Op + " " + e.Path + ": " + e.Err.Error() }
能够看到PathError类型实现了Error方法,该方法返回文件操做、路径及error字符串的拼接返回值。
为何须要自定义错误类型呢,试想一下若是一个错误咱们拿到的仅仅是错误的字符串描述,那显然没法从错误中获取更多的信息或者作一些逻辑相关的校验,这样咱们就能够经过自定义错误的结构体,经过实现Error()来使该结构体成为一个错误类型,使用时作一下类型推荐,咱们就能够从返回的错误经过结构体中的一些成员就能够作逻辑校验或者错误分类等工做。例如:
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") }
上面代码中咱们经过将error类型推断为实际的PathError类型,就能够拿到发生错误的Op、Path等数据,更有助于实际场景中错误的处理。
咱们组如今拉通了一套错误类型和错误码规范,以前工程里写的时候都是经过在代码中的controller里面去根据不一样状况去返回,这种处理方法有不少缺点,例以下层仅返回一个error类型,上层怎么判断该错误是哪一种错误,该使用哪一种错误码呢?另外就是程序中靠程序员写死某个逻辑错误码为xxxx,使程序缺少稳定性,错误码返回也较为为所欲为,所以我也去自定义了错误,具体以下:
var ( ErrSuccess = StandardError{0, "成功"} ErrUnrecognized = StandardError{-1, "未知错误"} ErrAccessForbid = StandardError{1000, "没有访问权限"} ErrNamePwdIncorrect = StandardError{1001, "用户名或密码错误"} ErrAuthExpired = StandardError{1002, "证书过时"} ErrAuthInvalid = StandardError{1003, "无效签名"} ErrClientInnerError = StandardError{4000, "客户端内部错误"} ErrParamError = StandardError{4001, "参数错误"} ErrReqForbidden = StandardError{4003, "请求被拒绝"} ErrPathNotFount = StandardError{4004, "请求路径不存在"} ErrMethodIncorrect = StandardError{4005, "请求方法错误"} ErrTimeout = StandardError{4006, "服务超时"} ErrServerUnavailable = StandardError{5000, "服务不可用"} ErrDbQueryError = StandardError{5001, "数据库查询错误"} ) //StandardError 标准错误,包含错误码和错误信息 type StandardError struct { ErrorCode int `json:"errorCode"` ErrorMsg string `json:"errorMsg"` } // Error 实现了 Error接口 func (err StandardError) Error() string { return fmt.Sprintf("errorCode: %d, errorMsg %s", err.ErrorCode, err.ErrorMsg) }
这样经过直接取StandardError的ErrorCode就能够知道应该返回的错误信息及错误码,调用时候也较为方便,而且作到了标准化,解决了以前项目中错误处理的问题。
有时候仅仅断言自定义错误类型可能在某些状况下不够方便,能够经过调用自定义错误的方法来获取更多信息,例如标准库中的net包中的DNSError
type DNSError struct { Err string // description of the error Name string // name looked for Server string // server used IsTimeout bool // if true, timed out; not all timeouts set this IsTemporary bool // if true, error is temporary; not all errors set this } func (e *DNSError) Timeout() bool { return e.IsTimeout } func (e *DNSError) Temporary() bool { return e.IsTimeout || e.IsTemporary }
能够看到不只仅自定义了DNSError的错误类型,还为该错误添加了两个方法用来让调用者判判定该错误是临时性错误,仍是由超时致使的。
package main import ( "fmt" "net" ) func main() { addr, err := net.LookupHost("gygolang.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) }
上述代码中,咱们试图获取 golangbot123.com(无效的域名) 的 ip。而后经过 *net.DNSError 的类型断言,获取到了错误的底层值。而后用错误的行为检查了该错误是由超时引发的,仍是一个临时性错误。
须要注意的是,你应该尽量地使用错误,而不是使用 panic 和 recover。只有当程序不能继续运行的时候,才应该使用 panic 和 recover 机制。
panic 有两个合理的用例:
内置的panic函数定义以下
func panic(v interface{})
当程序终止时,会打印传入 panic 的参数。咱们一块儿看一个例子加深下对panic的理解
package main import ( "fmt" ) func fullName(firstName *string, lastName *string) { if firstName == nil { panic("runtime error: first name cannot be nil") } if lastName == nil { panic("runtime error: last name cannot be nil") } fmt.Printf("%s %s\n", *firstName, *lastName) fmt.Println("returned normally from fullName") } func main() { firstName := "foo" fullName(&firstName, nil) fmt.Println("returned normally from main") }
上面的程序很简单,若是firstName和lastName有任何一个为空程序便会panic并打印出不一样的信息,程序输出以下:
panic: runtime error: last name cannot be nil goroutine 1 [running]: main.fullName(0x1042ff98, 0x0) /tmp/sandbox038383853/main.go:12 +0x140 main.main() /tmp/sandbox038383853/main.go:20 +0x40
出现panic时,程序终止运行,打印出传入 panic 的参数,接着打印出堆栈跟踪。程序首先会打印出传入 panic 函数的信息:
panic: runtime error: last name cannot be nil
而后打印堆栈信息,首先打印堆栈中的第一项
main.fullName(0x1042ff98, 0x0) /tmp/sandbox038383853/main.go:12 +0x140
接着打印堆栈中下一项
main.main() /tmp/sandbox038383853/main.go:20 +0x40
在这个例子中这一项就是栈顶了,因而结束打印。
当函数发生 panic 时,它会终止运行,在执行完全部的延迟函数后,程序控制返回到该函数的调用方。这样的过程会一直持续下去,直到当前协程的全部函数都返回退出,而后程序会打印出 panic 信息,接着打印出堆栈跟踪,最后程序终止。
在上面的例子中,咱们没有延迟调用任何函数。若是有延迟函数,会先调用它,而后程序控制返回到函数调用方。咱们来修改上面的示例,使用一个延迟语句。
package main import ( "fmt" ) func fullName(firstName *string, lastName *string) { defer fmt.Println("deferred call in fullName") if firstName == nil { panic("runtime error: first name cannot be nil") } if lastName == nil { panic("runtime error: last name cannot be nil") } fmt.Printf("%s %s\n", *firstName, *lastName) fmt.Println("returned normally from fullName") } func main() { defer fmt.Println("deferred call in main") firstName := "foo" fullName(&firstName, nil) fmt.Println("returned normally from main") }
能够看到输出以下:
deferred call in fullName deferred call in main panic: runtime error: last name cannot be nil goroutine 1 [running]: main.fullName(0x1042ff90, 0x0) /tmp/sandbox170416077/main.go:13 +0x220 main.main() /tmp/sandbox170416077/main.go:22 +0xc0
程序退出以前先执行了延迟函数。
程序发生panic后会崩溃,recover用于从新得到 panic 协程的控制。内建的recover函数定义以下
func recover() interface{}
只有在延迟函数的内部,调用 recover 才有用。在延迟函数内调用 recover,能够取到 panic 的错误信息,而且中止 panic 续发事件(Panicking Sequence),程序运行恢复正常。若是在延迟函数的外部调用 recover,就不能中止 panic 续发事件。
咱们来修改一下程序,在发生 panic 以后,使用 recover 来恢复正常的运行。
package main import ( "fmt" ) func recoverName() { if r := recover(); r!= nil { fmt.Println("recovered from ", r) } } func fullName(firstName *string, lastName *string) { defer recoverName() if firstName == nil { panic("runtime error: first name cannot be nil") } if lastName == nil { panic("runtime error: last name cannot be nil") } fmt.Printf("%s %s\n", *firstName, *lastName) fmt.Println("returned normally from fullName") } func main() { defer fmt.Println("deferred call in main") firstName := "foo" fullName(&firstName, nil) fmt.Println("returned normally from main") }
当 fullName 发生 panic 时,会调用延迟函数 recoverName(),它使用了 recover() 来中止 panic 续发事件。程序会输出
recovered from runtime error: last name cannot be nil returned normally from main deferred call in main
当程序发生 panic 时,会调用延迟函数 recoverName,它反过来会调用 recover() 来从新得到 panic 协程的控制。在执行完 recover() 以后,panic 会中止,程序控制返回到调用方(在这里就是 main 函数),程序在发生 panic 以后,会继续正常地运行。程序会打印 returned normally from main,以后是 deferred call in main。
运行时错误也会致使 panic。这等价于调用了内置函数 panic,其参数由接口类型 runtime.Error 给出。
package main import ( "fmt" ) func a() { n := []int{5, 7, 4} fmt.Println(n[3]) fmt.Println("normally returned from a") } func main() { a() fmt.Println("normally returned from main") }
上述代码是一个典型的数组越界形成的panic,程序输出以下:
panic: runtime error: index out of range goroutine 1 [running]: main.a() /tmp/sandbox100501727/main.go:9 +0x20 main.main() /tmp/sandbox100501727/main.go:13 +0x20
能够看到和咱们刚才手动出发panic没什么不一样,只是会打印运行时错误。
那是否能够恢复一个运行时 panic?固然是能够的,也跟刚才恢复panic的方法同样,在延迟函数中调用recover便可:
ackage main import ( "fmt" ) func r() { if r := recover(); r != nil { fmt.Println("Recovered", r) } } func a() { defer r() n := []int{5, 7, 4} fmt.Println(n[3]) fmt.Println("normally returned from a") } func main() { a() fmt.Println("normally returned from main") }
错误与异常有时候能够进行转化,
例如咱们工程中使用的Gin框架里有这么两个函数:
// Get returns the value for the given key, ie: (value, true). // If the value does not exists it returns (nil, false) func (c *Context) Get(key string) (value interface{}, exists bool) { value, exists = c.Keys[key] return } // MustGet returns the value for the given key if it exists, otherwise it panics. func (c *Context) MustGet(key string) interface{} { if value, exists := c.Get(key); exists { return value } panic("Key \"" + key + "\" does not exist") }
能够看到一样的功能不一样的设计:
能够看到错误跟异常能够进行转化,具体怎么转化要看业务场景来定。
以前看到项目里有错误在中间或者第一个返回的,这是很是不符合规范的。
参考以前章节咱们组内拉通的错误码和错误信息。
可能有些时候有些程序员犯懒写了这样的代码
foo, _ := getResult(1)
忽略了错误,也就不须要进行校验了,但这是很危险的,一旦某一个错误被忽略没处理极可能形成下面的程序出bug甚至直接panic。
好比咱们最先的os.Open函数,咱们去校验错误能这样写吗?
if err.Error == "No such file or directory"
这样显然不行,代码很挫,并且字符判断很不保险,怎么办呢?用上文讲的自定义错误去作。
本文详述了Go中错误与异常的概念及其处理方法,但愿对你们能有启发。