函数是一等公民
本质上,我们可以说 Go 程序就是一组函数的集合。
Go 语言的函数具有如下的特点:
- 以 func 关键字开头
- 支持多返回值
- 支持具名返回值
- 支持递归调用
- 支持同类型的可变参数
- 支持 defer、实现函数优雅返回
什么是一等公民
如果一门编程语言对某种语言元素的创建和使用没有限制,我们可以对待值(value)一样对待这种语法元素,那么我们就可以说这种语法元素是一等公民。
在 Go 语言中,函数是一等公民,这意味着函数可以作为参数传递给其他函数,可以作为其他函数的返回值,可以赋值给变量,可以存储在数据结构中。
我们来看一下 Go 语言的函数是如何满足上面的要求。
1、正常创建
func add(a, b int) int {
return a + b
}
2、在函数内创建
func action() {
add := func(a, b int) int {
return a + b
}
fmt.Println(add(1, 2))
}
3、作为类型
type addFunc func(a, b int) int
4、存储到变量中
add := func(a, b int) int {
return a + b
}
5、作为参数传递
func calc(a, b int, f addFunc) int {
return f(a, b)
}
6、作为返回值
func add() addFunc {
return func(a, b int) int {
return a + b
}
}
除了上面的例子外, 函数还可以被放入到数组、切片和 map 中。
对函数进行显式类型转换
显式类型转换的规则:
type(value) // 将 vaule 转换为 type 类型
下面我们来看一个函数的显式类型转换的例子:
type BinaryAdder interface {
Add(int, int) int
}
type MyAdderFunc func(int, int) int
func (f MyAdderFunc) Add(x, y int) int {
return f(x, y)
}
func MyAdd(x, y int) int {
return x + y
}
func main() {
var i BinaryAdder = MyAdderFunc(MyAdd)
fmt.Println(i.Add(1, 5))
}
BinaryAdder
是一个接口,MyAdderFunc
是一个函数类型,MyAdd
是一个函数。我们可以通过显式类型转换将函数转换为接口类型。
函数式编程
1、柯里化函数
柯里化函数是指将多个参数的函数转换为一系列单参数函数的过程。
func times(x, y int) int {
return x * y
}
func partialTimes(x int) func(int) int {
return func(y int) int {
return times(x, y)
}
}
func main() {
double := partialTimes(2)
triple := partialTimes(3)
fmt.Println(double(3))
fmt.Println(triple(4))
}
// 6
// 12
这个例子利用了函数的两点性质:1、使用函数作为返回值;2、闭包。
2、函子
函子需要满足两个条件:
- 函子本身是一个容器类型,这个容器可以是切片,map 甚至 channel;
- 该容器类型需要实现一个方法,该方法接受一个函数类型参数,并在容器的每个元素上应用那个函数,得到一个新函子,原函子容器内部的元素值不受影响。
type IntSliceFunctor interface {
Fmap(fn func(int) int) IntSliceFunctor
}
type IntSliceFunctorImpl struct {
ints []int
}
func (f IntSliceFunctorImpl) Fmap(fn func(int) int) IntSliceFunctor {
result := make([]int, len(f.ints))
for i, v := range f.ints {
result[i] = fn(v)
}
return IntSliceFunctorImpl{ints: result}
}
func NewIntSliceFunctor(slice []int) IntSliceFunctor {
return IntSliceFunctorImpl{ints: slice}
}
func main() {
f := NewIntSliceFunctor([]int{1, 2, 3, 4, 5})
mapped := f.Fmap(func(i int) int {
return i * 2
})
fmt.Println(mapped)
mapped1 := mapped.Fmap(func(i int) int {
return i + 1
})
fmt.Println(mapped1)
fmt.Println(f)
}
// 输出
// {[2 4 6 8 10]}
// {[3 5 7 9 11]}
// {[1 2 3 4 5]}
代码逻辑是:
- 定义了一个
IntSliceFunctorImpl
结构体, 用来作为函子的容器; - 我们把函子要实现的方法
Fmap
定义在了IntSliceFunctorImpl
结构体上;该方法也是IntSliceFunctor
接口的唯一方法; NewIntSliceFunctor
函数用来创建一个IntSliceFunctor
实例;- 在
main
中定义了两个转换函数,分别是i * 2
和i + 1
,然后分别应用到f
和mapped
上; - 无论如何应用转换函数,原函子中容器的元素值都不会受到影响。
函子非常适合用来对容器集合元素进行批量同构处理,而且代码要比每次都对容器中的元素进行循环处理要优雅很多。
想在 Go 中发挥更大效能,还需要 Go 对泛型提供支持,好在 Go 1.20 以及之后的版本都支持了,否则都需要对每个容器类型都实现一遍函子接口。
3、延续传递式
函数式编程离不开递归,以求阶乘函数为例,我们可以轻易用递归方法写出一个实现:
func factorial(n int) int {
if n == 1 {
return 1
}
return n * factorial(n-1)
}
函数式编程有一种被称为延续传递式的编程范式,它是一种将函数作为参数传递给另一个函数的编程风格。非常类似 JavaScript 的 Callback。
我们使用上面的例子进行改写:
func factorial(n int, f func(int)) {
if n == 1 {
f(1)
}
factorial(n-1, func(v int) {
f(n * v)
})
}
func main() {
factorial(5, func(y int) {
fmt.Printf("%d\n", y)
})
}
// 输出
// 120
这种风格其实还是难以理解。