9.Go语言-函数

1.函数

  • Go语言中支持函数,匿名和闭包,并且函数在Go语言中属于“一等公民”

  • 特点:

    • 无需声明原型。
    • 支持不定 变参。
    • 支持多返回值。
    • 支持命名返回参数。 
    • 支持匿名函数和闭包。
    • 函数也是一种类型,一个函数可以赋值给变量。
    
    • 不支持 嵌套 (nested) 一个包不能有两个名字一样的函数。
    • 不支持 重载 (overload) 
    • 不支持 默认参数 (default parameter)。
    

1函数的定义

  • 函数声明包含一个函数名,参数列表, 返回值列表和函数体。如果函数没有返回值,则返回列表可以省略。函数从第一条语句开始执行,直到执行return语句或者执行函数的最后一条语句。函数可以没有参数或接受多个参数。(注意类型在变量名之后 )当两个或多个连续的函数命名参数是同一类型,则除了最后一个类型之外,其他都可以省略。函数可以返回任意数量的返回值。

  • Go语言中定义函数使用func 关键字,具体格式如下:

    func 函数名(参数)(返回值){
            函数体
    }
    
  • 其中:

    • 函数名:由字母,数字,下划线组成,但函数名的第一个字母不能是数字,在同一个包内,函数名称不能重名
    • 参数:参数由参数变量和参数变量的类型,多个参数之间使用,分隔
    • 返回值:返回值由返回值变量和其变量类型组成,也可以只写返回值的类型,多个返回值,必须用()包裹,并用分隔。
    • 函数体:实现指定功能的代码块。
  • 定义一个简单函数

    package main
    
    import "fmt"
    
    //函数
    //定义一个不需要参数也没有返回值的函数:sayHello
    
    func sayHello() {
            fmt.Println("hello xjk")
    }
    
    func main() {
            //函数调用
            sayHello()
    }
    
  • 函数作为参数传递,可以将复杂签名定义为函数类型,以便于阅读

    package main
    
    
    import "fmt"
    
    
    func test(fn func() int) int {
            return fn()
    }
    
    // 定义函数类型
    type FormatFunc func(s string,x,y int) string
    
    // format接收fn 的类型为为一个函数类型,通过type定义一个函数类型更直观
    func format(fn FormatFunc,s string, x,y int) string {
            return fn(s,x,y)
    }
    
    func main() {
            // test 内部为一个匿名函数
            s1 := test(func() int { return 100})//直接将匿名函数当参数
            // 第一个参数为匿名函数,定义接收类型,返回类型和函数逻辑。
            // 第二个参数 "%d,%d"  字符串
            // 第三个,第四个 参数为数字
            s2 := format(func(s string, x, y int) string {
                    return fmt.Sprintf(s, x, y)
    }, "%d, %d", 10, 20)
    
            println(s1,s2)
        // 100 10, 20
    }
    

2.函数参数

  • 函数定义时指出,函数定义时有参数,该变量可称为函数的形参。形参就像定义在函数体内的局部变量。

    但当调用函数,传递过来的变量就是函数的实参,函数可以通过两种方式来传递参数:

    • 值传递:指在调用函数时将实际参数复制一份传递到函数中,这样在函数中如果对参数进行修改,将不会影响到实际参数。

      func swap(x,y int) int {
              ... ...
      }
      
    • 引用传递:是指在调用函数时将实际参数的地址传递到函数中,那么在函数中对参数所进行的修改,将影响到实际参数。

      func swap(x,y *int){
              *x,*y = *y,*x
      }
      func main() {
              var a, b int = 1, 2
              swap(&a, &b)
              fmt.Println(a, b)
      }
      // 2 1
      
  • 在默认情况下,Go 语言使用的是值传递,即在调用过程中不会影响到实际参数。

  • 注意1:无论是值传递,还是引用传递,传递给函数的都是变量的副本,不过,值传递是值的拷贝。引用传递是地址的拷贝,一般来说,地址拷贝更为高效。而值拷贝取决于拷贝的对象大小,对象越大,则性能越低。

  • 注意2:map、slice、chan、指针、interface默认以引用的方式传递。

  • 不定参数传值 就是函数的参数不是固定的,后面的类型是固定的。(可变参数)

    Golang 可变参数本质上就是 slice。只能有一个,且必须是最后一个。

    在参数赋值时可以不用用一个一个的赋值,可以直接传递一个数组或者切片,特别注意的是在参数后加上“…”即可。

      func myfunc(args ...int) {    //0个或多个参数
      }
    
      func add(a int, args…int) int {    //1个或多个参数
      }
    
      func add(a int, b int, args…int) int {    //2个或多个参数
      }
    

    注意:其中args是一个slice,我们可以通过arg[index]依次访问所有参数,通过len(arg)来判断传递参数的个数。

    任意类型的不定参数: 就是函数的参数和每个参数的类型都不是固定的。

    用interface{}传递任意类型数据是Go语言的惯例用法,而且interface{}是类型安全的。

    func myfunc(args ...interface{}) {
      }
    
    • demo
    func test(s string, n...int) string {
            var x int
            for _,i := range n{
                    x += i
            }
            return fmt.Sprintf(s,x)
    }
    
    func main() {
            println(test("sum:%d",1,2,3))
    }
    // sum:6
    
  • 使用 slice 对象做变参时,必须展开。(slice...)

    func test(s string, n...int) string {
            var x int
            for _,i := range n{
                    x += i
            }
            return fmt.Sprintf(s,x)
    }
    
    func main () {
            s := []int{1,2,3}
            // s... 将slice展开
            res := test("sum:%d",s...)
            println(res)
    }
    

3.函数返回值

  • 可以用"_"标识符,用来忽略函数的某个返回值。Go 的返回值可以被命名,并且就像在函数体开头声明的变量那样使用。返回值的名称应当具有一定的意义,可以作为文档使用。没有参数的 return 语句返回各个返回变量的当前值。这种用法被称作“裸”返回。直接返回语句仅应当用在像下面这样的短函数中。在长的函数中它们会影响代码的可读性。

  • demo

    func add(a,b int) (c int) {
            c = a + b
            return
    }
    
    // 指定返回的sum和avg类型,所以return 后面可以不接变量
    func calc(a,b int) (sum int,avg int) {
            sum = a + b
            avg = (a + b) / 2
            return
    }
    
    func main() {
            var a,b int = 1,2
            c := add(a,b)
            sum,avg := calc(a,b)
            fmt.Println(a,b,c,sum,avg)
    }
    // 1 2 3 3 1
    
  • Golang返回值不能用容器对象接收多返回值。只能用多个变量,或 "_" 忽略。

    func test() (int, int) {
            return 1,2
    }
    func main() {
            x,_ := test()
            println(x)//1
    }
    
  • 多返回值可直接作为其他函数调用实参。

    func test()(int,int) {
            return 1,2
    }
    
    func add(a,b int) int {
            return a+b
    }
    
    func sum(n ...int) int{
            var x int
            for _,i := range n{
                    x += i
            }
            return x
    }
    
    func main() {
            fmt.Println(add(test()))//3
            fmt.Println(sum(test()))//3
    }
    
  • 命令返回参数可看作与形参类似的局部变量,最后由return隐式返回。

    package main
    func add(x,y int) (z int) {
        z = x + y
        return
    }
    func main() {
        println(add(1,2))//3
    }
    
  • 命名返回参数可被同名局部变量遮蔽,此时需要显示返回。

    func add(x,y int)(z int){
            // var z不能再一个级别重复定义,会引发"z redeclared in this block" 错误
            var z = x+y
            // 必须显式返回
            return z
    }
    
  • 命名返回参数允许defer延迟调用通过闭包读取和修改

    func add(a,b int) (c int) {
            defer func() {
                    c += 100
            }()
            c = a + b
            return
    }
    
    func main() {
        println(add(1, 2)) //103
    }
    
  • 显示return返回前,会先修改命名返回参数。

    func add(x, y int) (z int) {
            defer func() {
                            println("defer:",z) // 输出: 203
            }()
    
            z = x + y
            return z + 200 // 执行顺序: (z = z + 200) -> (call defer) -> (return)
    }
    func main() {
            println(add(1,2))
    }
    // defer: 203
    // 203
    

4.匿名函数

  • 匿名函数是指不需要定义函数名的一种函数实现方式。1958年LISP首先采用匿名函数。

    在Go里面,函数可以像普通变量一样被传递或使用,Go语言支持随时在代码里定义匿名函数。

    匿名函数由一个不带函数名的函数声明和函数体组成。匿名函数的优越性在于可以直接使用函数内的变量,不必申明。

    package main
    
    import (
            "fmt"
            "math"
    )
    
    
    func main() {
            getSqrt := func(a float64) float64 {
            // 开平方根
                    return math.Sqrt(a)
            }
            fmt.Println(getSqrt(4))//2
    }
    

    上面先定义了一个名为getSqrt 的变量,初始化该变量时和之前的变量初始化有些不同,使用了func,func是定义函数的,可是这个函数和上面说的函数最大不同就是没有函数名,也就是匿名函数。这里将一个函数当做一个变量一样的操作。

  • Golang匿名函数可赋值给变量,做为结构字段,或者在 channel 里传送。

    package main
    
    import (
            "fmt"
            // "math"
    )
    
    
    func main() {
            // 复制变量通过() 调用
            fn := func() { fmt.Println("Hello World")}
            fn()
            // 切片定义多个函数
            fn2 := [](func(x int) int){
                    func(x int) int {return x+1},
                    func(x int) int {return x+2},
            }
            // 通过索引调用函数
            println(fn2[0](100))
            // 结构体内定义函数 
            d := struct {
                    fn func() string
            }{
                    fn: func() string {return "hello world!"},
            }
            // 通过点语法调用
            println(d.fn())
            // 再channel里传送
            fc := make(chan func() string,2)
            fc <- func() string {return "Hello,Go!"}
            println((<-fc)())
    }
    // Hello World
    // 101
    // hello world!
    // Hello,Go
    

5.闭包与递归

  • 由函数及其相关引用环境组合而成的实体(闭包=函数+引用环境)

  • Go语言支持闭包的,这里只是简单地讲一下在Go语言中闭包是如何实现的

    package main
    import "fmt"
    
    
    func a() func() int{
            i:= 0
            b := func() int {
                    i++
                    fmt.Println(i)
                    return i
            }
            return b
    }
    
    
    func main() {
            c := a()
            c()//1
            c()//2
            // c = a() 调用a() 不会输出 i 的值
    }
    
  • 因闭包复制时原对象指针,这样很容易解释延迟引用现象。

    func test() func() {
            x := 100
            fmt.Printf("x (%p) = %d\n", &x, x)
            return func() {
                    fmt.Printf("inner x (%p) = %d\n", &x, x)
            }
    }
    func main() {
            f := test()
            f()
            // x (0x1100a090) = 100
            // inner x (0x1100a090) = 100
    }
    

    在汇编层 ,test 实际返回的是 FuncVal 对象,其中包含了匿名函数地址、闭包对象指针。当调 匿名函数时,只需以某个寄存器传递该对象即可。

    FuncVal { func_address, closure_var_pointer ... }
    
  • 外部引用函数参数局部变量。

    package main
    
    import "fmt"
    
    // 外部引用函数参数局部变量
    func add(base int) func(int) int {
        return func(i int) int {
            base += i
            return base
        }
    }
    
    func main() {
        tmp1 := add(10)
        fmt.Println(tmp1(1), tmp1(2))
        // 此时tmp1和tmp2不是一个实体了
        tmp2 := add(100)
        fmt.Println(tmp2(1), tmp2(2))
    }
    
  • 返回2个闭包

    func test(base int) (func(int) int, func(int) int ){
            add := func(i int) int {
                    base += i
                    return base
            }
            sub := func(i int) int {
                    base -= i
                    return base
            }
            return add,sub
    }
    
    
    
    func main() {
            f1,f2 := test(1)
            // base一直是没有消
        // f1执行完base为11,f2执行会用base=11在函数进行运算,所以为-4
            fmt.Println(f1(10),f2(15))// 11 -4
    }
    
  • 递归函数

    • 递归,就是在运行的过程中调用自己。 一个函数调用自己,就叫做递归函数。

    • 条件:

      1.子问题须与原始问题为同样的事,且更为简单。
      2.不能无限制地调用本身,须有个出口,化简为非递归状况处理。
      
    • 数字阶乘

      • 一个正整数的阶乘(factorial)是所有小于及等于该数的正整数的积,并且0的阶乘为1。自然数n的阶乘写作n!。
      package main
      
      import "fmt"
      
      
      func factorial(i int) int {
              if i<=1{
                      return 1
              }
              return i * factorial(i-1)
      }
      
      func main() {
              var i int = 10
              fmt.Printf("Factorial of %d is %d\n",i,factorial(i))// Factorial of 10 is 3628800
      }
      
    • 斐波那契数列

      func fibonaci(i int) int {
              if (i ==0) {
                      return 0
              }
              if (i == 1) {
                      return 1
              }
              return fibonaci(i-1) + fibonaci(i-2)
      }
      
      func main() {
              var i int
          for i = 0; i < 10; i++ {
              fmt.Printf("%d\n", fibonaci(i))
          }
      }
      // 0
      // 1
      // 1
      // 2
      // 3
      // 5
      // 8
      // 13
      // 21
      // 34
      

6.延迟调用 defer

  • defer特性:

    1.关键字 defer 用于注册延迟调用
    2.这些调用直到return 前才被执行,因此可以用来做资源清理。
    3.多个defer语句,按先进后出的方式执行。
    4.defer语句中的变量,在defer声明时就决定了。
    
  • defer用途:

    1.关闭文件句柄
    2.锁资源释放
    3.数据库连接释放
    
  • Go语言中defer语句会将其后面跟随的语句进行延迟处理,在defer归属的函数即将返回时,将延迟处理的语句按defer定义的逆顺序进行执行,也就是说,先被defer的语句最后被执行,最后被defer的语句最先被执行。

    package main
    
    
    import "fmt"
    
    
    func main() {
            var w [5] struct{}
            for i := range w{
                    defer fmt.Println(i)
            }
    }
    // 4
    // 3
    // 2
    // 1
    // 0
    
  • defer碰上闭包

    package main
    
    
    import "fmt"
    
    
    func main() {
            var w [5] struct{}
            for i := range w{
                    defer func() {fmt.Println(i)}()
            }
    }
    // 4
    // 4
    // 4
    // 4
    // 4
    

    函数正常执行,由于闭包用到的变量 i 在执行的时候已经变成4,所以输出全都是4.

  • defer f.Close 存在陷阱

    type Test struct {
            name string
    }
    
    func (t *Test) Close() {
            fmt.Println(t.name," closed")
    }
    
    
    func main() {
            ts := []Test{{"a"},{"b"},{"c"}}
            for _,t := range ts {
                    defer t.Close()
            }
    }
    // c  closed
    // c  closed
    // c  closed
    

    上面代码不如我们预期输出的c b a 而是c c c

  • 可是按照前面go sepc中说明应该c b a 才对,那么换一种方式调用

    type Test struct {
            name string
    }
    
    func (t *Test) Close() {
            fmt.Println(t.name," closed")
    }
    func Close(t Test){
            t.Close()
    }
    
    func main() {
            ts := []Test{{"a"},{"b"},{"c"}}
            for _,t := range ts {
                    defer Close(t)
            }
    }
    // c  closed
    // b  closed
    // a  closed
    

    Go语言中并没有把这个struct的this指针做相应处理,没有明确写出来的this指针当作参数看待。而通过定义Close(t Test) 函数讲当前循环struct传入禁区,然后Test结构体内的方法。

  • 多个defer注册,按FILO次序(先进后出),最后哪怕函数或某个延迟调用发生错误,也会调用依旧被执行。

    func test(x int) {
            defer println("a")
            defer println("b")
            defer func() {
                    println(100/x)// 这里传入0,会报错,异常未被捕获,逐步往外传递,最终终止进程。
            }()
            defer println("c")
    }
    
    func main() {
            test(0)
    }
    // c
    // b
    // a
    // panic: runtime error: integer divide by zero
    
  • 延迟调用参数在注册时求值或复制,可用指针或闭包“延迟”读取。

    package main
    
    
    import "fmt"
    
    
    func test() {
            x,y := 10,20
            defer func(i int) {
                    fmt.Println("defer:",i,y)// y在内层函数,引用外层变量
            }(x) //此时x为值拷贝
    
            x+=10
            y+=100
            fmt.Println("x=",x,"y=",y)
    }
    
    func main() {
            test()
    }
    // x= 20 y= 120
    // defer: 10 120
    
  • 滥用defer可能会导致性能问题,尤其是在大循环里:

    package main
    
    import (
            "fmt"
            "sync"
            "time"
    )
    
    // 声明  sync.Mutex互斥锁
    var lock sync.Mutex
    
    
    func test() {
        // 加锁
            lock.Lock()
        // 解锁
            lock.Unlock()
    }
    
    func testdefer() {
            lock.Lock()
            defer lock.Unlock()
    }
    
    
    func main() {
            func() {
                    t1 := time.Now()
                    for i:=0;i<100000;i++ {
                            test()
                    }
                    // 计算程序运行时间
                    elapsed := time.Since(t1)
                    fmt.Println("test elapsed: ",elapsed)
            }()
            func() {
                    t1 := time.Now()
                    for i:=0;i<100000;i++ {
                            testdefer()
                    }
                    // 计算程序运行时间
                    elapsed := time.Since(t1)
                    fmt.Println("testdefer elapsed: ",elapsed)
            }()
    }
    // test elapsed:  1.9929ms
    // testdefer elapsed:  6.0194ms
    
  • defer 的陷阱

    • defer 与 closure(闭包),如果 defer 后面跟的不是一个 closure 最后执行的时候我们得到的并不是最新的值。
    package main
    
    import (
        "errors"
        "fmt"
    )
    
    func foo(a, b int) (i int, err error) {
        defer fmt.Printf("first defer err %v\n", err)
        //第二个defer值拷贝。
        defer func(err error) { fmt.Printf("second defer err %v\n", err) }(err)
        defer func() { fmt.Printf("third defer err %v\n", err) }()
        if b == 0 {
            err = errors.New("divided by zero!")
            return
        }
    
        i = a / b
        return
    }
    
    func main() {
        foo(2, 0)
    }
    // third defer err divided by zero!
    // second defer err <nil>
    // first defer err <nil>
    
  • defer和return

    func foo() (i int) {
            i = 0
            defer func() {
                    fmt.Println(i)
            }()
            return 2
    }
    
    func main() {
        foo()//2
    }
    

    在有具名返回值的函数中(这里具名返回值为i),执行return 2的时候实际上已经将i的值重新赋值为2,所以 defer closure输出结构为2,而不是1

  • defer nil 函数

    package main
    import "fmt"
    
    //Recover 是一个Go语言的内建函数,可以让进入宕机流程中的 goroutine 恢复过来,recover 仅在延迟函数 defer 中有效,在正常的执行过程中,调用 recover 会返回 nil 并且没有其他任何效果,如果当前的 goroutine 陷入恐慌,调用 recover 可以捕获到 panic 的输入值,并且恢复正常的执行。
    func test() {
        // 函数指向空地址
        var run func() = nil
        defer fun()
        fmt.Println("runs...")
    }
    func main() {
        defer func() {
            // recover捕捉panic输入值
            if err:= recover()l err != nil {
                fmt.Println(err)
            }
        }()
        test()
    }
    // runs
    // runtime error: invalid memory address or nil pointer dereference
    

    解释:名为 test 的函数一直运行至结束,然后 defer 函数会被执行且会因为值为 nil 而产生 panic 异常。然而值得注意的是,run() 的声明是没有问题,因为在test函数运行完成后它才会被调用。

  • 错误的位置使用defer

    package main
    
    import (
            "net/http"
            "fmt"
    )
    
    
    func do() error{
            //当 http.Get 失败时会抛出异常。
            res,err := http.Get("http://www.google.com")
            defer res.Body.Close()
            if err != nil {
                    return err
            }
            return nil
    }
    
    func main() {
            do()
    }
    

    因为在这里我们并没有检查我们的请求是否成功执行,当它失败的时候,我们访问了 Body 中的空变量 res ,因此会抛出异常

  • 改进:

    • 总是在一次成功的资源分配下面使用 defer ,对于这种情况来说意味着:当且仅当 http.Get 成功执行时才使用 defer
    package main
    
    import (
            "net/http"
            "fmt"
    )
    
    
    func do() error{
            //当 http.Get 失败时会抛出异常。
            res,err := http.Get("http://www.baidudddsfw45fgsdfsfds.com")
            // <nil> Get http://www.baidudddsfw45fgsdfsfds.com: dial tcp: lookup www.baidudddsfw45fgsdfsfds.com: no such host
            fmt.Println(res,err)
            if res != nil {
                    defer res.Body.Close()
            }
            if err != nil {
                    return err
            }
            return nil
    }
    
    func main() {
            do()
    }
    

    解释:在这里,你同样需要检查 res 的值是否为 nil ,这是 http.Get 中的一个警告。通常情况下,出错的时候,返回的内容应为空并且错误会被返回,可当你获得的是一个重定向 error 时, res 的值并不会为 nil ,但其又会将错误返回。上面的代码保证了无论如何 Body 都会被关闭,如果你没有打算使用其中的数据,那么你还需要丢弃已经接收的数据。

  • defer 文件操作:

    package main
    
    import "os"
    
    
    func do() (err error) {
            f,err := os.Open("book.txt")
            println(f,err)
            if err != nil {
                    println(err)
                    return err
            }
    
            if f != nil {
                    println("f-->",f)
                    defer func() {
                            if ferr:= f.Close();ferr != nil {
                                    err = ferr
                            }
                    }()
            }
    
            // code...
            return nil
    }
    
    func main() {
            do()
    }
    

    f.Close() 可能会返回一个错误,可这个错误会被我们忽略掉,通过命名的返回变量来返回 defer 内的错误。

  • 释放相同的资源

    package main
    
    import (
        "fmt"
        "io"
        "os"
    )
    
    func do() error {
        f, err := os.Open("book.txt")
        if err != nil {
            return err
        }
        if f != nil {
            defer func(f io.Closer) {
                if err := f.Close(); err != nil {
                    fmt.Printf("defer close book.txt err %v\n", err)
                }
            }(f)
        }
    
        // ..code...
    
        f, err = os.Open("another-book.txt")
        if err != nil {
            return err
        }
        if f != nil {
            defer func(f io.Closer) {
                if err := f.Close(); err != nil {
                    fmt.Printf("defer close another-book.txt err %v\n", err)
                }
            }(f)
        }
    
        return nil
    }
    
    func main() {
        do()
    }
    

    如果你尝试使用相同的变量释放不同的资源,那么这个操作可能无法正常执行。当延迟函数执行时,只有最后一个变量会被用到,因此,f 变量 会成为最后那个资源 (another-book.txt)。而且两个 defer 都会将这个资源作为最后的资源来关闭

7.异常处理

  • Golang没有结构化异常,使用panic抛出错误,recover捕获错误。异常的使用场景简单描述:Go中可以抛出一个panic的异常,然后再defer通过recover捕获异常,然后正常处理。

  • panic

    1、内置函数
    2、假如函数F中书写了panic语句,会终止其后要执行的代码,在panic所在函数F内如果存在要执行的defer函数列表,按照defer的逆序执行
    3、返回函数F的调用者G,在G中,调用函数F语句之后的代码不会执行,假如函数G中存在要执行的defer函数列表,按照defer的逆序执行
    4、直到goroutine整个退出,并报告错误
    
  • recover:

    1.内置函数
    2、用来控制一个goroutine的panicking行为,捕获panic,从而影响应用的行为
    3、一般的调用建议
        a). 在defer函数中,通过recever来终止一个goroutine的panicking过程,从而恢复正常代码的执行
        b). 可以获取通过panic传递的error
    
    
  • 注意

    1.利用recover处理panic指令,defer 必须放在 panic 之前定义,另外 recover 只有在 defer 调用的函数中才有效。否则当panic时,recover无法捕获到panic,无法防止panic扩散。
    2.recover 处理异常后,逻辑并不会恢复到 panic 那个点去,函数跑到 defer 之后的那个点。
    3.多个 defer 会形成 defer 栈,后定义的 defer 语句会被最先调用。
    
  • demo

    package main
    
    func main() {
            test()
    }
    
    func test() {
            defer func() {
                    if err:=recover();err != nil {
                            // fmt.Printf("%T\n",err)
                            println(err.(string))// 将 interface{} 转型为具体类型。
                    }
            }()
    
            panic("panic error!")
    }
    // panic error!
    
  • panic,recover 参数类型为interface{} ,因此可抛出任何类型对象。

    func panic(v interface{})
    func recover() interface{}
    
  • 向已经关闭的通道发送数据会引发panic

    func main() {
            defer func() {
                    // recover捕获异常
                    if err := recover(); err != nil {
                            // 打印异常
                            fmt.Println(err)//send on closed channel
                    }
            }()
            var ch chan int = make(chan int,10)
            // 关闭channel
            close(ch)
            // 向channel 传值
            ch <- 1
    }
    
  • 延迟调用中引发的错误,可被后续延迟调用捕获,但仅最后一个错误可被捕获。

    func test() {
        // 捕获最后一个错误。
            defer func() {
                    fmt.Println(recover())//defer panic
            }()
            defer func() {
                    panic("defer panic")
            }()
            panic("test panic")
    }
    
  • 捕获函数 recover 只有在延迟调用内直接调用才会终止错误,否则总是返回 nil。任何未捕获的错误都会沿调用堆栈向外传递。

    func test() {
            defer func() {
                    fmt.Println("-->",recover()) //无效
            }()
            defer recover()              //无效!
            defer fmt.Println(recover()) //无效!
            defer func() {
                            func() {
                                            println("defer inner")
                                            recover() //无效!
                            }()
            }()
            defer func() {
                    fmt.Println("---->",recover()) //有效
            }()
            panic("test panic")
    }
    // ----> test panic
    // defer inner
    // <nil>
    // --> <nil>
    
  • 使用延迟匿名函数或下面这样都是有效的。

    package main
    
    import (
        "fmt"
    )
    
    func except() {
        fmt.Println(recover())//test panic
    }
    
    func test() {
        defer except()
        panic("test panic")
    }
    
    func main() {
        test()
    }
    
  • 如果需要保护代码 段,可将代码块重构成匿名函数,如此可确保后续代码被执

    package main
    
    import "fmt"
    
    
    func test(x,y int) {
            var z int
            func() {
            // 捕获异常
                    defer func() {
                            if recover() != nil{
                                    z = 0
                            }
                    }()
            // 触发异常
                    panic("test panic")
                    z = x/y
                    return
            }()
            fmt.Printf("x / y = %d\n", z)//x / y = 0
    }
    
    func main() {
            test(1,2)
    }
    
  • 除用 panic 引发中断性错误外,还可返回 error 类型错误对象来表示函数调用状态。

    type error interface {
        Error() string
    }
    
  • 标准库 errors.New 和 fmt.Errorf 函数用于创建实现 error 接口的错误对象。通过判断错误对象实例来确定具体错误类型。

    package main
    
    import (
            "fmt"
            "errors"
    )
    // 定义异常对象
    var ErrDivZero = errors.New("division by zero")
    
    
    func div(x,y int) (int, error) {
            if y == 0{
                    return 0,ErrDivZero
            }
            return x/y,nil
    }
    
    func main() {
            defer func(){
                    fmt.Println(recover())//division by zero
            }()
            switch z,err := div(10,0);err {
            case nil:
                    println(z)
            case ErrDivZero:
                    panic(err)
            }
    }
    
    
  • Go实现try catch的异常

    package main
    
    
    import "fmt"
    
    func Try(fun func(),handler func(interface{})) {
            defer func() {
                    // 执行defer,通过recover捕获异常赋值给err.
                    // 执行第二个参数的函数,将err传递。
                    if err:= recover();err!=nil{
                            handler(err)
                    }
            }()
            // 执行第一个参数的函数,触发异常
            fun()
    }
    
    func main() {
            // 传入匿名函数:
            // 1.函数定义触发错误
            // 2.函数用于打印异常
            Try(func() {
                    panic("test panic")
            },func(err interface{}){
                    fmt.Println(err)// test panic
            })
    }
    
    

    如何区别使用 panic 和 error 两种方式?

    惯例是:导致关键流程出现不可修复性错误的使用 panic,其他使用 error。