函数进阶(作用域、内置函数、defer、panic、recover)

1|0一、作用域

  • 作用域分为全局作用域和局部作用域

1|11. 全局作用域

  • 全局作用域指的是包级别的区域,不在函数内部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也跟着变了 */

1|22. 局部作用域

  • 局部作用域即在函数内部、条件语句生效的{}内、for循环语句中、 等等的区域,在该区域定义的变量称为局部变量
  • 函数内定义的变量无法在该函数外使用(但是闭包函数例外)
  • 局部变量的使用分为两种情况
    • 局部变量和全局变量的名不同
    • 局部变量和全局变量的名相同

1|0(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 }

1|0(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 }

2|0二、函数类型与变量

  • 前面我们说过,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 }

3|0三、defer 方法

3|11. 什么是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 */

3|22. 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|33. 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 */

3|44. 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 */

4|0四、内置函数

  • Go中也有一些内置函数可以使用
名称 作用
close 主要用来关闭channel
len 用来求长度,比如string、array、slice、map、channel
new 用来分配内存,主要用来分配值类型,比如int、struct。返回的是指针
make 用来分配内存,主要用来分配引用类型,比如channel、map、slice
append 用来追加元素到数组、slice中
panic和recover 用来做错误处理

5|0五、panic和recover

  • Go语言目前1.22版本仍没有好用的异常处理机制(即像python一样的 try except),但是使用panic/recover模式来处理错误

  • panic作用等同python中的 raise,主动抛出异常

  • recover用来恢复程序

  • recover()必须搭配defer使用,recover()只有在defer调用的函数中有效

  • recover()是有返回值的,当recover()的值为nil时,表示没有异常,否则返回的就是异常信息

  • defer一定要在可能引发panic的语句之前定义

5|11. 简单示例

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 */

6|0六、自定义的error

  • Go 语言中的错误处理是把错误当成一种值来处理,更强调判断错误、处理错误,而不是用 catch 捕获异常

6|11. 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) } }

6|22. 创建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) } }

__EOF__

本文作者BigSun丶
本文链接https://www.cnblogs.com/Mcoming/p/18037185.html
关于博主:评论和私信会在第一时间回复。或者直接私信我。
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
声援博主:如果您觉得文章对您有帮助,可以点击文章右下角推荐一下。您的鼓励是博主的最大动力!
posted @   BigSun丶  阅读(44)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
点击右上角即可分享
微信分享提示