教程集 www.jiaochengji.com
教程集 >  Golang编程  >  golang教程  >  正文 golang异常机制

golang异常机制

发布时间:2022-02-06   编辑:jiaochengji.com
教程集为您提供golang异常机制等资源,欢迎您收藏本站,我们将为您提供最新的golang异常机制资源

[toc]

<h1>前言</h1>

最近golang越来越火,自己的项目的后续项目也在陆续转成go语言,因为有着其他语言的基础,所以学习起来难度尚可,不过go的异常处理机制真的让我忍不住吐槽,我从一个业务后端开发的角度整理一下我的感想,借着这个机会也顺便整理一下相关知识点。

<h1>错误处理初体验</h1> <pre><code class="lang-go hljs">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 </code></code></pre>

可以看见,代码中存在大量的 if err!= nil的判断,因为 Go 语言中不轻易使用异常语句,所以对于任何可能出错的地方都需要判断返回值的错误信息。

上面代码中除了访问Redis需要判断之外,字符串转整数也需要判断。go语言的数据类型上有非常严格的控制,在开发过程中,尤其是与其他系统的交互过程中,报文类型的转换是非常常见的场景,导致代码中出现大量的err判断,代码可读性严重下降。

比如下面这一段,这是一个与其他系统交互报文的代码,几乎所有字段都要单独转换一下

<pre><code class="lang-go hljs">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)) </code></code></pre>

里面的“_”就是error,go语言允许使用这种方式“偷懒”,事实上确实被我拿来偷懒了,毕竟原本就要几百行的一个方法,我不希望因为if err!= nil再写几百行

<h1>error</h1>

Go中返回的error类型究竟是什么呢?看源码发现error类型是一个非常简单的接口类型,具体如下

<pre><code class="lang-go hljs">// 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 } </code></code></pre>

在error包里面,还提供了一个New()函数让我们方便地创建一个通用错误。

<pre><code class="lang-go hljs">package errors func New(text string) error { return &errorString{text} } type errorString struct { s string } func (e *errorString) Error() string { return e.s } </code></code></pre>

注意这个结构体 errorString 是首字母小写的,意味着我们无法直接使用这个类型的名字来构造错误对象,而必须使用 New() 函数。

<pre><code class="lang-go hljs">var err = errors.New("something happened") </code></code></pre>

如果你的错误字符串需要定制一些参数,可使用 fmt 包提供的 Errorf 函数

<pre><code class="lang-go hljs">var thing = "something" var err = fmt.Errorf("%s happened", thing) </code></code></pre> <h2>自定义error</h2>

在web项目开发过程中,错误码的定义是一个非常常见的事情,这里看见一段代码封装的挺好,在这里贴一下代码

<pre><code class="lang-go hljs">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) } </code></code></pre> <h1>异常与捕捉</h1>

错误指的是可能出现问题的地方出现了问题,比如打开一个文件时失败,这种情况在人们的意料之中;而异常指的是不应该出现问题的地方出现了问题,比如引用了空指针,这种情况在人们的意料之外。

可见,错误是业务过程的一部分,而异常不是。这个应该就是go的设计理念,但是这里我就有疑问了,在其他语言里我使用“null”、“None”、“false”等方法也可以做到,为什么这里要多一个error?

比如一个简单的查询

<pre><code class="lang-go hljs">var name string err = db.QueryRow("select name from user where id = ?", 222).Scan(&name) </code></code></pre>

如果用户不存在,则返回error,谁规定用户不存在是“错误”,很多业务里用户不存在是很正常的,这种设计谁能合理的解释一下?

<h2>异常捕获</h2>

很明显,go的error不是万能的,毕竟一个项目那么大,谁能保证自己能够预见所有可能的错误?所以go也提供的异常捕获的机制,不过官方非常不推荐使用。

比如在开发中最常见的<em>json.Marshal(body)</em>

在 json 序列化过程中,逻辑上需要递归处理 json 内部的各种类型,每一种容器类型内部都可能会遇到不能序列化的类型。如果对每个函数都使用返回错误的方式来编写代码,会显得非常繁琐。所以在内置的 json 包里也使用了 panic,然后在调用的最外层包裹了 recover 函数来进行恢复,最终统一返回一个 error 类型。

<pre><code class="lang-go hljs">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 } </code></code></pre>

你可以想象一下,内置 json 包的开发者在设计开发这个包的时候应该也是纠结的焦头烂额,最终还是使用了 panic 和 recover 来让自己的代码变的好看一些。

<h2>panic 和 recover</h2>

在 Go 语言中,程序中一般是使用错误来处理异常情况。对于程序中出现的大部分异常情况,错误就已经够用了。

但在有些情况,当程序发生异常时,无法继续运行。在这种情况下,我们会使用 panic 来终止程序。当函数发生 panic 时,它会终止运行,在执行完所有的延迟函数后,程序控制返回到该函数的调用方。这样的过程会一直持续下去,直到当前协程的所有函数都返回退出,然后程序会打印出 panic 信息,接着打印出堆栈跟踪(Stack Trace),最后程序终止。在编写一个示例程序后,我们就能很好地理解这个概念了。

当程序发生 panic 时,使用 recover 可以重新获得对该程序的控制。

可以认为 panic 和 recover 与其他语言中的 try-catch-finally 语句类似,只不过一般我们很少使用 panic 和 recover。

<h3>panic</h3>

内置的panic函数定义如下

<pre><code class="lang-go hljs">func panic(v interface{}) </code></code></pre>

举例

<pre><code class="lang-go hljs">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") } </code></code></pre>

运行结果打印如下

<pre><code class="lang-go hljs">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 </code></code></pre> <h3>recover</h3>

当程序抛出panic,说明出现了致命错误,程序控制会一直到达顶层函数,并会打印出 panic 信息,然后是堆栈跟踪,最后终止程序。那么如果我们不希望因为一个异常就终止整个程序,可以使用recover来捕获异常
recover 是一个内建函数,用于重新获得 panic 协程的控制。

<pre><code class="lang-go hljs">func recover() interface{} </code></code></pre>

只有在延迟函数的内部,调用 recover 才有用。在延迟函数内调用 recover,可以取到 panic 的错误信息,并且停止 panic 续发事件(Panicking Sequence),程序运行恢复正常。

比如

<pre><code class="lang-go hljs">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") } </code></code></pre>

程序返回结果

<pre><code class="lang-go hljs">recovered from runtime error: last name cannot be nil returned normally from main deferred call in main </code></code></pre> <h3>panic,recover 和 Go 协程</h3>

只有在相同的 Go 协程中调用 recover 才管用。recover 不能恢复一个不同协程的 panic。我们用一个例子来理解这一点。

<pre><code class="lang-go hljs">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") } </code></code></pre>

程序输出结果

<pre><code class="lang-go hljs">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 </code></code></pre>

如果程序的第 17 行由 go b() 修改为 b(),就可以恢复 panic 了,因为 panic 发生在与 recover 相同的协程里。如果运行这个修改后的程序,会输出:

<pre><code class="lang-go hljs">Inside A Inside B recovered: oh! B panicked normally returned from main </code></code></pre> <h3>恢复后获得堆栈跟踪</h3>

当我们恢复 panic 时,我们就释放了它的堆栈跟踪。实际上,在上述程序里,恢复 panic 之后,我们就失去了堆栈跟踪。

有办法可以打印出堆栈跟踪,就是使用 Debug 包中的 PrintStack 函数。

<pre><code class="lang-go hljs">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") } </code></code></pre>

改后程序会输出

<pre><code class="lang-go hljs">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 </code></code></pre>

这里需要注意defer的位置,一定要放到panic前面

<h1>错误与异常的正确使用方式</h1>

regexp包中有两个函数Compile和MustCompile,它们的声明如下:

<pre><code class="lang-go hljs">func Compile(expr string) (*Regexp, error) func MustCompile(str string) *Regexp </code></code></pre>

同样的功能,不同的设计:

Compile函数基于错误处理设计,将正则表达式编译成有效的可匹配格式,适用于用户输入场景。当用户输入的正则表达式不合法时,该函数会返回一个错误。

MustCompile函数基于异常处理设计,适用于硬编码场景。当调用者明确知道输入不会引起函数错误时,要求调用者检查这个错误是不必要和累赘的。我们应该假设函数的输入一直合法,当调用者输入了不应该出现的输入时,就触发panic异常。

什么情况下用错误表达,什么情况下用异常表达,就得有一套规则,否则很容易出现一切皆错误或一切皆异常的情况。

这里推荐一下这篇文章:Golang错误和异常处理的正确姿势

<h1>小结</h1>

学习go的时间不长,但是以前写过python,java,php,各种语言都有自己的优缺点,比如php一直被人们诟病的性能,但是牺牲性能换取了超高的产品开发迭代速率。go语言的优点也非常明显,比如他的部署等,但是在语言设计上真的无法认同,属于各种语言特性都有一点,但是又那么反人类的感觉。

然而大趋势在这里,只能慢慢去习惯了。

参考文章

<ul><li>《快学 Go 语言》第 10 课 —— 错误与异常</li> <li>【Go语言】错误与异常处理机制</li> <li>Go 系列教程 —— 32. panic 和 recover</li> <li>Golang错误和异常处理的正确姿势</li> <li>Go语言的错误处理</li> </ul>

到此这篇关于“golang异常机制”的文章就介绍到这了,更多文章或继续浏览下面的相关文章,希望大家以后多多支持JQ教程网!

您可能感兴趣的文章:
golang 异常管理机制
recover 没有捕获异常_GO语言异常处理机制panic和recover分析
Golang panic 异常捕获
goang 错误&amp;异常处理机制
Go语言的错误&异常处理机制及其应用
DataWhale & Golang(十、异常处理)
golang异常处理机制
golang错误处理机制(异常处理)
golang会取代php吗
Golang错误和异常处理的正确姿势

[关闭]
~ ~