Go 错误和异常

Hi,我是行舟,今天和大家一起学习Go语言的错误和异常。

程序运行过程中难免会产生错误和异常,JavaJavaScript、PHP、Python等语言都是通过try catch(e Exception){}范式去处理,但是Go语言不同。接下来我们学习一下Go语言中的错误(error)和异常(painc)处理。

error

error是Go语言的通用错误类型,它的定义如下:

// The error built-in interface type is the conventional interface for
// representing an error condition, with the nil value representing no error.
type error interface {
   Error() string
}

在函数章节我们已经了解到Go语言支持多返回值,当需要错误处理时,通常会把函数的最后一个返回值定义为error类型。我们举个例子:

// SumMax100 求和不能大于100
func SumMax100(a, b int32) (int32, error) {
   s := a + b
   if s <= 100 {
      return s, nil // 当值<=100时候,error为nil代表没有错误
   } else {
      return s, errors.New("值过大"// 当值>100时候,返回错误
   }
}

在上面示例中,当返回值<=100时,返回error类型的实际值为nil;反之调用errors的New方法返回error。在调用SumMax100方法时会根据error是否为nil来执行相应的代码逻辑。这是Go语言处理错误的常用做法。比如经常用到的http包的get方法:resp, err := http.Get("www.baidu.com"),我们会根据err是否为nil来判断Get方法是否发生错误。

接下来我们看一下Go语言的内置包:errors。errors是error接口类型的实现,源码简短,内容如下:

package errors

// New returns an error that formats as the given text.
// Each call to New returns a distinct error value even if the text is identical.
func New(text string) error {
   return &errorString{text}
}

// errorString is a trivial implementation of error.
type errorString struct {
   s string
}

func (e *errorString) Error() string {
   return e.s
}

errors包的New方法,接收字符串,并返回error类型的值。在errors包中定义了errorString一个私有结构体,用来实现error接口。根据Go语言接口规范,只要一个类型实现Error方法就实现了error接口,所以我们调用New方法时返回的errorString类型实现了error接口。
Go语言内置包是如何使用errors的呢?

代码来自:go/src/internal/oserror/errors.go

package oserror

import "errors"

var (
   ErrInvalid    = errors.New("invalid argument") // 参数错误
   ErrPermission = errors.New("permission denied") // 权限错误
   ErrExist      = errors.New("file already exists") // 文件已存在
   ErrNotExist   = errors.New("file does not exist") // 文件不存在
   ErrClosed     = errors.New("file already closed") // 文件也关闭
)

包中定义了文件操作的常见错误。

代码来自:go/src/io/fs/fs.go

// Generic file system errors.
// Errors returned by file systems can be tested against these errors
// using errors.Is.
var (
   ErrInvalid    = errInvalid()    // "invalid argument"
   ErrPermission = errPermission() // "permission denied"
   ErrExist      = errExist()      // "file already exists"
   ErrNotExist   = errNotExist()   // "file does not exist"
   ErrClosed     = errClosed()     // "file already closed"
)

func errInvalid() error    { return oserror.ErrInvalid }
func errPermission() error { return oserror.ErrPermission }
func errExist() error      { return oserror.ErrExist }
func errNotExist() error   { return oserror.ErrNotExist }
func errClosed() error     { return oserror.ErrClosed }

在fs包中对每一种类型的错误封装了私有方法,并把返回值分别定义了相应的类型暴露出去。

代码来自:go/src/archive/zip/reader.go

// Open opens the named file in the ZIP archive,
// using the semantics of fs.FS.Open:
// paths are always slash separated, with no
// leading / or ../ elements.
func (r *Reader) Open(name string) (fs.File, error) {
   r.initFileList()

   if !fs.ValidPath(name) {
      return nil, &fs.PathError{Op: "open", Path: name, Err: fs.ErrInvalid}
   }
   e := r.openLookup(name)
   if e == nil {
      return nil, &fs.PathError{Op: "open", Path: name, Err: fs.ErrNotExist}
   }
   if e.isDir {
      return &openDir{e, r.openReadDir(name), 0}, nil
   }
   rc, err := e.file.Open()
   if err != nil {
      return nil, err
   }
   return rc.(fs.File), nil
}

当我们调用Open方法时,根据读取文件具体情况,返回相应的错误类型。在实际开发中,可以参考如上Go内置包对error类型的封装方式,也可以定义新的错误类型,实现更复杂的错误处理。比如我们在开发网关服务时,可以定义如下错误类型:

type GateWayError struct {
   Error error
   Time time.Duration
   Path string
}

Time表示发生error的时间,Path表示发生error的请求路径。

我们还可以调用fmt.Errorf方法返回格式化的错误。

fmt.Errorf("bad path, path = %vn", errors.New("www.baidu.com") )}

panic

Go语言在程序运行异常时会发生panic。如同其它语言中的throw Exception(),会直接导致程序终止。

上文讲述的error通常被认为是业务逻辑处理的一部分,panic更多的是不可预知的问题,如空指针引用、数组越界等。

我们看下panic的使用示例:

func Sum(a, b int32) int32 {
   fmt.Println(a + b)
   return a + b
}

func main() {
   Sum(12)
   panic("I am panic")
   Sum(23)
}

执行如上代码,控制台打印结果如下:

3
panic: I am panic

goroutine 1 [running]:
main.main()
        /Users/xingzhou/github/golang-teach/src/demo53/main.go:12 +0x38

Process finished with the exit code 2

代码执行到panic("I am panic")时,打印panic指定的字符串,然后打印堆栈信息,最后退出应用程序。

在stackoverflow上有这样一个问题“Should I use panic or return error?”。获得最多点赞的回答是:

You should assume that a panic will be immediately fatal, for the entire program, or at the very least for the current goroutine. Ask yourself “when this happens, should the application immediately crash?” If yes, use a panic; otherwise, use an error.

翻译过来就是:对于整个应用程序或者当前goroutine而言,如果发生某种情况时程序应该立刻崩溃则使用panic否则使用error。

为了捕获panic,Go语言还内置了recover()方法,recover捕获panic异常并阻止进程崩溃使运行恢复正常。在学习recover之前我们需要先学习一下defer。

defer

defer是Go语言的关键字,表示后面紧邻的语句延迟到函数退出之前执行。函数退出存在两种情况:一种是正常执行结束或return;另外一种是触发panic导致异常退出。

func Sum(a, b int32) int32 {
   fmt.Println(a + b)
   return a + b
}

func main() {
   defer func() {
      fmt.Println("defer 执行完毕")
   }()
   Sum(12)
   Sum(23)
}

执行上面的代码会看到fmt.Println("defer 执行完毕")语句,会在Sum执行完成之后执行,这正是defer的作用。

defer在一个方法中可以多次使用,多次使用时其执行顺序和定义的顺序刚好相反。

func Sum(a, b int32) int32 {
   fmt.Println(a + b)
   return a + b
}

func main() {
   defer func() {
      fmt.Println("defer1 执行完毕")
   }()
   defer func() {
      fmt.Println("defer2 执行完毕")
   }()
   defer func() {
      fmt.Println("defer3 执行完毕")
   }()
   Sum(12)
   Sum(23)
}

执行上面代码,输出结果如下:

3
5
defer3 执行完毕
defer2 执行完毕
defer1 执行完毕

在两个Sum函数执行完成之后,从后向前依次执行defer后的语句,优先执行最后定义的defer。

defer与返回值定义

我们构造以下示例:

func Test1() int {
   a := 1
   defer func() {
      a = 3
      fmt.Println(`defer a=`, a)
   }()
   a = 2
   return a
}

func Test2() (a int) {
   a = 1
   defer func() {
      a = 3
      fmt.Println(`defer a=`, a)
   }()
   a = 2
   return a
}

func Test3() (a int) {
   a = 1
   defer func() {
      a = 3
      fmt.Println(`defer a=:`, a)
   }()
   a = 2
   b := 4
   fmt.Println(`b=`, b)
   return b
}

func Test4() (a int) {
   a = 1
   defer func() {
      // a = 3
      fmt.Println(`defer a=:`, a)
   }()
   a = 2
   b := 4
   fmt.Println(`b=`, b)
   return b
}

func main() {
   fmt.Println(`Test1:`, Test1())
   fmt.Println()
   fmt.Println(`Test2:`, Test2())
   fmt.Println()
   fmt.Println(`Test3:`, Test3())
   fmt.Println()
   fmt.Println(`Test4:`, Test4())
}

Test1、Test2、Test3和Test4方法的主要区别是返回值的初始化方式不同,Test2、Test3和Test4都是命名返回值,Test1是匿名返回值。在使用匿名返回值时defer中的代码不会影响函数返回结果,命名返回值返回的数据会受到defer内代码逻辑的影响

我们执行上述代码,输出如下:

defer a= 3
Test1: 2

defer a= 3
Test2: 3

b= 4
defer a=: 3
Test3: 3

b= 4
defer a=: 4
Test4: 4

defer函数执行的时机是相同的,为什么Test1和Test2的结果不一致。因为匿名返回值在return之前会先对返回值进行一次值拷贝,defer语句执行时修改的值已经和return的不是同一个值,所以defer声明的延迟函数不影响返回结果;但是命名返回值函数返回值已经提前声明,defer语句修改的值和return的值是同一个,所以defer函数中的语句对返回结果产生了影响。

再比较Test3和Test4的返回结果,在方法Test3中return语句执行时,会拷贝b的值给a,然后又执行defer把a的值修改为3,所以Test3方法最终的返回值是3。Test4中把a=3注释了所以Test4的返回结果变成了4。

defer语句的值拷贝

func main() {
   fmt.Println(Sum(1, 1))
}

func Sum(a, b int) int {
   defer fmt.Println("a=", a)
   defer fmt.Println("b=", b)
   defer func() {
      fmt.Println("defer func a=", a)
   }()
   defer func() {
      fmt.Println("defer func b=", b)
   }()
   a++
   b++
   return a + b
}

上述示例的执行结果:

defer func b= 2
defer func a= 2
b= 1
a= 1
4

defer func b= 2defer func a= 2比较容易理解,但是为什么会输出a=1,b=1呢?因为defer语句入栈时,会对执行语句的参数做值拷贝。a和b在定义时已经拷贝入栈了,后面a和b修改已经不会影响到前两行defer语句中的a和b了。

recover

本来是讲异常,为什么突然介绍了defer呢?因为执行panic时程序会停止执行,就没有时机去处理异常,defer刚好提供了处理panic的时机。换句话说只有在defer声明的延迟函数内部调用recover()方法,才会真正生效。

看一个recover方法的使用示例:

func S() (s int) {
   defer func() {
      if err := recover(); err != nil {
         fmt.Printf("recover panic:%v n", err)
         s = 2
      }
   }()
   s = 1
   panic("I am panic")
   return s
}

func main() {
   s := S()
   fmt.Printf("s=%d", s)
}

我们在S方法中使用defer声明了延迟函数,在延迟函数中调用recover()方法,没有发生panic时,recover()方法返回nil,当发生panic时候,recover会返回panic信息。我们手动调用panic("I am panic")触发panic,所以执行结果会打印:

recover panic:I am panic
s=2

recover方法防止程序退出的同时也掩盖了panic的错误堆栈,我们可以通过调用debug.Stack()方法打印错误堆栈,方便定位问题。

总结

本文我们主要介绍了Go语言的错误处理和异常处理,还介绍了defer语句及其常见问题。如果大家对文章内容有任何疑问或建议,欢迎私信交流。

我把Go语言基础知识相关文章的Demo都放到了下面这个仓库中,方便大家互相交流学习。https://github.com/jialj/golang-teach


原文始发于微信公众号(一行舟):Go 错误和异常

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由半码博客整理,本文链接:https://www.bmabk.com/index.php/post/20287.html

(0)

相关推荐

发表回复

登录后才能评论
半码博客——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!