GO:2.5 使用函数的正确姿势

1、函数是一等的数据类型

在 Go 语言中,函数类型是一等的数据类型。

简单来说,这意味着函数不但可以用于封装代码、分割功能、解耦逻辑,还可以化身为普通的值,在其他函数间传递、赋予变量、做类型判断和转换等等,就像切片和字典的值那样。

深层次的含义就是:函数值可以由此成为能够被随意传播的独立逻辑组件(或者说功能模块)。

对于函数类型来说,它是一种对一组输入、输出进行模板化的重要工具,它比接口类型更加轻巧、灵活,它的值也借此变成了可被热替换的逻辑组件。
例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package main

import "fmt"

type Printer func(contents string) (n int, err error)
//先声明一个函数类型Printer

func printToStd(contents string) (bytesNum int, err error) {
//定义一个函数printToStd
return fmt.Println(contents)
}

func main() {
var p Printer
p = printToStd
p("something")
}

只要两个函数的参数列表和结果列表中的元素顺序及其类型是一致的,我们就可以说它们是一样的函数,或者说是实现了同一个函数类型的函数。


*函数的签名其实就是函数的参数列表和结果列表的统称,它定义了可用来鉴别不同函数的那些特征,同时也定义了我们与函数交互的方式。*


注意,各个参数和结果的名称不能算作函数签名的一部分,甚至对于结果声明来说,没有名称都可以。

严格来说,函数的名称也不能算作函数签名的一部分,它只是我们在调用函数时,需要给定的标识符而已。

所以函数Printer的签名与printToStd的是一致的,printToStd可以看作是Printer的一个实现,即使它们的名称以及有的结果名称是不同的。

通过main函数中的代码,我们就可以证实这两者的关系了,顺利地把printToStd函数赋给了Printer类型的变量p,并且成功地调用了它。

2、高阶函数

1、什么是高阶函数?

简单地说,高阶函数可以满足下面的两个条件:

  • 1、接受其他的函数作为参数传入;
  • 2、把其他的函数作为结果返回。

只要满足了其中任意一个特点,我们就可以说这个函数是一个高阶函数。高阶函数也是函数式编程中的重要概念和特征。

例:编写calculate函数来实现两个整数间的加减乘除运算,但是希望两个整数和具体的操作都由该函数的调用方给出,那么,这样一个函数应该怎样编写呢?
1、首先声明一个名叫operate的函数类型,它有两个参数和一个结果,都是int类型的。

1
type operate func(x, y int) int

2、然后编写calculate函数的签名部分。这个函数除了需要两个int类型的参数之外,还应该有一个operate类型的参数。

该函数的结果应该有两个,一个是int类型的,代表真正的操作结果,另一个应该是error类型的,因为如果那个operate类型的参数值为nil,那么就应该直接返回一个错误。(函数类型是引用类型,它的零值是nil)

1
2
3
4
5
6
func calculate(x int, y int, op operate) (int, error) {
if op == nil {
return 0, errors.New("invalid operation")
}
return op(x, y), nil
}

calculate函数实现:先用卫述语句检查一下参数,如果operate类型的参数op为nil,那么就直接返回0和一个代表了具体错误的error类型值。如果检查无误,那么就调用op并把那两个操作数传给它,最后返回op返回的结果和代表没有错误发生的nil。


卫述语句是指被用来检查关键的先决条件的合法性,并在检查未通过的情况下立即终止当前代码块执行的语句。在 Go 语言中,if 语句常被作为卫述语句。


3、在该例中,重点是:让函数在其他函数间传递。
calculate函数的第三个参数是operate类型,而且operate类型就是一个函数类型。在调用calculate函数的时候,我们需要传入一个operate类型的函数值。这个函数值应该怎么写?

只要它的签名与operate类型的签名一致,并且实现得当就可以了。我们可以像上一个例子那样先声明好一个函数,再把它赋给一个变量,也可以直接编写一个实现了operate类型的匿名函数。

1
2
3
op := func(x, y int) int {
return x + y
}

calculate函数就是一个高阶函数。但是我们说高阶函数的特点有两个,而该函数只展示了其中一个特点,即:接受其他的函数作为参数传入

那另一个特点,把其他的函数作为结果返回。如下;
声明函数类型calculateFunc和函数genCalculator。其中,genCalculator函数的唯一结果的类型就是calculateFunc。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
package main

import (
"errors"
"fmt"
)

type operate func(x, y int) int

// 方案1。
func calculate(x int, y int, op operate) (int, error) {
if op == nil {
return 0, errors.New("invalid operation")
}
return op(x, y), nil
}

// 方案2。
type calculateFunc func(x int, y int) (int, error)

func genCalculator(op operate) calculateFunc {
return func(x int, y int) (int, error) {
if op == nil {
return 0, errors.New("invalid operation")
}
return op(x, y), nil
}
}

func main() {
// 方案1。
x, y := 12, 23
op := func(x, y int) int {
return x + y
}
result, err := calculate(x, y, op)
fmt.Printf("The result: %d (error: %v)\n",
result, err)
result, err = calculate(x, y, nil)
fmt.Printf("The result: %d (error: %v)\n",
result, err)

// 方案2。
x, y = 56, 78
add := genCalculator(op)
result, err = add(x, y)
fmt.Printf("The result: %d (error: %v)\n",
result, err)
}

3、如何实现闭包?

1、闭包是什么?
你可以想象一下,在一个函数中存在对外来标识符的引用。所谓的外来标识符,既不代表当前函数的任何参数或结果,也不是函数内部声明的,它是直接从外边拿过来的。

还有个专门的术语称呼它,叫自由变量,可见它代表的肯定是个变量。实际上,如果它是个常量,那也就形成不了闭包了,因为常量是不可变的程序实体,而闭包体现的却是由“不确定”变为“确定”的一个过程

这个函数(以下简称闭包函数)就是因为引用了自由变量,而呈现出了一种“不确定”的状态,也叫“开放”状态。

也就是说,它的内部逻辑并不是完整的,有一部分逻辑需要这个自由变量参与完成,而后者到底代表了什么在闭包函数被定义的时候却是未知的。

即使对于像 Go 语言这种静态类型的编程语言而言,我们在定义闭包函数的时候最多也只能知道自由变量的类型。

在genCalculator函数内部,实际上就实现了一个闭包,而genCalculator函数也是一个高阶函数。

1
2
3
4
5
6
7
8
func genCalculator(op operate) calculateFunc {
return func(x int, y int) (int, error) {
if op == nil {
return 0, errors.New("invalid operation")
}
return op(x, y), nil
}
}

enCalculator函数只做了一件事,那就是定义一个匿名的、calculateFunc类型的函数并把它作为结果值返回。

而这个匿名的函数就是一个闭包函数。它里面使用的变量op既不代表它的任何参数或结果,也不是它自己声明的,而是定义它的genCalculator函数的参数,所以是一个自由变量。

只有给定了该函数的参数op,我们才能知道它返回给我们的闭包函数可以用于什么运算。

看到if op == nil {那一行了吗?Go 语言编译器读到这里时会试图去寻找op所代表的东西,它会发现op代表的是genCalculator函数的参数,然后,它会把这两者联系起来。这时可以说,自由变量op被“捕获”了。

当程序运行到这里的时候,op就是那个参数值了。如此一来,这个闭包函数的状态就由“不确定”变为了“确定”,或者说转到了“闭合”状态,至此也就真正地形成了一个闭包。

我们在用高阶函数实现闭包。这也是高阶函数的一大功用。
fun1
(高阶函数与闭包)

那么,实现闭包的意义又在哪里呢?
表面上看,我们只是延迟实现了一部分程序逻辑或功能而已,但实际上,我们是在动态地生成那部分程序逻辑。

4、传入函数的那些参数值后来怎么样了?

例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package main

import "fmt"

func main() {
array1 := [3]string{"a", "b", "c"}
fmt.Printf("The array: %v\n", array1)
array2 := modifyArray(array1)
fmt.Printf("The modified array: %v\n", array2)
fmt.Printf("The original array: %v\n", array1)
}

func modifyArray(a [3]string) [3]string {
a[1] = "x"
return a
}

运行:

1
2
3
The array: [a b c]
The modified array: [a x c]
The original array: [a b c]

由输出结果知:
数组不会改变,因为所有传给函数的参数值都会被复制函数在其内部使用的并不是参数值的原值,而是它的副本

由于数组是值类型,所以每一次复制都会拷贝它,以及它的所有元素值。我在modify函数中修改的只是原数组的副本而已,并不会对原数组造成任何影响。

注意,对于引用类型,比如:切片、字典、通道,像上面那样复制它们的值,只会拷贝它们本身而已,并不会拷贝它们引用的底层数据。也就是说,这时只是浅表复制,而不是深层复制。

切片值为例,如此复制的时候,只是拷贝了它指向底层数组中某一个元素的指针,以及它的长度值容量值,而它的底层数组并不会被拷贝

另外还要注意,就算我们传入函数的是一个值类型的参数值,但如果这个参数值中的某个元素是引用类型的,那么我们仍然要小心。
如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package main

import "fmt"

func main() {
complexArray1 := [3][]string{
[]string{"d", "e", "f"},
[]string{"g", "h", "i"},
[]string{"j", "k", "l"},
}

complexArray2 := modifyArray(complexArray1)
fmt.Printf("The modified array: %v\n", complexArray2)
fmt.Printf("The original array: %v\n", complexArray1)

}

func modifyArray(a [3][]string) [3][]string {
a[0]=[]string{"a","b","c"}
return a
}

变量complexArray1是[3][]string类型的,也就是说,虽然它是一个数组,但是其中的每个元素又都是一个切片。

这样一个值被传入函数的话,函数中对该参数值的修改会影响到complexArray1本身吗?
答案是会的

运行结果:

1
2
The modified array: [[a b c] [g h i] [j k l]]
The original array: [[d e f] [g h i] [j k l]]

函数传参相关原则:既不要把你程序的细节暴露给外界,也尽量不要让外界的变动影响到你的程序