一、错误和异常的区别

错误指的是可能出现问题的地方出现了问题。比如打开一个文件时失败,这种情况在人们的意料之中 。

异常指的是不应该出现问题的地方出现了问题。比如引用了空指针,这种情况在人们的意料之外。

可见,错误是业务过程的一部分,而异常不是 。

二、错误演示

go语言中,错误是一种数据类型,使用内置的error类型,和其他数据类型一样使用。

package main

import (

"fmt"

"os"

)

func main() {

_, err := os.Open("a.txt")

if err != nil {

fmt.Println(err)

return

}

fmt.Println("open file success")

}

运行结果

open a.txt: The system cannot find the file specified.

三、错误类型

go语言通过内置的error接口提供了非常简单的错误处理机制。

type error interface {

Error() string

}

这个接口中包含一个Error() string,任何实现了该方法的类型都可以作为一个错误使用。这个方法提供了错误的描述。

打印错误是,fmt.Println函数在内部调用Error()方法来获取错误的描述。

从错误中提取更多信息的方法:

1、断言底层结构体类型并从结构体字段中获取更多的信息。

查看go语言源码,可以发现上面的示例中打开文件的代码中返回值error实现类error接口,可以看做error类型,但这个返回值error本身是PathError结构体类型。

go语言源码:

// PathError records an error and the operation and file path that caused it.

type PathError struct {

Op string

Path string

Err error

}

func (e *PathError) Error() string { return e.Op + " " + e.Path + ": " + e.Err.Error() }

func (e *PathError) Unwrap() error { return e.Err }

// Timeout reports whether this error represents a timeout.

func (e *PathError) Timeout() bool {

t, ok := e.Err.(timeout)

return ok && t.Timeout()

}

通过类型断言,可以将error作为PathError类型的实例,访问其属性和方法。

示例:

package main

import (

"fmt"

"os"

)

func main() {

_, err := os.Open("a.txt")

if err != nil {

if err, ok := err.(*os.PathError); ok {

fmt.Println(err.Op) // open

fmt.Println(err.Path) // a.txt

fmt.Println(err.Err) // The system cannot find the file specified.

}

return

}

fmt.Println("open file success")

}

2、断言底层结构体类型,调用其方法获取更多信息

go语言源码:

type DNSError struct {

...

}

func (e *DNSError) Error() string {

...

}

func (e *DNSError) Timeout() bool {

...

}

func (e *DNSError) Temporary() bool {

...

}

DNSError struct有两个方法Timeout() bool和Temporary() bool,它们返回一个布尔值,表示错误是由于超时还是临时的。

示例:

package main

import (

"fmt"

"net"

)

func main() {

add, err := net.LookupHost("www.baiddfudfsadf.com")

if err != nil {

if err, ok := err.(*net.DNSError); ok {

if err.Timeout() {

fmt.Println("operation time out")

} else if err.Temporary() {

fmt.Println("temporary error")

} else {

fmt.Println("genration error", err)

}

}

return

}

fmt.Println(add)

}

运行结果

genration error lookup www.baiddfudfsadf.com: no such host

四、自定义错误

1、使用errors包下的New()函数

go语言源码:

// Package errors implements functions to manipulate errors.

package errors

// New returns an error that formats as the given text.

func New(text string) error {

return &errorString{text}

}

// errorString is a trivial implementation of error.

type errorString struct {

s string

}

func (e *errorString) Error() string {

return e.s

}

示例:

package main

import (

"errors"

"fmt"

"math"

)

func main() {

s := -4.2

res, err := squareArea(s)

if err != nil {

fmt.Println(err)

return

}

fmt.Println("squareArea =", res)

}

func squareArea(sideLength float64) (float64, error) {

if sideLength < 0 {

return 0, errors.New("sideLength < 0")

}

return math.Pow(sideLength, 2), nil

}

运行结果

sideLength < 0

2、使用fmt包下的Errorf()函数

go语言源码:

func Errorf(format string, a ...interface{}) error {}

示例:

package main

import (

"fmt"

"math"

)

func main() {

s := -4.2

res, err := squareArea(s)

if err != nil {

fmt.Println(err)

return

}

fmt.Println("squareArea =", res)

}

func squareArea(sideLength float64) (float64, error) {

if sideLength < 0 {

return 0, fmt.Errorf("sideLength < 0, sideLength = %v\n", sideLength)

}

return math.Pow(sideLength, 2), nil

}

运行结果

sideLength < 0, sideLength = -4.2

3、使用struct类型属性和方法提供更多关于错误的信息。

示例:

package main

import (

"fmt"

)

// areaError 结构体 错误类型

type areaError struct {

err string

length float64

width float64

}

// Error() 方法,实现error接口

func (e *areaError) Error() string {

return fmt.Sprintf("length: %v, width: %v, error: %v", e.length, e.width, e.err)

}

func (e *areaError) lengthNegative() bool {

return e.length < 0

}

func (e *areaError) widthNegative() bool {

return e.width < 0

}

func main() {

length := -10.2

width := 3.4

res, err := rectangleArea(length, width)

if err != nil {

fmt.Println("err:", err)

if err, ok := err.(*areaError); ok {

if err.lengthNegative() {

fmt.Println("length < 0", err.length)

}

if err.widthNegative() {

fmt.Println("width < 0", err.width)

}

}

}

fmt.Println("rectangle area =", res)

}

func rectangleArea(length, width float64) (float64, error) {

err := ""

if length < 0 {

err += "length < 0"

}

if width < 0 {

if length < 0 {

err += "&width < 0"

} else {

err = "width < 0"

}

}

if err != "" {

return 0, &areaError{err, length, width}

}

return length * width, nil

}

运行结果

err: length: -10.2, width: 3.4, error: length < 0

length < 0 -10.2

rectangle area = 0

五、panic和recover

Golang中引入两个内置函数panic和recover来触发和终止异常处理流程,同时引入关键字defer来延迟执行defer后面的函数。

panic:

1、内建函数

2、假如函数F中书写了panic语句,会终止其后要执行的代码,在panic所在函数F内如果存在要执行的defer函数列表,按照defer的逆序执行

3、返回函数F的调用者G,在G中,调用函数F语句之后的代码不会执行,假如函数G中存在要执行的defer函数列表,按照defer的逆序执行,这里的defer 有点类似 try-catch-finally 中的 finally

4、直到goroutine整个退出,并报告错误

recover:

1、内建函数

2、用来控制一个goroutine的panicking行为,捕获panic,从而影响应用的行为

3、一般的调用建议

a). 在defer函数中,通过recever来终止一个gojroutine的panicking过程,从而恢复正常代码的执行

b). 可以获取通过panic传递的error

以下给出异常处理的作用域(场景):

空指针引用

下标越界

除数为0

不应该出现的分支,比如default

输入不应该引起函数错误

六、错误处理的正确姿势

姿势一:失败的原因只有一个时,不使用error

我们看一个案例:

func (self *AgentContext) CheckHostType(host_type string) error {

switch host_type {

case "virtual_machine":

return nil

case "bare_metal":

return nil

}

return errors.New("CheckHostType ERROR:" + host_type)

}

我们可以看出,该函数失败的原因只有一个,所以返回值的类型应该为bool,而不是error,重构一下代码:

func (self *AgentContext) IsValidHostType(hostType string) bool {

return hostType == "virtual_machine" || hostType == "bare_metal"

}

说明:大多数情况,导致失败的原因不止一种,尤其是对I/O操作而言,用户需要了解更多的错误信息,这时的返回值类型不再是简单的bool,而是error。

姿势二:没有失败时,不使用error

错误示例:

func (self *CniParam) setTenantId() error {

self.TenantId = self.PodNs

return nil

}

正确示例:

func (self *CniParam) setTenantId() {

self.TenantId = self.PodNs

}

姿势三:error应该放在返回值列表的最后

对于返回值类型error,用来传递错误信息,在Golang中通常放在最后一个。

resp, err := http.Get(url)

if err != nil {

return nill, err

}

bool作为返回值类型时也一样。

value, ok := cache.Lookup(key)

if !ok {

// ...cache[key] does not exist…

}

姿势四:错误统一定义,而不是跟着感觉走

很多人写代码时,到处return errors.New(value),而错误value在表达同一个含义时也可能形式不同,比如“记录不存在”的错误value可能为:

"record is not existed."

"record is not exist!"

"###record is not existed!!!"

...

姿势五:错误逐层传递时,层层都加日志

层层都加日志非常方便故障定位。

说明:至于通过测试来发现故障,而不是日志,目前很多团队还很难做到。如果你或你的团队能做到,那么请忽略这个姿势。

姿势六:错误处理使用defer

我们一般通过判断error的值来处理错误,如果当前操作失败,需要将本函数中已经create的资源destroy掉,示例代码如下:

func deferDemo() error {

err := createResource1()

if err != nil {

return ERR_CREATE_RESOURCE1_FAILED

}

err = createResource2()

if err != nil {

destroyResource1()

return ERR_CREATE_RESOURCE2_FAILED

}

err = createResource3()

if err != nil {

destroyResource1()

destroyResource2()

return ERR_CREATE_RESOURCE3_FAILED

}

err = createResource4()

if err != nil {

destroyResource1()

destroyResource2()

destroyResource3()

return ERR_CREATE_RESOURCE4_FAILED

}

return nil

}

当Golang的代码执行时,如果遇到defer的闭包调用,则压入堆栈。当函数返回时,会按照后进先出的顺序调用闭包。

对于闭包的参数是值传递,而对于外部变量却是引用传递,所以闭包中的外部变量err的值就变成外部函数返回时最新的err值。

根据这个结论,我们重构上面的示例代码:

func deferDemo() error {

err := createResource1()

if err != nil {

return ERR_CREATE_RESOURCE1_FAILED

}

defer func() {

if err != nil {

destroyResource1()

}

}()

err = createResource2()

if err != nil {

return ERR_CREATE_RESOURCE2_FAILED

}

defer func() {

if err != nil {

destroyResource2()

}

}()

err = createResource3()

if err != nil {

return ERR_CREATE_RESOURCE3_FAILED

}

defer func() {

if err != nil {

destroyResource3()

}

}()

err = createResource4()

if err != nil {

return ERR_CREATE_RESOURCE4_FAILED

}

return nil

}

姿势七:当尝试几次可以避免失败时,不要立即返回错误

如果错误的发生是偶然性的,或由不可预知的问题导致。一个明智的选择是重新尝试失败的操作,有时第二次或第三次尝试时会成功。在重试时,我们需要限制重试的时间间隔或重试的次数,防止无限制的重试。

两个案例:

我们平时上网时,尝试请求某个URL,有时第一次没有响应,当我们再次刷新时,就有了惊喜。

团队的一个QA曾经建议当Neutron的attach操作失败时,最好尝试三次,这在当时的环境下验证果然是有效的。

姿势八:当上层函数不关心错误时,建议不返回error

对于一些资源清理相关的函数(destroy/delete/clear),如果子函数出错,打印日志即可,而无需将错误进一步反馈到上层函数,因为一般情况下,上层函数是不关心执行结果的,或者即使关心也无能为力,于是我们建议将相关函数设计为不返回error。

姿势九:当发生错误时,不忽略有用的返回值

通常,当函数返回non-nil的error时,其他的返回值是未定义的(undefined),这些未定义的返回值应该被忽略。然而,有少部分函数在发生错误时,仍然会返回一些有用的返回值。比如,当读取文件发生错误时,Read函数会返回可以读取的字节数以及错误信息。对于这种情况,应该将读取到的字符串和错误信息一起打印出来。

说明:对函数的返回值要有清晰的说明,以便于其他人使用。

七、异常处理的正确姿势

姿势一:在程序开发阶段,坚持速错

速错,简单来讲就是“让它挂”,只有挂了你才会第一时间知道错误。在早期开发以及任何发布阶段之前,最简单的同时也可能是最好的方法是调用panic函数来中断程序的执行以强制发生错误,使得该错误不会被忽略,因而能够被尽快修复。

姿势二:在程序部署后,应恢复异常避免程序终止

在Golang中,某个Goroutine如果panic了,并且没有recover,那么整个Golang进程就会异常退出。所以,一旦Golang程序部署后,在任何情况下发生的异常都不应该导致程序异常退出,我们在上层函数中加一个延迟执行的recover调用来达到这个目的,并且是否进行recover需要根据环境变量或配置文件来定,默认需要recover。

这个姿势类似于C语言中的断言,但还是有区别:一般在Release版本中,断言被定义为空而失效,但需要有if校验存在进行异常保护,尽管契约式设计中不建议这样做。在Golang中,recover完全可以终止异常展开过程,省时省力。

我们在调用recover的延迟函数中以最合理的方式响应该异常:

打印堆栈的异常调用信息和关键的业务信息,以便这些问题保留可见;

将异常转换为错误,以便调用者让程序恢复到健康状态并继续安全运行。

我们看一个简单的例子:

func funcA() error {

defer func() {

if p := recover(); p != nil {

fmt.Printf("panic recover! p: %v", p)

debug.PrintStack()

}

}()

return funcB()

}

func funcB() error {

// simulation

panic("foo")

return errors.New("success")

}

func test() {

err := funcA()

if err == nil {

fmt.Printf("err is nil\\n")

} else {

fmt.Printf("err is %v\\n", err)

}

}

我们期望test函数的输出是:

err is foo

实际上test函数的输出是:

err is nil

原因是panic异常处理机制不会自动将错误信息传递给error,所以要在funcA函数中进行显式的传递,代码如下所示:

func funcA() (err error) {

defer func() {

if p := recover(); p != nil {

fmt.Println("panic recover! p:", p)

str, ok := p.(string)

if ok {

err = errors.New(str)

} else {

err = errors.New("panic")

}

debug.PrintStack()

}

}()

return funcB()

}

姿势三:对于不应该出现的分支,使用异常处理

当某些不应该发生的场景发生时,我们就应该调用panic函数来触发异常。比如,当程序到达了某条逻辑上不可能到达的路径:

switch s := suit(drawCard()); s {

case "Spades":

// ...

case "Hearts":

// ...

case "Diamonds":

// ...

case "Clubs":

// ...

default:

panic(fmt.Sprintf("invalid suit %v", s))

}

姿势四:针对入参不应该有问题的函数,使用panic设计

入参不应该有问题一般指的是硬编码,我们先看这两个函数(Compile和MustCompile),其中MustCompile函数是对Compile函数的包装:

func MustCompile(str string) *Regexp {

regexp, error := Compile(str)

if error != nil {

panic(`regexp: Compile(` + quote(str) + `): ` + error.Error())

}

return regexp

}

所以,对于同时支持用户输入场景和硬编码场景的情况,一般支持硬编码场景的函数是对支持用户输入场景函数的包装。

对于只支持硬编码单一场景的情况,函数设计时直接使用panic,即返回值类型列表中不会有error,这使得函数的调用处理非常方便(没有了乏味的"if err != nil {/ 打印 && 错误处理 /}"代码块)。

panic函数c语言,【go语言学习】错误error和异常panic相关推荐

  1. java出现errors是什么错误_java中错误(error)和异常(exception)有什么主要区别?

    jdk8中文发翻译Throwable类的描述:Throwable类是Java语言中所有错误和异常的Throwable类. 只有作为此类(或其一个子类)的实例的对象由Java虚拟机抛出,或者可以由Jav ...

  2. 《Go语言圣经》学习笔记 第五章函数

    <Go语言圣经>学习笔记 第五章 函数 目录 函数声明 递归 多返回值 匿名函数 可变参数 Deferred函数 Panic异常 Recover捕获异常 注:学习<Go语言圣经> ...

  3. Go语言系列——31-自定义错误、32-panic和recover、33-函数是一等公民(头等函数)、34-反射、35-读取文件、36-写入文件

    文章目录 31-自定义错误 使用 New 函数创建自定义错误 使用 Errorf 给错误添加更多信息 使用结构体类型和字段提供错误的更多信息 使用结构体类型的方法来提供错误的更多信息 32-panic ...

  4. 【C语言进阶深度学习记录】三十八 C/C++语言中的函数声明与函数定义

    文章目录 1 函数的声明和定义 1.1 代码分析 2 总结 1 函数的声明和定义 声明的意义在于告诉编译器程序单元的存在.只是告诉编译器它存在但是不在声明这里定义,有可能在当前文件中的其他地方或者其他 ...

  5. r语言c函数怎么用,R语言学习笔记——C#中如何使用R语言setwd()函数

    在R语言编译器中,设置当前工作文件夹可以用setwd()函数. > setwd("e://桌面//") > setwd("e:\桌面\") > ...

  6. 《JavaScript语言精粹》学习笔记(函数(2))

    <JavaScript语言精粹>学习笔记(函数(2)) 函数(Functions) 参数(Arguments) 当参数被调用时,会得到一个"免费"的参数数组argume ...

  7. go语言错误和异常处理,panic、defer、recover的执行顺序

    一.panic()和recover() Golang中引入两个内置函数panic和recover来触发和终止异常处理流程,同时引入关键字defer来延迟执行defer后面的函数. 一直等到包含defe ...

  8. 【C语言进阶深度学习记录】三十七 C/C++中造成程序内存错误的原因(野指针)

    什么是野指针? 指针变量存的地址是一块非法内存地址.进而形成野指针.但是需要注意一点,野指针不是NULL指针. 文章目录 1 野指针的概念 1.1 野指针代码案例初探 2 如何避免野指针 2.1 野指 ...

  9. 6-7 使用函数输出水仙花数_学习C语言居然对printf函数不理解???

    对于C语言的printf函数,可以说是我们学习C语言第一个接触到的函数了. printf命令的作用是格式化输出函数,一般用于向标准输出设备按规定格式输出信息.printf()函数的调用格式为:prin ...

最新文章

  1. 点击某个a标签,禁止页面自动跳转到该页面的顶部
  2. 2015-12-08 English
  3. 045_Unicode对照表十一
  4. 用GPUDirect RDMA技术做的代码测试
  5. mysql uncompress_如何在php中实现mysql compress()函数
  6. 【数据结构与算法基础】线性表
  7. 你可能不知道的Chrome-devtools用法总结
  8. 使用MDT2012部署WindowsLinux操作系统(4)
  9. 互联网大牛们的电脑桌面,佩服!
  10. 再谈“学微积,用手机”
  11. android中实现GPS定位功能,Android GPS定位步骤
  12. 使用Foxmail登录阿里企业邮箱(钉钉邮箱)
  13. css3实现缺角四边形_利用CSS3实现折角效果实例源码
  14. c语言里的字体怎么设置,C语言中如何添加文字
  15. 基于web的模型管理系统的设计
  16. 第四周day19-三剑客之awk
  17. word2016(office 365)中安装mathtype相关问题及解决办法
  18. gimp: 如何透明化背景
  19. 三角形面积的两种计算方法
  20. 苹果股价两月累计跌25% 市值跌破5000亿

热门文章

  1. RL串联等效转换为RL并联的方法及其仿真
  2. 如何获取微信小程序包
  3. [Study]Vue
  4. OpenSea:NFT界的eBay,月交易额30亿美元后的护城河与未来
  5. texlive+texstudio数学建模排版
  6. kswapd和pdflush
  7. funcode项目笔记
  8. 存储微博内容小技巧--印象笔记
  9. 光猫路由器与交换机的连接方式
  10. 利用Python绘制小狗小猫