8、函数

函数

函数构成了代码执行的逻辑结构,在Go语言中,函数的基本组成为:关键字 func、函数名、参数列表、返回值、函数体、返回语句,每一个程序都包含很多的函数,函数是基本的代码块。

因为Go语言是编译型语言,所以函数编写的顺序是无关紧要的,鉴于可读性的需求,最好把main()函数写在文件的前面,其他函数按照一定逻辑顺序进行编写(例如函数被调用的顺序)。

Go语言里面拥三种类型的函数:

普通函数

定义

函数声明包括函数名参数列表返回值列表(可省略)以及函数体

func 函数名 (参数列表) (返回值列表) {
    return 函数体
}

如果一个函数在声明时,包含返回值列表,那么该函数必须以 return 语句结尾,除非函数明显无法运行到结尾处。

在函数中,实参通过值传递的方式进行传递,因此函数的形参是实参的拷贝,对形参进行修改不会影响实参,但是,如果实参包括引用类型,如指针、slice(切片)、map、function、channel 等类型,实参可能会由于函数的间接引用被修改。

参数列表

同类型参数

多个同类型参数可以省略前面的参数的类型声明,此时的参数a类型为string,参数b、c类型为int

func funcname(a string, b, c int) {}

可变参数

本质上是一个切片,也就是[]int,可变参数必须位于参数列表的最后

func funcname(args ...int) {}

返回值

注意:在函数有多个返回值时,只要有一个返回值有指定命名,其他的也必须有命名。如果返回值有有多个返回值必须加上括号; 如果只有一个返回值并且有命名也需要加上括号。

package main

import "fmt"

func main() {
	fmt.Println(add(1, 2)) // 3
	fmt.Println(red(1, 2)) // -1
	x, y := getPoint()
	fmt.Println(x, y) //52 13
}

func add(a int, b int) int {
	return a + b
}
// 函数声明时,可以给返回值声明一个变量,在函数体内进行操作
func red(a int, b int) (c int) {
	c = a - b
	return
}
// 函数支持返回多个返回值
func getPoint() (int, int) {
	return 52, 13
}

函数变量

在Go语言中,函数也是一种类型,可以和其他类型一样保存在变量中

package main

import "fmt"

func main() {
	// 声明一个函数类型的变量
	var f1 func(int, int) int
	// 将函数add赋值给f2
	f1 = add
	// 自动类型推导
	f2 := add
	fmt.Println(f1(1, 2)) // 3
	fmt.Println(f2(1, 2)) // 3
}

func add(a int, b int) int {
	return a + b
}

匿名函数

匿名函数是指不需要定义函数名的一种函数实现方式,由一个不带函数名的函数声明和函数体组成,匿名函数可以声明在另一个函数的函数体中

func (参数列表) (返回参数列表) {
    函数体
}

匿名函数的用途非常广泛,它本身就是一种值,可以方便地保存在各种容器中实现回调函数和操作封装。

// 定义的同时进行调用
func(a int, b int) {
  fmt.Println(a + b)
}(1, 2)

// 匿名函数赋值给变量
f := func(a int, b int) int {
    return a + b
}
fmt.Println(f(1, 2))

作为回调函数

package main

import "fmt"

func main() {
	names := []string{"tom", "lucy", "lily"}
	f := func(name string) {
		fmt.Printf("%s,Hello\n", name)
	}
	// 调用函数,并传入回调函数
	handlerNames(names, f)
}

// 遍历名单,对名字执行操作
func handlerNames(names []string, f func(string)) {
	for _, k := range names {
		f(k)
	}
}

闭包

Go语言中闭包是引用了另一个函数局部变量的函数,被引用的局部变量和闭包函数一同存在,即使已经离开了局部变量的环境也不会被释放或者删除,在闭包中可以继续使用这个局部变量。

同一个函数与不同引用环境组合,可以形成不同的实例。

img

一个函数类型就像结构体一样,可以被实例化,函数本身不存储任何信息,只有与引用环境结合后形成的闭包才具有记忆性

函数是编译期静态的概念,而闭包是运行期动态的概念。

例如,实现一个累加器

package main

import "fmt"

func main() {
	f := add()
	fmt.Println(f()) //1
	fmt.Println(f()) //2
	fmt.Println(f()) //3
	fmt.Println(f()) //4
}

func add() func() int {
	i := 0
	return func() int {
		i++
		return i
	}
}

defer关键字

Go语言的 defer 语句会将其后面跟随的语句进行延迟处理,在 defer所属的函数即将返回时,将延迟处理的语句按 defer声明的逆序进行执行。也就是说,先被 defer 的语句最后被执行;最后被 defer 的语句,最先被执行。

在同一个函数中,defer可以看作为栈操作,也就是先注册的defer语句在栈底,所以后进先出

关键字 defer 的用法类似于面向对象编程语言Java的 finally 语句块,它一般用于释放某些已分配的资源,典型的例子就是对一个互斥解锁,或者关闭一个文件。

package main

import "fmt"

func main() {
	fmt.Println("start")
	defer fmt.Println("end")
	defer fmt.Println(3)
	defer fmt.Println(2)
	defer fmt.Println(1)
	/*
		start
		1
		2
		3
		end
	*/
}

defer执行匿名函数

package main

import "fmt"

func main() {
	defer func() {
		fmt.Println("hello")
		fmt.Printf("world")
	}()
}

即时求值的变量快照

程序执行到defer语句的时候(而不是defer语句执行的时候),如果defer后调用的函数需要传参,那么就会把变量值传递给函数。

package main

import "fmt"

func main() {
	name := "go"
	defer func(n string) {
		fmt.Println(n) // go
	}(name)
	name = "python"
}

但是,如果defer后面是无参的匿名函数,那么程序无参可传,就会继续往下执行。直到defer语句执行的时候,才会去获取变量值。

package main

import "fmt"

func main() {
	name := "go"
	defer func() {
		fmt.Println(name) // python
	}()
	name = "python"
}

defer和return

在Go语言中,return语句在defer前执行

package main

import "fmt"

func main() {
	compare()
}

func compare() int {
	defer deferFunc()
	return returnFunc()
}

func deferFunc() int {
	fmt.Println("defer")
	return 0
}

func returnFunc() int {
	fmt.Println("return")
	return 0
}

/*
	return
	defer
*/

虽然return语句是在defer语句前执行,但是对于底层,return语句并不是原子操作,它分为给返回值赋值和RET指令两步。defer语句的执行时机就在返回值赋值操作后,RET指令执行前。

image-20240119164319340

package main

import "fmt"

func main() {
	fmt.Println(ff()) // 0
	fmt.Println(tt()) // 1
}

/*
ff()的return过程如下:
1、返回值赋值:returnVariable = (a = 0)
2、执行defer:a++
3、RET指令执行,函数结束
第二步a++实际是改变了局部变量a,而并没有改变返回值returnVariable,所以返回值还是0
*/
func ff() int {
	a := 0
	defer func() {
		a++
	}()
	return a
}

/*
tt()的return过程如下:
1、返回值赋值:a = 0(根据Go语言规则,类型声明默认赋值为零值)
2、执行defer:a++
3、RET指令执行,函数结束
由于是命名返回值,所以返回值就是变量a,第二步a++改变了返回值a,所以返回值是1
*/
func tt() (a int) {
	defer func() {
		a++
	}()
	return
}

内置函数

Go 语言拥有一些不需要进行导入操作就可以使用的内置函数。它们有时可以针对不同的类型进行操作,例如:len、cap 和 append,或必须用于系统级的操作,例如:panic。因此,它们需要直接获得编译器的支持。

append            -- 用来追加元素到数组、slice中,返回修改后的数组、slice
close             -- 主要用来关闭channel
delete            -- 从map中删除key对应的value
panic             -- 停止常规的goroutine  (panic和recover:用来做错误处理)
recover           -- 允许程序定义goroutine的panic动作
real              -- 返回complex的实部   (complex、real imag:用于创建和操作复数)
imag              -- 返回complex的虚部
make              -- 用来分配内存,返回Type本身(只能应用于slice, map, channel)
new               -- 用来分配内存,主要用来分配值类型指针,比如*int、*string。
cap               -- capacity是容量的意思,用于返回某个类型的最大容量(只能用于切片和 map)
copy              -- 用于复制和连接slice,返回复制的数目
len               -- 来求长度,比如string、array、slice、map、channel ,返回长度
print、println     -- 底层打印函数,在开发中建议使用 fmt 包