GO 语言函数
函数是基本的代码块,用于执行一个任务。
你可以通过函数划分不同的功能,逻辑上每个函数执行的是指定的任务。
Go语言最少有个main函数
Golang函数的特点:
支持:
- 无需声明原型
- 支持不定 参数
- 支持多返回值
- 支持命名返回参数
- 支持匿名函数和闭包
- 函数也是一种类型,一个函数可以赋值给变量
不支持:
- 不支持 嵌套 一个包不能有两个名字一样的函数。
- 不支持重载
- 不支持 默认参数
函数的声明
函数的声明告诉了编译器函数的名称,参数,返回值类型。
格式如下:
func name( [parameter list] ) [return_types] {
函数体
}
解析:
- 函数声明包含一个函数名,参数列表,返回值列表和函数体
- func : 函数由关键字 func 声明,左大括号依旧不能另起一行
- nane 函数名称
- parameter list : 参数列表,参数就像是一个占位符,当函数被调用时,你可以将值传递给参数,这个值被称为实际参数。参数列表指定的是参数的类型、顺序、以及参数的个数,函数可以没有参数或者接受多个参数。主义类型在变量名之后,当两个或者多个连续的参数是同一类型,则除了最后一个类型之外,其他的都可以省略。
- 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 的特征:
- 关键字defer 用于注册延迟调用。
- 这些调用直到return 跳转前才被执行。因此可用来做资源清理。
- 多个defer 语句,按照先进先出的方式执行。
- defer 语句中的变量在defer声明时就决定了。
defer 的用途:
- 关闭文件句柄
- 锁资源释放
- 数据库连接释放
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, 我是一个int。
100 main.MyInt
注意事项: 非本地类型不能定义方法,也就是说我们不能给别的包的类型定义方法。
递归函数
内置函数
Go 语言标准库提供了多种可动用的内置函数。
例如,len() 函数可以接受不同类型参数并返回其长度。如果我们传入的是字符串则返回字符串的长度,如果传入的是数组,则返回数组中包含的元素个数。
变量的作用域
变量的作用域由 变量声明的地方 和 函数 的 相对位置 决定。
作用域为已声明的标识符所表示的常量、类型、变量、函数或包在源代码中的作用范围。
Go 语言中变量可以在三个地方声明:
- 函数内定义的变量称为局部变量
- 函数外定义的变量称为全局变量
- 函数定义中的变量称为形式参数
局部变量
在函数体内声明的变量称之为局部变量,它们的作用域只在函数体内,参数和返回值变量也是局部变量。
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)总结
变量可见性:
- 声明在函数内部,是函数的本地值,类似 private
- 声明在函数外部,是对当前包可见(包内所有.go文件都可见)的全局值,类似 protect
- 声明在函数外部且首字母大写是所有包可见的全局值,类似 public
(2)默认初始化值
不同类型的局部和全局变量初始化 默认值(就是不初始化时,系统自动给的值)为:
| 数据类型 | 初始化默认值 |
|---|---|
| int | 0 |
| float32 | 0 |
| pointer | nil |
异常处理
Golang 没有结构化异常,使用 panic 抛出错误,recover 捕获错误。
(结构化异常指的是C/C++程序语言中,程序控制结构try-except与try-finally语句用于处理异常事件。)
异常的使用场景简单描述:Go中可以抛出一个panic的异常,然后在defer中通过recover捕获这个异常,然后正常处理。
panic 介绍:
- 内置函数
- 假如函数 F 中书写了 panic 语句,会终止其后要执行的代码,在 panic 所在函数 F 内如果存在要执行的 defer 函数列表,按照 defer 的逆序执行
- 返回函数 F 的调用者 G ,在 G 中,调用函数 F 语句之后的代码不会执行,假如函数 G 中存在要执行的 defer 函数列表,按照 defer 的逆序执行
- 直到 goroutine 整个退出,并报告错误
recover 介绍:
- 内置函数
- 用来控制一个 goroutine 的 panicking 行为,捕获 panic ,从而影响应用的行为
- 一般的调用建议
a. 在 defer 函数中,通过 recever 来终止一个 goroutine 的 panicking 过程,从而恢复正常代码的执行
b. 可以获取通过 panic 传递的 error
注意:
- 利用 recover 处理 panic 指令,defer 必须放在 panic 之前定义,另外 recover 只有在 defer 调用的函数中才有效。否则当 panic 时,recover无法捕获到 panic ,无法防止 panic 扩散。
- recover 处理异常后,逻辑并不会恢复到 panic 那个点去,函数跑到 defer 之后的那个点。
- 多个 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。