内容简介:[toc]最近golang越来越火,自己的项目的后续项目也在陆续转成go语言,因为有着其他语言的基础,所以学习起来难度尚可,不过go的异常处理机制真的让我忍不住吐槽,我从一个可以看见,代码中存在大量的
[toc]
前言
最近golang越来越火,自己的项目的后续项目也在陆续转成 go 语言,因为有着其他语言的基础,所以学习起来难度尚可,不过go的异常处理机制真的让我忍不住吐槽,我从一个 业务后端开发 的角度整理一下我的感想,借着这个机会也顺便整理一下相关知识点。
错误处理初体验
package main import "fmt" import "strconv" import "github.com/go-redis/redis" func main() { // 定义客户端对象,内部包含一个连接池 var client = redis.NewClient(&redis.Options { Addr: "localhost:6379", }) // 定义三个重要的整数变量值,默认都是零 var val1, val2, val3 int // 获取第一个值 valstr1, err := client.Get("value1").Result() if err == nil { val1, err = strconv.Atoi(valstr1) if err != nil { fmt.Println("value1 not a valid integer") return } } else if err != redis.Nil { fmt.Println("redis access error reason:" + err.Error()) return } // 获取第二个值 valstr2, err := client.Get("value2").Result() if err == nil { val2, err = strconv.Atoi(valstr2) if err != nil { fmt.Println("value1 not a valid integer") return } } else if err != redis.Nil { fmt.Println("redis access error reason:" + err.Error()) return } // 保存第三个值 val3 = val1 * val2 ok, err := client.Set("value3",val3, 0).Result() if err != nil { fmt.Println("set value error reason:" + err.Error()) return } fmt.Println(ok) } ------ OK
可以看见,代码中存在大量的 if err!= nil 的判断,因为 Go 语言中不轻易使用异常语句,所以对于任何可能出错的地方都需要判断返回值的错误信息。
上面代码中除了访问 Redis 需要判断之外,字符串转整数也需要判断。go语言的数据类型上有非常严格的控制,在开发过程中,尤其是与其他系统的交互过程中,报文类型的转换是非常常见的场景,导致代码中出现大量的err判断,代码可读性严重下降。
比如下面这一段,这是一个与其他系统交互报文的代码,几乎所有字段都要单独转换一下
item.AlarmNO, _ = utils.DesDecrypt(req.AlarmID, []byte(Key)) req.Lat, _ = utils.DesDecrypt(req.Lat, []byte(Key)) item.Lat, _ = strconv.ParseFloat(req.Lat, 64) req.Lng, _ = utils.DesDecrypt(req.Lng, []byte(Key)) item.Lng, _ = strconv.ParseFloat(req.Lng, 64) item.SmsPoiName, _ = utils.DesDecrypt(req.PoiName, []byte(Key)) item.SmsRoadInfo, _ = utils.DesDecrypt(req.RoadInfo, []byte(Key))
里面的“_”就是error,go语言允许使用这种方式“偷懒”,事实上确实被我拿来偷懒了,毕竟原本就要几百行的一个方法,我不希望因为 if err!= nil 再写几百行
error
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包里面,还提供了一个New()函数让我们方便地创建一个通用错误。
package errors func New(text string) error { return &errorString{text} } type errorString struct { s string } func (e *errorString) Error() string { return e.s }
注意这个结构体 errorString 是首字母小写的,意味着我们无法直接使用这个类型的名字来构造错误对象,而必须使用 New() 函数。
var err = errors.New("something happened")
如果你的错误字符串需要定制一些参数,可使用 fmt 包提供的 Errorf 函数
var thing = "something" var err = fmt.Errorf("%s happened", thing)
自定义error
在web项目开发过程中,错误码的定义是一个非常常见的事情,这里看见一段代码封装的挺好,在这里贴一下代码
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) }
异常与捕捉
错误指的是可能出现问题的地方出现了问题,比如打开一个文件时失败,这种情况在人们的意料之中;而异常指的是不应该出现问题的地方出现了问题,比如引用了空指针,这种情况在人们的意料之外。
可见,错误是业务过程的一部分,而异常不是。这个应该就是go的设计理念,但是这里我就有疑问了,在其他语言里我使用“null”、“None”、“false”等方法也可以做到,为什么这里要多一个error?
比如一个简单的查询
var name string err = db.QueryRow("select name from user where id = ?", 222).Scan(&name)
如果用户不存在,则返回error,谁规定用户不存在是“错误”,很多业务里用户不存在是很正常的,这种设计谁能 合理的 解释一下?
异常捕获
很明显,go的error不是万能的,毕竟一个项目那么大,谁能保证自己能够预见所有可能的错误?所以go也提供的异常捕获的机制,不过官方非常不推荐使用。
比如在开发中最常见的 json.Marshal(body)
在 json 序列化过程中,逻辑上需要递归处理 json 内部的各种类型,每一种容器类型内部都可能会遇到不能序列化的类型。如果对每个函数都使用返回错误的方式来编写代码,会显得非常繁琐。所以在内置的 json 包里也使用了 panic,然后在调用的最外层包裹了 recover 函数来进行恢复,最终统一返回一个 error 类型。
func (e *encodeState) marshal(v interface{}, opts encOpts) (err error) { defer func() { if r := recover(); r != nil { if je, ok := r.(jsonError); ok { err = je.error } else { panic(r) } } }() e.reflectValue(reflect.ValueOf(v), opts) return nil }
你可以想象一下,内置 json 包的开发者在设计开发这个包的时候应该也是纠结的焦头烂额,最终还是使用了 panic 和 recover 来让自己的代码变的好看一些。
panic 和 recover
在 Go 语言中,程序中一般是使用错误来处理异常情况。对于程序中出现的大部分异常情况,错误就已经够用了。
但在有些情况,当程序发生异常时,无法继续运行。在这种情况下,我们会使用 panic 来终止程序。当函数发生 panic 时,它会终止运行,在执行完所有的延迟函数后,程序控制返回到该函数的调用方。这样的过程会一直持续下去,直到当前协程的所有函数都返回退出,然后程序会打印出 panic 信息,接着打印出堆栈跟踪(Stack Trace),最后程序终止。在编写一个示例程序后,我们就能很好地理解这个概念了。
当程序发生 panic 时,使用 recover 可以重新获得对该程序的控制。
可以认为 panic 和 recover 与其他语言中的 try-catch-finally 语句类似,只不过一般我们很少使用 panic 和 recover。
panic
内置的panic函数定义如下
func panic(v interface{})
举例
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 := "Elon" fullName(&firstName, nil) fmt.Println("returned normally from main") }
运行结果打印如下
panic: runtime error: last name cannot be nil goroutine 1 [running]: main.fullName(0x1040c128, 0x0) /tmp/sandbox135038844/main.go:12 +0x120 main.main() /tmp/sandbox135038844/main.go:20 +0x80
recover
当程序抛出panic,说明出现了致命错误,程序控制会一直到达顶层函数,并会打印出 panic 信息,然后是堆栈跟踪,最后终止程序。那么如果我们不希望因为一个异常就终止整个程序,可以使用recover来捕获异常
recover 是一个内建函数,用于重新获得 panic 协程的控制。
func recover() interface{}
只有在延迟函数的内部,调用 recover 才有用。在延迟函数内调用 recover,可以取到 panic 的错误信息,并且停止 panic 续发事件(Panicking Sequence),程序运行恢复正常。
比如
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 := "Elon" fullName(&firstName, nil) fmt.Println("returned normally from main") }
程序返回结果
recovered from runtime error: last name cannot be nil returned normally from main deferred call in main
panic,recover 和 Go 协程
只有在相同的 Go 协程中调用 recover 才管用。recover 不能恢复一个不同协程的 panic。我们用一个例子来理解这一点。
package main import ( "fmt" "time" ) func recovery() { if r := recover(); r != nil { fmt.Println("recovered:", r) } } func a() { defer recovery() fmt.Println("Inside A") go b() time.Sleep(1 * time.Second) } func b() { fmt.Println("Inside B") panic("oh! B panicked") } func main() { a() fmt.Println("normally returned from main") }
程序输出结果
Inside A Inside B panic: oh! B panicked goroutine 5 [running]: main.b() /tmp/sandbox388039916/main.go:23 +0x80 created by main.a /tmp/sandbox388039916/main.go:17 +0xc0
如果程序的第 17 行由 go b() 修改为 b(),就可以恢复 panic 了,因为 panic 发生在与 recover 相同的协程里。如果运行这个修改后的程序,会输出:
Inside A Inside B recovered: oh! B panicked normally returned from main
恢复后获得堆栈跟踪
当我们恢复 panic 时,我们就释放了它的堆栈跟踪。实际上,在上述程序里,恢复 panic 之后,我们就失去了堆栈跟踪。
有办法可以打印出堆栈跟踪,就是使用 Debug 包中的 PrintStack 函数。
package main import ( "fmt" "runtime/debug" ) func r() { if r := recover(); r != nil { fmt.Println("Recovered", r) debug.PrintStack() } } 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") }
改后程序会输出
Recovered runtime error: index out of range goroutine 1 [running]: runtime/debug.Stack(0x1042beb8, 0x2, 0x2, 0x1c) /usr/local/go/src/runtime/debug/stack.go:24 +0xc0 runtime/debug.PrintStack() /usr/local/go/src/runtime/debug/stack.go:16 +0x20 main.r() /tmp/sandbox949178097/main.go:11 +0xe0 panic(0xf0a80, 0x17cd50) /usr/local/go/src/runtime/panic.go:491 +0x2c0 main.a() /tmp/sandbox949178097/main.go:18 +0x80 main.main() /tmp/sandbox949178097/main.go:23 +0x20 normally returned from main
这里需要注意defer的位置,一定要 放到panic前面 。
错误与异常的正确使用方式
regexp包中有两个函数Compile和MustCompile,它们的声明如下:
func Compile(expr string) (*Regexp, error) func MustCompile(str string) *Regexp
同样的功能,不同的设计:
Compile函数基于错误处理设计,将正则表达式编译成有效的可匹配格式,适用于用户输入场景。当用户输入的正则表达式不合法时,该函数会返回一个错误。
MustCompile函数基于异常处理设计,适用于硬编码场景。当调用者明确知道输入不会引起函数错误时,要求调用者检查这个错误是不必要和累赘的。我们应该假设函数的输入一直合法,当调用者输入了不应该出现的输入时,就触发panic异常。
什么情况下用错误表达,什么情况下用异常表达,就得有一套规则,否则很容易出现一切皆错误或一切皆异常的情况。
这里推荐一下这篇文章: Golang错误和异常处理的正确姿势
小结
学习go的时间不长,但是以前写过python,java,php,各种语言都有自己的优缺点,比如 php 一直被人们诟病的性能,但是牺牲性能换取了超高的产品开发迭代速率。go语言的优点也非常明显,比如他的部署等,但是在语言设计上真的无法认同,属于各种语言特性都有一点,但是又那么反人类的感觉。
然而大趋势在这里,只能慢慢去习惯了。
参考文章
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:- 快速失败机制 & 失败安全机制
- JavaScript线程机制与事件机制
- 区块链是怎样将分布式组网机制、合约机制、共识机制等技术结合并应用
- Java内存机制和GC回收机制-----笔记
- javascript垃圾回收机制 - 标记清除法/引用计数/V8机制
- Android 8.1 源码_机制篇 -- 全面解析 Handler 机制(原理篇)
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
你必须知道的495个C语言问题
Steve Summit / 孙云、朱群英 / 人民邮电出版社 / 2009-2 / 45.00元
“本书是Summit以及C FAQ在线列表的许多参与者多年心血的结晶,是C语言界最为珍贵的财富之一。我向所有C语言程序员推荐本书。” ——Francis Glassborow,著名C/C++专家,ACCU(C/C++用户协会)前主席 “本书清晰阐明了Kernighan与Ritchie《The C programming Language》一书中许多简略的地方,而且精彩地总结了C语言编程......一起来看看 《你必须知道的495个C语言问题》 这本书的介绍吧!