GO 编程模式系列(二):错误处理
GoCN
共 7869字,需浏览 16分钟
·
2020-12-30 21:10
原文作者:陈皓(左耳朵耗子)
内容出处:
https://coolshell.cn/articles/21128.html
if err != nil
。if err != nil
怎么办这个事之前,我想先说一说编程中的错误处理。这样可以让大家在更高的层面理解编程中的错误处理。本文是全系列中第2 / 9篇:Go编程模式
Go编程模式:切片,接口,时间和性能 Go 编程模式:错误处理 Go 编程模式:Functional Options Go编程模式:委托和反转控制 Go编程模式:Map-Reduce Go 编程模式:Go Generation Go编程模式:修饰器 Go编程模式:Pipeline Go 编程模式:k8s Visitor 模式
C语言的错误检查
errno
变量并配合一个 errstr
的数组来告诉你为什么出错。read()
, write()
, open()
这些函数的返回值其实是返回有业务逻辑的值。也就是说,这些函数的返回值有两种语义,一种是成功的值,比如 open()
返回的文件句柄指针 FILE*
,或是错误 NULL
。这样会导致调用者并不知道是什么原因出错了,需要去检查 errno
来获得出错的原因,从而可以正确地处理错误。int atoi(const char *str)
0
,那么会和正常的对 “0” 字符的返回值完全混淆在一起。这样就无法判断出错的情况。你可能会说,是不是要检查一下 errno
,按道理说应该是要去检查的,但是,我们在 C99 的规格说明书中可以看到这样的描述——7.20.1The functions atof, atoi, atol, and atoll need not affect the value of the integer expression errno on an error. If the value of the result cannot be represented, the behavior is undefined.
atoi()
, atof()
, atol()
或是 atoll()
这样的函数是不会设置 errno
的,而且,还说了,如果结果无法计算的话,行为是undefined。所以,后来,libc 又给出了一个新的函数strtol()
,这个函数在出错的时会设置全局变量 errno
:long val = strtol(in_str, &endptr, 10); //10的意思是10进制
//如果无法转换
if (endptr == str) {
fprintf(stderr, "No digits were found\n");
exit(EXIT_FAILURE);
}
//如果整型溢出了
if ((errno == ERANGE && (val == LONG_MAX || val == LONG_MIN)) {
fprintf(stderr, "ERROR: number out of range for LONG\n");
exit(EXIT_FAILURE);
}
//如果是其它错误
if (errno != 0 && val == 0) {
perror("strtol");
exit(EXIT_FAILURE);
}
strtol()
函数解决了 atoi()
函数的问题,但是我们还是能感觉到不是很舒服和自然。程序员一不小心就会忘记返回值的检查,从而造成代码的 Bug; 函数接口非常不纯洁,正常值和错误值混淆在一起,导致语义有问题。
HRESULT
的返回来统一错误的返回值,这样可以明确函数调用时的返回值是成功还是错误。但这样一来,函数的 input 和 output 只能通过函数的参数来完成,于是出现了所谓的 入参 和 出参 这样的区别。Java的错误处理
try-catch-finally
通过使用异常的方式来处理错误,其实,这比起C语言的错处理进了一大步,使用抛异常和抓异常的方式可以让我们的代码有这样的一些好处:函数接口在 input(参数)和 output(返回值)以及错误处理的语义是比较清楚的。 正常逻辑的代码可以与错误处理和资源清理的代码分开,提高了代码的可读性。 异常不能被忽略(如果要忽略也需要 catch 住,这是显式忽略)。 在面向对象的语言中(如 Java),异常是个对象,所以,可以实现多态式的 catch。 与状态返回码相比,异常捕捉有一个显著的好处是,函数可以嵌套调用,或是链式调用。比如: int x = add(a, div(b,c));
Pizza p = PizzaBuilder().SetSize(sz).SetPrice(p)...;
Go语言的错误处理
参数上基本上就是入参,而返回接口把结果和错误分离,这样使得函数的接口语义清晰; 而且,Go 语言中的错误参数如果要忽略,需要显式地忽略,用 _ 这样的变量来忽略; 另外,因为返回的 error
是个接口(其中只有一个方法Error()
,返回一个string
),所以你可以扩展自定义的错误处理。
error
,你也可以使用下面这样的方式:if err != nil {
switch err.(type) {
case *json.SyntaxError:
...
case *ZeroDivisionError:
...
case *NullPointerError:
...
default:
...
}
}
资源清理
C语言 – 使用的是 goto fail;
的方式到一个集中的地方进行清理(有篇有意思的文章可以看一下《由苹果的低级BUG想到的》)C++语言- 一般来说使用 RAII模式,通过面向对象的代理模式,把需要清理的资源交给一个代理类,然后在析构函数来解决。 Java语言 – 可以在finally 语句块里进行清理。 Go语言 – 使用 defer
关键词进行清理。
func Close(c io.Closer) {
err := c.Close()
if err != nil {
log.Fatal(err)
}
}
func main() {
r, err := Open("a")
if err != nil {
log.Fatalf("error opening 'a'\n")
}
defer Close(r) // 使用defer关键字在函数退出时关闭文件。
r, err = Open("b")
if err != nil {
log.Fatalf("error opening 'b'\n")
}
defer Close(r) // 使用defer关键字在函数退出时关闭文件。
}
Error Check Hell
if err !=nil
的代码了,这样的代码的确是能让人写到吐。那么有没有什么好的方式呢,有的。我们先看如下的一个令人崩溃的代码。func parse(r io.Reader) (*Point, error) {
var p Point
if err := binary.Read(r, binary.BigEndian, &p.Longitude); err != nil {
return nil, err
}
if err := binary.Read(r, binary.BigEndian, &p.Latitude); err != nil {
return nil, err
}
if err := binary.Read(r, binary.BigEndian, &p.Distance); err != nil {
return nil, err
}
if err := binary.Read(r, binary.BigEndian, &p.ElevationGain); err != nil {
return nil, err
}
if err := binary.Read(r, binary.BigEndian, &p.ElevationLoss); err != nil {
return nil, err
}
}
func parse(r io.Reader) (*Point, error) {
var p Point
var err error
read := func(data interface{}) {
if err != nil {
return
}
err = binary.Read(r, binary.BigEndian, data)
}
read(&p.Longitude)
read(&p.Latitude)
read(&p.Distance)
read(&p.ElevationGain)
read(&p.ElevationLoss)
if err != nil {
return &p, err
}
return &p, nil
}
if err!=nil
处理的很干净了。但是会带来一个问题,那就是有一个 err
变量和一个内部的函数,感觉不是很干净。bufio.Scanner()
中似乎可以学习到一些东西:scanner := bufio.NewScanner(input)
for scanner.Scan() {
token := scanner.Text()
// process token
}
if err := scanner.Err(); err != nil {
// process the error
}
scanner
在操作底层的I/O的时候,那个for-loop中没有任何的 if err !=nil
的情况,退出循环后有一个 scanner.Err()
的检查。看来使用了结构体的方式。模仿它,我们可以把我们的代码重构成下面这样:type Reader struct {
r io.Reader
err error
}
func (r *Reader) read(data interface{}) {
if r.err == nil {
r.err = binary.Read(r.r, binary.BigEndian, data)
}
}
func parse(input io.Reader) (*Point, error) {
var p Point
r := Reader{r: input}
r.read(&p.Longitude)
r.read(&p.Latitude)
r.read(&p.Distance)
r.read(&p.ElevationGain)
r.read(&p.ElevationLoss)
if r.err != nil {
return nil, r.err
}
return &p, nil
}
package main
import (
"bytes"
"encoding/binary"
"fmt"
)
// 长度不够,少一个Weight
var b = []byte {0x48, 0x61, 0x6f, 0x20, 0x43, 0x68, 0x65, 0x6e, 0x00, 0x00, 0x2c}
var r = bytes.NewReader(b)
type Person struct {
Name [10]byte
Age uint8
Weight uint8
err error
}
func (p *Person) read(data interface{}) {
if p.err == nil {
p.err = binary.Read(r, binary.BigEndian, data)
}
}
func (p *Person) ReadName() *Person {
p.read(&p.Name)
return p
}
func (p *Person) ReadAge() *Person {
p.read(&p.Age)
return p
}
func (p *Person) ReadWeight() *Person {
p.read(&p.Weight)
return p
}
func (p *Person) Print() *Person {
if p.err == nil {
fmt.Printf("Name=%s, Age=%d, Weight=%d\n",p.Name, p.Age, p.Weight)
}
return p
}
func main() {
p := Person{}
p.ReadName().ReadAge().ReadWeight().Print()
fmt.Println(p.err) // EOF 错误
}
if err != nil
的方式。包装错误
err
给返回到上层,我们需要把一些执行的上下文加入。fmt.Errorf()
来完成这个事,比如:if err != nil {
return fmt.Errorf("something failed: %v", err)
}
type authorizationError struct {
operation string
err error // original error
}
func (e *authorizationError) Error() string {
return fmt.Sprintf("authorization failed during %s: %v", e.operation, e.err)
}
causer
接口中实现 Cause()
方法来暴露原始错误,以供进一步检查:type causer interface {
Cause() error
}
func (e *authorizationError) Cause() error {
return e.err
}
import"github.com/pkg/errors"
//错误包装
if err != nil{
return errors.Wrap(err, "read failed")
}
// Cause接口
switch err := errors.Cause(err).(type){
case *MyError:
// handle specifically
default:
// unknown error
}
参考文章
Golang Error Handling lesson by Rob Pike
http://jxck.hatenablog.com/entry/golang-error-handling-lesson-by-rob-pikeErrors are values
https://blog.golang.org/errors-are-values
评论