Go 语言函数

62 阅读24分钟

GO 语言函数

函数是基本的代码块,用于执行一个任务。
你可以通过函数划分不同的功能,逻辑上每个函数执行的是指定的任务。
Go语言最少有个main函数
Golang函数的特点:
支持:

  • 无需声明原型
  • 支持不定 参数
  • 支持多返回值
  • 支持命名返回参数
  • 支持匿名函数和闭包
  • 函数也是一种类型,一个函数可以赋值给变量

不支持:

  • 不支持 嵌套 一个包不能有两个名字一样的函数。
  • 不支持重载
  • 不支持 默认参数

函数的声明

函数的声明告诉了编译器函数的名称,参数,返回值类型。
格式如下:

func name( [parameter list] ) [return_types] {
   函数体
}

解析:

  1. 函数声明包含一个函数名,参数列表,返回值列表和函数体
  2. func : 函数由关键字 func 声明,左大括号依旧不能另起一行
  3. nane 函数名称
  4. parameter list : 参数列表,参数就像是一个占位符,当函数被调用时,你可以将值传递给参数,这个值被称为实际参数。参数列表指定的是参数的类型、顺序、以及参数的个数,函数可以没有参数或者接受多个参数。主义类型在变量名之后,当两个或者多个连续的参数是同一类型,则除了最后一个类型之外,其他的都可以省略。
  5. return_types:返回类型,函数返回一列值。return_types 是该列值的数据类型,如(string, string)返回两个字符串。有些功能不需要返回值,如果函数没有返回值,则返回列表可以省略。也就是说,函数可以返回任意数量的返回值。有返回值的函数,必须有明确的终止语句,否则会引发编译错误。
    func test(x, y int, s string) (int, string) {
    // 类型相同的相邻参数,参数类型可合并。 多返回值必须用括号。
    n := x + y          
    return n, fmt.Sprintf(s, n)
    }
    
    

6.函数体: 代码集合(一般实现一个功能)。函数从第一条语句开始执行,直到执行 return 语句或者执行函数的最后一条语句。 例子:max()函数传图两个参数返回,返回这两个参数的最大值

/* 函数返回两个数的最大值 */
func max(num1, num2 int) int {
   /* 声明局部变量 */
   var result int

   if (num1 > num2) {
      result = num1
   } else {
      result = num2
   }
   return result
}

函数的调用

函数的声明定义了函数的功能和使用方式,想要真正执行任务需要调用该函数。
调用函数,向函数传递参数,并返回值:

package main

import "fmt"

func main() {
   var a int = 100
   var b int = 200
   var ret int

   /* 调用函数 */
   ret = max(a, b)

   fmt.Printf( "最大值是 : %d\n", ret )
}

func max(num1, num2 int) int {
   var result int

   if (num1 > num2) {
      result = num1
   } else {
      result = num2
   }
   return result
}

函数的参数

值传递和引用传递

函数如果是使用参数,该变量可以称为函数的形参。形参就像是定义在函数内的局部变量。
但当调用函数,传递过来的变量就是函数的实参,函数可以通过两种方式来传递参数:

传递类型描述
值传递值传递是指在调用函数时将实际参数赋值一份传递到函数中,这样在函数中如果对参数进行修改,将不会影响到实际参数
引用传递引用传递是指在调用函数时将实际参数的地址传递到函数中,那么在函数中对参数所进行的修改,将影响到实际参数
默认情况下,Go 语言使用的是值传递,即在调用的过程中不会影响到实际参数。
值传递

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

默认情况下,Go 语言使用的是值传递,即在调用过程中不会影响到实际参数。
例如:

package main

import "fmt"

func main() {
	/* 定义局部变量 */
	var a int = 100
	var b int = 200

	fmt.Printf("交换前 a 的值为 : %d\n", a)
	fmt.Printf("交换前 b 的值为 : %d\n", b)

	/* 通过调用函数来交换值 */
	swap(a, b)

	fmt.Printf("交换后 a 的值 : %d\n", a)
	fmt.Printf("交换后 b 的值 : %d\n", b)
}

/* 定义相互交换值的函数 */
func swap(x, y int) int {
	var temp int

	temp = x /* 保存 x 的值 */
	x = y    /* 将 y 值赋给 x */
	y = temp /* 将 temp 值赋给 y*/

	return temp
}
交换前 a 的值为 : 100
交换前 b 的值为 : 200
交换后 a 的值 : 100
交换后 b 的值 : 200
//交换前后变量的值没有发生改变,所有值传递不会改变所传入实参的值,只是复制一份值用于函数体执行而已。
引用传递

引用传递是指在调用函数时将实际参数的地址传递到函数中,那么在函数中对参数所进行的修改,将影响到实际参数。

引用传递将指针参数传递到函数内,以下是交换函数 swap() 使用了引用传递:

package main

import "fmt"

func main() {
	/* 定义局部变量 */
	var a int = 100
	var b int = 200

	fmt.Printf("交换前,a 的值 : %d\n", a)
	fmt.Printf("交换前,b 的值 : %d\n", b)

	/* 调用 swap() 函数
	 * &a 指向 a 指针,a 变量的地址
	 * &b 指向 b 指针,b 变量的地址
	 */
	swap(&a, &b)

	fmt.Printf("交换后,a 的值 : %d\n", a)
	fmt.Printf("交换后,b 的值 : %d\n", b)
}

func swap(x *int, y *int) {
	var temp int
	temp = *x /* 保存 x 地址上的值 */
	*x = *y   /* 将 y 值赋给 x */
	*y = temp /* 将 temp 值赋给 y */
}

输出结果:

交换前,a 的值 : 100
交换前,b 的值 : 200
交换后,a 的值 : 200
交换后,b 的值 : 100

注意: 1.无论是值传递,还是引用传递,传递给函数的都是变量的副本,不过,值传递是值的拷贝。引用传递是地址的拷贝,一般来说,地址拷贝更为高效。而值拷贝取决于拷贝的对象大小,对象越大,则性能越低。 2. map、slice、chan、指针、interface默认以引用的方式传递。

不定参数传递

不定参数传递就是函数的参数数量不确定,后面的类型是固定的。(可变参数) Golang 可变参数本质上是slice ,该 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) 来判断传递参数的个数。
逐一赋值:

package main

import (
	"fmt"
)

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 对象作为变参时,必须展开。

package main

import (
	"fmt"
)

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}
	res := test("sum: %d", s...) // slice... 展开slice
	println(res)
}

输出结果:sum: 6
注意:任意类型的不定参数和每个参数的类型都不是固定的。 用 interface{} 传递任意类型的参数是go 语言的惯例用法,而 interface{} 是类型安全的。

func myfunc(args ...interface{}) {
}

函数返回值

返回值的省略
_ 标识符,用来忽略函数的某个返回值。 Golang 返回值不能用容器对象接受多返回值。只能用多个变量,或 _ 忽略
多返回值可直接作为其他函数调用实参

package main

func test() (int, int) {
	return 1, 2
}

func add(x, y int) int {
	return x + y
}

func sum(n ...int) int {
	var x int
	for _, i := range n {
		x += i
	}

	return x
}

func main() {
	println(add(test()))
	println(sum(test()))
}
输出结果:
3
3

命名返回值
Go 函数的返回值可以被命名,就像在函数体开头声明变量。
返回值的名称应当具有一定的意义,可以作为文档使用。
命名返回参数可看做与形参类似的局部变量,最后由 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) {
    { // 不能在一个级别,引发 "z redeclared in this block" 错误。
        var z = x + y
        // return   // Error: z is shadowed during return
        return z // 必须显式返回。
    }
}

没有返回参数的 return 语句将返回变量的当前值。这种用法被称为裸返回
直接返回语句仅应当用在像下面这样的短函数中。在长的函数中它们会影响代码的可读性。

package main

import (
	"fmt"
)

func add(a, b int) (c int) {
	c = a + b
	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
命名返回参数允许 defer延迟调用通过闭包读取和修改

package main

func add(x, y int) (z int) {
    defer func() {
        z += 100
    }()

    z = x + y
    return
}

func main() {
    println(add(1, 2)) 
}

输出结果:103 显示 return 返回会先修改返回命名参数。

package main

func add(x, y int) (z int) {
    defer func() {
        println(z) // 输出: 203
    }()

    z = x + y
    return z + 200 // 执行顺序: (z = z + 200) -> (call defer) -> (return)
}

func main() {
    println(add(1, 2)) // 输出: 203
}

输出结果:

203
203

理解Go的延迟调用

defer 的特征:

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

defer 的用途:

  1. 关闭文件句柄
  2. 锁资源释放
  3. 数据库连接释放


Go 语言中的defer语句用于演说词函数的调用,每次defer 都会把一个函数压入栈中,函数返回前再把延迟的函数出取出来并执行,Go 中的defer 可以帮助我们处理容易忽略的问题,如资源释放,连接关闭等。
go 语言的defer 功能强大,对于资源管理很方便,但是如果没有用好也会有陷阱。

defer 的行为规则:

延迟函数的参数在defer语句出现时就已经确定下来了

package main

import "fmt"

func a() {
	i := 0
	defer fmt.Println(i)
	i++
	return
}

func main() {
	a()
}

输出结果:0 defer 语句中的 fmt.Println()参数i 的值在defer出现的时候就已经确定下来了。实际上是拷贝一份。后面对变量i 的修改不会影响到fmt.Println()函数的执行,仍然打印 “0”
注意:对于指针类型参数,规则依然适应,只不过延迟函数的参数是一个地址,这种情况下,defer后面的语句变量的修改可能会影响延迟函数。
延迟函数执行按照先进先出顺序执行,即先出现的defer最后执行。
这个规则很好理解,定义 defer 类似于入栈操作,执行 defer 类似于出栈操作。 设计 defer 的初衷是简化函数返回时资源清理的动作,资源往往有依赖顺序,比如先申请 A 资源,再跟据 A 资源申请 B 资源,根据 B 资源申请 C 资源,即申请顺序是:A–>B–>C,释放时往往又要反向进行。这就是把 defer 设计成 FIFO 的原因。 每申请到一个用完需要释放的资源时,立即定义一个 defer 来释放资源是个很好的习惯。 多个 defer 注册,按 FILO 次序执行 ( 先进后出 )。哪怕函数或某个延迟调用发生错误,这些调用依旧会被执行。
延迟函数可能操作主函数的具名返回值(命名返回值)
定义 defer 的函数,即主函数可能有返回值,返回值有没有名字没有关系,defer 所作用的函数,即延迟函数可能会影响到返回值。若要理解延迟函数是如何影响主函数返回值的,只要明白函数是如何返回的就足够了。

1 .函数返回过程(匿名返回值的情况)
有一个事实必须要了解,关键字 return 不是一个原子操作,实际上 return 只代理汇编指令 ret,即将跳转程序执行。比如语句 return i,实际上分两步进行,即将 i 值存入栈中作为返回值,然后执行跳转,而 defer 的执行时机正是跳转前,所以说 defer 执行时还是有机会操作返回值的。 举个例子:

func deferFuncReturn() (result int) {   
	i := 1
	defer func() {
	   result++
	}()    
	return i
}

该函数的return 语句可以拆分成下面两行:

result = i
return

而延迟函数的执行正是在 return 之前,即加入defer 后的执行过程如下:

result = i
result++
return

所以上面函数实际返回 i++ 值。
2.主函数有用匿名返回值,返回字面值:
一个主函数拥有一个匿名的返回值,返回时使用字面值,比如返回 “1”、“2”、“Hello” 这样的值,这种情况下 defer 语句是无法操作返回值的。一个返回字面值的函数,如下所示:

func foo() int {    
	var i int
	defer func() {
	    i++
	}()    
	return 1
}

上面的 return 语句,直接把 1 写入栈中作为返回值,延迟函数无法操作该返回值,所以就无法影响返回值 3.主函数拥有匿名返回值,返回变量 一个主函数拥有一个匿名返回值,返回使用本地或全局变量,这种情况下defer语句可以引用到返回值,但不会改变返回值。一个返回本地变量的函数,如下所示:

func foo() int {    
	var i int
	defer func() {
	    i++
	}()    
	return i
}

上面的函数,返回一个局部变量,同时 defer 函数也会操作这个局部变量。对于匿名返回值来说,可以假定系统给分配了一个命名变量来存储返回值,假定返回值变量为 “anony”,上面的返回语句可以拆分成以下过程:

anony = i
i++
return

由于 i 是整型,会将值拷贝给 anony,所以 defer 语句中修改 i 值,对函数返回值不造成影响
4.主函数拥有具名返回值
主函声明语句中带名字的返回值,会被初始化成一个局部变量,函数内部可以像使用局部变量一样使用该返回值。如果 defer 语句操作该返回值,可能会改变返回结果。一个影响函返回值的例子:

func foo() (ret int) {    
	defer func() {
	    ret++
	}()    
	return 0
}

上面的函数拆解出来,如下所示

ret = 0
ret++
return

函数真正返回前,在 defer 中对返回值做了 +1 操作,所以函数最终返回 1

匿名函数

匿名函数是指不需要定义函数名的一种函数实现方式。
在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

func main() {
    // --- function variable ---
    fn := func() { println("Hello, World!") }
    fn()

    // --- function collection ---
    fns := [](func(x int) int){
        func(x int) int { return x + 1 },
        func(x int) int { return x + 2 },
    }
    println(fns[0](100))

    // --- function as field ---
    d := struct {
        fn func() string
    }{
        fn: func() string { return "Hello, World!" },
    }
    println(d.fn())

    // --- channel of function ---
    fc := make(chan func() string, 2)
    fc <- func() string { return "Hello, World!" }
    println((<-fc)())
}

输出结果:

Hello, World!
101
Hello, World!
Hello, World!

函数用法

函数的用法:

函数用法描述
函数作为另外一个函数的实参函数定义后可另外一个函数的实参传入
闭包闭包是匿名函数,可在动态编程中使用
方法方法就是一个包含了接受者的函数

函数作为实参

Go 语言可以灵活的创建函数,并作为另外一个函数的实参。
函数是一类对象,可作为参数传递。
以下实例中我们在定义的函数中初始化一个变量,该函数仅仅是为了使用内置函数 math.sqrt(),实例为:

package main

import (
   "fmt"
   "math"
)
func main(){
   /* 声明函数变量 */
   getSquareRoot := func(x float64) float64 {
      return math.Sqrt(x)
   }
   /* 使用函数 */
   fmt.Println(getSquareRoot(9))
}

输出结果:3

package main

import "fmt"

// 声明一个函数类型
type cb func(int) int

func main() {
	testCallBack(1, callBack)
	testCallBack(2, func(x int) int {
		fmt.Printf("我是回调,x:%d\n", x)
		return x
	})
}

func testCallBack(x int, f cb) {
	f(x)
}

func callBack(x int) int {
	fmt.Printf("我是回调,x:%d\n", x)
	return x
}
    我是回调,x:
    我是回调,x:2

将复杂签名定义为函数类型,以便阅读:

package main

import "fmt"

func test(fn func() int) int {
	return fn()
}

// 定义函数类型。
type FormatFunc func(s string, x, y int) string

func format(fn FormatFunc, s string, x, y int) string {
	return fn(s, x, y)
}

func main() {
	s1 := test(func() int { return 100 }) // 直接将匿名函数当参数。

	s2 := format(func(s string, x, y int) string {
		return fmt.Sprintf(s, x, y)
	}, "%d, %d", 10, 20)

	println(s1, s2)
}

建议将复杂签名定义为函数类型,以便阅读:

package main

import "fmt"

func test(fn func() int) int {
	return fn()
}

// 定义函数类型。
type FormatFunc func(s string, x, y int) string

func format(fn FormatFunc, s string, x, y int) string {
	return fn(s, x, y)
}

func main() {
	s1 := test(func() int { return 100 }) // 直接将匿名函数当参数。

	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

闭包(略)

方法

Go 语言中同时有函数和方法。
一个方法就是一个包含了接受者的函数,接受者可以是任何命名类型(接口类型除外)或者结构体类型的一个值或者是一个指针。给定类型的所有方法属于该类型的方法集。 方法的声明语法:

//方法function_name()在(variable_name variable_data_type)这个变量上做工作
//(variable_name variable_data_type)是接受者
func (variable_name variable_data_type) function_name() [return_type]{
   /* 函数体*/
}

例子1:定义一个结构体和该结构体类型的方法:

package main

import (
   "fmt"  
)

/* 定义结构体 */
type Circle struct {
  radius float64
}

func main() {
  var c1 Circle
  c1.radius = 10.00
  fmt.Println("圆的面积 = ", c1.getArea())
}

//该 method 属于 Circle 类型对象中的方法
func (c Circle) getArea() float64 {
  //c.radius 即为 Circle 类型对象中的属性
  return 3.14 * c.radius * c.radius
}

输出结果: 圆的面积 = 314 关于值和指针,如果想在方法中改变结构体类型的属性,需要对方法传递指针,体会如下对结构体类型改变的方法 changRadis() 和普通的函数 change() 中的指针操作:

package main

import (
   "fmt"  
)

/* 定义结构体 */
type Circle struct {
  radius float64
}


func main()  { 
   var c Circle
   fmt.Println(c.radius)
   c.radius = 10.00
   fmt.Println(c.getArea()) 
   c.changeRadius(20)
   fmt.Println(c.radius)
   change(&c, 30)
   fmt.Println(c.radius)
}
func (c Circle) getArea() float64  {
   return c.radius * c.radius
}
// 注意如果想要更改成功c的值,这里需要传指针
func (c *Circle) changeRadius(radius float64)  {
   c.radius = radius
}

// 以下操作将不生效
//func (c Circle) changeRadius(radius float64)  {
//   c.radius = radius
//}
// 引用类型要想改变值需要传指针
func change(c *Circle, radius float64)  {
   c.radius = radius
}
0
100
20
30

说明:
getArea() 和 changeRadius() 是方法,因为它们是定义在某个接收对象上的。
调用方法的语法是c.方法(),针对某个对象 c 调用定义在其上的方法。
注意和函数 change() 的直接调用方法不同哦。
实例2: 实际上,除了结构体类型之外,可以为任意类型(接口类型除外)添加方法。

在Go语言中,接收者的类型可以是任何类型,不仅仅是结构体,任何类型都可以拥有方法(接口类型除外)。

举个例子,我们基于内置的 int 类型使用type关键字可以定义新的自定义类型,然后为我们的自定义类型添加方法。

package main

import (
	"fmt"
)

//MyInt 将int定义为自定义MyInt类型
type MyInt int

//SayHello 为MyInt添加一个SayHello的方法
func (m MyInt) SayHello() {
	fmt.Println("Hello, 我是一个int。")
}
func main() {
	var m1 MyInt
	m1.SayHello() //Hello, 我是一个int。
	m1 = 100
	fmt.Printf("%#v  %T\n", m1, m1) //100  main.MyInt
}
Hello, 我是一个int100  main.MyInt

注意事项: 非本地类型不能定义方法,也就是说我们不能给别的包的类型定义方法。

递归函数

内置函数

Go 语言标准库提供了多种可动用的内置函数。

例如,len() 函数可以接受不同类型参数并返回其长度。如果我们传入的是字符串则返回字符串的长度,如果传入的是数组,则返回数组中包含的元素个数。

变量的作用域

变量的作用域由 变量声明的地方 和 函数 的 相对位置 决定。

作用域为已声明的标识符所表示的常量、类型、变量、函数或包在源代码中的作用范围。

Go 语言中变量可以在三个地方声明:

  1. 函数内定义的变量称为局部变量
  2. 函数外定义的变量称为全局变量
  3. 函数定义中的变量称为形式参数

局部变量

在函数体内声明的变量称之为局部变量,它们的作用域只在函数体内,参数和返回值变量也是局部变量。

package main
import "fmt"
func main() {
   /* 声明局部变量 */
   var a, b, c int

   /* 初始化参数 */
   a = 10
   b = 20
   c = a + b

   fmt.Printf ("结果: a = %d, b = %d and c = %d\n", a, b, c)
}

结果: a = 10, b = 20 and c = 30

全局变量

在函数体外声明的变量称之为全局变量,全局变量可以在整个包甚至外部包(被导出后)使用

package main

import "fmt"

/* 声明全局变量 */
var g int

func main() {

   /* 声明局部变量 */
   var a, b int

   /* 初始化参数 */
   a = 10
   b = 20
   g = a + b

   fmt.Printf("结果: a = %d, b = %d and g = %d\n", a, b, g)
}

结果: a = 10, b = 20 and g = 30
一个说明:
Go 语言程序中全局变量与局部变量名称可以相同,但是函数内的局部变量会被优先考虑。实例如下

package main

import "fmt"

/* 声明全局变量 */
var g int = 20

func main() {
   /* 声明局部变量 */
   var g int = 10

   fmt.Printf ("结果: g = %d\n",  g)
}

结果: g = 10

形式参数

形式参数会作为函数的局部变量来使用

package main

import "fmt"

/* 声明全局变量 */
var a int = 20

func main() {
	/* main 函数中声明局部变量 */
	var a int = 10
	var b int = 20
	var c int = 0

	fmt.Printf("main()函数中 a = %d\n", a)
	c = sum(a, b)
	fmt.Printf("main()函数中 c = %d\n", c)
}

/* 函数定义-两数相加 */
func sum(a, b int) int {
	fmt.Printf("sum() 函数中 a = %d\n", a)
	fmt.Printf("sum() 函数中 b = %d\n", b)

	return a + b
}

两个重要说明

(1)总结
变量可见性:

  1. 声明在函数内部,是函数的本地值,类似 private
  2. 声明在函数外部,是对当前包可见(包内所有.go文件都可见)的全局值,类似 protect
  3. 声明在函数外部且首字母大写是所有包可见的全局值,类似 public

(2)默认初始化值

不同类型的局部和全局变量初始化 默认值(就是不初始化时,系统自动给的值)为:

数据类型初始化默认值
int0
float320
pointernil

异常处理

Golang 没有结构化异常,使用 panic 抛出错误,recover 捕获错误。

(结构化异常指的是C/C++程序语言中,程序控制结构try-except与try-finally语句用于处理异常事件。)

异常的使用场景简单描述: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 语句会被最先调用。
panic 和 recover 函数的配合使用
package main

func main() {
	test()
}

func test() {
	defer func() {
		if err := recover(); err != nil {
			println(err.(string)) // 将 interface{} 转型为具体类型。
		}
	}()

	panic("panic error!")
}

输出结果panic error!
说明:由于 panic,recover 参数类型为interface{} 因此可抛出任何类型对象。

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

import (
	"fmt"
)

func main() {
	defer func() {
		if err := recover(); err != nil {
			fmt.Println(err)
		}
	}()

	var ch chan int = make(chan int, 10)
	close(ch)
	ch <- 1
}

send on closed channel

延迟调用中引发的错误,可被后续延迟调用捕获,但仅最后一个错误可被捕获
package main

import "fmt"

func test() {
	defer func() {
		fmt.Println(recover())
	}()

	defer func() {
		panic("defer panic")
	}()

	panic("test panic")
}

func main() {
	test()
}

defer panic

捕获函数 recover 只有在 defer 延迟调用内 直接调用 才会终止错误,否则总是返回 nil。任何未捕获的错误都会沿调用堆栈向外传递。
package main

import "fmt"

func test() {
	defer func() {
		fmt.Println(recover()) //有效
	}()
	defer recover()              //无效!
	defer fmt.Println(recover()) //无效!
	defer func() {
		func() {
			println("defer inner")
			recover() //没有在defer函数内直接调用,无效!
		}()
	}()

	panic("test panic")
}

func main() {
	test()
}
defer inner
<nil>
test panic
使用延迟匿名函数或下面这样都是有效的
package main

import (
	"fmt"
)

func except() {
	fmt.Println(recover())
}

func test() {
	defer except()
	panic("test panic")
}

func main() {
	test()
}

test panic

如果需要保护代码段,可将代码块重构成匿名函数,如此可确保后续代码可以被执
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)  //panic + recover结束了匿名函数内部的执行,跳出了匿名函数。但这行代码仍然可以被执行。
}

func main() {
	test(2, 1)
}

x / y = 0
另外:
除用 panic 引发中断性错误外,还可返回 error 类型错误对象来表示函数调用状态:

type error interface {
    Error() string
}

如何区别使用 panic 和 error 两种方式 ? 惯例是:导致关键流程出现不可修复性错误的使用 panic,其他使用 error。