Go 语言
函数和方法
函数是一等公民

函数是一等公民

本质上,我们可以说 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 * 2i + 1,然后分别应用到 fmapped 上;
  • 无论如何应用转换函数,原函子中容器的元素值都不会受到影响。

函子非常适合用来对容器集合元素进行批量同构处理,而且代码要比每次都对容器中的元素进行循环处理要优雅很多。

想在 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

这种风格其实还是难以理解。