Go语言之函数
函数这种语法元素的诞生,源于将大问题分解为若干小任务与代码复用;函数是唯一一种基于特定输入,实现特定任务并可返回任务执行结果的代码块。
1.函数定义
// func:函数由 func 开始声明 // function_name:函数名:唯一,首字母大写可以在包外引用,小写则包内可见 // parameter list:参数列表,可有可无,可少可多,逗号分隔 // return_types: 返回值的类型定义,可省可多,多个返回值需要用括号包裹,逗号分隔 func function_name( [parameter list] ) [return_types] { 函数体 }
(1)函数参数
参数可以不传,参数也可以传递多个,也可以参数数量不固定
函数的参数一般称为形参,而实际调用时使用的参数称为实参
函数参数的传递采用的是值传递的方式
值传递就是将实际参数在内存中的表示逐位拷贝(Bitwise Copy)到形式参数中
- 1.自身传递:代表类型有整型,数组,结构体,拷贝自身数据内容
- 2.引用传递:代表类型有string,slice,map,不拷贝实际数据内容而拷贝自身地址,故开销固定,称之为浅拷贝
// 不传参数 func num() (int, int) { return 10, 20 } // 传多个参数 func nums(a, b int) (int, int) { return b, a } // 不固定参数 只能写在最后 func nums(a int, x ...int) (int, int) { return x[0], x[1] }
(2)函数返回值
返回值可以没有,可以是一个,也可以是多个
// 无返回值 func num(x, y int) { } // 多返回值 func nums(x, y int) (int, int, string) { return x, y, "" } // 具名返回值 相当于局部变量 return隐式返回 func nums2(x, y int) (a int, b int, c string) { a = 10 b = 20 c = "hehe" return }
2.高阶函数
(1)函数可以作为数据类型
func main() { // 定义a,数据类型函数 var a func(x, y int) (int, int) // 构造函数体(函数实例化) a = func(x, y int) (int, int) { return y, x } // 传参并输出 fmt.Println(a(10, 20)) } // 结果 20 10
(2)函数可以作为返回值
func main() { // 最后必须加括号,不然a得到的是函数的地址而非具体返回值 a := re(1, 2)() fmt.Println(a) } func re(a, b int) func() int { // 构造返回值函数体 return func() int { return (a + b) } }
(3)函数可以作为参数传递
func main() { // 定义a,数据类型函数 var a func(x, y int) (int, int) // 构造数据类型函数体 a = func(x, y int) (int, int) { return y, x } // 传参 r := call(a) // 输出 20 + 10 的结果 r(10) } // call函数,传参为函数,返回值为函数 func call(a func(x, y int) (int, int)) func(x int) { // 取出20 y, _ := a(10, 20) // 返回值中打印 return func(x int) { fmt.Println(y + x) } }
(4)匿名函数
匿名函数就是没有函数名的函数,它可以在定义的地方直接使用,或者将其赋值给变量进行后续调用。匿名函数通常用于需要在函数内部定义并使用的简单逻辑块。匿名函数多用于实现回调函数和闭包。
func main() { // 这是一个匿名函数 funA := func() int { return 10 } // 其实在这里funA就是函数的名字,可以如此调用 funA() // 这是一个匿名函数调用 可以不用将函数声明为一个变量再使用 func() { fmt.Println("这是一个匿名函数") }() }
(5)函数闭包
闭包可以简单理解为函数内部的匿名函数,其引用了函数体之外的变量,可以简单理解为由函数+引用环境组成
闭包允许函数内部定义的匿名函数捕获并访问函数外部的变量,形成一个封闭的作用域。
这种特性使得函数成为第一类对象,能够方便地进行参数传递和返回值使用
举个形象的例子:
你想吃邻居家树上的苹果,但是无法去他家院子
所以你叫出邻居家小孩,和他搞好关系,让他给你摘苹果
这个小孩就是闭包函数,苹果就是局部变量
// 本函数没有任何实用性,只是展示知识点 func main() { // 创建一个玩家a a := player("张三") // 给他一个血包b b := 100 // 返回玩家的名字,初始血量和目前血量 name, hp, x := a(b) // 打印值 fmt.Println(name, hp, x) // 或者可以直接点 //fmt.Println(a(b)) } // 创建一个玩家生成器, 输入名称, 输出生成器 func player(name string) func(x int) (string, int,int) { // 初始血量为100 hp := 100 // 返回创建的闭包 return func(x int) (string, int,int) { // 可以捕获变量hp将初始血量调整为200 hp = 200 x += hp // 将变量引用到闭包中 return name, hp, x } }
从这个例子可以看出,闭包函数的一些特点
1.可以让我们访问到在其周围函数中定义的变量
2.更改捕获到的变量
3.逃逸变量,变量被闭包捕获后必须分配在堆上,确保函数被返回后仍可以访问它
但是这个变量不被闭包之外的其他代码使用,因此可以用编译器优化使其分配在栈中
缺点:
- 内存泄露:闭包可能导致其引用的外部变量生命周期延长,如果不小心可能会造成内存泄漏。
- 循环引用:如果闭包捕获的变量包含闭包自身的引用,可能会形成循环引用,需要注意避免。
- 并发安全:如果闭包在并发环境中被多个协程使用,而闭包又操作共享变量,则必须确保并发安全,比如通过互斥锁
(6)内置函数
内置函数 | 描述 |
---|---|
close | 关闭一个通道(channel) |
len | 返回字符串、数组、切片、字典或通道的长度 |
cap | 返回切片的容量,通道的缓冲区大小 |
new | 为类型分配内存并返回指向该类型的指针 |
make | 用于创建切片、映射和通道 |
append | 将元素追加到切片的末尾 |
copy | 将源切片的元素复制到目标切片 |
delete | 从字典中删除指定键的键值对 |
panic | 触发一个运行时错误。 |
recover | 从 panic 中恢复,用于处理运行时错误 |
2.defer 语句
在Go语言中,
defer
是一种用于延迟执行函数调用的关键字。
(1)defer定义
延迟调用:
可以让函数或方法在当前函数执行完毕后,在return赋值之后返回之前执行,同时也在
panic之前
执行(注:跟在defer后的函数,我们一般称之为延迟函数,无论正常还是错误defer都会被执行)
func main() { x := 10 defer func() { x++ //这里后打印11 fmt.Println("我后执行:", x) }() //这里先打印10 fmt.Println("我先执行:", x) return }
(2)defer底层实现
type _defer struct { siz int32 // 参数和返回值的内存大小 started bool heap bool // 是否分配在堆上面 openDefer bool // 是否经过开放编码优化 sp uintptr // sp 计数器值,栈指针 pc uintptr // pc 计数器值,程序计数器 fn *funcval // defer 传入的函数地址,也就是延后执行的函数 _panic *_panic // defer 的 panic 结构体 link *_defer // 同一个协程里面的defer 延迟函数,会通过该指针连接在一起 }
defer逆序执行的原因:
link指针指向的是defer单链表的头,每次插入defer都是从表头插入,每次执行也是从表头去取
defer如何实现延迟:
defer代码在编译后会有两个方法,分别负责创建和执行
- 1.
deferproc()
:在defer的声明处调用,将defer函数存于goroutine
的链表中负责保存要执行的函数,称为defer注册- 2.
deferreturn()
,在return指令执行跳出函数前调用,负责将defer函数从链表中取出执行
可以简单理解为在defer声明时插入了一个deferproc()
函数保存数据,在return内部执行退出之前插入后了一个deferreturn()
函数
(3)defer规则
- 1.延迟函数的参数在
defer
语句出现时就已经确定 - 2.延迟函数执行按
后进先出
顺序执行(类似于栈), 即先出现的defer最后执行 - 3.延迟函数可以
操作
主函数的具名返回值
- 4.如果
defer 执行的函数为 nil
, 那么会在最终调用函数的产生 panic
- 5.defer一定要定义在
return或panic之前
,否则会不执行
规则1: 延迟函数的参数在defer语句出现时就已经确定
// 例子1 var a = 1 defer fmt.Println(a) a = 2 return // 这段代码最后会打印1而不是2,如果将defer后改成函数包裹,则输出2 // 例子2 var b = 1 defer func(a int) { b += a fmt.Println(b) }(b + 1) b = 10 return // 猜猜b是几? // 首先defer预加载参数,函数传入的实参为2 // 其次全部执行结束后执行函数此时b为10,所以就是10+2
规则2: 延迟函数执行按后进先出顺序执行, 即先出现的defer最后执行
func main() { x := 10 defer func(x int) { fmt.Println("我最后执行:", x) }(x) defer func(x int) { fmt.Println("我再执行:", x) }(x) x++ fmt.Println("我先执行:", x) return }
规则3: 延迟函数可以操作主函数的具名返回值
func main() { // 打印结果为:2 // return i 并不是一个原子操作 // return会分两步 1. 设值 2 return 所以result为先被赋值为i=1 x := deferTest() fmt.Println(x) } func deferTest() (result int) { i := 1 defer func() { result++ }() return i }
规则4: 如果 defer 执行的函数为 nil, 那么会在最终调用函数的产生 panic
var a func() func deferTest() *int { i := 1 defer a() return &i }
规则5: defer一定要定义在return或panic之前
,否则会不执行
(4)使用场景
一般用于资源的释放和异常的捕捉((比如:文件打开、加锁、数据库连接、异常捕获)
1.当函数执行完毕释放资源时
2.打开网络连接socket的时候
3.连接数据库时需要defer关闭数据库连接,不然会造成连接数过多
4.可以用来捕获
panic
异常,让程序正常执行