您好,Habrahabr亲爱的读者。 在讨论错误处理的可能新设计并讨论显式错误处理的好处时,我建议考虑Go中错误,恐慌及其恢复的某些功能,这些功能在实践中将很有用。

错误
错误是接口。 和Go中的大多数接口一样, 错误的定义简短而又简单:
type error interface { Error() string }
事实证明,具有Error方法的任何类型都可以用作错误。 正如罗伯·派克(Rob Pike)所讲, 错误是值 ,并且值可以用于操纵和编程各种逻辑。
Go标准库中有两个函数可以方便地用于创建错误。 errors.New函数非常适合创建简单错误。 fmt.Errorf函数允许使用标准格式。
err := errors.New("emit macho dwarf: elf header corrupted") const name, id = "bimmler", 17 err := fmt.Errorf("user %q (id %d) not found", name, id)
通常,错误类型足以处理错误。 但是有时可能需要传输带有错误的其他信息;在这种情况下,您可以添加自己的错误类型。
一个很好的例子是os包中的PathError类型
此类错误的值将包含操作,路径和错误。
它们以这种方式初始化:
... return nil, &PathError{"open", name, syscall.ENOENT} ... return nil, &PathError{"close", file.name, e}
处理可以具有标准形式:
_, err := os.Open("---") if err != nil{ fmt.Println(err) }
但是,如果需要获取其他信息,则可以将错误解压缩为* os.PathError :
_, err := os.Open("---") if pe, ok := err.(*os.PathError);ok{ fmt.Printf("Err: %s\n", pe.Err) fmt.Printf("Op: %s\n", pe.Op) fmt.Printf("Path: %s\n", pe.Path) }
如果函数可以返回几种不同类型的错误,则可以应用相同的方法。
玩
声明几种错误,每种都有自己的数据:
代号 type ErrTimeout struct { Time time.Duration Err error } func (e *ErrTimeout) Error() string { return e.Time.String() + ": " + e.Err.Error() } type ErrPermission struct { Status string Err error } func (e *ErrPermission) Error() string { return e.Status + ": " + e.Err.Error() }
可以返回以下错误的函数:
代号 func proc(n int) error { if n <= 10 { return &ErrTimeout{Time: time.Second * 10, Err: errors.New("timeout error")} } else if n >= 10 { return &ErrPermission{Status: "access_denied", Err: errors.New("permission denied")} } return nil }
通过类型转换进行错误处理:
代号 func main(){ err := proc(11) if err != nil { switch e := err.(type) { case *ErrTimeout: fmt.Printf("Timeout: %s\n", e.Time.String()) fmt.Printf("Error: %s\n", e.Err) case *ErrPermission: fmt.Printf("Status: %s\n", e.Status) fmt.Printf("Error: %s\n", e.Err) default: fmt.Println("hm?") os.Exit(1) } } }
如果错误不需要特殊属性,则Go中的优良作法是创建变量以将错误存储在包级别。 例如io.EOF,io.ErrNoProgress之类的错误。
在下面的示例中,当错误为io.EOF时,我们将中断读取并继续运行该应用程序,否则,我们将关闭该应用程序以解决其他任何错误。
func main(){ reader := strings.NewReader("hello world") p := make([]byte, 2) for { _, err := reader.Read(p) if err != nil{ if err == io.EOF { break } log.Fatal(err) } } }
这是有效的,因为错误仅生成一次并重新使用。
堆栈跟踪
堆栈捕获时调用的函数列表。 堆栈跟踪可以帮助您更好地了解系统中正在发生的事情。 在调试时,将跟踪记录保存在日志中会非常有帮助。
Go通常会错误地缺少此信息,但是幸运的是,在Go中进行转储并不困难。
您可以使用debug.PrintStack()将跟踪输出到标准输出:
func main(){ foo() } func foo(){ bar() } func bar(){ debug.PrintStack() }
结果,以下信息将被写入Stderr:
叠 goroutine 1 [running]: runtime/debug.Stack(0x1, 0x7, 0xc04207ff78) .../Go/src/runtime/debug/stack.go:24 +0xae runtime/debug.PrintStack() .../Go/src/runtime/debug/stack.go:16 +0x29 main.bar() .../main.go:13 +0x27 main.foo() .../main.go:10 +0x27 main.main() .../main.go:6 +0x27
debug.Stack()返回带有堆栈转储的字节片,以后可以将其记录下来或在其他地方进行记录。
b := debug.Stack() fmt.Printf("Trace:\n %s\n", b)
还有一点,如果我们这样做:
go bar()
然后我们在输出中获得以下信息:
main.bar() .../main.go:19 +0x2d created by main.foo .../main.go:14 +0x3c
每个goroutine都有一个单独的堆栈,我们仅获得其转储。 顺便说一句,goroutines有自己的堆栈,recover仍然与此连接,但稍后会介绍更多。
因此,要查看所有goroutine的信息,可以使用runtime.Stack()并传递第二个参数true。
func bar(){ buf := make([]byte, 1024) for { n := runtime.Stack(buf, true) if n < len(buf) { break } buf = make([]byte, 2*len(buf)) } fmt.Printf("Trace:\n %s\n", buf) }
叠 Trace: goroutine 5 [running]: main.bar() .../main.go:21 +0xbc created by main.foo .../main.go:14 +0x3c goroutine 1 [sleep]: time.Sleep(0x77359400) .../Go/src/runtime/time.go:102 +0x17b main.foo() .../main.go:16 +0x49 main.main() .../main.go:10 +0x27
将此信息添加到错误中,从而大大增加了其信息内容。
例如,像这样:
type ErrStack struct { StackTrace []byte Err error } func (e *ErrStack) Error() string { var buf bytes.Buffer fmt.Fprintf(&buf, "Error:\n %s\n", e.Err) fmt.Fprintf(&buf, "Trace:\n %s\n", e.StackTrace) return buf.String() }
您可以添加一个函数来创建此错误:
func NewErrStack(msg string) *ErrStack { buf := make([]byte, 1024) for { n := runtime.Stack(buf, true) if n < len(buf) { break } buf = make([]byte, 2*len(buf)) } return &ErrStack{StackTrace: buf, Err: errors.New(msg)} }
然后您就可以使用此功能了:
func main() { err := foo() if err != nil { fmt.Println(err) } } func foo() error{ return bar() } func bar() error{ err := NewErrStack("error") return err }
叠 Error: error Trace: goroutine 1 [running]: main.NewErrStack(0x4c021f, 0x5, 0x4a92e0) .../main.go:41 +0xae main.bar(0xc04207ff38, 0xc04207ff78) .../main.go:24 +0x3d main.foo(0x0, 0x48ebff) .../main.go:21 +0x29 main.main() .../main.go:11 +0x29
因此,可以将错误和轨迹划分为:
func main(){ err := foo() if st, ok := err.(*ErrStack);ok{ fmt.Printf("Error:\n %s\n", st.Err) fmt.Printf("Trace:\n %s\n", st.StackTrace) } }
当然,已经有了现成的解决方案。 其中之一是https://github.com/pkg/errors包。 它允许您创建一个已经包含跟踪堆栈的新错误,并且可以向现有错误添加跟踪和/或其他消息。 加上方便的输出格式。
import ( "fmt" "github.com/pkg/errors" ) func main(){ err := foo() if err != nil { fmt.Printf("%+v", err) } } func foo() error{ err := bar() return errors.Wrap(err, "error2") } func bar() error{ return errors.New("error") }
叠 error main.bar .../main.go:20 main.foo .../main.go:16 main.main .../main.go:9 runtime.main .../Go/src/runtime/proc.go:198 runtime.goexit .../Go/src/runtime/asm_amd64.s:2361 error2 main.foo .../main.go:17 main.main .../main.go:9 runtime.main .../Go/src/runtime/proc.go:198 runtime.goexit .../Go/src/runtime/asm_amd64.s:2361
%v将仅显示消息
error2: error
惊慌/恢复
通常,紧急情况(又称为事故,也称为紧急情况)表示存在故障,因此系统(或特定子系统)无法继续运行。 如果调用了panic,则Go运行时将查看堆栈,尝试为其找到处理程序。
未经处理的紧急情况将终止应用程序。 这从根本上将它们与允许您不进行处理的错误区分开来。
您可以将任何参数传递给紧急函数调用。
panic(v interface{})
传递错误类型的错误很方便,它可以简化恢复并帮助调试。
panic(errors.New("error"))
Go中的灾难恢复基于延迟的函数调用,也称为defer 。 这样的函数可以保证在从父函数返回时执行。 不管原因是什么-return语句,函数结尾还是恐慌。
现在, recover函数使获得有关事故的信息并停止展开调用堆栈成为可能。
典型的紧急呼叫和处理程序:
func main(){ defer func() { if err := recover(); err != nil{ fmt.Printf("panic: %s", err) } }() foo() } func foo(){ panic(errors.New("error")) }
恢复返回接口{}(我们传递给恐慌的接口)或如果没有调用恐慌则返回nil。
考虑紧急处理的另一个例子。 我们具有某些功能,例如我们将其转移到资源上,并且从理论上讲,这可能会引起恐慌。
func bar(f *os.File) { panic(errors.New("error")) }
首先,您可能需要始终在最后执行一些操作,例如,清理资源(在我们的情况下为关闭文件)。
其次,这种功能的不正确执行不应导致整个程序的结束。
此问题可以通过延迟,恢复和关闭来解决:
func foo()(err error) { file, _ := os.Open("file") defer func() { if r := recover(); r != nil { err = r.(error)
关闭允许我们转到上面声明的变量,因此,我们保证关闭文件,并在发生事故的情况下,从文件中提取错误,并将其传递给通常的错误处理机制。
在相反的情况下,带有某些自变量的函数应始终正确运行,如果这种情况没有发生,那么它将变得非常糟糕。
在这种情况下,请添加一个包装函数,在该包装函数中调用目标函数,并在发生错误的情况下调用panic。
Go通常具有Must前缀:
值得记住的另一件事与恐慌和goroutines有关。
以上讨论的部分论文:
- 为每个goroutine分配一个单独的堆栈。
- 调用恐慌时,将在堆栈上搜索recover。
- 在找不到恢复的情况下,整个应用程序将终止。
main中的处理程序将不会拦截foo的恐慌,程序将崩溃:
func main(){ defer func() { if err := recover(); err != nil{ fmt.Printf("panic: %s", err) } }() go foo() time.Sleep(time.Minute) } func foo(){ panic(errors.New("error")) }
例如,如果调用处理程序以连接到服务器,则将是一个问题。 如果任何处理程序出现紧急情况,整个服务器将完成执行。 而且由于某些原因,您无法控制这些功能中的事故处理。
在简单的情况下,解决方案可能看起来像这样:
type f func() func Def(fn f) { go func() { defer func() { if err := recover(); err != nil { log.Println("panic") } }() fn() }() } func main() { Def(foo) time.Sleep(time.Minute) } func foo() { panic(errors.New("error")) }
处理/检查
也许将来我们会看到错误处理方面的变化。 您可以通过以下链接熟悉它们:
go2draft
Go 2中的错误处理
今天就这些了。 谢谢你