Go语言入门之函数

avatar
作者
猴君
阅读量:3

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, intint) { 	// 初始血量为100 	hp := 100 	// 返回创建的闭包 	return func(x int) (string, intint) {         // 可以捕获变量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异常,让程序正常执行

广告一刻

为您即时展示最新活动产品广告消息,让您随时掌握产品活动新动态!