# 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}
在上面的示例中,f1
和 f2
函数的声明其实是等价的,也就是说如果一组形参或返回值有相同的类型,我们不必为每个形参都写出参数类型; 而函数 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 匿名函数
匿名函数,即在需要使用函数时再定义函数,匿名函数没有函数名只有函数体,函数可以作为一种类型被赋值给函数类型的变量,匿名函数也往往以变量方式传递。
匿名函数定义时调用
d := func(x, y int) int { return x + y }(2, 8) fmt.Println(d)
匿名函数在声明后直接调用,将调用后的返回值存储打印。
匿名函数赋值给变量
匿名函数可以被赋值,例如:
f := func(x, y int) int { return x + y } f(8, 9)
匿名函数的用途非常广泛,它本身就是一种值,可以方便地保存在各种容器中实现回调函数和操作封装。
匿名函数作为回调函数
匿名函数作为回调函数的设计在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 }