Notes
Go
函数

函数

函数在 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 语言中的一个特殊写法,foperateFuncs[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。