We read every piece of feedback, and take your input very seriously.
To see all available qualifiers, see our documentation.
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
错误处理这块是Go日常被大家吐槽较多的地方,我在工作中也观察到一些现象,比较严重的是在各层级的逻辑代码中对错误的处理有些重复。
比如,有人写代码就会在每一层都判断错误并记录日志,从代码层面看,貌似很严谨,但是如果看日志会发现一堆重复的信息,等到排查问题时反而会造成干扰。
这里总结三点Go代码的错误处理相关的最佳实践。
Go
这些最佳实践也是网上一些前辈分享的,我自己实践后在这里用自己的语言描述出来,希望能对大家有所帮助
Go程序通过error类型的值表示错误
error
error类型是一个内建接口类型,该接口只规定了一个返回字符串值的Error方法。
Error方法
type error interface { Error() string }
Go语言的函数经常会返回一个error值,调用者通过测试error值是否是nil来进行错误处理。
nil
i, err := strconv.Atoi("42") if err != nil { fmt.Printf("couldn't convert number: %v\n", err) return } fmt.Println("Converted integer:", i)
error 为 nil 时表示成功;非 nil 的 error 表示失败。
我们经常会自己定义符合自己需要的错误类型,但是记住要让这些类型实现error接口,这样就不用向调用方暴露额外的类型。
比如下面我们自己定义了myError这个类型,如果不实现error接口的话,调用者的代码中就会被myError这个类型侵入。即,下面的run函数,在定义返回值类型时,直接定义成error即可。
myError
run
如果myError不实现error接口的话,这里的返回值类型就要定义成myError类型。可想而知,紧接着调用者的程序里就要通过myError.Code == xxx 来判断到底是那种具体的错误(当然想要这么干得先把myError改成导出的MyError)。
myError.Code == xxx
MyError
package myerror import ( "fmt" "time" ) type myError struct { Code int When time.Time What string } func (e *myError) Error() string { return fmt.Sprintf("at %v, %s", e.When, e.What) } func run() error { return &MyError{ 1002, time.Now(), "it didn't work", } } func TryIt() { if err := run(); err != nil { fmt.Println(err) } }
那调用者判断自定义error是具体哪种错误的时候应该怎么办呢,myError并未向包外暴露,答案是通过向包外暴露检查错误行为的方法来实现。
myerror.IsXXXError(err) ...
抑或是通过比较error本身与包向外暴露的常量错误是否相等来判断,比如操作文件时常用来判断文件是否结束的io.EOF。
io.EOF
if err != io.EOF { return err }
先看一段简单的程序,看大家能不能发现一些细微的问题
func WriteAll(w io.Writer, buf []byte) error { _, err := w.Write(buf) if err != nil { log.Println("unable to write:", err) // annotated error goes to log file return err // unannotated error returned to caller } return nil } func WriteConfig(w io.Writer, conf *Config) error { buf, err := json.Marshal(conf) if err != nil { log.Printf("could not marshal config: %v", err) return err } if err := WriteAll(w, buf); err != nil { log.Println("could not write config: %v", err) return err } return nil } func main() { err := WriteConfig(f, &conf) fmt.Println(err) // io.EOF }
上面程序的错误处理暴露了两个问题:
底层函数WriteAll在发生错误后,除了向上层返回错误外还向日志里记录了错误,上层调用者做了同样的事情,记录日志然后把错误再返回给程序顶层。
WriteAll
因此在日志文件中得到一堆重复的内容,
unable to write: io.EOF could not write config: io.EOF ...
WriteConfig
针对这两个问题的解决方案可以是,在底层函数WriteAll、WriteConfig中为发生的错误添加上下文信息,然后将错误返回上层,由上层程序最后处理这些错误。
一种简单的保证错误的方法是使用fmt.Errorf函数,给错误添加信息
fmt.Errorf
func WriteConfig(w io.Writer, conf *Config) error { buf, err := json.Marshal(conf) if err != nil { return fmt.Errorf("could not marshal config: %v", err) } if err := WriteAll(w, buf); err != nil { return fmt.Errorf("could not write config: %v", err) } return nil } func WriteAll(w io.Writer, buf []byte) error { _, err := w.Write(buf) if err != nil { return fmt.Errorf("write failed: %v", err) } return nil }
fmt.Errorf只是给错误添加了简单的注解信息,如果你想在添加信息的同时还加上错误的调用栈,可以借助github.com/pkg/errors这个包,提供的包装错误的能力
github.com/pkg/errors
//只附加新的信息 func WithMessage(err error, message string) error //只附加调用堆栈信息 func WithStack(err error) error //同时附加堆栈和信息 func Wrap(err error, message string) error
有包装方法,就有对应的解包方法,Cause方法会返回包装错误对应的最原始错误--即会递归地进行解包
func Cause(err error) error
下面是使用github.com/pkg/errors改写后的错误处理程序
func ReadFile(path string) ([]byte, error) { f, err := os.Open(path) if err != nil { return nil, errors.Wrap(err, "open failed") } defer f.Close() buf, err := ioutil.ReadAll(f) if err != nil { return nil, errors.Wrap(err, "read failed") } return buf, nil } func ReadConfig() ([]byte, error) { home := os.Getenv("HOME") config, err := ReadFile(filepath.Join(home, ".settings.xml")) return config, errors.WithMessage(err, "could not read config") } func main() { _, err := ReadConfig() if err != nil { fmt.Printf("original error: %T %v\n", errors.Cause(err), errors.Cause(err)) fmt.Printf("stack trace:\n%+v\n", err) os.Exit(1) } }
上面格式化字符串时用的 %+v 是在 % v 基础上,对值进行展开,即展开复合类型值,比如结构体的字段值等明细。
这样既能给错误添加调用栈信息,又能保留对原始错误的引用,通过Cause可以还原到最初始引发错误的原因。
Cause
总结一下,错误处理的原则就是:
x
The text was updated successfully, but these errors were encountered:
No branches or pull requests
错误处理这块是Go日常被大家吐槽较多的地方,我在工作中也观察到一些现象,比较严重的是在各层级的逻辑代码中对错误的处理有些重复。
比如,有人写代码就会在每一层都判断错误并记录日志,从代码层面看,貌似很严谨,但是如果看日志会发现一堆重复的信息,等到排查问题时反而会造成干扰。
这里总结三点
Go
代码的错误处理相关的最佳实践。认识error
Go
程序通过error
类型的值表示错误error
类型是一个内建接口类型,该接口只规定了一个返回字符串值的Error方法
。Go
语言的函数经常会返回一个error
值,调用者通过测试error
值是否是nil
来进行错误处理。error 为 nil 时表示成功;非 nil 的 error 表示失败。
自定义错误记得要实现error接口
我们经常会自己定义符合自己需要的错误类型,但是记住要让这些类型实现
error
接口,这样就不用向调用方暴露额外的类型。比如下面我们自己定义了
myError
这个类型,如果不实现error
接口的话,调用者的代码中就会被myError
这个类型侵入。即,下面的run
函数,在定义返回值类型时,直接定义成error即可。如果
myError
不实现error
接口的话,这里的返回值类型就要定义成myError
类型。可想而知,紧接着调用者的程序里就要通过myError.Code == xxx
来判断到底是那种具体的错误(当然想要这么干得先把myError
改成导出的MyError
)。那调用者判断自定义
error
是具体哪种错误的时候应该怎么办呢,myError
并未向包外暴露,答案是通过向包外暴露检查错误行为的方法来实现。抑或是通过比较
error
本身与包向外暴露的常量错误是否相等来判断,比如操作文件时常用来判断文件是否结束的io.EOF
。错误处理常犯的错误和解决方案
先看一段简单的程序,看大家能不能发现一些细微的问题
错误处理常犯的两个问题
上面程序的错误处理暴露了两个问题:
底层函数
WriteAll
在发生错误后,除了向上层返回错误外还向日志里记录了错误,上层调用者做了同样的事情,记录日志然后把错误再返回给程序顶层。因此在日志文件中得到一堆重复的内容,
WriteAll
、WriteConfig
记录到log里的那些信息包装到错误里,返回给上层。针对这两个问题的解决方案可以是,在底层函数
WriteAll
、WriteConfig
中为发生的错误添加上下文信息,然后将错误返回上层,由上层程序最后处理这些错误。一种简单的保证错误的方法是使用
fmt.Errorf
函数,给错误添加信息给错误附加上下文信息
fmt.Errorf
只是给错误添加了简单的注解信息,如果你想在添加信息的同时还加上错误的调用栈,可以借助github.com/pkg/errors
这个包,提供的包装错误的能力有包装方法,就有对应的解包方法,Cause方法会返回包装错误对应的最原始错误--即会递归地进行解包
下面是使用
github.com/pkg/errors
改写后的错误处理程序这样既能给错误添加调用栈信息,又能保留对原始错误的引用,通过
Cause
可以还原到最初始引发错误的原因。总结
总结一下,错误处理的原则就是:
参考链接
x
The text was updated successfully, but these errors were encountered: