Go语言36讲笔记--12func(函数式编程)

179 阅读11分钟

编程语言中,集合类的数据类型都是最常用和最重要的。Go 语言进行模块化编程时,必须了解的知识,这包括几个重要的数据类型以及一些模块化编程的技巧

首先我们需要系统了解的是 Go 语言的函数以及函数类型


前导内容:Go 语言在语言层面支持函数式编程

简单来说,在 Go 语言中,函数不但可以用于封装代码、分割功能、解耦逻辑,还可以作为普通实体类型,在其他函数间传递、赋予变量、做类型判断和转换等。(类比切片和字典的值)

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

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

从demo理解上述抽象的概念

package main
 
import "fmt"
 
type Printer func(contents string) (n int, err error) //先声明了一个函数类型,Printer
//在类型声明的名称右边,是`func`关键字,我们由此就可知道这是一个函数类型的声明。
//在`func`右边的就是这个函数类型的参数列表和结果列表。
//参数列表必须由圆括号包裹。
//若结果列表中只有一个结果声明,并且没有为它命名,我们就可以省略掉外围的圆括号。
 
func printToStd(contents string) (bytesNum int, err error) { //此处是函数签名
//格式,与声明有一点顺序的小区别
	return fmt.Println(contents)
}

//Printer与printToStd函数的参数列表和结果列表中的元素顺序及其类型是一致的
//我们就可以说它们是一样的函数,或者说是实现了同一个函数类型的函数。
//因此后者是前者的一个实现,即使它们的名称以及有的结果名称是不同的。
 
func main() {
	var p Printer //声明一个Printer类型的变量p
	p = printToStd //成功的将函数printToStd赋值给p
	p("something") //成功的调用函数
}

今天的问题是:怎样编写高阶函数?

what高阶函数? :很直观,把函数作为入参or返回值

满足下面的两个条件:

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

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

具体一点描述该问题

我想通过编写calculate函数来实现两个整数间的加减乘除运算,但是希望两个整数和具体的操作都由该函数的调用方给出,那么,这样一个函数应该怎样编写呢。


典型回答

首先,我们来声明一个名叫operate的函数类型,它有两个参数和一个结果,都是int类型的。

type operate func(x, y int) int

然后,我们编写calculate函数的签名部分。

这个函数除了需要两个int类型的参数之外,还应该有一个operate类型的参数。

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

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类型的参数opnil,那么就直接返回0和一个代表了具体错误的error类型值

卫述语句是指被用来检查关键的先决条件的合法性,并在检查未通过的情况下立即终止当前代码块执行的语句。在 Go 语言中,if 语句常被作为卫述语句。 如果检查无误,那么就调用op并把那两个操作数传给它,最后返回op返回的结果和代表没有错误发生nil


问题解析

👆👆个例子中,展示了把函数作为一个普通的值赋给一个变量。

👆个例子中中,展示了让函数在其他函数间传递。

calculate函数的其中一个参数是operate类型的,即函数类型。在调用calculate函数的时候,我们需要传入一个operate类型的函数值。

这个函数值应该怎么写?

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

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

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

另一个特点,把其他的函数作为结果返回。

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) //参数func,返回值是func的声明
	result, err = add(x, y) //这边是调用
	fmt.Printf("The result: %d (error: %v)\n", result, err)
}

知识扩展

问题 1:关于如何实现闭包?[高阶函数实现闭包]

what(闭包)?

在一个函数中存在对外来标识符的引用。所谓的外来标识符,指从外边拿过来的。

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

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

闭包函数内部逻辑并不是完整的,有一部分逻辑需要这个自由变量参与完成,闭包函数被定义的时候并不知道该自由变量的具体内容,最多只能知道自由变量的类型。

举个栗子genCalculator函数内部,实际上就实现了一个闭包,并且genCalculator函数也是一个高阶函数。

	
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
                
	}
	
}

image.png

how(使用闭包)

genCalculator函数只做了一件事:定义一个匿名的calculateFunc类型的函数并把它作为结果值返回。

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

自由变量op不是在定义这个闭包函数的时候确定的,而是在genCalculator函数被调用的时候确定的。只有给定了该函数的参数op,我们才能知道它返回给我们的闭包函数可以用于什么运算。

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

当程序运行到这里的时候,op就是那个参数值了。

此时,闭包函数的状态由“不确定”变为了“确定”,或者说转到了“闭合”状态,至此也就真正地形成了一个闭包。

why(使用闭包)

表面上看,我们只是延迟实现了一部分程序逻辑或功能而已,但实际上,我们是在动态地生成部分程序逻辑。

我们可以借此在程序运行的过程中,根据需要生成功能不同的函数,继而影响后续的程序行为。这与 GoF 设计模式中的“模板方法”模式有着异曲同工之妙


问题 2:传入函数的那些参数值后来怎么样了?

demo

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
}

运行后,原数组会发生改变吗

答案是:原数组不会改变

why: 所有传给函数的参数值都会被复制,函数在其内部使用的并不是参数值的原值,而是它的副本。即,Go语言函数中所有的参数传递都是值传递。

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

对于引用类型,比如:切片、字典、通道,若复制它们的值,仅仅是拷贝它们本身而已,并不会拷贝它们引用的底层数据(不会为此创造新的内存空间)。也就是说,这时只是浅表复制,而不是深层复制。

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

当传入函数的是一个值类型的参数值(比如数组),但如果这个参数值中的某个元素是引用类型的(比如切片)。

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

👆这种情况,需要讨论一下。

若增减数组中的元素值,则不会影响原数组;

若直接修改数组中切片的值,相当于修改底层数据结构中的值,会对原数组产生影响。

总结

这一讲主要是介绍函数的使用方法。

在 Go 语言中,支持函数式编程的一些特性。

函数既可以被独立声明,也可以被作为普通的值来传递或赋予变量。

我们还可以在其他函数的内部声明匿名函数并把它直接赋给变量。

Go 语言是怎样鉴别一个函数的?

函数的签名在这里起到了至关重要的作用。

只看结果列表与参数列表,就可以确定一个函数。

函数是 Go 语言支持函数式编程的主要体现。

我们可以通过“把函数传给函数”以及“让函数返回函数”来编写高阶函数,也可以用高阶函数来实现闭包,并以此做到部分程序逻辑的动态生成。

关于函数传参

值传递。

对于基本数据类型与引用数据类型的结果有不同的影响。

思考题

  1. 👆.go中complexArray1被传入函数的话,这个函数中对该参数值的修改会影响到它的原值吗?

这要看怎样修改了。虽然complexArray1本身是一个数组,但是其中的元素却都是切片。如果对complexArray1中的元素进行增减,那么原值就不会受到影响。但若要修改它已有的元素值,那么原值也会跟着改变。

  1. 函数真正拿到的参数值其实只是它们的副本,那么函数返回给调用方的结果值也会被复制吗?

函数返回给调用方的结果值也会被复制。

不过,在一般情况下,我们不用太在意。但如果函数在返回结果值之后依然保持执行并会对结果值进行修改,那么我们就需要注意了。