这意味着函数不但能够用于封装代码、分割功能、解耦逻辑,还能够化身为普通的值,在其余函数间传递、赋予变量、作类型判断和转换等等,就像切片和字典的值那样。
而更深层次的含义就是:函数值能够由此成为可以被随意传播的独立逻辑组件(或者说功能模块)。
对于函数类型来讲,它是一种对一组输入、输出进行模板化的重要工具,它比接口类型更加轻巧、灵活,它的值也借此变成了可被热替换的逻辑组件。编程
我先声明了一个函数类型,名叫Printer,注意这里的写法,在类型声明的名称右边的是func关键字,咱们由此就可知道这是一个函数类型的声明。设计模式
nc右边的就是这个函数类型的参数列表和结果列表。其中,参数列表必须由圆括号包裹,而只要结果列表中只有一个结果声明,而且没有为它命名,咱们就能够省略掉外围的圆括号。数组
书写函数签名的方式与函数声明的是一致的。只是紧挨在参数列表左边的不是函数名称,而是关键字func。这里函数名称和func互换了一下位置而已闭包
函数的签名其实就是函数的参数列表和结果列表的统称,它定义了可用来鉴别不一样函数的那些特征,同时也定义了咱们与函数交互的方式。编程语言
注意,各个参数和结果的名称不能算做函数签名的一部分,甚至对于结果声明来讲,没有名称均可以。只要两个函数的参数列表和结果列表中的元素顺序及其类型是一致的,咱们就能够说它们是同样的函数,或者说是实现了同一个函数类型的函数。严格来讲,函数的名称也不能算做函数签名的一部分,它只是咱们在调用函数时,须要给定的标识符而已。ide
声明的函数printToStd的签名与Printer的是一致的,所以前者是后者的一个实现,即便它们的名称以及有的结果名称是不一样的。函数式编程
经过main函数中的代码,咱们就能够证明这二者的关系了,我顺利地把printToStd函数赋给了Printer类型的变量p,而且成功地调用了它。函数
总之,“函数是一等的公民”是函数式编程(functional programming)的重要特征。Go 语言在语言层面支持了函数式编程工具
package main import "fmt" //先声明了一个函数类型,名叫Printer,函数签名:函数的参数列表和结果列表的统称 type Printer func(contents string) (n int, err error) //定义了一个函数,printToStd的签名与Printer的是一致的,所以printToStd是Printer的一个实现,即便它们的名称以及有的结果名称是不一样的 func printToStd(contents string) (bytesNum int, err error) { return fmt.Println(contents) } func main() { var p Printer //初始化一个Printer 类型的p p = printToStd //顺利地把printToStd函数赋给了Printer类型的变量p,而且成功地调用了它 p("something") }
go run demo26.go something
什么是高阶函数?只要知足了其中任意一个特色,咱们就能够说这个函数是一个高阶函数设计
我想经过编写calculate函数来实现两个整数间的加减乘除运算,可是但愿两个整数和具体的操做都由该函数的调用方给出,那么,这样一个函数应该怎样编写呢。
咱们编写calculate函数的签名部分。这个函数除了须要两个int类型的参数以外,还应该有一个operate类型的参数。该函数的结果应该有两个,一个是int类型的,表明真正的操做结果,另外一个应该是error类型的,由于若是那个operate类型的参数值为nil,那么就应该直接返回一个错误
函数类型属于引用类型,它的值能够为nil,而这种类型的零值偏偏就是nil。
calculate函数实现起来就很简单了。咱们须要先用卫述语句检查一下参数,若是operate类型的参数op为nil,那么就直接返回0和一个表明了具体错误的error类型值。
卫述语句是指被用来检查关键的先决条件的合法性,并在检查未经过的状况下当即终止当前代码块执行的语句。在 Go 语言中,if 语句常被做为卫述语句。若是检查无误,那么就调用op并把那两个操做数传给它,最后返回op返回的结果和表明没有错误发生的nil。
calculate函数的其中一个参数是operate类型的,并且后者就是一个函数类型。在调用calculate函数的时候,咱们须要传入一个operate类型的函数值。这个函数值应该怎么写?
只要它的签名与operate类型的签名一致,而且实现得当就能够了。咱们能够像上一个例子那样先声明好一个函数,再把它赋给一个变量,也能够直接编写一个实现了operate类型的匿名函数。
calculate函数就是一个高阶函数。可是咱们说高阶函数的特色有两个,而该函数只展现了其中一个特色,即:接受其余的函数做为参数传入。
那另外一个特色,把其余的函数做为结果返回。这又是怎么玩的呢?你能够看看我在 demo27.go 文件中声明的函数类型calculateFunc和函数genCalculator。其中,genCalculator函数的惟一结果的类型就是calculateFunc
package main import ( "errors" "fmt" ) type operate func(x, y int) int //咱们来声明一个名叫operate的函数类型,它有两个参数和一个结果,都是int类型的。 // 方案1。calculate函数就是一个高阶函数。该函数只展现了其中一个特色,即:接受其余的函数做为参数传入。 func calculate(x int, y int, op operate) (int, error) { if op == nil { //卫述语句检查op的合法性 return 0, errors.New("invalid operation") } return op(x, y), nil } // 方案2。calculateFunc也是高阶函数,把其余的函数op做为结果返回 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) //把函数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) }
go run demo27.go The result: 35 (error: <nil>) The result: 0 (error: invalid operation) The result: 134 (error: <nil>)
闭包又是什么?你能够想象一下,在一个函数中存在对外来标识符的引用。所谓的外来标识符,既不表明当前函数的任何参数或结果,也不是函数内部声明的,它是直接从外边拿过来的。
还有个专门的术语称呼它,叫自由变量,可见它表明的确定是个变量。实际上,若是它是个常量,那也就造成不了闭包了,由于常量是不可变的程序实体,而闭包体现的倒是由“不肯定”变为“肯定”的一个过程。
咱们说的这个函数(如下简称闭包函数)就是由于引用了自由变量,而呈现出了一种“不肯定”的状态,也叫“开放”状态。
也就是说,它的内部逻辑并非完整的,有一部分逻辑须要这个自由变量参与完成,然后者到底表明了什么在闭包函数被定义的时候倒是未知的。
即便对于像 Go 语言这种静态类型的编程语言而言,咱们在定义闭包函数的时候最多也只能知道自由变量的类型
在咱们刚刚提到的genCalculator函数内部,实际上就实现了一个闭包,而genCalculator函数也是一个高阶函数。
genCalculator函数只作了一件事,那就是定义一个匿名的、calculateFunc类型的函数并把它做为结果值返回。
而这个匿名的函数就是一个闭包函数。它里面使用的变量op既不表明它的任何参数或结果也不是它本身声明的,而是定义它的genCalculator函数的参数,因此是一个自由变量。
这个自由变量究竟表明了什么,这一点并非在定义这个闭包函数的时候肯定的,而是在genCalculator函数被调用的时候肯定的。只有给定了该函数的参数op,咱们才能知道它返回给咱们的闭包函数能够用于什么运算。
看到if op == nil {那一行了吗?Go 语言编译器读到这里时会试图去寻找op所表明的东西,它会发现op表明的是genCalculator函数的参数,而后,它会把这二者联系起来。这时能够说,自由变量op被“捕获”了。
当程序运行到这里的时候,op就是那个参数值了。如此一来,这个闭包函数的状态就由“不肯定”变为了“肯定”,或者说转到了“闭合”状态,至此也就真正地造成了一个闭包。
看出来了吗?咱们在用高阶函数实现闭包。这也是高阶函数的一大功用。
(高阶函数与闭包)
那么,实现闭包的意义又在哪里呢?表面上看,咱们只是延迟实现了一部分程序逻辑或功能而已,但实际上,咱们是在动态地生成那部分程序逻辑。
咱们能够借此在程序运行的过程当中,根据须要生成功能不一样的函数,继而影响后续的程序行为。这与 GoF 设计模式中的“模板方法”模式有着殊途同归之妙,不是吗?
这个命令源码文件(也就是 demo28.go示例一)在运行以后会输出什么?
答案是:原数组不会改变。为何呢?缘由是,全部传给函数的参数值都会被复制,函数在其内部使用的并非参数值的原值,而是它的副本。因为数组是值类型,因此每一次复制都会拷贝它,以及它的全部元素值。我在modify函数中修改的只是原数组的副本而已,并不会对原数组形成任何影响。
对于引用类型,好比:切片、字典、通道,像上面那样复制它们的值,只会拷贝它们自己而已,并不会拷贝它们引用的底层数据。也就是说,这时只是浅表复制,而不是深层复制。
以切片值为例,如此复制的时候,只是拷贝了它指向底层数组中某一个元素的指针,以及它的长度值和容量值,而它的底层数组并不会被拷贝。
另外还要注意,就算咱们传入函数的是一个值类型的参数值,但若是这个参数值中的某个元素是引用类型的,那么咱们仍然要当心。
变量complexArray1是[3][]string类型的,也就是说,虽然它是一个数组,可是其中的每一个元素又都是一个切片。这样一个值被传入函数的话,函数中对该参数值的修改会影响到complexArray1自己吗?我想,这能够留做今天的思考题。
package main import "fmt" func main() { // 示例1。底层数组不会被修改,全部传给函数的参数值都会被复制,函数在其内部使用的并非参数值的原值,而是它的副本 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) fmt.Println() // 示例2。切片会被修改掉,可是切片底层的数组不变 slice1 := []string{"x", "y", "z"} fmt.Printf("The slice: %v\n", slice1) slice2 := modifySlice(slice1) fmt.Printf("The modified slice: %v\n", slice2) fmt.Printf("The original slice: %v\n", slice1) fmt.Println() // 示例3。 /切片被修改,底层数组不变 complexArray1 := [3][]string{ []string{"d", "e", "f"}, []string{"g", "h", "i"}, []string{"j", "k", "l"}, } fmt.Printf("The complex array: %v\n", complexArray1) complexArray2 := modifyComplexArray(complexArray1) //切片被修改,底层数组不变 fmt.Printf("The modified complex array: %v\n", complexArray2) fmt.Printf("The original complex array: %v\n", complexArray1) } // 示例1。 func modifyArray(a [3]string) [3]string { a[1] = "x" return a } // 示例2。 func modifySlice(a []string) []string { a[1] = "i" return a } // 示例3。 func modifyComplexArray(a [3][]string) [3][]string { a[1][1] = "s" a[2] = []string{"o", "p", "q"} return a }
go run demo28.go The array: [a b c] The modified array: [a x c] The original array: [a b c] The slice: [x y z] The modified slice: [x i z] The original slice: [x i z] The complex array: [[d e f] [g h i] [j k l]] The modified complex array: [[d e f] [g s i] [o p q]] //切片被修改,底层数组不变 The original complex array: [[d e f] [g s i] [j k l]]
问题:
一、complexArray1被传入函数的话,这个函数中对该参数值的修改会影响到它的原值吗?
若是修改了引用类型的值会受影响,1.数组的操做不影响原值 2.切片的操做会影响原值。
若是是进行一层修改,即数组的某个完整元素进行修改(指针变化),那么原有数组不变;若是进行二层修改,即数组中某个元素切片内的某个元素再进行修改(指针未改变),那么原有数据也会跟着改变,传参能够理解是浅copy,参数自己的指针是不一样,可是元素指针相同,对元素指针所指向目的的操做会影响传参过程当中的原始数据;
二、函数真正拿到的参数值其实只是它们的副本,那么函数返回给调用方的结果值也会被复制吗?好比你传出去一个数组,它还会是函数中的那个数组吗?通常来讲应该是复制的,传参和返回应该是一个对称的过程,自己对这一片内存数据的操做只发生在函数内部,脱离函数就应该脱离这块内存区域