函数进阶(作用域、内置函数、defer、panic、recover)
目录
一、作用域
- 作用域分为全局作用域和局部作用域
1. 全局作用域
- 全局作用域指的是包级别的区域,不在函数内部for循环的条件上,在全局作用域定义的变量称作全局变量,它在程序整个运行周期内都有效。 在函数中可以引用到全局变量,注意是引用
package main
import "fmt"
//定义全局变量num
var num int64 = 10
func testGlobalVar() {
fmt.Printf("函数内部的 num=%d\n", num) // 函数中可以访问全局变量num
num++
fmt.Printf("函数内部运算之后的 num=%d\n", num) // 函数中可以访问全局变量num
}
func main() {
testGlobalVar()
fmt.Printf("全局的 num=%d\n", num) // num=11
}
/*
函数内部的 num=10
函数内部运算之后的 num=11
全局的 num=11
可以看到,在函数内部可以访问到全局变量 num,且函数内部修改num之后,全局的num也跟着变了
*/
2. 局部作用域
- 局部作用域即在函数内部、条件语句生效的{}内、for循环语句中、 等等的区域,在该区域定义的变量称为局部变量
- 函数内定义的变量无法在该函数外使用(但是闭包函数例外)
- 局部变量的使用分为两种情况
- 局部变量和全局变量的名不同
- 局部变量和全局变量的名相同
(1)局部变量和全局变量的名不同
1. 示例1 (函数内部定义的变量)
func testLocalVar() {
// 定义一个函数局部变量x,仅在该函数内生效
var x int64 = 100
fmt.Printf("x=%d\n", x)
}
func main() {
testLocalVar()
fmt.Println(x) // 此时无法使用变量x
}
2. 示例2 (在条件体生效的内部定义的变量)
func testLocalVar2(x, y int) {
fmt.Println(x, y) // 函数的参数也是只在本函数中生效
if x > 0 {
z := 100 // 变量z只在if语句块生效
fmt.Println(z)
}
//fmt.Println(z) // 此处无法使用变量z
}
3. 示例3 (在for循环的for语句中定义的变量)
func testLocalVar3() {
for i := 0; i < 10; i++ {
fmt.Println(i) // 变量i只在当前for语句块中生效
}
//fmt.Println(i) // 此处无法使用变量i
}
(2)局部变量和全局变量的名相同
- 如果局部变量和全局变量重名,优先访问局部变量(同python一样,就近原则)
package main
import "fmt"
//定义全局变量num
var num int64 = 10
func testNum() {
num := 100
fmt.Printf("num=%d\n", num) // 函数中优先使用局部变量
}
func main() {
testNum() // num=100
}
二、函数类型与变量
- 前面我们说过,Go是强类型语言,函数的参数类型和返回值类型都属于函数的类型
- 关键字
type可以用来定义一种类型
// 1. 我们定义一个calculation 类型,满足参数为两个int类型,返回值为int类型的函数,都是 calculation 类型
type calculation func(int, int) int
// 2. 再定义俩个函数add sub,显然它们都是 calculation 类型
func add(x, y int) int {
return x + y
}
func sub(x, y int) int {
return x - y
}
// 3. 此时,再声明一个 calculation 类型的变量,则上面的 add 和 sub都能赋值给该变量了
func main() {
var c calculation // 声明一个calculation类型的变量c
c = add // 把add赋值给c
fmt.Printf("type of c:%T\n", c) // type of c:main.calculation
fmt.Println(c(1, 2)) // 像调用add一样调用c
f := add // 将函数add赋值给变量f
fmt.Printf("type of f:%T\n", f) // type of f:func(int, int) int
fmt.Println(f(10, 20)) // 像调用add一样调用f
}
三、defer 方法
1. 什么是defer
- Go语言中的
defer语句会将其后面跟随的语句进行延迟处理。在defer归属的函数即将返回时,将延迟处理的语句按defer定义的逆序进行执行,也就是说,先被defer的语句最后被执行,最后被defer的语句,最先被执行(类似于堆栈) defer所在的函数在执行过程中报错,最后defer的语句仍会执行defer在定义要延迟执行的函数时,该函数所有的参数都需要确定其值defer语句为函数时,若函数的参数中含有之前定义的变量,则在定义defer时已经对其进行了值拷贝,若defer语句的函数的参数中不含该外部作用域的变量,而函数体内部直接使用,则是对该变量的引用- 由于
defer语句延迟调用的特性,所以defer语句能非常方便的处理资源释放问题。比如:资源清理、文件关闭、解锁及记录时间等
// 简单的小示例
package main
import (
"fmt"
)
func test5() {
println("11111")
defer fmt.Println("22222")
println("5555")
defer fmt.Println("33333")
a := []int{1,2,3}
println("44444",a[0])
println("55555",a[10]) // 这里报错了,下面的66666也没有打印,但是defer部分仍旧执行了
println("66666")
}
func main() {
test5()
}
/*
11111
5555
44444 1
33333
22222
panic: runtime error: index out of range [10] with length 3
*/
2. defer的执行时机
- 在Go语言的函数中
return语句在底层并不是原子操作,它拆分为两步:
- 给返回值赋值(将return 的值赋值给变量)
- 执行RET指令(将值返回出去)
- 而
defer语句执行的时机就在返回值赋值操作后,RET指令执行前
// 经典案例
package main
import "fmt"
func f1() int {
x := 5
fmt.Printf("f1中x的地址 %p\n", &x)
defer func() {
x++
fmt.Printf("f1的匿名函数中x的地址 %p\n", &x)
}()
return x
}
func f2() (x int) {
fmt.Printf("f2中x的地址 %p\n", &x)
defer func() {
x++
fmt.Printf("f2的匿名函数中x的地址 %p\n", &x)
}()
return 5
}
func f3() (y int) {
x := 5
fmt.Printf("f3中x的地址 %p\n", &x)
defer func() {
x++
fmt.Printf("f3的匿名函数中x的地址 %p\n", &x)
fmt.Printf("f3的匿名函数中y的地址 %p\n", &y)
}()
return x
}
func f4() (x int) {
fmt.Printf("f4中x的地址 %p\n", &x)
defer func(x int) {
x++
fmt.Printf("f4的匿名函数中x的地址 %p\n", &x)
}(x)
return 5
}
func main() {
fmt.Println(f1())
fmt.Println(f2())
fmt.Println(f3())
fmt.Println(f4())
}
/*
打印结果:
f1中x的地址 0xc00000a0d8
f1的匿名函数中x的地址 0xc00000a0d8
5
f2中x的地址 0xc00000a118
f2的匿名函数中x的地址 0xc00000a118
6
f3中x的地址 0xc00000a130
f3的匿名函数中x的地址 0xc00000a130
f3的匿名函数中y的地址 0xc00000a128
5
f4中x的地址 0xc00000a140
f4的匿名函数中x的地址 0xc00000a148
5
分析:上面f1 和 f3 道理是一样的,函数f1的返回值虽然只指定了类型,未指定变量名,而函数f2返回值指定了变量名和类型,
但是,函数在返回的时候都是相当于将返回值赋值给一个新的变量,若指定了这个返回值的变量,就直接赋值过去(赋值是值传递或者叫拷贝传递,非地址传递),若未指定返回值的变量名,则Go内部也会创建临时的变量再赋值过去。
所以上面的 f1 f3 它们各自的匿名函数中的x地址和匿名函数外x的地址虽然一致,但是f3中可以清晰的看到返回值的地址跟x是不同的,所以修改x,不影响y的值
f2 中其实是匿名函数引用了 f2的返回值x,当return 5 时,x就被赋值了5,然后defer 部分的匿名函数修改了x的值,此时x变成了6,defer执行完成后再执行 RET指令,最后f2的执行结果就为 6
f4 是因为,其匿名函数将x当作参数传入匿名函数内部,不是对f4的返回值x的直接引用,函数的参数传递都是值传递(拷贝传递),所以传入匿名函数的x和f4
的返回变量x的地址不同,所以返回值x并不会受影响
*/
3. defer语句中函数参数为执行函数
-
defer在定义要延迟执行的函数时,该函数所有的参数都需要确定其值 -
defer语句为函数时,若函数的参数中含有之前定义的变量,则在定义defer时已经对其进行了值拷贝,若defer语句的函数的参数中不含该变量,而函数体内部直接使用,则是对改变量的引用 -
defer语句为函数时,若其参数为执行函数,则按照正常执行顺序执行,和defer无关
package main
import "fmt"
func calc(index string, a, b int) int {
ret := a + b
fmt.Println(index, a, b, ret)
return ret
}
func main() {
x := 1
y := 2
fmt.Println("QQ", x, y)
defer calc("AA", x, calc("A", x, y)) // 定义时,已经对x y进行了值拷贝,calc("AA", x, calc("A", x, y)) 中 x x y分别为 1 1 2
x = 10
defer calc("BB", x, calc("B", x, y)) // 定义时,已经对x y进行了值拷贝,calc("BB", x, calc("B", x, y)) 中 x x y分别为 10 10 2
y = 20
defer func() { // 定义时,是对x y进行了引用,相当于先记录了x y的地址,当等到defer的语句执行时,再去取x y的值
ret := x + y
fmt.Println("CC", x, y, ret)
}()
x++
y++
ret := x + y
fmt.Println("DD", x, y, ret)
}
/*
QQ 1 2
A 1 2 3
B 10 2 12
DD 11 21 32
CC 11 21 32
BB 10 12 22
AA 1 3 4
*/
4. for循环中的defer
func test1() {
for i := 0; i < 3; i++ {
defer fmt.Println(i)
}
}
func test2() {
for i := 0; i < 3; i++ {
fmt.Printf("111 %p\n", &i)
defer func() {
fmt.Printf("222 %p\n", &i)
fmt.Println(i)
}()
}
}
func test2() {
i := 0
for ; i < 3; i++ {
fmt.Printf("111 %p\n", &i)
defer func() {
fmt.Printf("222 %p\n", &i)
fmt.Println(i)
}()
}
}
/*
test1 的打印结果为:
2
1
0
test2 的打印结果为:
111 0xc00000a0d8
111 0xc00000a0f8
111 0xc00000a120
222 0xc00000a120
2
222 0xc00000a0f8
1
222 0xc00000a0d8
0
test3 的打印结果:
111 0xc000096068
111 0xc000096068
111 0xc000096068
222 0xc000096068
3
222 0xc000096068
3
222 0xc000096068
3
Go之前的版本,for循环的for语句中定义的变量,其内存地址一直不变,但是新版本之后,for循环的for语句中定义的变量在每次循环时都会创建新的地址
简单来说,就是for循环的机制变了,按照之前的版本,test2打印结果于test3都是3 3 3
*/
四、内置函数
- Go中也有一些内置函数可以使用
| 名称 | 作用 |
|---|---|
| close | 主要用来关闭channel |
| len | 用来求长度,比如string、array、slice、map、channel |
| new | 用来分配内存,主要用来分配值类型,比如int、struct。返回的是指针 |
| make | 用来分配内存,主要用来分配引用类型,比如channel、map、slice |
| append | 用来追加元素到数组、slice中 |
| panic和recover | 用来做错误处理 |
五、panic和recover
-
Go语言目前1.22版本仍没有好用的异常处理机制(即像python一样的
try except),但是使用panic/recover模式来处理错误 -
panic作用等同python中的raise,主动抛出异常 -
recover用来恢复程序 -
recover()必须搭配defer使用,recover()只有在defer调用的函数中有效 -
recover()是有返回值的,当recover()的值为nil时,表示没有异常,否则返回的就是异常信息 -
defer一定要在可能引发panic的语句之前定义
1. 简单示例
package main
import "fmt"
func funcA() {
fmt.Println("func A")
}
func funcB() {
panic("panic in B")
fmt.Println("func B") // 异常后面的代码都不会执行,除了defer语句
}
func funcC() {
fmt.Println("func C")
}
func main() {
funcA()
funcB()
funcC()
}
/*
func A
panic: panic in B
goroutine 1 [running]:
main.funcB(...)
C:/GolandProjects/hsw_test/test3.go:10
main.main()
C:/GolandProjects/hsw_test/test3.go:18 +0x65
可以看到,程序运行期间funcB中引发了panic导致程序崩溃,异常退出了。这个时候我们就可以通过recover将程序恢复回来,继续往后执行,如下
*/
package main
import "fmt"
func funcA() {
fmt.Println("func A")
}
func funcB() {
defer func() {
//如果程序出出现了panic错误,可以通过recover恢复过来
if err := recover();err != nil {
fmt.Println("报错内容:", err)
fmt.Println("recover in B")
}
}()
panic("panic in B")
fmt.Println("func B") // 异常后面的代码都不会执行,除了defer语句
}
func funcC() {
fmt.Println("func C")
}
func main() {
funcA()
funcB()
funcC()
}
/*
func A
报错内容: panic in B
recover in B
func C
*/
六、自定义的error
- Go 语言中的错误处理是把错误当成一种值来处理,更强调判断错误、处理错误,而不是用 catch 捕获异常
1. errors 库
- 我们可以根据需求自定义 error,最简单的方式是使用
errors包提供的New函数创建一个错误
New 函数的定义:
func New(text string) error
- 简单示例
package main
import (
"errors"
"fmt"
)
// 返回错误
func getCircleArea1(radius float32) (float32, error) {
if radius <= 0 {
return 0, errors.New("半径不能小于0")
}
return 3.14 * radius * radius, nil
}
func main() {
res, err := getCircleArea1(-9)
if err != nil {
fmt.Println(err)
} else {
fmt.Println("圆的面积为", res)
}
}
2. 创建error接口实现自定义
- Go 语言中使用一个名为
error接口来表示错误类型(接口可以理解为包含了某种功能的集合,通过调用接口,可以实现其中的功能,结构体和接口后面会介绍)
自定义的error接口:
type error interface {
Error() string
}
- error 接口只包含一个方法——Error,这个函数需要返回一个描述错误信息的字符串
- 当一个函数或方法需要返回错误时,我们通常是把错误作为最后一个返回值
- 由于 error 是一个接口类型,默认零值为nil。所以我们通常将调用函数返回的错误与nil进行比较,以此来判断函数是否返回错误
- 示例:
package main
import (
"fmt"
)
type error interface {
Error() string
}
type radiusError struct {
radius string
}
// Error方法首字母大写,实现别的包能够调用该方法
func (r *radiusError) Error() string {
return "出现错误,错误原因为:" + r.radius
}
func getCircleArea(radius float32) (float32, error) {
if radius <= 0 {
return 0, &radiusError{radius: "半径不能小于0"}
}
return 3.14 * radius * radius, nil
}
func main() {
res, err := getCircleArea(-9)
if err != nil {
fmt.Println(err)
} else {
fmt.Println("圆的面积为", res)
}
}

浙公网安备 33010602011771号