跟着我们学Golang之异常处理

9次阅读

共计 9924 个字符,预计需要花费 25 分钟才能阅读完成。

Java 中的异常分为 Error 和 Exception 来处理,这里也以错误和异常两种,来分别讲一讲 Go 的异常处理。

Go 语言没有类似 Java 或 .NET 中的异常处理机制,虽然可以使用 defer、panic、recover 模拟,但官方并不主张这样做。Go 语言的设计者认为其他语言的异常机制已被过度使用,上层逻辑需要为函数发生的异常付出太多的资源。同时,如果函数使用者觉得错误处理很麻烦而忽略错误,那么程序将在不可预知的时刻崩溃。
Go 语言希望开发者将错误处理视为正常开发必须实现的环节,正确地处理每一个可能发生错误的函数。同时,Go 语言使用返回值返回错误的机制,也能大幅降低编译器、运行时处理错误的复杂度,让开发者真正地掌握错误的处理。
— 摘自:C 语言中文网

error 接口

Go 处理错误的思想

通过返回 error 接口的方式来处理函数的错误,在调用之后进行错误的检查。如果调用该函数出现错误,就返回 error 接口的实现,指出错误的具体内容,如果成功,则返回 nil 作为 error 接口的实现。

error 接口声明了一个Error() string 的函数,实际使用时使用相应的接口实现,由函数返回 error 信息,函数的调用之后进行错误的判断从而进行处理。

// 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
}

Error() 方法返回错误的具体描述,使用者可以通过这个字符串知道发生了什么错误。下面看一个例子。

package main

import (
    "errors"
    "fmt"
)

func main() {sources := []string{"hello", "world", "souyunku", "gostack"}
    fmt.Println(getN(0, sources))// 直接调用,会打印两项内容,字符串元素以及 error 空对象
    fmt.Println(getN(1, sources))
    fmt.Println(getN(2, sources))
    fmt.Println(getN(3, sources))
    
    target, err := getN(4, sources)// 将返回结果赋值
    if err != nil {// 常见的错误处理,如果 error 不为 nil,则进行错误处理
        fmt.Println(err)
        return
    }

    fmt.Println(target)
}

// 定义函数获取第 N 个元素,正常返回元素以及为 nil 的 error,异常返回空元素以及 error
func getN(n int, sources []string) (string, error) {if n > len(sources)-1 {return "", fmt.Errorf("%d, out of index range %d", n, len(sources) - 1)
    }
    return sources[n], nil
}

/*
打印内容:hello <nil>
world <nil>
souyunku <nil>
gostack <nil>
 4, out of index range 3
*/

常见的错误处理就是在函数调用结束之后进行 error 的判断,确定是否出现错误,如果出现错误则进行相应的错误处理;没有错误就继续执行下面的逻辑。

遇到多个函数都带有 error 返回的时候,都需要进行 error 的判断,着实会让人感到非常的苦恼,但是它的作用是很好的,其鲁棒性也要比其他静态语言要好的多。

自定义 error

身为一个接口,任何定义实现了 Error() string 函数,都可以认为是 error 接口的实现。所以可以自己定义具体的接口实现来满足业务的需求。

error 接口的实现有很多,各大项目也都喜欢自己实现 error 接口供自己使用。最常用的是官方的 error 包下的 errorString 实现。

// Copyright 2011 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

// Package errors implements functions to manipulate errors.
package errors

// New returns an error that formats as the given text.
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}

可以看到,官方 error 包通过定义了 errorString 来实现了 error 接口,在使用的时候通过 New(text string) error 这个函数进行调用从而返回 error 接口内容(该函数在返回的时候是一个 errorString 类型的指针,但是定义的返回内容是 error 接口类型,这也举例说明了上节讲到的接口的内容)下面看例子。

package main

import (
    "errors"
    "fmt"
)

func main() {
    // 直接使用 errors.New 来定义错误消息
    notFound := errors.New("404 not found")
    fmt.Println(notFound)

    // 也可以使用 fmt 包中包装的 Errorf 来添加
    fmt.Println(fmt.Errorf("404: page %v is not found","index.html"))
}

/*
打印内容
404 not found
404: page index.html is not found
*/

自己试着实现一个 404notfound 的异常

type NOTFoundError struct {name string}

func (e *NOTFoundError) Error() string {return fmt.Sprintf("%s  is not found, please new again", e.name)
}

func NewNotFoundError(name string) error{return &NOTFoundError{name}
}

func runDIYError() {err := NewNotFoundError("your girl")

    // 根据 switch,确定是哪种 error
    switch err.(type) {
    case *NOTFoundError:
        fmt.Printf("error : %v \n",err)
    default: // 其他类型的错误
        fmt.Println("other error")
    }
}

/** 调用 runDIYError()结果
error : your girl  is not found, please new again 
*/

自己定义异常 NotFoundError 只是简单的实现 Error() string 函数,并在出错的时候提示内容找不到,不支持太多的功能,如果业务需要,还是可以继续扩展。

defer

在将 panic 和 recover 之前插播一下 defer 这个关键字,这个关键字在 panic 和 recover 中也会用到。

defer 的作用就是指定某个函数在执行 return 之前在执行,而不是立即执行。下面是 defer 的语法

defer func(){}()

defer 指定要执行的函数,或者直接声明一个匿名的函数并直接执行。这个还是结合实例进行了解比较合适。


func runDefer(){defer func() {fmt.Println("3")
    }()// 括号表示定义 function 之后直接执行

    fmt.Println("1")

    defer func(index string) {fmt.Println(index)
    }("2")// 括号表示定义 function 之后直接执行,如果定义的 function 包含参数,括号中也要进行相应的赋值操作
}

/**
执行结果:1
2
3
*/

执行该函数能看到顺序打印出了 123 三个数字,这就是 defer 的执行过程。其特点就是 LIFO,先进后出,先指定的函数总是在后面执行,是一个逆序的执行过程。

defer 在 Go 中也是经常被用到的,而且设计的极其巧妙,举个例子

file.Open()
defer file.Close()// 该语句紧跟着 file.Open()被指定

file.Lock()
defer file.Unclock()// 该语句紧跟着 file.Lock()被指定

像这样需要开关或者其他操作必须执行的操作都可以在相邻的行进行执行指定,可以说很好的解决了那些忘记执行 Close 操作的痛苦。

defer 面试题

 
package main
 
import ("fmt")
 
func main() {defer_call()
}
 
func defer_call() {defer func() {fmt.Println("打印前") }()
    defer func() { fmt.Println("打印中") }()
    defer func() { fmt.Println("打印后") }()
 
    panic("触发异常")
}

考点:defer 执行顺序
解答:defer 是后进先出。panic 需要等 defer 结束后才会向上传递。出现 panic 恐慌时候,会先按照 defer 的后入先出的顺序执行,最后才会执行 panic。结果:打印后
打印中
打印前
panic: 触发异常
 --- 
// 摘自:https://blog.csdn.net/weiyuefei/article/details/77963810
func calc(index string, a, b int) int {
    ret := a + b
    fmt.Println(index, a, b, ret)
    return ret
}
 
func main() {
    a := 1
    b := 2
    defer calc("1", a, calc("10", a, b))
    a = 0
    defer calc("2", a, calc("20", a, b))
    b = 1
}

考点:defer 执行顺序
解答:这道题类似第 1 题 需要注意到 defer 执行顺序和值传递 index:1 肯定是最后执行的,但是 index:1 的第三个参数是一个函数,所以最先被调用 calc("10",1,2)==>10,1,2,3 执行 index:2 时, 与之前一样,需要先调用 calc("20",0,2)==>20,0,2,2 执行到 b = 1 时候开始调用,index:2==>calc("2",0,2)==>2,0,2,2 最后执行 index:1==>calc("1",1,3)==>1,1,3,4

结果:10 1 2 3
20 0 2 2
2 0 2 2
1 1 3 4

---
摘自:https://blog.csdn.net/weiyuefei/article/details/77963810

defer 虽然是基础知识,其调用过程也非常好理解,但是往往在面试的过程中会出现一些比较绕的题目,这时候不要惊慌,只需要好好思考其执行的过程还是可以解出来的。

panic & recover

panic 英文直译是 恐慌 ,在 Go 中意为程序出现了崩溃。recover 直译是 恢复,其目的就是恢复恐慌。

在其他语言里,宕机往往以异常的形式存在。底层抛出异常,上层逻辑通过 try/catch 机制捕获异常,没有被捕获的严重异常会导致宕机,捕获的异常可以被忽略,让代码继续运行。
Go 没有异常系统,其使用 panic 触发宕机类似于其他语言的抛出异常,那么 recover 的宕机恢复机制就对应 try/catch 机制。– 摘自:C 语言中文网

panic

程序崩溃就像遇到电脑蓝屏时一样,大家都不希望遇到这样的情况。但有时程序崩溃也能终止一些不可控的情况,以此来做出防范。出于学习的目的,咱们简单了解一下 panic 造成的崩溃,以及如何处理。先看一下 panic 的定义。

// The panic built-in function stops normal execution of the current
// goroutine. When a function F calls panic, normal execution of F stops
// immediately. Any functions whose execution was deferred by F are run in
// the usual way, and then F returns to its caller. To the caller G, the
// invocation of F then behaves like a call to panic, terminating G's
// execution and running any deferred functions. This continues until all
// functions in the executing goroutine have stopped, in reverse order. At
// that point, the program is terminated and the error condition is reported,
// including the value of the argument to panic. This termination sequence
// is called panicking and can be controlled by the built-in function
// recover.
func panic(v interface{})

从定义中可以了解到,panic 可以接收任何类型的数据。而接收的数据可以通过 recover 进行获取,这个后面 recover 中进行讲解。

从分类上来说,panic 的触发可以分为两类,主动触发和被动触发。

在程序运行期间,主动执行 panic 可以提前中止程序继续向下执行,避免造成更恶劣的影响。同时还能根据打印的信息进行问题的定位。

func runSimplePanic(){defer func() {fmt.Println("before panic")
    }()
    panic("simple panic")
}

/**
调用 runSimplePanic()函数结果:before panic
panic: simple panic

goroutine 1 [running]:
main.runSimplePanic()
    /Users/fyy/go/src/github.com/souyunkutech/gosample/chapter6/main.go:102 +0x55
main.main()
    /Users/fyy/go/src/github.com/souyunkutech/gosample/chapter6/main.go:18 +0x22
*/

从运行结果中能看到,panic 执行后先执行了 defer 中定义的函数,再打印的 panic 的信息,同时还给出了执行 panic 的具体行(行数需要针对具体代码进行定论),可以方便的进行检查造成 panic 的原因。

还有在程序中不可估计的 panic,这个可以称之为被动的 panic,往往由于空指针和数组下标越界等问题造成。

func runBePanic(){fmt.Println(ss[100])//ss 集合中没有下标为 100 的值,会造成 panic 异常。}

/**
调用 runBePanic()函数结果:panic: runtime error: index out of range

goroutine 1 [running]:
main.runBePanic(...)
    /Users/fyy/go/src/github.com/souyunkutech/gosample/chapter6/main.go:106
main.main()
    /Users/fyy/go/src/github.com/souyunkutech/gosample/chapter6/main.go:21 +0x10f
*/

从运行结果中看到,数组下标越界,直接导致 panic,panic 信息也是有 Go 系统运行时 runtime 所提供的信息。

recover

先来简单看一下 recover 的注释。

// The recover built-in function allows a program to manage behavior of a
// panicking goroutine. Executing a call to recover inside a deferred
// function (but not any function called by it) stops the panicking sequence
// by restoring normal execution and retrieves the error value passed to the
// call of panic. If recover is called outside the deferred function it will
// not stop a panicking sequence. In this case, or when the goroutine is not
// panicking, or if the argument supplied to panic was nil, recover returns
// nil. Thus the return value from recover reports whether the goroutine is
// panicking.
func recover() interface{}

注释指明 recover 可以管理 panic,通过 defer 定义在 panic 之前的函数中的 recover,可以正确的捕获 panic 造成的异常。

结合 panic 来看一下 recover 捕获异常,并继续程序处理的简单实现。


import "fmt"

func main() {runError()

    fmt.Println("---------------------------")
    runPanicError()}

type Student struct {
    Chinese int
    Math    int
    English int
}

var ss = []Student{{100, 90, 89},
    {80, 80, 80},
    {70, 80, 80},
    {70, 80, 60},
    {90, 80, 59},
    {90, 40, 59},
    {190, 40, 59},
    {80, 75, 66},
}

func runError() {

    i := 0

    for ; i < len(ss); i++ {flag, err := checkStudent(&ss[i])
        if err != nil {fmt.Println(err)
            return
        }// 遇到异常数据就会立即返回,不能处理剩余的数据
        // 而且,正常逻辑中参杂异常处理,使得程序并不是那么优雅

        fmt.Printf("student %#v, 及格?:%t \n", ss[i], flag)
    }

}

func checkStudent(s *Student) (bool, error) {
    if s.Chinese > 100 || s.Math > 100 || s.English > 100 {return false, fmt.Errorf("student %#v, something error", s)
    }

    if s.Chinese > 60 && s.Math > 60 && s.English > 60 {return true, nil}

    return false, nil
}

func runPanicError() {
    i := 0
    defer func() {if err := recover(); err != nil {fmt.Println(err)
        }
        i ++// 跳过异常的数据,继续处理剩余的数据
        for ; i < len(ss); i ++ {fmt.Printf("student %#v, 及格?:%t \n", ss[i], checkStudentS(&ss[i]))
        }
    }()

    for ; i < len(ss); i++ {fmt.Printf("student %#v, 及格?:%t \n", ss[i], checkStudentS(&ss[i]))
    }

}

func checkStudentS(s *Student) bool {
    if s.Chinese > 100 || s.Math > 100 || s.English > 100 {panic(fmt.Errorf("student %#v, something error", s))
    }

    if s.Chinese > 60 && s.Math > 60 && s.English > 60 {return true}

    return false
}
结果:student main.Student{Chinese:100, Math:90, English:89}, 及格?:true 
student main.Student{Chinese:80, Math:80, English:80}, 及格?:true 
student main.Student{Chinese:70, Math:80, English:80}, 及格?:true 
student main.Student{Chinese:70, Math:80, English:60}, 及格?:false 
student main.Student{Chinese:90, Math:80, English:59}, 及格?:false 
student main.Student{Chinese:90, Math:40, English:59}, 及格?:false 
student &main.Student{Chinese:190, Math:40, English:59}, something error
---------------------------
student main.Student{Chinese:100, Math:90, English:89}, 及格?:true 
student main.Student{Chinese:80, Math:80, English:80}, 及格?:true 
student main.Student{Chinese:70, Math:80, English:80}, 及格?:true 
student main.Student{Chinese:70, Math:80, English:60}, 及格?:false 
student main.Student{Chinese:90, Math:80, English:59}, 及格?:false 
student main.Student{Chinese:90, Math:40, English:59}, 及格?:false 
student &main.Student{Chinese:190, Math:40, English:59}, something error
student main.Student{Chinese:80, Math:75, English:66}, 及格?:true 

从结果中可以看出 runPanicError 函数将全部正常的数据都输出了,并给出了是否及格的判断,runError 并没有全部将数据输出,而是遇到错误就中止了后续的执行,导致了执行的不够彻底。

panic 和 recover 的用法虽然简单,但是一般程序中用到的却很少,除非你对 panic 有着很深的了解。但也可以通过 Panic 来很好的美化自己的代码,从程序上看,runPanicError 中的异常处理与正常逻辑区分开,也使得程序看起来非常的舒畅 -_-!

相对于那些对 panic 和 recover 掌握非常好的人来说,panic 和 recover 能随便用,真的可以御剑飞行那种;但是如果掌握不好的话,还是尽可能的使用相对简单但不失高效又能很好的解决问题的 error 来处理就好了,以此来避免过度的使用从而造成的意外影响。毕竟我们的经验甚少,复杂的事物还是交给真正的大佬比较合适。

总结

Go 中的异常处理相对比 Java 这些有着相对完善的错误处理机制的语言来说,还是显得非常的低级的,这也是 Go 一直被大家诟病的一点,但 Go 的更新计划中也有针对异常处理的改善,相信用不了多久就能看到不一样的错误处理机制。

源码可以通过 ’github.com/souyunkutech/gosample’ 获取。

关注我们的「微信公众号」


首发微信公众号:Go 技术栈,ID:GoStack

版权归作者所有,任何形式转载请联系作者。

作者:搜云库技术团队

出处:https://gostack.souyunku.com/…

正文完
 0