函数进阶(作用域、内置函数、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语句在底层并不是原子操作,它拆分为两步:
  1. 给返回值赋值(将return 的值赋值给变量)
  2. 执行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)
	}

}

posted @ 2024-02-27 16:34  BigSun丶  阅读(81)  评论(0)    收藏  举报