社区微信群开通啦,扫一扫抢先加入社区官方微信群
社区微信群
错误处理是现实中经常碰到的、难以处理好的问题,下面会从下面几个方面探讨错误处理:
我们总会遇到非预期的非正常情况,有一种是符合预期的,比如函数返回error并处理,这种叫做可以预见到的错误,还有一种是预见不到的比如除零、空指针、数组越界等叫做panic,panic的处理主要参考Defer, Panic, and Recover。
错误处理的模型一般有两种,一般是错误码模型比如C/C++和Go,还有异常模型比如Java和C#。Go没有选择异常模型,因为错误码比异常更有优势,参考文章Cleaner, more elegant, and wrong 以及Cleaner, more elegant, and harder to recognize。看下面的代码:
try {
AccessDatabase accessDb = new AccessDatabase();
accessDb.GenerateDatabase();
} catch (Exception e) {
// Inspect caught exception
}
public void GenerateDatabase()
{
CreatePhysicalDatabase();
CreateTables();
CreateIndexes();
}
这段代码的错误处理有很多问题,比如如果CreateIndexes
抛出异常,会导致数据库和表不会删除,造成脏数据。从代码编写者和维护者的角度看这两个模型,会比较清楚:
Really Easy | Hard | Really Hard |
---|---|---|
Writing bad error-code-based code Writing bad exception-based code |
Writing good error-code-based code |
Writing good exception-based code |
错误处理不容易做好,要说容易那说明做错了;要把错误处理写对了,基于错误码模型虽然很难,但比异常模型简单。
Really Easy | Hard | Really Hard |
---|---|---|
Recognizing that error-code-based code is badly-written Recognizing the difference between bad error-code-based code and not-bad error-code-based code. |
Recognizing that error-code-base code is not badly-written |
Recognizing that exception-based code is badly-written Recognizing that exception-based code is not badly-written Recognizing the difference between bad exception-based code and not-bad exception-based code |
如果使用错误码模型,非常容易就能看出错误处理没有写对,也能很容易知道做得好不好;要知道是否做得非常好,错误码模型也不太容易。
如果使用异常模型,无论做的好不好都很难知道,而且也很难知道怎么做好。
Go官方的error介绍,简单一句话就是返回错误对象的方式,参考Error handling and Go,解释了error是什么,如何判断具体的错误,显式返回错误的好处。文中举的例子就是打开文件错误:
func Open(name string) (file *File, err error)
Go可以返回多个值,最后一个一般是error,我们需要检查和处理这个错误,这就是Go的错误处理的官方介绍:
if err := Open("src.txt"); err != nil {
// Handle err
}
看起来非常简单的错误处理,有什么难的呢?骚等,在Go2的草案中,提到的三个点Error Handling、Error Values和Generics泛型,两个点都是错误处理的,这说明了Go1中对于错误是有改进的地方。
再详细看下Go2的草案,错误处理:Error Handling中,主要描述了发生错误时的重复代码,以及不能便捷处理错误的情况。比如草案中举的这个例子No Error Handling: CopyFile,没有做任何错误处理:
package main
import (
"fmt"
"io"
"os"
)
func CopyFile(src, dst string) error {
r, _ := os.Open(src)
defer r.Close()
w, _ := os.Create(dst)
io.Copy(w, r)
w.Close()
return nil
}
func main() {
fmt.Println(CopyFile("src.txt", "dst.txt"))
}
还有草案中这个例子Not Nice and still Wrong: CopyFile,错误处理是特别啰嗦,而且比较明显有问题:
package main
import (
"fmt"
"io"
"os"
)
func CopyFile(src, dst string) error {
r, err := os.Open(src)
if err != nil {
return err
}
defer r.Close()
w, err := os.Create(dst)
if err != nil {
return err
}
defer w.Close()
if _, err := io.Copy(w, r); err != nil {
return err
}
if err := w.Close(); err != nil {
return err
}
return nil
}
func main() {
fmt.Println(CopyFile("src.txt", "dst.txt"))
}
当io.Copy
或w.Close
出现错误时,目标文件实际上是有问题,那应该需要删除dst文件的。而且需要给出错误时的信息,比如是哪个文件,不能直接返回err。所以Go中正确的错误处理,应该是这个例子Good: CopyFile,虽然啰嗦繁琐不简洁:
package main
import (
"fmt"
"io"
"os"
)
func CopyFile(src, dst string) error {
r, err := os.Open(src)
if err != nil {
return fmt.Errorf("copy %s %s: %v", src, dst, err)
}
defer r.Close()
w, err := os.Create(dst)
if err != nil {
return fmt.Errorf("copy %s %s: %v", src, dst, err)
}
if _, err := io.Copy(w, r); err != nil {
w.Close()
os.Remove(dst)
return fmt.Errorf("copy %s %s: %v", src, dst, err)
}
if err := w.Close(); err != nil {
os.Remove(dst)
return fmt.Errorf("copy %s %s: %v", src, dst, err)
}
return nil
}
func main() {
fmt.Println(CopyFile("src.txt", "dst.txt"))
}
具体应该如何简洁的处理错误,可以读Error Handling,大致是引入关键字handle和check,由于本文重点侧重Go1如何错误处理,就不展开分享了。
明显上面每次都返回的fmt.Errorf
信息也是不够的,所以Go2还对于错误的值有提案,参考Error Values。大规模程序应该面向错误编程和测试,同时错误应该包含足够的信息。Go1中判断error具体是什么错误有几种办法:
io.EOF
这个全局变量,那么可以直接比较是否是这个错误。os.IsNotExist
判断是否是指定错误。error.Error()
返回的字符串匹配,看是否是某个错误。在复杂程序中,有用的错误需要包含调用链的信息。例如,考虑一次数据库写,可能调用了RPC,RPC调用了域名解析,最终是没有权限读/etc/resolve.conf
文件,那么给出下面的调用链会非常有用:
write users database: call myserver.Method:
dial myserver:3333: open /etc/resolv.conf: permission denied
由于Go1的错误值没有完整的解决这个问题,才导致出现非常多的错误处理的库,比如:
errors.Is
、errors.As
和errors.Match
。%+v
来格式化错误的额外信息比如堆栈。pkg/errors
的Cause返回最底层的错误不同,它只反馈错误链的下一个错误。Go1.13改进了errors,参考如下实例代码:
package main
import (
"errors"
"fmt"
"io"
)
func foo() error {
return fmt.Errorf("read err: %w", io.EOF)
}
func bar() error {
if err := foo(); err != nil {
return fmt.Errorf("foo err: %w", err)
}
return nil
}
func main() {
if err := bar(); err != nil {
fmt.Printf("err: %+vn", err)
fmt.Printf("unwrap: %+vn", errors.Unwrap(err))
fmt.Printf("unwrap of unwrap: %+vn", errors.Unwrap(errors.Unwrap(err)))
fmt.Printf("err is io.EOF? %vn", errors.Is(err, io.EOF))
}
}
运行结果如下:
err: foo err: read err: EOF
unwrap: read err: EOF
unwrap of unwrap: EOF
err is io.EOF? true
从上面的例子可以看出:
errors.Is
能判断出是否是最里面的那个error。另外,错误处理往往和log是容易混为一谈的,因为遇到错误一般会打日志,特别是在C/C++中返回错误码一般都会打日志记录下,有时候还会记录一个全局的错误码比如linux的errno,而这种习惯,造成了error和log混淆造成比较大的困扰。考虑以前写了一个C++的服务器,出现错误时会在每一层打印日志,所以就会形成堆栈式的错误日志,便于排查问题,如果只有一个错误,不知道调用上下文,排查会很困难:
avc decode avc_packet_type failed. ret=3001
Codec parse video failed, ret=3001
origin hub error, ret=3001
这种比只打印一条日志origin hub error, ret=3001
要好,但是还不够好:
改进后的错误日志变成了在底层返回,而不在底层打印在调用层打印,有调用链和堆栈,有线程切换的ID信息,也有文件的行数:
Error processing video, code=3001 : origin hub : codec parser : avc decoder
[100] video_avc_demux() at [srs_kernel_codec.cpp:676]
[100] on_video() at [srs_app_source.cpp:1076]
[101] on_video_imp() at [srs_app_source:2357]
从Go2的描述来说,实际上这个错误处理也还没有考虑完备。从实际开发来说,已经比较实用了。
总结下Go的error,错误处理应该注意的点:
推荐用github.com/pkg/errors这个错误处理的库,基本上是够用的,参考Refine: CopyFile,可以看到CopyFile中低级重复的代码已经比较少了:
package main
import (
"fmt"
"github.com/pkg/errors"
"io"
"os"
)
func CopyFile(src, dst string) error {
r, err := os.Open(src)
if err != nil {
return errors.Wrap(err, "open source")
}
defer r.Close()
w, err := os.Create(dst)
if err != nil {
return errors.Wrap(err, "create dest")
}
nn, err := io.Copy(w, r)
if err != nil {
w.Close()
os.Remove(dst)
return errors.Wrap(err, "copy body")
}
if err := w.Close(); err != nil {
os.Remove(dst)
return errors.Wrapf(err, "close dest, nn=%v", nn)
}
return nil
}
func LoadSystem() error {
src, dst := "src.txt", "dst.txt"
if err := CopyFile(src, dst); err != nil {
return errors.WithMessage(err, fmt.Sprintf("load src=%v, dst=%v", src, dst))
}
// Do other jobs.
return nil
}
func main() {
if err := LoadSystem(); err != nil {
fmt.Printf("err %+vn", err)
}
}
改写的函数中,用
errors.Wrap
和errors.Wrapf
代替了fmt.Errorf
,我们不记录src和dst的值,因为在上层会记录这个值(参考下面的代码),而只记录我们这个函数产生的数据,比如nn
。
import "github.com/pkg/errors"
func LoadSystem() error {
src, dst := "src.txt", "dst.txt"
if err := CopyFile(src, dst); err != nil {
return errors.WithMessage(err, fmt.Sprintf("load src=%v, dst=%v", src, dst))
}
// Do other jobs.
return nil
}
在这个上层函数中,我们用的是
errors.WithMessage
添加了这一层的错误信息,包括src
和dst
,所以CopyFile
里面就不用重复记录这两个数据了。同时我们也没有打印日志,只是返回了带完整信息的错误。
func main() {
if err := LoadSystem(); err != nil {
fmt.Printf("err %+vn", err)
}
}
在顶层调用时,我们拿到错误,可以决定是打印还是忽略还是送监控系统。
比如我们在调用层打印错误,使用%+v
打印详细的错误,有完整的信息:
err open src.txt: no such file or directory
open source
main.CopyFile
/Users/winlin/t.go:13
main.LoadSystem
/Users/winlin/t.go:39
main.main
/Users/winlin/t.go:49
runtime.main
/usr/local/Cellar/go/1.8.3/libexec/src/runtime/proc.go:185
runtime.goexit
/usr/local/Cellar/go/1.8.3/libexec/src/runtime/asm_amd64.s:2197
load src=src.txt, dst=dst.txt
但是这个库也有些小毛病:
CopyFile
中还是有可能会有重复的信息,还是Go2的handle
和check
方案是最终解决。Wrap
,有时候是调用WithMessage
(不需要加堆栈时),这个真是非常不好用的地方(这个我们已经修改了库,可以全部使用Wrap不用WithMessage,会去掉重复的堆栈)。由于简书限制了文章字数,只好分成不同章节:
欢迎加入SRS流媒体服务器钉钉群:
或者用微信扫二维码加公众号,然后在公众号中点加微信群:
推荐加钉钉群哦,有各种实时消息,有直播和回看,还有机器人等你撩~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!