GoLang学习笔记(三十七)错误、异常的处理

在C语言中,默认采用一个证书类型的errno来表达错误,这样就可以根据需要定义多种错误类型。
在Go语言中,syscall.Errno就是对应C语言中errno类型的错误。
在syscall包中的接口,如果有返回错误的话,底层也是syscall.Errno错误类型。
通过syscall包的接口来修改文件的模型时,如果遇到错误可以通过将err强制断言为syscall.Errno错误类型来处理。
还可以进一步地通过类型查询或类型断言来获取底层真实的错误类型,这样就可以获取更详细的错误信息。
一般我们并不关心错误在底层的表达式,只需要知道它是一个错误。

先上一段错误代码:

func testErr01() {
	num1,num2 := 10 ,0
	fmt.Println(num1 /num2)
}

运行这个函数铁定报错,在默认情况下,发生错误后,程序就会崩溃退出,如果我们希望发生错误后,可以捕获到错误,并进行处理保证程序可以继续执行。还可以在捕获到错误后,给管理员一个提示(邮件,短信。。。)

Go语言不支持传统的try...catch...finally这种处理方式,其处理方式为defer...panic...recover,简单来说就是go可以抛出一个panic异常,然后再defer中通过recover捕获这个异常,然后正常处理。将上面这个函数加上处理代码:

func testErr01() {
	defer func() {
		err := recover()
		if err != nil {
			fmt.Println(err)
		}
	}()
	num1,num2 := 10 ,0
	fmt.Println(num1 /num2)
}

运行后,会得到:

runtime error: integer divide by zero

你还可以更完善的提示用户:

func testErr01() {
	defer func() {
		err := recover()
		if err != nil {
			fmt.Println(err)
			fmt.Println("请联系系统管理员")
		}
	}()
	num1,num2 := 10 ,0
	fmt.Println(num1 /num2)
}

在Go中,你还能使用errors.New和panic内置函数来自定义错误。

errors.New("错误说明"),会返回一个error类型的值,表示一个错误

panic内置函数,接收一个interface{}类型的值(也就是任何值了)作为参数。可以接收error类型的变量,输出错误信息,并退出程序。

func testErr02(n int) (err error) {
	if n <= 150 {
		return nil
	} else {
		return errors.New("人活这大,就是妖了")
	}
}

func main() {
	err := testErr02(300)
	if err != nil {
		fmt.Println(err)
	}
}

上面这个例子中,就可以通过判断整数的大小,来返回一个自定义错误。

下面是一个文件复制的例子,函数需要打开两个文件,然后将其中一个文件的内容复制到另一个文件。

func testError01(dstName, srcName string) (written int64, err error) {
	src, err := os.Open(srcName)
	if err != nil {
		return
	}
	dst, err := os.Create(dstName)
	if err != nil {
		return
	}
	written, err = io.Copy(dst, src)
	dst.Close()
	src.Close()
	return
}

上面这个函数虽然能用,但如果第一个os.Open()函数打开了srcName,而第二个os.Create()函数调用失败,那么就会造成src未释放资源的情况下返回。
我们可以通过defer语句来确保每个被正常打开的文件都能被正常关闭。
defer语句可以让我们在打开文件时马上思考如何关闭文件。
不管函数如何返回,文件关闭语句始终会被执行。
同时defer语句可以保证,即使io.Copy发生异常,文件依然可以安全的关闭。

func testError02(dstName, srcName string) (written int64, err error) {
	src, err := os.Open(srcName)
	if err != nil {
		return
	}
	defer src.Close()
	dst, err := os.Create(dstName)
	if err != nil {
		return
	}
	defer dst.Close()
	return io.Copy(dst, src)
}

有时候为了方便上层用户理解,底层实现者会将底层得错误重新包装为新的错误类型返回给上层用户。
上层用户在遇到错误时,很容易从业务层面理解错误发生的原因,在上层用户获得新的错误类型的同时,也丢失了底层最原始的错误类型。
为了记录这种错误类型在包装的变迁过程中的信息,我们一般会定义一个辅助的WrapError()函数,用于包装原始的错误,同时保留完整的原始错误类型。
为了问题定位的方便,同时也为了能记录错误发生时的函数调用状态,我们很多时候希望在出现致命错误的时候保存完整的函数调用信息。
为了支持RPC等跨网络的传输,可能需要将错误序列化为类似JSON格式的数据,然后再从这些数据中将错误解码恢复出来。

在Go语言中,错误被认为时一种可以预期的结果,而异常则是一种非预期的结果,发生异常可能表示程序中存在Bug或发生了其他不可控的问题。
Go语言推荐使用recover()函数将内部异常转为错误来处理,这使得用户可以正真地关心业务相关的错误处理。
如果某个接口简单地将所有普通地错误当异常抛出,将会使错误信息杂乱且没有价值,就像在main()函数中直接捕获全部一样,是没有意义的。
捕获异常不是最终目的。如果异常不可预测,直接输出异常信息是最好的处理方式。

Go语言中的导出函数一般不抛出异常,一个未受控的异常可以看成是程序的Bug。
对于那些提供类似Web服务的框架而言,它门经常需要接入第三方的中间件。
因为对于第三方中间件是否存在Bug而抛出异常,Web框架本身无法确定,
为了提高稳定性,Web框架一般会通过recover来防御性地捕获所有处理流程中可能产生的异常,然后将异常转化为错误返回。
以JSON解析器为例,说明recover()的使用场景。
JSON解析器的复杂性,即使某个语言解析器目前工作正常,也无法肯定它没有漏洞。
当某个异常出现时,我们不会选择让解析器崩溃,而是会将panic异常当作普通的解析错误,并附加信息提醒用户报告此错误。

func parseJOSN01(input string) (s *Syntax., err error) {
    defer func() {
        if p := recover(); p != nil {
            err = fmt.Errorf("JSON : internal error : %v" ,p)
        }
    }()
    //开始解析工作
}

标准库中的json包,在内部递归解析JSON数据的时候,如果遇到了错误,会通过爆出异常的方式来快速跳出深度嵌套的函数调用,
然后由最外一级的接口通过recover()捕获panic,然后返回相应的错误信息。
即使在包内部使用了panic,在导出函数时也会被转换未明确的错误值。

panic()支持抛出任意类型的异常,recover()函数调用的返回值和panic()函数的输入参数类型一致,它们的函数签名如下:
func panic(interface{})
func recover(interface{})
Go语言函数调用的正常流程时函数执行返回语句返回结果,在这个流程中是没有异常的,因此在这个流程中执行recover()异常捕获函数始终返回nil
另一种是异常流程,当函数调用panic()抛出异常时,函数将停止执行后续的普通语句,但之前注册的defer()函数调用任然保证会被正常执行,然后再返回到调用者。
对于当前函数的调用者,因为处理异常状态还没有被捕获,所以和直接调用panic()函数的行为类似。
在异常发生时,如果在defer()中执行recover()调用,它可以捕获触发panic()时的参数,并且恢复到正常的执行流程。

在非defer语句中执行recover()调用时初学者常犯的错误。
下面代码中的两个recover()调用都不能捕获任何异常。
在第一个recover()调用执行时,函数必然时在正常的非异常执行流程中,这个时候必定返回nil。
发生异常时,第二个recover()调用将没有机会被执行到,因为panic()调用会导致马上执行已经注册defer的函数后返回。

func testException01() {
	if r := recover(); r != nil {
		log.Fatal(r)
	}
	panic(123)
	if r := recover(); r != nil {
		log.Fatal(r)
	}
}

对recover()函数的调用有着更严格的要求:我们必须在defer()函数中直接调用recover()。
如果defer()中调用的时recover()函数的包装函数的话,异常捕的捕获工作将失败。
有时候我们可能希望包装自己的MyRecover01()函数,在内部增加必要的日志信息然后再调用recover(),这是错误的做法。

func MyRecover01() interface{} {
	log.Println("trace......")
	return recover()
}

func testException02() {
	defer func() {
		if r := MyRecover01(); r != nil {
			fmt.Println(r)
		}
	}()
	panic(1)
}

嵌套的defer()函数中调用recover(),也会导致无法捕获异常。
两层嵌套的defer()函数中直接调用recover()和一层defer()函数中调用包装的MyRecover01()函数一样,
都是经过了两个函数帧才到达真正的recover()函数,这时Goroutine对应的上一级栈帧中已经没有异常信息了。

func testException03() {
	defer func() {
		defer func() {
			if r := recover(); r != nil {
				fmt.Println(r)
			}
		}()
	}()
	panic(1)
}

如果直接使用MyRecover01()函数,则可以正常使用

func MyRecover02() interface{} {
	return recover()
}

func testException04() {
	defer MyRecover02()
	panic(1)
}

直接再defer里使用recover(),不能捕获异常

func testException05() {
	defer recover()
	panic(1)
}

必须要和由异常的栈帧只隔一个栈帧,recover()函数才能正常捕获异常。
简单来说recover()函数捕获的时祝福一级调用函数栈帧的异常,刚好可以跨越一层defer()函数。

func testException06() {
	defer func() {
		if r := recover(); r != nil {
			fmt.Println(r)
		}
	}()
	panic("异常001")
}

如果想将捕获到的异常转换成为错误时,希望忠实返回原始的信息,则需要针对不同的类型分别处理。

func testFoo01() (err error) {
	defer func() {
		if r := recover(); r != nil {
			switch x := r.(type) {
			case string:
				err = errors.New(x)
			case error:
				err = x
			default:
				err = fmt.Errorf("Unknown panic: %v", r)
			}
		}
	}()
	panic("TODO")
}

基于上面的代码,我们可以模拟出不同类型的异常,通过定义不同类型的保护接口我们就可以区分异常的类型了。

func testException07() {
	defer func() {
		if r := recover(); r != nil {
			switch x := r.(type) {
			case runtime.Error:
				//这是运行时错误类型异常
			case error:
				//这是普通错误类型异常
			default:
				//其他类型异常
			}
		}
	}()
}

 

你可能感兴趣的:(GoLang)