1 介绍
每一个程序都包含很多的函数:函数是基本的代码块。
Go是编译型语言,所以函数编写的顺序是无关紧要的;鉴于可读性的需求,最好把 main()
函数写在文件的前面,其他函数按照一定逻辑顺序进行编写(例如函数被调用的顺序)。
编写多个函数的主要目的是将一个需要很多行代码的复杂问题分解为一系列简单的任务(那就是函数)来解决。而且,同一个任务(函数)可以被调用多次,有助于代码重用。
(事实上,好的程序是非常注意DRY原则的,即不要重复你自己(Don't Repeat Yourself),意思是执行特定任务的代码只能在程序里面出现一次。)
Go 里面有三种类型的函数:
- 普通的带有名字的函数
- 匿名函数或者lambda函数
- 方法(Methods)
除了main()、init()函数外,其它所有类型的函数都可以有参数与返回值。函数参数、返回值以及它们的类型被统称为函数签名。
函数重载(function overloading)指的是可以编写多个同名函数,只要它们拥有不同的形参与/或者不同的返回值,在 Go 里面函数重载是不被允许的。这将导致一个编译错误:
funcName redeclared in this book, previous declaration at lineno
Go 语言不支持这项特性的主要原因是函数重载需要进行多余的类型匹配影响性能;没有重载意味着只是一个简单的函数调度。所以你需要给不同的函数使用不同的名字,我们通常会根据函数的特征对函数进行命名
如果需要申明一个在外部定义的函数,你只需要给出函数名与函数签名,不需要给出函数体:
func flushICache(begin, end uintptr) // implemented externally
函数也可以以申明的方式被使用,作为一个函数类型,就像:
type binOp func(int, int) int
目前 Go 没有泛型(generic)的概念,也就是说它不支持那种支持多种类型的函数。不过在大部分情况下可以通过接口(interface),特别是空接口与类型选择(type switch)与/或者通过使用反射(reflection)来实现相似的功能。使用这些技术将导致代码更为复杂、性能更为低下,所以在非常注意性能的的场合,最好是为每一个类型单独创建一个函数,而且代码可读性更强。
2. 按值传递(call by value) 按引用传递(call by reference)
Go 默认使用按值传递来传递参数,也就是传递参数的副本。函数接收参数副本之后,在使用变量的过程中可能对副本的值进行更改,但不会影响到原来的变量,比如 Function(arg1)
。
如果你希望函数可以直接修改参数的值,而不是对参数的副本进行操作,你需要将参数的地址(变量名前面添加&符号,比如 &variable)传递给函数,这就是按引用传递,比如 Function(&arg1)
,此时传递给函数的是一个指针。如果传递给函数的是一个指针,指针的值(一个地址)会被复制,但指针的值所指向的地址上的值不会被复制;我们可以通过这个指针的值来修改这个值所指向的地址上的值。(译者注:指针也是变量类型,有自己的地址和值,通常指针的值指向一个变量的地址。所以,按引用传递也是按值传递。)
几乎在任何情况下,传递指针(一个32位或者64位的值)的消耗都比传递副本来得少。
在函数调用时,像切片(slice)、字典(map)、接口(interface)、通道(channel)这样的引用类型都是默认使用引用传递(即使没有显式的指出指针)。
有些函数只是完成一个任务,并没有返回值。我们仅仅是利用了这种函数的副作用,就像输出文本到终端,发送一个邮件或者是记录一个错误等。
但是绝大部分的函数还是带有返回值的。
3 传递变长参数
如果函数的最后一个参数是采用 ...type
的形式,那么这个函数就可以处理一个变长的参数,这个长度可以为 0,这样的函数称为变参函数。
func myFunc(a, b, arg ...int) {}
如果参数被存储在一个 slice 类型的变量 slice
中,则可以通过 slice...
的形式来传递参数,调用变参函数。
package main
import "fmt"
func main() {
x := min(1, 3, 2, 0)
fmt.Printf("The minimum is: %d\n", x)
slice := []int{7,9,3,5,1}
x = min(slice...)
fmt.Printf("The minimum in the slice is: %d", x)
}
func min(s ...int) int {
if len(s)==0 {
return 0
}
min := s[0]
for _, v := range s {
if v < min {
min = v
}
}
return min
}
4 defer 和追踪
关键字 defer 允许我们推迟到函数返回之前(或任意位置执行 return
语句之后)一刻才执行某个语句或函数(为什么要在返回之后才执行这些语句?因为 return
语句同样可以包含一些操作,而不是单纯地返回某个值)。
关键字 defer 的用法类似于面向对象编程语言 Java 和 C# 的 finally
语句块,它一般用于释放某些已分配的资源。
package main
import "fmt"
func main() {
function1()
}
func function1() {
fmt.Printf("In function1 at the top\n")
defer function2()
fmt.Printf("In function1 at the bottom!\n")
}
func function2() {
fmt.Printf("Function2: Deferred until the end of the calling function!")
}
当有多个 defer 行为被注册时,它们会以逆序执行(类似栈,即后进先出):
func f() {
for i := 0; i < 5; i++ {
defer fmt.Printf("%d ", i)
}
}
上面的代码将会输出:4 3 2 1 0
。
5 内置函数
Go 语言拥有一些不需要进行导入操作就可以使用的内置函数。它们有时可以针对不同的类型进行操作,例如:len、cap 和 append,或必须用于系统级的操作,例如:panic。因此,它们需要直接获得编译器的支持。
名称 | 说明 |
---|---|
close | 用于管道通信 |
len、cap | len 用于返回某个类型的长度或数量(字符串、数组、切片、map 和管道);cap 是容量的意思,用于返回某个类型的最大容量(只能用于切片和 map) |
new、make | new 和 make 均是用于分配内存:new 用于值类型和用户定义的类型,如自定义结构,make 用于内置引用类型(切片、map 和管道)。它们的用法就像是函数,但是将类型作为参数:new(type)、make(type)。new(T) 分配类型 T 的零值并返回其地址,也就是指向类型 T 的指针(详见第 10.1 节)。它也可以被用于基本类型:v := new(int) 。make(T) 返回类型 T 的初始化之后的值,因此它比 new 进行更多的工作(详见第 7.2.3/4 节、第 8.1.1 节和第 14.2.1 节)new() 是一个函数,不要忘记它的括号 |
copy、append | 用于复制和连接切片 |
panic、recover | 两者均用于错误处理机制 |
print、println | 底层打印函数(详见第 4.2 节),在部署环境中建议使用 fmt 包 |
complex、real imag | 用于创建和操作复数(详见第 4.5.2.2 节) |
6 递归函数
当一个函数在其函数体内调用自身,则称之为递归。最经典的例子便是计算斐波那契数列,即前两个数为1,从第三个数开始每个数均为前两个数之和。
package main
import "fmt"
func main() {
result := 0
for i := 0; i <= 10; i++ {
result = fibonacci(i)
fmt.Printf("fibonacci(%d) is: %d\n", i, result)
}
}
func fibonacci(n int) (res int) {
if n <= 1 {
res = 1
} else {
res = fibonacci(n-1) + fibonacci(n-2)
}
return
}
7 将函数作为参数
函数可以作为其它函数的参数进行传递,然后在其它函数内调用执行,一般称之为回调。下面是一个将函数作为参数的简单例子(function_parameter.go):
package main
import (
"fmt"
)
func main() {
callback(1, Add)
}
func Add(a, b int) {
fmt.Printf("The sum of %d and %d is: %d\n", a, b, a+b)
}
func callback(y int, f func(int, int)) {
f(y, 2) // this becomes Add(1, 2)
}
8 闭包
当我们不希望给函数起名字的时候,可以使用匿名函数,例如:func(x, y int) int { return x + y }
。
下面是一个计算从 1 到 1 百万整数的总和的匿名函数:
func() {
sum := 0
for i := 1; i <= 1e6; i++ {
sum += i
}
}()
defer 语句和匿名函数
关键字 defer
(详见第 6.4 节)经常配合匿名函数使用,它可以用于改变函数的命名返回值。
匿名函数还可以配合 go
关键字来作为 goroutine 使用(详见第 14 章和第 16.9 节)。
匿名函数同样被称之为闭包(函数式语言的术语):它们被允许调用定义在其它环境下的变量。闭包可使得某个函数捕捉到一些外部状态,例如:函数被创建时的状态。另一种表示方式为:一个闭包继承了函数所声明时的作用域。这种状态(作用域内的变量)都被共享到闭包的环境中,因此这些变量可以在闭包中被操作,直到被销毁,详见第 6.9 节中的示例。闭包经常被用作包装函数:它们会预先定义好 1 个或多个参数以用于包装,详见下一节中的示例。另一个不错的应用就是使用闭包来完成更加简洁的错误检查(详见第 16.10.2 节)。
9 应用闭包:将函数作为返回值
在程序 function_return.go
中我们将会看到函数 Add2 和 Adder 均会返回签名为 func(b int) int
的函数
func Add2() (func(b int) int)
func Adder(a int) (func(b int) int)
函数 Add2 不接受任何参数,但函数 Adder 接受一个 int 类型的整数作为参数。
我们也可以将 Adder 返回的函数存到变量中(function_return.go)。
package main
import "fmt"
func main() {
// make an Add2 function, give it a name p2, and call it:
p2 := Add2()
fmt.Printf("Call Add2 for 3 gives: %v\n", p2(3))
// make a special Adder function, a gets value 2:
TwoAdder := Adder(2)
fmt.Printf("The result is: %v\n", TwoAdder(3))
}
func Add2() func(b int) int {
return func(b int) int {
return b + 2
}
}
func Adder(a int) func(b int) int {
return func(b int) int {
return a + b
}
}