函数
函数在 Go 语言中地位非常重要。
函数的声明
Go 中的函数定义格式为:
func 函数名(参数列表) (返回值列表) {
// do something
}
例如:
func add(a int, b int) int {
return a + b
}
如果参数类型相同,可以简写为:
func add(a, b int) int {
return a + b
}
func main() {
fmt.Println(add(1, 2))
}
Go 支持多个返回值,例如:
// 求和并求差
func sumAndSub(a, b int) (int, int) {
return a + b, a - b
}
func main() {
sum, sub := sumAndSub(1, 2)
fmt.Println(sum, sub)
}
和 JS 类似,我们可以通过声明变量的形式来声明函数:
func main() {
add := func(a, b int) int {
return a + b
}
fmt.Println(add(1, 2))
}
这个函数被称为匿名函数,因为没有函数名。
函数的参数
正如之前提到的,函数的参数传入时的策略是完全复制传入的参数。
对于值类型,传入的是值的拷贝。对于引用类型,传入的是引用的拷贝。在函数中修改值类型的参数不会影响原值,但是修改引用类型的参数会影响原值。
package main
import "fmt"
func changeElement(arr [5]int) {
arr[0] = 10
}
func changeElementByPointer(arr *[5]int) {
arr[0] = 10
}
func main() {
arr := [5]int{1, 2, 3, 4, 5}
changeElement(arr)
fmt.Println("changeElement:", arr) // [1 2 3 4 5]
changeElementByPointer(&arr)
fmt.Println("changeElementByPointer:", arr) // [10 2 3 4 5]
}
在考虑修改值时,我们可以依然传入值类型,无非是将修改后的内容返回。
package main
import "fmt"
func changeElement(arr [5]int) [5]int {
arr[0] = 10
return arr
}
func main() {
arr := [5]int{1, 2, 3, 4, 5}
arr = changeElement(arr)
fmt.Println(arr) // [10 2 3 4 5]
}
函数的返回值
Go 中的函数支持多个返回值,例如在这个求最大值和最小值的函数中:
func maxAndMin(arr []int) (int, int) {
max, min := arr[0], arr[0]
for _, v := range arr {
if v > max {
max = v
}
if v < min {
min = v
}
}
return max, min
}
func main() {
arr := []int{1, 2, 3, 4, 5}
max, min := maxAndMin(arr)
fmt.Println(max, min)
}
返回的多个值可以被忽略:
func main() {
max, _ := maxAndMin(arr)
fmt.Println(max)
}
我们也可以在返回的声明中提前声明返回值的变量名,这样我们就不用操心需要返回的变量名了:
func maxAndMin(arr []int) (max int, min int) { // 这里声明了返回值的变量名
max, min = arr[0], arr[0]
for _, v := range arr {
if v > max {
max = v
}
if v < min {
min = v
}
}
return // 这里不需要再写返回值
}
很多编程语言不支持多个返回值,然而 Go 天然支持多个返回值。
将函数作为变量
有返回值的函数可以被赋值给变量:
func add(a, b int) int {
return a + b
}
func main() {
addFunc := add // 将 add 函数赋值给 addFunc 变量,变量就具有了函数的功能
fmt.Println(addFunc(1, 2)) // 3
}
这样看上去毫无意义!但是在日常开发中我们常常提到开闭原则,即对于扩展我们是开放的,对于修改我们是封闭的。这样的设计可以让我们在不修改原有代码的情况下,扩展功能。试想有这样的场景:
我们希望完成一个 operate
函数,这个函数接受两个参数,一个是操作符,一个是操作数,然后根据操作符进行操作。一般来讲我们可以这样写:
package main
import "fmt"
func add(a, b int) int {
return a + b
}
func sub(a, b int) int {
return a - b
}
func mul(a, b int) int {
return a * b
}
func div(a, b int) int {
return a / b
}
func operate(op string, a, b int) (res int) {
switch op {
case "+":
res = add(a, b)
case "-":
res = sub(a, b)
case "*":
res = mul(a, b)
case "/":
res = div(a, b)
}
return
}
现在的代码毫无问题,但是如果我们希望添加一个 mod
函数,我们就需要修改 operate
函数,这样就违反了开闭原则。我们的修改可能会影响到其他地方,这是我们不希望看到的。
我们可以将函数暂存到一个 map 中,然后根据操作符取出对应的函数:
package main
import "fmt"
var operateFuncs make(map[string]func(x, y int) int) // 定义一个 map,key 为 string,value 为函数(func(x, y int) int)
func init() {
operateFuncs["+"] = add
operateFuncs["-"] = sub
operateFuncs["*"] = mul
operateFuncs["/"] = div
operateFuncs["%"] = mod
}
func add(a, b int) int {
return a + b
}
func sub(a, b int) int {
return a - b
}
func mul(a, b int) int {
return a * b
}
func div(a, b int) int {
return a / b
}
func mod(a, b int) int {
return a % b
}
func operate(op string, a, b int) int {
if f, ok := operateFuncs[op]; ok {
return f(a, b)
}
return 0
}
func main() {
fmt.Println(operate("+", 1, 2)) // 3
fmt.Println(operate("-", 1, 2)) // -1
fmt.Println(operate("*", 1, 2)) // 2
fmt.Println(operate("/", 1, 2)) // 0
fmt.Println(operate("%", 1, 2)) // 1
}
这样我们就可以在不修改 operate
函数的情况下,添加新的操作符。
注:上面的
if f, ok := operateFuncs[op]; ok
是 Go 语言中的一个特殊写法,f
是operateFuncs[op]
的值,ok
是一个布尔值,表示是否存在这个值这是 map 会携带的一个返回值。其写法可以拆解为:f, ok := operateFuncs[op] if ok { return f(a, b) } return 0
匿名函数和闭包
如果一个函数只在特定位置出现且不用考虑复用,我们可以使用匿名函数。
func main() {
add := func(a, b int) int {
return a + b
}
fmt.Println(add(1, 2))
}
或者:
func invoke(f func()) {
fmt.Println("before")
f()
fmt.Println("after")
}
func main() {
invoke(func() {
fmt.Println("invoke")
})
}
// before
// invoke
// after
闭包是匿名函数的一个重要特性,它会引用外部环境中的变量,其内部操作会对外部环境产生副作用。闭包相当于封装了一个环境。例如可通过闭包修改局部变量的值:
func main() {
i := 0
add := func() {
i++
}
add()
fmt.Println(i) // 1
}
如果变量 i 只与闭包函数有关,那么我们可以抽取闭包函数:
func getAnonymouseFunc() func() {
i := 0
return func() {
i++
fmt.Println(i)
}
}
func main() {
add := getAnonymouseFunc()
add() // 1
add() // 2
}
这与我们想象的不同,我们可能会认为每次调用 add
函数都会初始化 i
为 0,但是这里我们接连调用两次 add
函数,i
的值会保留且递增。这里就能凸显闭包的意义。
即闭包中的局部变量与普通函数不同,它内部内存分配在堆上,而不是栈上,所以不会随着函数的调用而销毁。由于匿名函数一致引用了 i
,所以 i
的也不会被垃圾回收。正如“闭包”一词所言,它封闭了一个环境,形成了一个独立的小王国,具有一直驻留在内存的环境变量的特性。
举一个更贴近实际的例子,当我们需要一个计数器时,例如用户访问一次网站,计数器加一,我们可以使用闭包:
func counter() func() int {
i := 0
return func() int {
i++
return i
}
}
func main() {
c := counter()
fmt.Println(c()) // 1
fmt.Println(c()) // 2
fmt.Println(c()) // 3
}
这和直接使用全局变量有什么区别呢?闭包的好处在于封装,我们可以将计数器的逻辑封装在一个函数中,而不用担心外部环境对计数器的影响。
强制转换
Go 提供了函数的强制转换语法。
package main
import "fmt"
type add func(a, b int) int
func addFunc(a, b int) int {
return a + b
}
func main() {
var f add = addFunc
fmt.Println(f(1, 2))
}
这里我们定义了一个 add
类型,它是一个函数类型,接受两个 int
类型的参数,返回一个 int
类型的值。然后我们定义了一个 addFunc
函数,它符合 add
类型的定义。最后我们将 addFunc
函数赋值给 f
变量,这样 f
就具有了 add
类型的功能。
假设有下面的函数类型 F1:
type F1 func(int, int) int
创建一个函数 A1:
func A1(c,d int) int {
return c+d
}
A1 遵循 F1 的签名。
a1 := F1(A1)
a2 := F1(A1)
通过上面的操作,我们得到了两个函数对象 a1 和 a2,它们都是 F1 类型的。我们可以对 a1 和 a2 分别进行调用:
fmt.Println(a1(1,2)) // 3
fmt.Println(a2(3,4)) // 7
有了类型和强制转换之后,我们不仅可以像传递普通变量一样传递函数对象,还可与为函数绑定方法,而绑定后的方法可以被函数对象调用,相当于为函数拓展了功能。如:
type F1 func(a,b int) int
func (f F1) show(a,b,c int) {
fmt.Println("Show Called")
}
func (f F1) calc(a,b,c int) int {
f(a,b)
}
我们为 F1 类型绑定了两个方法 show 和 calc,每个方法具有三个参数,都不能返回值。show 方法只是打印一行信息,而 calc 方法调用了 F1 类型的函数对象。
package main
import "fmt"
type F1 func(a,b int) int
func (f F1) show(a,b,c int) {
fmt.Println("Show Called")
}
func (f F1) calc(a,b,c int) int {
f(a,b)
fmt.Println("Calc Called")
}
func A1(c,d int) int {
return c+d
}
func main() {
// 因为 A1 遵循 F1 的签名,所以可以强制转换
a1 := F1(A1)
// 变量 a1 是 F1 类型的,那么就自动拥有了 F1 类型的方法
a1.show(1,2,3)
a1.calc(1,2,3)
}
还是回到之前的加减乘除的例子,四个函数的定义如下:
func add(a, b int) int {
return a + b
}
func sub(a, b int) int {
return a - b
}
func mul(a, b int) int {
return a * b
}
func div(a, b int) int {
return a / b
}
这四个函数的声明类型是相同的,我们可以定义一个二元运算的函数类型:
type BinaryOperationFunc func(a, b int) int
然后给该类型绑定一个 calc 方法:
func (f BinaryOperationFunc) calc(a, b int) int {
return f(a, b)
}
这样我们就可以通过强制转换将四个函数转换为 BinaryOperationFunc 类型,然后调用 calc 方法:
package main
import "fmt"
type BinaryOperationFunc func(a, b int) int
func (f BinaryOperationFunc) calc(a, b int) int {
return f(a, b)
}
func add(a, b int) int {
return a + b
}
func sub(a, b int) int {
return a - b
}
func mul(a, b int) int {
return a * b
}
func div(a, b int) int {
return a / b
}
func main() {
addFunc := BinaryOperationFunc(add)
subFunc := BinaryOperationFunc(sub)
mulFunc := BinaryOperationFunc(mul)
divFunc := BinaryOperationFunc(div)
fmt.Println(addFunc.calc(1, 2)) // 3
fmt.Println(subFunc.calc(1, 2)) // -1
fmt.Println(mulFunc.calc(1, 2)) // 2
fmt.Println(divFunc.calc(1, 2)) // 0
}
这样的话,我们就可以通过一个函数类型来统一管理四个函数,而不用为每个函数定义一个计算方法。
defer
Go 中的 defer
关键字用于延迟执行函数。
func main() {
defer fmt.Println("Hello, World!")
fmt.Println("Hello, Go!")
}
输出结果:
Hello, Go!
Hello, World!
defer
关键字会将函数推迟到外层函数返回之后执行。
延迟函数的参数会被立即计算,但是不会被执行。
func main() {
i := 0
defer fmt.Println(i)
i++
}
输出结果:
0
而不是 1。