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
异常,让程序正常执行