# 4. GO语言函数

# 4.1 函数声明与使用

Go语言中,函数属于一等公民,具有以下特点:

  • func关键字开头
  • 函数允许多返回值
  • 支持递归调用
  • 支持同类型的可变参数
  • 函数可以作为变量的值,也可以作为参数和返回值
  • 支持defer,让函数优雅的返回
  • 可以满足接口
  • 支持匿名函数和闭包(closure)

普通函数声明规则:

  • 在同一个包(同一个文件夹下)内,函数名不允许重名
  • 函数名:第一个字母非数字的字母、下划线、数字组成
  • 在参数列表中。多个参数变量,以逗号分隔
  • 命名的返回值变量的默认值为类型的默认值,即数值为0,字符串为空字符串,布尔值为false,引用为nil
  • 参数列表中的变量作为局部变量存在
  • 函数在声明有返回值时,必须在函数体中使用 return 语句提供返回值列表

函数声明包括函数名、形式参数列表、返回值列表(可省略)以及函数体, 语法如下:

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

参数

形式参数列表描述了函数的参数名以及参数类型,这些参数作为局部变量,其值由参数调用者提供,返回值列表描述了函数返回值的变量名以及类型,如果函数返回一个无名变量或者没有返回值,返回值列表的括号是可以省略的。

下面给出几种参数传值的示例, 返回值部分暂时可以忽略:

func f1(x int, y int) int {return x + y}
func f2(x, y int) (sum int){sum = x + y;return}
func f3(x int, _ int) int {return x}
func f4(int, int) int {return 0}

在上面的示例中,f1f2 函数的声明其实是等价的,也就是说如果一组形参或返回值有相同的类型,我们不必为每个形参都写出参数类型; 而函数 f3 中的 _ 空白标识符可以强调某个参数未被使用; f4 函数则不指定形参名称,也代表着后期不适用形参的值,所以直接返回一个固定值0

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

返回值

Go语言支持多返回值,多返回值能方便地获得函数执行后的多个返回参数,返回参数可以不带变量名,也可以带变量名,下面示例将展示多种带返回参数的例子。

  • 单值返回

    func getAdd(x int, y int) int { //不带返回变量名的单个值返回声明
    	addNum := x + y
    	return addNum
    }
    
    func getAdd1(x int, y int) (sum int) { //带返回变量名的单个值返回声明
    	sum = x + y
    	return
    }
    
  • 多值返回

    func getAdd(x int, y int) (int, int, int) { //不带返回变量名的多个值返回声明
    	addNum := x + y
    	return x, y, addNum
    }
    
    func getAdd1(x int, y int) (a int, b int, sum int) { //带返回变量名的多个值返回声明
    	a = x
    	b = y
    	sum = x + y
    	return
    }
    

函数调用

函数调用就不需要过多篇幅说明,几个例子就能说明以切,示例如下(例子的函数调用了参数和返回值篇幅中例子):

sum := f1(1,2) //调用f1函数
sum2 := f2(2,3)//调用f2函数
x,y,sum3 := getAdd(1,2) //调用getAdd函数

基本的参数,返回值和函数调用后,再来讲几个其他操作,第一个是可变参数,第二个是函数作为形参。

可变参数

可变参数是指函数传入的参数个数是可变的,为了做到这点,首先需要将函数定义为可以接受可变参数的类型:

func myFun(args ...int) {
	for _, arg := range args {
		fmt.Println(arg)
	}

}

func main() {
	myFun(1, 23, 4, 5, 7)
}

编译执行:

1
23
4
5
7

上面这段代码的意思是,函数 myFun() 接受不定数量的参数,这些参数的类型全部是 int,形如...type格式的类型只能作为函数的参数类型存在,并且必须是最后一个参数

从内部实现机理上来说,类型...type本质上是一个数组切片,也就是[]type,这也是为什么上面的参数 args 可以用 for 循环来获得每个传入的参数。

上面的例子中将可变参数类型约束为 int,如果你希望传任意类型,可以指定类型为 interface{},改写的示例:

func myFun(data string, args ...interface{}) {
	for _, arg := range args {
		fmt.Println(arg)
	}

}

func main() {
	myFun("hello", 23, "xxxx", 5, 7)
}

函数入参

Go中,函数也被视作一种类型,因此可以同基本类型一样被保存到变量, 也可以作为参数传递给其他函数。示例:

func p(str string) string {
	return str
}

func show(data string, f func(string) string) {
	d := f(data)
	fmt.Println(d)
}

func main() {
	var f1 func(string) string
	f1 = p //函数当变量
	show("hello xjx", p) //函数当参数传输
	show("hello xjx2", f1)
}

将函数作为值传递给函数变量,同样利用这个特性,可以通过把函数赋值给各类型的变量,例如将数据的处理函数赋值给切片类型函数变量,可以对数据进行链式处理等等。

# 4.2 匿名函数

匿名函数,即在需要使用函数时再定义函数,匿名函数没有函数名只有函数体,函数可以作为一种类型被赋值给函数类型的变量,匿名函数也往往以变量方式传递。

  1. 匿名函数定义时调用

    	d := func(x, y int) int {
    		return x + y
    	}(2, 8)
    	fmt.Println(d)
    

    匿名函数在声明后直接调用,将调用后的返回值存储打印。

  2. 匿名函数赋值给变量

    匿名函数可以被赋值,例如:

    	f := func(x, y int) int {
    		return x + y
    	}
    	f(8, 9)
    

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

  3. 匿名函数作为回调函数

    匿名函数作为回调函数的设计在Go语言的系统包中也比较常见,示例如下:

    // 遍历切片的每个元素, 通过给定函数进行元素访问
    func show(list []int, f func(int)) {
    	for _, v := range list {
    		f(v)
    	}
    }
    
    func main() {
    	// 使用匿名函数打印切片内容
    	show([]int{1, 2, 3, 4, 5, 6}, func(i int) {
    		fmt.Println(i)
    	})
    }
    

# 4.3 闭包(Closure)

闭包就是一个函数和与其相关的引用环境组合的一个整体(实体), 闭包是匿名函数的一种特殊类型,它引用在函数本身之外声明的变量。 这也就是说我们在使用函数的时候不是将变量传递给函数,而是使用在函数之外声明的变量。

Go语言中闭包是引用了自由变量的函数,被引用的自由变量和函数一同存在,即使已经离开了自由变量的环境也不会被释放或者删除,在闭包中可以继续使用这个自由变量,因此简单的说就是:

函数 + 引用环境 = 闭包

这与常规函数如何引用全局变量非常相似。 不是直接将这些变量作为参数传递给函数,而是在调用函数时可以访问它们。

【示例】

	var n int = 0
	counter := func() int {
		n++
		return n
	}
	fmt.Println(counter())
	fmt.Println(counter())

编译输出:

1
2

上述例子中 变量 n 会随着函数 counter的定义而一直存在,即使离开了变量环境也不会被被释放或者删除,而如此未经过函数传递,直接使用这个变量,这样就形成了闭包的概念。

闭包中的变量让闭包本身拥有了记忆效应,闭包中的逻辑可以修改闭包捕获的变量,变量会跟随闭包生命期一直存在,闭包本身就如同变量一样拥有了记忆效应,累减器的示例:

// 提供一个值, 每次调用函数会指定对值进行递减
func ReduceCount(x int) func() int {
	return func() int {
		x--
		return x
	}
}

func main() {
	num := ReduceCount(10) // 创建一个递减器, 初始值为10
	fmt.Println(num())     // 递减1, 并打印
	fmt.Println(num())     // 递减1, 并打印
	fmt.Printf("%p\n", &num)
}

编译运行:

9
8
0xc000006028

每调用一次 ReduceCount都会自动对引用的变量进行递减。

# 4.4 延时执行(defer)

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

defer用途主要资源清理类场景,例如:

  • 关闭文件句柄
  • 释放锁资源
  • 数据库连接释放

当有多个 defer 行为被注册时,它们会以逆序执行(类似栈,即后进先出),示例如下:

	fmt.Println("defer begin")
	defer fmt.Println("one")
	defer fmt.Println("two")
	defer fmt.Println("three")
	fmt.Println("defer end")

编译运行:

defer begin
defer end
three
two
one

从上述代码以及运行结果可以看出 defer 具有以下特点:

  • 多个defer语句,会按先进后出的方式执行
  • defer语句在函数退出前才被执行,适合资源清理类的工作

由于defer 语句正好是在函数退出时执行的语句,所以使用 defer 能非常方便地处理资源释放问题。下面举例说明几个资源清理类的示例。

  • 并发解锁

    import (
    	"fmt"
    	"sync"
    )
    
    var (
    	// 一个演示map
    	testMap = make(map[string]int)
    	//并发安全的互斥锁
    	mapLock sync.Mutex
    )
    
    func readData(key string) int {
    	// 对map资源加锁
    	mapLock.Lock()
    	// 对map延迟解锁, defer后面的语句不会马上调用, 而是延迟到函数结束时调用
    	defer mapLock.Unlock()
    	// 取值返回
    	return testMap[key]
    }
    
    func main() {
    	testMap = map[string]int{"one": 1, "two": 2}
    	d := readData("one")
    	fmt.Println(d)
    }
    
  • 释放文件句柄

    import (
    	"os"
    )
    
    func fileSize(file string) int64 {
    	f, err := os.Open(file)
    	if err != nil {
    		return 0
    	}
    	// 延迟调用Close, 此时Close不会被调用
    	defer f.Close()
    	info, err := f.Stat()
    	if err != nil {
    		// defer机制触发, 调用Close关闭文件
    		return 0
    	}
    	size := info.Size()
    	// defer机制触发, 调用Close关闭文件
    	return size
    }
    
最后更新时间: 3/2/2023, 4:55:04 PM