本系列文章并不会停留在Go语言的语法层面,更关注语言特性、学习和使用中出现的问题以及引发的一些思考。php
任何一行代码均可能存在不可预知的问题,而这些问题就是bug的根源。为了妥善处理这类问题,咱们须要编写一些代码,这类代码被称为运维代码。一般状况下,咱们须要发现问题、判断问题的种类、而后根据问题的种类,分别进行响应与处理。这些处理多是写入日志、也多是直接让代码中止运行,这些都视你的业务逻辑而定。这样一来,在咱们编写了足够健壮的运维代码以后,咱们便能快速的定位并解决问题。程序员
咱们先看一个最原始的错误处理解决方案:Unix读取文件的API:json
int open(const char *pathname, int flags);
若是成功打开这个文件,open()会返回一个int类型的文件描述符fd;若是打开失败,便会返回-1。为了正确处理该函数的错误,咱们会编写如下代码:运维
if ((fd = open("/etc/hosts", O_RDONLY)) < 0) { printf("%s", "open failed") exit(1); }
这样作会有什么问题呢?因为错误码和正确的业务逻辑混在一个返回值里,假如你忘了去判断fd的值,代码就会继续往下执行,就会把错误的-1当成正确的fd,代码就会发生不可预知的错误。除此以外,这种错误处理方式的语义也并不清晰。
那么,咱们该如何解决这个问题呢?函数
咱们想了一下,一个返回值不行,那么搞两个返回值,把错误处理逻辑与正常逻辑区分开,代码逻辑就会变得更加清晰了。Go语言就是这样作的,咱们一般可以看到以下代码:学习
func main() { res, err := json.Marshal(payload) if err != nil { return "", errors.New("序列化请求参数失败") } }
经过将正确执行Marshal的返回结果与错误的返回结果分离,使代码语义更加清晰,并且这样会提醒程序员更加关注错误的返回值,而不会忘记进行错误处理。可是,这样仍然存在一个问题,会出现大量的相似代码:优化
if err != nil { // 错误处理逻辑 }
在Go语言的代码中,会出现大量对err的if判断逻辑,重复率太高,并且错误处理逻辑仍然和正常的代码逻辑混淆在一块儿,咱们若是想进一步将错误与正常逻辑分离,该如何作呢?spa
Java、PHP等语言提供了try-catch-finally的解决方案。日志
try { // 正常代码逻辑 } catch(\Exception $e) { // 错误处理逻辑 } finally { // 释放资源逻辑 }
try-catch完全完成了对错误与正常代码逻辑的分离。咱们用try代码块中包裹可能出现问题的代码,在catch中对这些问题代码统一进行错误处理。code
finally代码块比较特殊,它被经常用来作一些资源及句柄的释放工做。若是没有finally,咱们的代码可能会像这样:
func main() { mutex := sync.Mutex{} // 加锁 mutex.Lock() res, err := json.Marshal("abc") if err != nil { // 释放锁资源 mutex.Unlock() // ....其他错误处理逻辑 } file, err := os.Open("abvc") if err != nil { // 释放锁资源 mutex.Unlock() // ....其他错误处理逻辑 } mutex.Unlock() }
为了确保锁资源在代码结束以前必定要被释放,咱们每次在错误处理逻辑中,都须要写一次mutex.Unlock代码,致使大量的代码冗余。finally代码块内的语句会在代码返回或者退出以前执行,并且是百分百会执行。这样,咱们就能够把释放锁资源这一行代码放到finally块便可,且只用写一次,这样就解决了以前代码冗余率高的问题。在Go语言中,defer()也一样解决了这个问题。咱们用Go中的defer语句改写一下上述代码:
func main() { mutex := sync.Mutex{} defer mutex.Unlock() mutex.Lock() res, err := json.Marshal("abc") if err != nil { // 错误处理 } file, err := os.Open("abvc") if err != nil { // 错误处理 } }
这就是错误处理的演化过程了。我我的比较喜欢Java和PHP中的try-catch-finally语法。据说Go2.0要对代码冗余度高的问题进行优化,咱们拭目以待吧。
接下来咱们深刻讲解Go语言中的错误处理实现。咱们看一下以前讲过的例子中,json.Marshal方法的签名:
func Marshal(v interface{}) ([]byte, error)
咱们重点关注最后一个error类型的参数,它是一个Go语言内置的接口类型。那么,咱们为何要用接口类型来抽象全部的错误类型呢?先别急,咱们先本身想一想。
在咱们对字符串进行marshal操做的过程当中,可能会产生好多种类型的错误。为了在marshal函数内部区分不一样的错误类型,咱们简单粗暴一点,可能会进行以下的处理:
func (e *encodeState) marshal(v interface{}, opts encOpts) (errorMsg string) { // 操做1可能的错误 if errType1 := doOp1(), errType1 != nil { err1 := errType1.getErrorMessage() // 获取errorType1的错误信息 return err1 } // 操做2可能的错误 if errType2 := doOp2(), errType2 != nil { err2 := errType2.getErrMsg() // 方法名和errorType1不一样 return err2 } return "" }
咱们分析一下上面这段代码,操做doOp1可能会发生errorType1类型的错误,咱们要返回给调用者errorType1类型中错误的字符串信息;doOp2也同理。这样作确实能够,可是仍是有一些麻烦,咱们看看还有没有其余方案来优化一下。
咱们先简单介绍一下,Go语言用一个接口类型抽象了全部错误类型:
type error interface { Error() string }
这个接口定义了一个Error()方法,用于返回错误信息,咱们先记下来,等会要用。同上个例子,咱们给以前自定义的两种错误类型加点料,实现这个error接口:
type errType1 struct {} // 实现接口方法 func (*errType1) Error() { fmt.Println("我是错误类型1的信息") } type errType2 struct {} // 实现接口方法 func (*errType2) Error() { fmt.Println("我是错误类型1的信息") }
而后在marshal()函数上稍做改动,使用这两种实现接口的错误类型:
func (e *encodeState) marshal(v interface{}, opts encOpts) (errorMsg string) { // 操做1可能的错误 if errType1 := doOp1(), errType1 != nil { return errType1.Error() } // 操做2可能的错误 if errType2 := doOp2(), errType2 != nil { return errType2.Error() } return "" }
你们看到优点在哪里了吗?在咱们调用每一个错误类型的返回信息方法的时候,若是用咱们一开始的方式,咱们须要进入每个错误类型的实现类中去翻看他的API,看看函数名是什么;而在第二种实现方案中,因为两种错误的实现类型均实现了Error()方法,这样,在marshal函数中若是想进行错误信息的获取,咱们统一调用Error()函数,便可返回对应错误实现类的错误信息。
这其实就是一种依赖的倒置。调用方marshal()函数再也不关注错误类型的具体实现类,里面有哪些方法,而转为依赖抽象的接口。下面给你们看一下与marshal函数相关的几种Go语言内部定义的错误类型,他们均实现了error接口中的Error()方法:
第一种错误类型:
type MarshalerError struct { Type reflect.Type Err error sourceFunc string } func (e *MarshalerError) Error() string { srcFunc := e.sourceFunc if srcFunc == "" { srcFunc = "MarshalJSON" } return "json: error calling " + srcFunc + " for type " + e.Type.String() + ": " + e.Err.Error() }
第二种错误类型:
type UnsupportedValueError struct { Value reflect.Value Str string } func (e *UnsupportedValueError) Error() string { return "json: unsupported value: " + e.Str }
而其余语言对错误类型的抽象化处理,有些是用继承来实现的。如PHP中的根Exception与众多继承它的子异常类xxxException。在PHP中,咱们用Exception便可接收全部的异常类型,并能够调用通用的$exception->getMessage()、$exception->getFile()等方法。
Go语言的panic和其余语言的error有点像。若是调用了panic,代码会马上中止运行,一层一层向上冒泡并积累堆栈信息,直到调用栈顶结束,并打印出全部堆栈信息。
panic没什么好说的,而recover咱们须要好好聊一聊。recover专门用来恢复panic。也就是说,若是你在panic以前声明了recover语句,那么你就能够在panic以后使用recover接收到panic的信息。可是问题又来了,咱们panic不是直接就退出程序了吗,就算声明了recover也执行不了呀。这个时候,咱们就须要配合defer来使用了。defer可以让程序在panic以后,仍然执行一段收尾的代码逻辑。这样一来,咱们就能够经过recover得到panic的信息,并对信息做出识别与处理了。仍然举上述的marshal的源码的例子,此次是真的源码了,不是我编的:
func (e *encodeState) marshal(v interface{}, opts encOpts) (err error) { defer func() { // defer收尾 if r := recover(); r != nil { // recover恢复案发现场 if je, ok := r.(jsonError); ok { // 拿到panic的值,并转为错误来返回 err = je.error } else { panic(r) } } }() e.reflectValue(reflect.ValueOf(v), opts) return nil }
咱们看到,源码中将defer与recover配合使用,直接改变了panic的运行逻辑。本来是panic以后会直接退出程序,这样一来,如今程序并不会直接退出,而是被转为了jsonError类型,并返回。经过使用recover捕获运行时的panic,可让代码继续运行下去而不至于直接中止。
【Go语言踩坑系列(六)】面向对象
欢迎对本系列文章感兴趣的读者订阅咱们的公众号,关注博主下次不迷路~