函数指:可重复使用的、用于执行指定任务的代码块。Go语言中支持函数、匿名函数和闭包,函数在Go语言中属于“一等公民”。

函数定义#

函数定义格式如下:

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

注意:

  • 函数名:由字母、数字、下划线组成。但函数名的第一个字母不能是数字。同一个包内函数名也称不能重名
  • 参数:参数由参数变量和参数变量的类型组成,多个参数之间使用 , 分隔
  • 返回值:返回值由返回值变量和其变量类型组成,也可以只写返回值的类型,多个返回值必须用 () 包裹,并用 , 分隔
  • 函数体:实现指定功能的代码块

比如:定义求两数之和的函数:

func func1(x int, y int) int {
	return x + y
}

函数的参数和返回值都是可选的,比如:可以定义无参数无返回值的函数:

func func2() {
	fmt.Println("hello world")
}

函数调用#

通过 函数名() 的方式调用函数,比如:

func main() {
	sum := func1(1, 2)
    func2()
	fmt.Println(sum)
}

参数#

类型简写#

函数的参数中如果相邻变量的类型相同,则可以省略类型,比如:

func func4(x, y int) int {
	return x + y
}

可变参数#

可变参数是指函数的参数数量不固定。可变参数通过在参数名后加...标识。比如:

func func5(x ...int) int {
	sum := 0
	for _, v := range x {
		sum += v
	}
	return sum
}

调用:

	a := func5(1)
	b := func5(2, 3)
	c := func5(2, 3, 4)
	fmt.Println(a, b, c) // 1 5 9

固定参数搭配可变参数使用时,可变参数要放在固定参数的后面,比如:

func func6(x int, y ...int) int {
	for _, v := range y {
		x += v
	}
	return x
}

本质上,函数的可变参数是通过切片实现的。

返回值#

Go语言中通过 return 关键字向外输出返回值。

多返回值#

Go语言中函数支持多返回值,函数如果有多个返回值时必须用 () 将所有返回值包裹起来。比如:

func func7(x, y int) (int, int) {
	sum := x + y
	sub := x - y
	return sum, sub
}

返回值命名#

函数定义时可以给返回值命名,并在函数体中直接使用这些变量,最后通过 return 关键字返回。比如:

func func8(x, y int) (sum, sub int) {
	sum = x + y
	sub = x - y
	// 这里可以直接return,不需要指定sum和sub
	return sum, sub
}

返回值补充#

当函数返回值类型为 slice 时,nil 可以看做是一个有效的 slice ,没必要显式返回一个长度为0的切片。

func func9(x string) []int {
	if x == "" {
		return nil // 没必要返回[]int{}
	}
	return []int{0}
}

变量作用域#

全局变量#

全局变量是定义在函数外部的变量,在程序整个运行周期内都有效。 在函数中可以访问到全局变量。

package main

import "fmt"

// 定义全局变量value
var value int64 = 10

func test() {
	fmt.Printf("value=%d\n", value) // 函数中可以访问全局变量value
}
func main() {
	test() // value=10
}

局部变量#

局部变量分为两种:

  • 函数内定义的变量无法在该函数外使用
  • 如果局部变量和全局变量重名,优先访问局部变量
  • 语句块定义的变量,只在 ifforswitch 语句内有效

函数类型与变量#

定义函数类型#

可以使用 type 关键字来定义一个函数类型,比如:

type calculation func(int, int) int

上面定义了一个 calculation 类型,它是一种函数类型,接收两个 int 类型的参数并且返回一个 int 类型的返回值。凡是满足这个条件的函数都是 calculation 类型的函数,例如下面的 sumsub 都是 calculation 类型。

// calculation类型的函数sum
func sum(x, y int) int {
	return x + y
}

// calculation类型的函数sub
func sub(x, y int) int {
	return x - y
}

sumsub 都能赋值给 calculation 类型的变量。

var calc1 calculation = sum
var calc2 calculation = sub

函数类型变量#

可以声明函数类型的变量并且为该变量赋值:

func main() {
	var a calculation = sum
	fmt.Printf("type of a:%T\n", a) // type of c:main.calculation
	fmt.Println(a(1, 2))            // 像调用sum一样调用a

	var b calculation = sub
	fmt.Printf("type of b:%T\n", b) // type of c:main.calculation
	fmt.Println(b(1, 2))            // 像调用sub一样调用b
}
type of a:main.calculation
3
type of b:main.calculation
-1

高阶函数#

函数作为参数#

// 将函数作为参数传递,该函数接收两个int类型变量x/y,一个函数参数sum。
func functionAsArgument(x, y int, sum func(int, int) int) int {
	return sum(x, y)
}

func func1(x int, y int) int {
	return x + y
}

func functionAsArgumentTest() {
	ret2 := functionAsArgument(10, 20, func1)
	fmt.Println(ret2) //30
}

函数作为返回值#

// 接收一个切片参数patients,返回一个函数
func functionAsTheReturnValue(patients []string) func(string) bool {
	// 定义匿名函数并返回
	return func(name string) bool {
		for _, soul := range patients {
			if soul == name {
				return true
			}
		}
		return false
	}
}

func functionAsTheReturnValueTest() {
	testValue := []string{"a", "b", "c", "d", "e", "f"}
	result := functionAsTheReturnValue(testValue)
	// 调用筛选器函数获取字母是否已存在
	fmt.Println(result("a"))  // true
	fmt.Println(result("ff")) // false
}

匿名函数#

已经知道函数可以作为返回值,但是在Go语言函数内部不能再像之前那样定义函数了,只能定义匿名函数。匿名函数就是没有函数名的函数,匿名函数的定义格式如下:

func(参数)(返回值){
    函数体
}

匿名函数因为没有函数名,所以没办法像普通函数那样调用,所以匿名函数需要保存到某个变量或者作为立即执行函数:

func anonymousFunc() {
	// 将匿名函数保存到变量
	add := func(x, y int) {
		fmt.Println(x + y)
	}
	// 通过变量调用匿名函数
	add(1, 2)

	// 匿名函数作为立即执行函数,一般用于匿名函数只用于一次的情况下就不需要再指定变量存储
	func(x, y int) {
		fmt.Println(x - y)
	}(1, 2)
}

匿名函数多用于实现回调函数和闭包。

闭包#

闭包是函数式编程语言中的概念。指内层函数引用了外层函数中的变量或称为引用了自由变量(全局变量)的函数,其返回值也是一个函数。在Go语言中可以理解为匿名函数与其所引用环境的组合

闭包只是在形式和表现上像函数,但实际上不是函数。函数是一些可执行的代码,这些代码在函数被定义后就确定了,不会在执行时发生变化,所以一个函数只有一个实例。闭包在运行时可以有多个实例,不同的引用环境和相同的函数组合可以产生不同的实例。所谓引用环境是指在程序执行中的某个点所有处于活跃状态的约束所组成的集合。其中的约束是指一个变量的名字和其所代表的对象之间的联系。那么为什么要把引用环境与函数组合起来呢?这主要是因为在支持嵌套作用域的语言中,有时不能简单直接地确定函数的引用环境。这样的语言一般具有这样的特性

函数是一等公民(First-class value),即函数可以作为另一个函数的返回值或参数,还可以作为一个变量的值。 函数可以嵌套定义,即在一个函数内部可以定义另一个函数。

比如:

func add() func(int) int {
	var x int
	return func(y int) int {
		x += y
		return x
	}
}

func addTest() {
	var f = add()
	fmt.Println(f(10)) //10
	fmt.Println(f(20)) //30
	fmt.Println(f(30)) //60
}

上述代码中

	var x int
	return func(y int) int {
		x += y
		return x
	}

此时 f 就是一个闭包,f 不仅仅是存储了一个函数的返回值,同时存储了一个闭包的状态。该状态会一直存在外部被赋值的变量 f 中,直到 f 被销毁,整个闭包才被销毁。

再看一个例子:

func add2(x int) func(int) int {
	return func(y int) int {
		x += y
		return x
	}
}

func add2Test() {
	var f = add2(10)
	fmt.Println(f(10)) //20
	fmt.Println(f(20)) //40
	fmt.Println(f(30)) //70
}

函数 add2 返回了一个函数,返回的这个函数就是一个闭包。这个函数本身中没有定义变量 x 的,而是引用了它所在的环境(函数 add2)中的变量 x

每调用一次函数 add2,就形成了一个新的环境,对应的闭包中,函数都是同一个函数,环境却是引用不同的环境。变量 x 是函数 add2 中的局部变量,这个变量不会在函数 add2 的栈中分配,因为函数 add2 返回后,对应的栈就失效了,add2 返回的那个函数中变量 x 就引用了一个失效的位置。所以闭包的环境中引用的变量不能够在栈上分配。

闭包的陷阱#

TODO

defer 语句#

Go语言中 defer 语句会将其后面跟随的语句进行延迟处理。在 defer 归属的函数即将返回时,将延迟处理的语句按 defer 定义的逆序执行。先被 defer 的语句最后被执行,最后被 defer 的语句,最先被执行。比如:

// defer将后面的语句延迟到函数即将退出时逆序执行,一般常用于资源释放
func deferTest() {
	fmt.Println("start")
	defer fmt.Println("...")
	fmt.Println("end")
}
start
end
...

函数中存在多个 defer 时,逆序执行后进先出,比如:

func deferTest2() {
	fmt.Println("start")
	defer fmt.Println("1111")
	defer fmt.Println("2222")
	defer fmt.Println("3333")
	fmt.Println("end")
}
start
end  
3333
2222
1111

由于 defer 语句延迟调用的特性,所以 defer 语句一般用于处理资源释放问题。比如:资源清理、文件关闭、解锁及记录时间等。

defer 执行时机#

Go语言的函数中 return 语句在底层并不是原子操作,它分为两步:

  1. 给返回值赋值
  2. 执行 RET 指令

defer 语句执行的时机是在返回值赋值操作后,执行 RET 指令前。如下图所示:

defer执行时机