人非圣贤,孰能无过,有则改之,无则加勉。在编程语言层面,错误处理方式大体上有两大流派,分别是以Python为代表的异常捕获机制(try....catch);以及以Go lang为代表的错误返回机制(return error),前者是自动化流程,模式化的语法隔离正常逻辑和错误逻辑,而后者,需要将错误处理判断编排在正常逻辑中。虽然模式化语法更容易让人理解,但从系统资源开销角度看,错误返回机制明显更具优势。

返回错误

Go lang的错误(error)也是一种数据类型,错误用内置的error 类型表示,就像其他的数据类型的,比如字符串、整形之类,错误的具体值可以存储在变量中,从函数中返回:

package main  

import "fmt"  

func handle() (int, error) {
return 1, nil
} func main() {
i, err := handle()
if err != nil {
fmt.Println("报错了")
return
} fmt.Println("逻辑正常")
fmt.Println(i)
}

程序返回:



逻辑正常
1

这里的逻辑是,如果handle函数成功执行并且返回,那么入口函数就会正常打印返回值i,假设handel函数执行过程中出现错误,将返回一个非nil错误。

如果一个函数返回一个错误,那么理论上,它肯定是函数返回的最后一个值,因为在执行阶段中可能会返回正常的值,而错误位置是未知的,所以,handle函数返回的值是最后一个值。

go lang中处理错误的常见方式是将返回的错误与nil进行比较。nil值表示没有发生错误,而非nil值表示出现错误。在我们的例子中,我们检查错误是否为nil。如果它不是nil,我们会通过fmt.Println方法提醒用户并且从主函数返回,结束逻辑。

再来个例子:

package main  

import (
"fmt"
"net/http"
) func main() { resp, err := http.Get("123123")
if err != nil {
fmt.Println(err)
return
} fmt.Println(resp.StatusCode) }

这回我们使用标准库包http向一个叫做123123的网址发起请求,当然了,请求过程中有可能发生一些未知错误,所以我们使用err变量获取Get方法的最后一个返回值,如果err不是nil,那么就说明请求过程中报错了,这里打印具体错误,然后从主函数中返回。

程序返回:

Get "123123": unsupported protocol scheme ""

很明显,肯定报错了,因为Go lang并不知道所谓的123123到底是什么网络协议。

具体错误类型

在Go lang中,错误本质上是一个接口:



type error interface {
Error() string
}

包含一个带有Error字符串的函数。任何实现这个接口的类型都可以作为一个错误使用。这个函数可以打印出具体错误的说明。

当打印错误时,fmt.Println函数在内部调用Error() 方法来获取错误的说明:

Get "123123": unsupported protocol scheme ""

但有的时候,除了系统级别的错误说明,我们还需要针对错误进行分类,通过不同的错误类型的种类来决定下游的处理方式。

既然有了错误说明,为什么还需要错误类型,直接通过说明判断不就行了?这是因为系统的错误说明可能会随着go lang版本的迭代而略有不同,而一个错误的错误类型则大概率不会发生变化。

通过对标准库文档的解读:https://pkg.go.dev/net/http#ProtocolError,我们就可以对返回的错误类型进行判断:

package main  

import (
"fmt"
"net"
"net/http"
) func main() { resp, err := http.Get("123123")
if err, ok := err.(net.Error); ok && err.Timeout() {
fmt.Println("超时错误")
fmt.Println(err) } else if err != nil {
fmt.Println("其他错误")
fmt.Println(err)
} fmt.Println(resp.StatusCode) }

程序返回:

其他错误
Get "123123": unsupported protocol scheme ""

这里我们把超时(Timeout)和其他错误区分开来,分别进入不同的错误处理逻辑。

定制错误

定制错误通过标准库errors为程序的错误做个性化定制,假设某个函数的作用是做除法运算,而如果除数为0,则返回一个错误:

package main  

import (
"errors"
"fmt"
) func test(num1 int, num2 int) (int, error) {
if num2 == 0 {
return 0, errors.New("除数不能为0")
}
return num1 / num2, nil
} func main() { res, err := test(2, 1)
if err != nil {
fmt.Println(err)
return
}
fmt.Println("结果是", res)
}

程序返回:

结果是 2

但如果参数不合法:

package main  

import (
"errors"
"fmt"
) func test(num1 int, num2 int) (int, error) {
if num2 == 0 {
return 0, errors.New("除数不能为0")
}
return num1 / num2, nil
} func main() { res, err := test(2, 0)
if err != nil {
fmt.Println(err)
return
}
fmt.Println("结果是", res)
}

程序返回:

除数不能为0

假设,出于某种原因,我们对除数有定制化需求,比如不能为0或者为1,但条件变成了多条件,此时需要将除数显性的展示在错误说明中,以便更具象化的提醒用户:

package main  

import (
"fmt"
) func test(num1 int, num2 int) (int, error) {
if (num2 == 0) || (num2 == 1) {
return 0, fmt.Errorf("除数为%d,除数不能为0或者1", num2)
}
return num1 / num2, nil
} func main() { res, err := test(2, 1)
if err != nil {
fmt.Println(err)
return
}
fmt.Println("结果是", res)
}

程序返回:

除数为1,除数不能为0或者1

这里使用fmt包的Errorf函数根据一个格式说明器格式化错误,并返回一个字符串作为值来满足错误。

此外,还可以使用使用结构体和结构体中的属性提供关于错误的更多信息:

type testError struct {
err string
num int
}

这里定义结构体testError,里面两个属性,分别是错误说明和除数值。

随后,我们使用一个指针接收器区域错误来实现错误接口的Error() string方法。这个方法打印出错误的除数值和错误说明:

func (e *testError) Error() string {
return fmt.Sprintf("除数 %d:%s", e.num, e.err)
}

接着通过结构体寻址调用:

func test(num1 int, num2 int) (int, error) {
if (num2 == 0) || (num2 == 1) {
return 0, &testError{"除数非法", num2}
}
return num1 / num2, nil
}

完整代码:

package main  

import (
"fmt"
) type testError struct {
err string
num int
} func (e *testError) Error() string {
return fmt.Sprintf("除数 %d:%s", e.num, e.err)
} func test(num1 int, num2 int) (int, error) {
if (num2 == 0) || (num2 == 1) {
return 0, &testError{"除数非法", num2}
}
return num1 / num2, nil
} func main() { res, err := test(2, 1)
if err != nil {
fmt.Println(err)
return
}
fmt.Println("结果是", res)
}

程序返回:

除数 1:除数非法

通过结构体的定义,错误说明更加规整,并且更易于维护。

异常(panic/recover)

异常的概念是,本来不应该出现问题的地方出现了问题,某些情况下,当程序发生异常时,无法继续运行,此时,我们会使用 panic 来终止程序。当函数发生 panic 时,它会终止运行,在执行完所有的延迟函数后,程序返回到该函数的调用方,这样的过程会一直持续下去,直到当前协程的所有函数都返回退出,然后程序会打印出 panic 信息,接着打印出堆栈跟踪,最后程序终止:

package main  

import "fmt"  

func main() {  

	panic("panic error")  

	fmt.Println("下游逻辑")  

}

程序返回:

panic: panic error

可以看到,panic方法执行后,程序下游逻辑并未执行,所以panic使用场景是,当下游依赖上游的操作,而上游的问题导致下游无计可施的时候,使用panic抛出异常。

但延迟执行是个例外:

package main  

import "fmt"  

func myTest() {
defer fmt.Println("defer myTest")
panic("panic myTest")
}
func main() {
defer fmt.Println("defer main")
myTest()
}

程序返回:

defer myTest
defer main
panic: panic myTest

这里当函数发生 panic 时,它会终止运行,在执行完所有的延迟函数后,程序返回到该函数的调用方,这样的过程会一直持续下去,直到当前协程的所有函数都返回退出,然后程序会打印出 panic 信息,接着打印出堆栈跟踪,最后程序终止。

此外,recover方法可以捕获异常的异常,从而打印异常信息后,继续执行下游逻辑:

package main  

import "fmt"  

func outOfArray(x int) {
defer func() {
// recover() 可以将捕获到的 panic 信息打印
if err := recover(); err != nil {
fmt.Println(err)
}
}()
var array [5]int
array[x] = 1
}
func main() { outOfArray(20) fmt.Println("下游逻辑")
}

程序返回:

runtime error: index out of range [20] with length 5
下游逻辑

结语

综上,Go lang的错误处理,属实不太优雅,大多数情况下会有很多重复代码:if err != nil,这在一定程度上影响了代码的可读性和可维护性,同时容易丢失底层错误类型,且定位错误时,很难得到错误链,也就是在一定程度上阻碍了错误的追根溯源,但反过来想,错误本来就是业务的一部分,从业务角度上看,Golang这种返回错误的方式更贴合业务逻辑,你可以用多返回值包含 error处理业务异常,用 recover 处理系统异常。业务异常,可以定义为不会引起系统崩溃下游瘫痪的异常;系统异常可以定义为会引起系统崩溃下游瘫痪的异常。所以,归根结底,一套功夫的威力,真的不在于其招式的设计,而在于运用功夫的那个人能否发挥这套武功的全部潜力。

人非圣贤孰能无过,Go lang1.18入门精炼教程,由白丁入鸿儒,Go lang错误处理机制EP11的更多相关文章

  1. 延宕执行,妙用无穷,Go lang1.18入门精炼教程,由白丁入鸿儒,Golang中defer关键字延迟调用机制使用EP17

    先行定义,延后执行.不得不佩服Go lang设计者天才的设计,事实上,defer关键字就相当于Python中的try{ ...}except{ ...}finally{...}结构设计中的finall ...

  2. 仙人指路,引而不发,Go lang1.18入门精炼教程,由白丁入鸿儒,Golang中New和Make函数的使用背景和区别EP16

    Golang只有二十五个系统保留关键字,二十几个系统内置函数,加起来只有五十个左右需要记住的关键字,纵观编程宇宙,无人能出其右.其中还有一些保留关键字属于"锦上添花",什么叫锦上添 ...

  3. 清源正本,鉴往知来,Go lang1.18入门精炼教程,由白丁入鸿儒,Golang中引用类型是否进行引用传递EP18

    开篇明义,Go lang中从来就不存在所谓的"引用传递",从来就只有一种变量传递方式,那就是值传递.因为引用传递的前提是存在"引用变量",但是Go lang中从 ...

  4. 百亿数据百亿花, 库若恒河沙复沙,Go lang1.18入门精炼教程,由白丁入鸿儒,Go lang数据库操作实践EP12

    Golang可以通过Gorm包来操作数据库,所谓ORM,即Object Relational Mapping(数据关系映射),说白了就是通过模式化的语法来操作数据库的行对象或者表对象,对比相对灵活繁复 ...

  5. 你有对象类,我有结构体,Go lang1.18入门精炼教程,由白丁入鸿儒,go lang结构体(struct)的使用EP06

    再续前文,在面向对象层面,Python做到了超神:万物皆为对象,而Ruby,则干脆就是神:飞花摘叶皆可对象.二者都提供对象类操作以及继承的方式为面向对象张目,但Go lang显然有一些特立独行,因为它 ...

  6. 层次分明井然有条,Go lang1.18入门精炼教程,由白丁入鸿儒,Go lang包管理机制(package)EP10

    Go lang使用包(package)这种概念元素来统筹代码,所有代码功能上的可调用性都定义在包这个级别,如果我们需要调用依赖,那就"导包"就行了,无论是内部的还是外部的,使用im ...

  7. 兔起鹘落全端涵盖,Go lang1.18入门精炼教程,由白丁入鸿儒,全平台(Sublime 4)Go lang开发环境搭建EP00

    Go lang,为并发而生的静态语言,源于C语言又不拘泥于性能,高效却不流于古板,Python灵活,略输性能,Java严谨,稍逊风骚.君不见各大厂牌均纷纷使用Go lang对自己的高并发业务进行重构, ...

  8. 化整为零优化重用,Go lang1.18入门精炼教程,由白丁入鸿儒,go lang函数的定义和使用EP07

    函数是基于功能或者逻辑进行聚合的可复用的代码块.将一些复杂的.冗长的代码抽离封装成多个代码片段,即函数,有助于提高代码逻辑的可读性和可维护性.不同于Python,由于 Go lang是编译型语言,编译 ...

  9. 因势而变,因时而动,Go lang1.18入门精炼教程,由白丁入鸿儒,Go lang泛型(generic)的使用EP15

    事实上,泛型才是Go lang1.18最具特色的所在,但为什么我们一定要拖到后面才去探讨泛型?类比的话,我们可以想象一下给小学一年级的学生讲王勃的千古名篇<滕王阁序>,小学生有多大的概率可 ...

随机推荐

  1. C/C++ 单元自动化测试解决方案实践

    vivo 互联网服务器团队 - Li Qingxin C/C++ 开发效率一直被业内开发人员诟病,单元测试开发效率也是如此,以至于开发人员不愿花时间来写单元测试.那么我们是不是可以通过改善编写单元测试 ...

  2. SQL数据库操作总结

    1. 如何增加(创建)数据库 语法 CREATE DATABASE database_name 2.如何增加(创建)数据表 语法 CREATE TABLE table_name { column_na ...

  3. 前端2CSS

    内容概要 form表单 网络请求方式 CSS简介 CSS查找标签之基本选择器(重要) CSS查找标签之组合选择器(重要) 属性选择器 分组与嵌套 伪类选择器 内容详情 form表单 "&qu ...

  4. 十二张图:从0开始理解对称/非对称加密、CA认证、以及K8S各组件颁发证书原由

    目录 一.对称加密 二.对称加密-不安全 三.非对称加密 四.非对称加密-不安全 五.对称加密和非对称加密结合 六.对称加密和非对称加密结合-不安全 七.Https的做法-引入CA机构 八.乘胜追击理 ...

  5. 接口开发-restful

    数据库表设计 1 --员工表 2 create table Employee 3 ( 4 id NUMBER primary key, 5 employeeID NUMBER not null, 6 ...

  6. Quartus II 13.0 sp1的官方下载页面

    今天为了下个ModelSim跑到网上去找下载资源,清一色的百度网盘,下载速度60k/s,简直有病,于是跑到Intel官网上把连接挖出来了,供各位直接下载 实测使用IDM多线程下载速度可以轻松上到数MB ...

  7. SpringBoot之:SpringBoot中使用HATEOAS

    目录 简介 我们的目标 构建Entity和Repository 构建HATEOAS相关的RepresentationModel 构建Controller HATEOAS的意义 总结 简介 HATEOA ...

  8. SQL server设置连接数

    SQLServer查看及设置最大连接数   很多时候自己本地开发会遇到 ,打开几个连接正常访问 之后就报错误,这时候需要调整sqlserver 最大连接数. 1. 查询最大连接数 SELECT val ...

  9. Spring IOC 为什么能降低耦合

    有同学在学习 Spring 框架中可能会问这样的问题,为什么通过依赖注入就可以降低代码间的耦合呢?我通过 new 生产对象不也可以吗,不就是一行代码的不同,一个是 @Resource 注入,一个是 n ...

  10. 嵌入式中 动态阿拉伯语字符串 转换 LCD显示字符串【感谢建国雄心】

    本文参考CSDBN:建国雄心 的博客,这里找不到该帖子,放一个类似的仅供参考https://blog.csdn.net/qiaojiongzeng6321/article/details/748572 ...