Custom Functions
函数是过程式编程的基石,而Go语言中的方法跟函数非常像。所以这一节关系到过程式编程和面向对像编程。
以下是函数定义的基本语法
func functionName(optionalParameters) optionalReturnType {
body
}
func functionName(optionalParameters) (optionalReturnValues) {
body
}
一个函数可以接收零个或者多个参数。如果没有参数,则圆括号为空。如果有一个或者多个,可以写成params1 type1, ... paramsN typeN, 这里的params1可以是单个参数的名字,或者相同类型的两个或者多个以逗号分隔的参数名字。参数传递时必须按照给定的顺序传递.
最后一个参数的类型前可以添加省略号(...). 这样的函数称为可变函数。这个意思是它可以接受0个或者多个些类型的参数,而在函数内部可以通过[]type访问这些参数。
一个函数可以返回零个或者多个值。如果在左花括号和参数的右括号之间什么都没有,则表示不返回值。 如果是一个非命名的返回值,可以只写返回值的类型type. 如果是两个或者多个非命名返回值,则必须使用圆括号,如(type1, ..., typeN). 如果是一个或者多个命名的返回值,也必须使用圆括号括起(values1 type1, ..., valuesN typeN). valus1可以是单个或者是多个相同类型以逗号分隔的返回值名称。 函数的返回值只能是全命名或者全部都非命名,不能混合使用。
函数有一个或者多个返回值时,必须有一条返回语句——或者最后一条语句有调用panic(). 如果返回值是非命名的, return语句必须指定要返回的值。如果返回值是命名的,则返回语句可以为空(i.e.,没有明确的指定要返回的值). 注意空返回虽然是合法的,但最好还是不要使用这种风格。
如果一个函数要求一个或者多个返回值,在最后一条可执行语句必须为return 或者 panic(). Go编译器足够智能的识别到函数以panic结束,不会正常返回,所以不在需要return语句。 但是当前的Go编译器不会理解,一个函数中 以if或者switch中的返回语句。对于这种情况,我们就不须要使用到else和defaut分支,而是在if和switch之后,添加返回语句。或者简单的以panic("unreachable")语句结束。
Function Arguments
我们已经看过多个自定义函数的例子,可以接受固定数量的指定类型的参数,通过使用interface{},我们创建的函数则可以接受任意类型的参数。如果一个参数的类型是接口类型——可以是自定义接口或者是标准库中的接口——我们创建的函数可接受任意类型,但是必须满足接口指定的方法集。
在本节,我们将看看关于函数参数的其他可能性。在第一个小节中,我们将看到如何使用一个函数的返回值,直接作为另一个函数的参数。 在第二个小节中,我们将看到如何创建一个函数,它可以接受任意数量的参数。而在最后一个小节中,我们将讨论如何通过技术手段创建一个可接受可选参数。
Function Calls as Function Arguments
如果我们有一个函数或者方法可以接收一个或者多个参数,我们当然可以直接传递相应的参数进行调用,此外,我们也可以调用其它函数或者方法作为参数,传递给这个函数,只需要这些函数返回值的类型和数量跟要求的一样。 以下这个例子是一个函数, 它接受三角形的三条边的长度作为参数,并根据Heron公式返回三角形的面积.
for i := 1; i <= 4; i++ {
a, b, c := PythagoreanTriple(i, i+1)
Δ1 := Heron(a, b, c)
Δ2 := Heron(PythagoreanTriple(i, i+1))
fmt.Printf("Δ1 == %10f == Δ2 == %10f\n", Δ1, Δ2)
}
/*
Δ1 == 6.000000 == Δ2 == 6.000000
Δ1 == 30.000000 == Δ2 == 30.000000
Δ1 == 84.000000 == Δ2 == 84.000000
Δ1 == 180.000000 == Δ2 == 180.000000
*/
首先我们通过PythagoreanTriple获得三条边的长度。然后我们使用Heron()函数,它接受三个int参数,计算三角形的面积。然后在是计算相同长度的三角形,但这一样,我们直接使用PythagoreanTriple()函数作为Heron()函数的参数。
func Heron(a, b, c int) float64 {
α, β, γ := float64(a), float64(b), float64(c)
s := (α + β + γ) / 2
return math.Sqrt(s * (s - α) * (s - β) * (s - γ))
}
func PythagoreanTriple(m, n int) (a, b, c int) {
if m < n {
m, n = n, m
}
return (m * m) - (n * n), (2 * m * n), (m * m) + (n * n)
}
Variadic Functions
一个variadic函数(可变参数函数),它可以从最后一个参数中,接收零个或者多个参数。这样的函数是通过在最后一个参数类型前放置省略号(...)进行定义。在这个函数内部,参数变成了一个指定类型的slice. 比如,如果我们有一个函数,它的签名为Join(xs ...string) string, 则xs是一个[]string类型。
以下是一个简单的例子展示如何使用可变参数函数;它返回传递给它的参数中,最小的一个int.
fmt.Println(MinimumInt1(5, 3), MinimumInt1(7, 3, -2, 4, 0, -8, -5))
//3 -8
MinimumInt1()可以传递一个或者多个int,并返回它们最好的一个.
func MinimumInt1(first int, rest ...int) int {
for _, x := range rest {
if x < first {
first = x
}
}
return first
}
我们也只要求0个参数——比如,MinimumInt0(ints ...int);或者要求至少两个int——比如MininumInt2(first, second int, rest ...int).
如果我们已经有一个int类型的slice.我们依然可以调用MinimumInt1()函数
numbers := []int{7, 6, 2, -1, 7, -3, 9}
fmt.Println(MinimumInt1(numbers[0], numbers[1:]...))
//-3
当我们调用一个可变参数的函数或者方法,并且传递一个slice作为参数时,我们必须在slice后面放置省略号...。这跟之前的append方法一样。所以在这,我们将numbers[1:]...变成了独立的参数6,-2,-1,7,-3,9。
Functions with Multiple Optional Arguments
Go不能直接创建支持多个不同类型的可选参数。但是,可以通过struct类型,并且依赖于Go为会为每个值初始化为零值,我们可以很容易的创建可选参数的函数。
假如我们有一个函数用于处理一些自定义数据,它的默认行为是简单的处理所有的数据。但有的情况下,我们希望它只处理第一项和最后一项,是否记录函数的操作,和提供对无效元素的错误处理函数。
一个方法是,创建一个函数,它的方法签名是ProcessItems(items Items, first, last int, audit bool, errorHandler func(item Item)). 在这种模式下,变量last的值为0,而不管它是否为最后一个元素的索引,而errorHandler函数仅在这个函数存在时才可以被调用. 这个函数的默认调用是ProcessItems(items, 0, 0, false, nil).
一个更好的方式是函数的签名为ProcessItems(items Items, options Options), options是自定义类型Options Struct, 用于保存其它参数的值。然后可以通过ProcessItems(items, Options{})调用。
type Options struct {
First int // First item to process
Last int // Last item to process (0 means process all from First)
Audit bool // If true all actions are logged
ErrorHandler func(item Item) // Called for each bad item if not nil
}
一个struct可以聚合和嵌入一个或者多个任意的字段(aggregation 和 embedding不同点将在第六章讲解). 在这里的Options struct聚合了两个int的字段, 一个bool字段和一个函数。
ProcessItems(items, Options{})
errorHandler := func(item Item) { log.Println("Invalid:", item) }
ProcessItems(items, Options{Audit: true, ErrorHandler: errorHandler})
这段代码展示了两种调用ProcesItems()函数的方法。第一个调用,处理items使用默认的选项(i.e.,处理所有的数据,不要记录,并且不要对无效的元素进行处理). 在第二个调用时,First和Second都为零值(处理所有的元素), 并且重定了Audit和ErrorHandler字段,所以这个函数将会记录它的行为,并且当出现无效元素时,调用错误处理函数。 传递一个struct给可选参数的这种技术,常用于Go标准库中——比如,image.jpeg.Encode()函数。我们也会在第6章中看到这种技术的使用。
The init() and main() Functions
Go保留了两个函数的名字用于特定的目的:int()(在所有的包中都可以有) 和 main() (only in pakcage main). 这两个函数都必须被定义为零参数和返回为空。一个包中可以有多个init()函数。可是,在写这本书的时候,Go编译器只支持一个包中只能有一个init()方法。所以我们推荐在每个包中最多定义一个init()方法。
Go自动调用包中的init()方法和main package中的main()方法,所以它们不需要明确的调用。 对于programs and package来说,init()函数是可选的。但每一个程序都必须在主包中定义main()方法。
Go程序的初始化和执行都是从main package开始。如果这有imports,每一个要被导入的包则被依次导入。被导入的包只能被导入一次,即使在多个包中的import语句都是导入同一个包(比如,多个包中都有可能会导入fmt package)。当一个包被导入后,它也有import。则首先执行,然后在创建包级别的常量和变量。然后在调用这个包的init()函数(如果有的话). 最后,在main package中完成所有包的导入后,创建main package的常量和变量,然后调用main package的init()方法,在最后调用main package的main()方法,程序才完全的开始执行。这个事件顺序如5.1所示
我们可以在init()函数中定义go语句,但是需要记住的是,它们会有main.main()之前调用,所以这些语句不能依赖于在main()函数中创建的任何东西。
让我们看这个例子(从第一章的americanise/americanise.go文件中摘录)
package main
import (
"bufio"
"fmt"
// ...
"strings"
)
var britishAmerican = "british-american.txt"
func init() {
dir, _ := filepath.Split(os.Args[0])
britishAmerican = filepath.Join(dir, britishAmerican)
}
func main() {
// ...
}
Go从main package开始, 由于有import语句,所以按顺序先加载bufio package. bufio package有它自己的imports,然后加载bufio依赖的包,以及创建bufio package的常量和变量,并用调用bufio package的init()函数。 一旦bufio package被导入,则导入fmt package——它依赖于strings package. 所以当Go回到main packages时,strings package已经被导入,所以导入strings的语句会跳过。
当所有的包都被导入后,包级别的变量britishAmerican被创建。然后调用main package的init()函数。最后调用main package的main()函数,程序开始执行。
闭包
闭包是一个函数可以捕获跟它创建时相同作用域的任何常量和变量(如果在函数内有引用它们)。这个意思是当一个闭包被调用时,这个闭包可以访问这些常量和变量。而不管这些被捕获的变量是否超出了作用域——只要它们被闭包引用,它们就会存在以被闭包使用。
在Go中,每一个匿名函数(或者函数字面量, 一个函数赋值给一个变量名)都是一个闭包。
一个闭包的创建语法跟一个正常的函数是一样的,但是有一个关键的不同:闭包没有名字(func关键词之后立即跟圆括号). 为了使用这个闭包,我们通常将它赋值一个变量或者放入到一个数据结构(比如一个map,slice).
我们在之前的多个例子中使用到了闭包——比如,当我们使用defer或者go statement中的匿名函数都是闭包。 我们也在其它的上下文中使用了闭包,比如,在americanise例子中makeReplaceFunction()函数,以及当我们传递给strings.FieldFunc()和strings.Map()的匿名函数,还有createCounter()和logPanics(). 尽管如此,我们也将会在这里看些小例子。
一种使用到闭包是提供包装函数,它会为要包装的函数提供预定义的参数。比如,我们想要为大量不同文件名添加不同的后缀。本质上这个包装函数是对字符串连接操作符进行包装,这样就可以只变一个变量(filename)而另一个是固定的(后缀),而不是两个变量都需要传入。
addPng := func(name string) string { return name + ".png" }
addJpg := func(name string) string { return name + ".jpg" }
fmt.Println(addPng("filename"), addJpg("filename"))
//filename.png filename.jpg
addPng和addJpg两个变量都是对一个匿名函数的引用(i.e.,to closures).
实际上,当我们想要创建多个相似的函数时,我们经常使用factory function, 即一个函数返回另一个函数。以下这个工厂函数返回一个函数,这个函数为一个文件名添加后缀——但仅文件名不包含这个后缀才会添加
func MakeAddSuffix(suffix string) func(string) string {
return func(name string) string {
if !strings.HasSuffix(name, suffix) {
return name + suffix
}
return name
}
}
MakeAddSuffix()工厂函数返回一个闭包,它可以捕获suffix变量。返回的闭包接收一个字符串(e.g., a filename), 并返回filename加上suffix的字符串。
addZip := MakeAddSuffix(".zip")
addTgz := MakeAddSuffix(".tar.gz")
fmt.Println(addTgz("filename"), addZip("filename"), addZip("gobook.zip"))
//filename.tar.gz filename.zip gobook.zip
递归函数
recursive函数是一个函数调用它自己, 相互递归函数多个函数彼此调用。
递归函数一般都有相同的结构:一个"outcase"和 一个"body". outcase通常是一个条件语句,比如if语句在基于某一个传入的参数的判断终止函数的递归。 body是整个函数的逻辑部分,包括至少一条对自身调用的语句(或者相互递归的另一个函数)——传递的参数必须要改变,并且会用于outcase中,以终止一个递归。
递归函数使它很容易操作递归的数据结构(比如二叉树), 但这可能是低效率的。
我们以一个简单的例子来举例说明(低效)。首先我们看到调用一个递归函数和它的输后,之后在看递归函数本身的实现。
for n := 0; n < 20; n++ {
fmt.Print(Fibonacci(n), " ")
}
fmt.Println()
//0 1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987 1597 2584 4181
func Fibonacci(n int) int {
if n < 2 {
return n
}
return Fibonacci(n-1) + Fibonacci(n-2)
}
从图中明显能看出Fibonacci()做了大量重复计算,我们将在5.6.7.1中看到如何避免。
Hofstadter Female and Male sequences是一个整型序列,它们基于相互递归函数产生。以下代码打印出两个序列的前20项。
females := make([]int, 20)
males := make([]int, len(females))
for n := range females {
females[n] = HofstadterFemale(n)
males[n] = HofstadterMale(n)
}
fmt.Println("F", females)
fmt.Println("M", males)
/*
F [1 1 2 2 3 3 4 5 5 6 6 7 8 8 9 9 10 11 11 12]
M [0 0 1 2 2 3 4 4 5 6 6 7 7 8 9 9 10 11 11 12]
*/
以下是两个相互递归函数,用于产生两个序列。
func HofstadterFemale(n int) int {
if n <= 0 {
return 1
}
return n - HofstadterMale(HofstadterFemale(n-1))
}
func HofstadterMale(n int) int {
if n <= 0 {
return 0
}
return n - HofstadterFemale(HofstadterMale(n-1))
}
像往常一样,在函数的开始位置是一个outcase用于保证递归的终止。在body部分,发生递归,并减小传入的值,以便满足于outcase中的条件。
许多语言对于使用Hofstadter函数有一个问题——失败的原因是HofstadterFemale()定义在HofstadterMale()之前,而失败的原因是HofstadterFemale又在它内部调用了HofstadterMale()。所以这些语言就要求我们先声明HofstadterMale()函数。而Go语言没有这样的限制,允许函数以任意的顺序定义。
让我们在看最后一个递归函数,一个函数判断某一个单词是否为回文(i.e.,相同的字每是对称的,"PULLUP"和"ROTOR").
func IsPalindrome(word string) bool {
if utf8.RuneCountInString(word) <= 1 {
return true
}
first, sizeOfFirst := utf8.DecodeRuneInString(word)
last, sizeOfLast := utf8.DecodeLastRuneInString(word)
if first != last {
return false
}
return IsPalindrome(word[sizeOfFirst : len(word)-sizeOfLast])
}
函数以outcase: 如果一个单词的只有零个或者1个字符,则终止递归,并表示这个单词是回文。函数body部分用于比较第一个和最后一个字符:如果它们是不同的,则这个单词不是回文,并且返回false,同时结束递归。但如果第一个和最后一个字符是相同的,则测试去掉了第一个字符和最后一个字符的子字符串。
在这里我们回顾一下第三章的utf8.DecodeRuneInString()函数,它返回字符串的第一个字符(as a rune)和第一个字符包含的字节数。 utf.DecodeLastRuneInString()原理类似,但返回最后一个字符。
Choosing Functions at Runtime
由于Go函数首先也是值,它们可以赋值给变量——这使得可以在运行时选择执行哪个函数。此外,Go可以创建闭包,这意味着我们可以在运行时创建函数——所以我们可以对相同函数(但每一个使用不同的算法)有两个或者更多的实现。
Branching Using Maps and Function References
在之前的章节中(5.2.1 and 5.2.2.1),自定义的ArchiveFileList()函数会根据文件后缀调用特定的函数。它的第一个版本的实现是通过if语句(7行代码). 标准的版本是switch语句(5行代码). 但如果我们要处理的文件后缀增加了会发生什么? 对于if版本,我们需要为每一个额外的else if子句增加两行代码。 对于switch版本,我们需要为新的分支增加1行。如果这个函数需要处理几百个文件后缀,那么整个函数将非常长。
var FunctionForSuffix = map[string]func(string) ([]string, error){
".gz": GzipFileList, ".tar": TarFileList, ".tar.gz": TarFileList,
".tgz": TarFileList, ".zip": ZipFileList}
func ArchiveFileListMap(file string) ([]string, error) {
if function, ok := FunctionForSuffix[Suffix(file)]; ok {
return function(file)
}
return nil, errors.New("unrecognized archive")
}
这个版本使用了map. 它键为string(文件后缀),它的值为一个函数,这个函数必须符合 func(string)([]string, error).
函数使用[]索引操作符检索跟后缀相匹配的函数。如果有对应后缀的处理函数,返回这个函数和true. 如果没有这个函数,则返回nil和false.
这个函数相对于if和switch版本来说更加灵活,由于不需要关注map中的元素有多少个,都不需要修改到这个函数。并且不像大的if和switch语句,map的查询不会随着元素的增加而降速。此外,map的使用使得整个事情更清晰,并且可以运态的添加元素。
Dynamic Function Creation
另一个使用到运行时函数的场景是,当我们有两个或者多个函数使用不同的算法实现相同的功能,而我们又不能在程序被编译的时候承诺使用哪一个(e.g.,允许我们动态的选择基准测试或者回归测试)。
举例来说,我们之前的IsPalindrome()函数,对于7位的ASCII的字符串来说可以创建一个更简单的IsPalindrome()函数。而于用UTF-8字符串就使用之前的代码。
一种方法是声明一个包级别的变量和函数签名,然后在init()方法中创建相应的函数。
var IsPalindrome func(string) bool // Holds a reference to a function
func init() {
if len(os.Args) > 1 &&
(os.Args[1] == "-a" || os.Args[1] == "--ascii") {
os.Args = append(os.Args[:1], os.Args[2:]...)
// Strip out arg.
IsPalindrome = func(s string) bool {
// Simple ASCII-only version
if len(s) <= 1 {
return true
}
if s[0] != s[len(s)-1] {
return false
}
return IsPalindrome(s[1 : len(s)-1])
}
} else {
IsPalindrome = func(s string) bool { // UTF-8 version
// ... same as earlier ...
}
}
}
我们创建的这个IsPalindrome()函数实现,依赖于命令行参数。如果给了这个参数,如果有指定这个参数,则将它删除(之后的代码不需要知道这个参数,只需要第一个,第三个以及以后的参数),并且创建了一个处理7-bit ASCII的IsPalindrome()函数。在剔除第二个参数的过程中,我们不能在append()调用中使用os.Args[0],这是因为它的第一个参数必须为slice. 所以我们使用os.Args[:1], 它是包含一个元素的slice. 如果在命令行中没有指定ASCII参数,我们创建另一个版本,即可以处理7位的ASCII,也可以处理UTF-8字符串。在之后的程序里IsPalindrome()函数可以被正常的调用。但实际执行的代码依赖于我们创建的版本。
Generic Functions
在这一章开始的地方,我们创建了一个函数,用来查找传入的参数中,最好整型int. 这个函数中使用到的算法,也适合于其它的数字类型,甚至是字符串。只需要类型支持 < 操作符。在C++中,像这样的情况,我们可以创建一个泛型函数,泛型函数其实就是类型参数化,类型参数化可以使用编译器可以根据需要创建这个函数的多个版本。但在Go语言中,是不支持类型参数化的,所以为了达到跟C++一样的效果,我们必须手动的创建这个函数的多个版本(e.g.,MinimumInt(), MinimumFloat(), MinimumString()). 这样每个类型就相应的处理方法。
以下的这个例子是一个泛行函数
i := Minimum(4, 3, 8, 2, 9).(int)
fmt.Printf("%T %v\n", i, i)
f := Minimum(9.4, -5.4, 3.8, 17.0, -3.1, 0.0).(float64)
fmt.Printf("%T %v\n", f, f)
s := Minimum("K", "X", "B", "C", "CC", "CA", "D", "M").(string)
fmt.Printf("%T %q\n", s, s)
/*
int 2
float64 -5.4
string "B"
*/
这个函数返回一个interface{}类型的值,我们unchecked type assertion将它转变为内置类型。
func Minimum(first interface{}, rest ...interface{}) interface{} {
minimum := first
for _, x := range rest {
switch x := x.(type) {
case int:
if x < minimum.(int) {
minimum = x
}
case float64:
if x < minimum.(float64) {
minimum = x
}
case string:
if x < minimum.(string) {
minimum = x
}
}
}
return minimum
}
我们在参数中使用了interface{}是因为它可以表示任何类型。我们首先假设第一个值是最好的,然后遍历rest变量。每当我们找到的值要比当前的minimum要小,我们就设置minimum为这个值。在最后返回mininum——也是interface{}, 所以我们在调用Minimum()函数时什么要通过断言将它们转换回内置类型。
相对于指定了类型的找最小值的函数,这个函数在性能更低。
泛型函数中的重复代码不能从根本上解决, 如果一个或者多个interface()类型的参数实际是slice. 比如下面这个例子,给一个slice和item,它们具有相同的类型。返回slice中第一次出现在这个item的位置。如果没有找到,则返回-1.
func Index(xs interface{}, x interface{}) int {
switch slice := xs.(type) {
case []int:
for i, y := range slice {
if y == x.(int) {
return i
}
}
case []string:
for i, y := range slice {
if y == x.(string) {
return i
}
}
}
return -1
}
我们只实现了int和string,它们基本上是相同的代码。
以下是这个函数的调用
xs := []int{2, 4, 6, 8}
fmt.Println("5 @", Index(xs, 5), " 6 @", Index(xs, 6))
ys := []string{"C", "B", "K", "A"}
fmt.Println("Z @", Index(ys, "Z"), " A @", Index(ys, "A"))
/*
5 @ -1 6 @ 2
Z @ -1 A @ 3
*/
其实我们真正需要做的处理slice的泛性——这样我们就可以仅在一个循环里指定类型的比较,而不是对每一种可能的类型,都有一个循环做测试。以下这个例子就是——它的输出跟上面的代码wdmj一样的。
func IndexReflectX(xs interface{}, x interface{}) int { // Long-winded way
if slice := reflect.ValueOf(xs); slice.Kind() == reflect.Slice {
for i := 0; i < slice.Len(); i++ {
switch y := slice.Index(i).Interface().(type) {
case int:
if y == x.(int) {
return i
}
case string:
if y == x.(string) {
return i
}
}
}
}
return -1
}
这个函数首先使用了Go的reflection, 将一个xs interface{}转换为slice-typed reflect.Value. 这个值提供了遍历和访问slice的方法。在这里,我们访问每一个元素,并且使用reflect.Value.Interface()函数将这个值提取出来,并作为interface{}返回。这样就可以保证y是元素的实际类型(e.g., int or string)。
实际上, reflect package可以做更多的事情,所以我们可以简化这个函数
func IndexReflect(xs interface{}, x interface{}) int {
if slice := reflect.ValueOf(xs); slice.Kind() == reflect.Slice {
for i := 0; i < slice.Len(); i++ {
if reflect.DeepEqual(x, slice.Index(i)) {
return i
}
}
}
return -1
}
这里我们依赖于reflect.DeepEqual()函数为我们做比较。这个通用的反射函数也可以用于比较数组,slices和structs.
以下是特定类型的版本:
func IntSliceIndex(xs []int, x int) int {
for i, y := range xs {
if x == y {
return i
}
}
return -1
}
这比通用函数更好也更简单,但是要求我们为每一种类型创建一个函数,只是函数的名字和签名不同。
我们可以通过自定义类型组合泛型的好处(只在一处实现逻辑),和特定类型函数的简洁和高效。这个主题贯穿整个下一章。
以下是一个特定类型的函数用于查找[]int中的item, 并返回这个元素的位置,而泛型函数用来处理实际逻辑。
func IntIndexSlicer(ints []int, x int) int {
return IndexSlicer(IntSlice(ints), x)
}
func IndexSlicer(slice Slicer, x interface{}) int {
for i := 0; i < slice.Len(); i++ {
if slice.EqualTo(i, x) {
return i
}
}
return -1
}
IntIndexSlicer()函数接受一个[]int和一个整型参数。并且将它们传递给一个泛型函数IndexSlicer()函数。 IndexSlicer()函数操作是的一个Slicer类型的型——Slicer类型是一个自定义的接口,它提供了Slicer.EqualTo()和Slicer.Len()两个方法.
type Slicer interface {
EqualTo(i int, x interface{}) bool
Len() int
}
type IntSlice []int
func (slice IntSlice) EqualTo(i int, x interface{}) bool {
return slice[i] == x.(int)
}
func (slice IntSlice) Len() int { return len(slice) }
Slicer接口提供了两个需要在实现泛型函数中IndexSlicer()中用到的方法。
IntSlice类型是基于一个[]int(这就是什么IntIndexSlicer()函数可以将一个[]int转换为一个IntSlice), 同时实现了Slicer 接口中提供的两个方法。IntSlice.EqualTo()方法接受一个slice的索引位置和一个值,如果给定的索引跟值相等,则返回true. Slicer接口指定的比较值是一个interface{}类型,而不是一个int. 所以Slicer接口也可以用于其它slice类型的实现(e.g.,FloatSlice和StringSlice), 所以在实际比较中,我们需要将一个值转换为它的实际类型。
我们可以实现其它的自定义slice types, 也满足足Slicer接口,然后可以用于IndexSlicer()函数。
type StringSlice []string
func (slice StringSlice) EqualTo(i int, x interface{}) bool {
return slice[i] == x.(string)
}
func (slice StringSlice) Len() int { return len(slice) }
StringSlice和IntSlice两者的不同在于底成的slice类型不同([]string和[]int),以及断言时指定的类型(string和int). 这同样适用于FloatSlice([]float64和float64)
最后的一个例子中使用到的技术我们在之前的自定义排序时看到过,并且将自定义类型用于标准库sort package的sort函数。自定义接口和自定义类型将在第六章有更全面的讲解。
当我们在使用到slice或者maps时,通常创建的泛型函数可以不需要类型测试或才断言,也可以不需要自定义类型。相反,我们可以使用高阶函数,将我们的泛型函数抽像为所以特定类型,我们将在下一节中讲到。
高阶函数
一个高阶函数是一个函数接收另一个或者多个其它函数作为它的参数。
让我们先看一个简单的高阶函数——
func SliceIndex(limit int, predicate func(i int) bool) int {
for i := 0; i < limit; i++ {
if predicate(i) {
return i
}
}
return -1
}
这个泛型函数在当predicate()返回true时,返回一个元素索引位置. 所以这个函数跟上一节的Index(), IndexReflect(), IntSliceIndex()和IntIndexSlicer()中讨论的是一样的功能。但这里没有代码复制以及type switching or type assertions.
SliceIndex()函数不需要知道或者关心这个slice或者要查找的元素类型——甚至,这个函数上都没有slice和item上的操作。 这个函数的第一个参数是slice的长度,第二个参数是一个函数,它返回给定的索引位置在slice, 是否我们想要查找元素的位置。
以下有四个例子的调用和它们的返回值。
xs := []int{2, 4, 6, 8}
ys := []string{"C", "B", "K", "A"}
fmt.Println(
SliceIndex(len(xs), func(i int) bool { return xs[i] == 5 }),
SliceIndex(len(xs), func(i int) bool { return xs[i] == 6 }),
SliceIndex(len(ys), func(i int) bool { return ys[i] == "Z" }),
SliceIndex(len(ys), func(i int) bool { return ys[i] == "A" }))
//-1 2 -1 3
这些匿名函数作为第二个参数传递给SliceIndex()函数。这种技术也用于sort.Search()函数。
实际上,SliceIndex()是一个一般用途的函数,它不需要对slice进行任何操作。
i := SliceIndex(math.MaxInt32,
func(i int) bool { return i > 0 && i%27 == 0 && i%51 == 0 })
fmt.Println(i)
在这里,我们使用SliceIndex()函数用于查找可以除以27和51的最小自然数。SliceIndex()函数遍历从0到给定限制的数(在这里是math.MaxInt32), 并且每次遍历它都会调用匿名函数(predicate).一旦predicate返回true, SliceIndex()函数会返回它到达的数字。在这种情况下"index position"实际上就是我们要寻找的自然数。
除了在无序的slice中进行搜索,还会经常使用到filter. 过滤我们不感兴趣的元素。以下是一个简单的高阶过滤函数。这个过滤函数使用一个函数,来确定[]int中哪些元素需要,哪些不需要。
readings := []int{4, -3, 2, -7, 8, 19, -11, 7, 18, -6}
even := IntFilter(readings, func(i int) bool { return i%2 == 0 })
fmt.Println(even)
//[4 2 8 18 -6]
这里我们是从readings中过滤出偶数。
func IntFilter(slice []int, predicate func(int) bool) []int {
filtered := make([]int, 0, len(slice))
for i := 0; i < len(slice); i++ {
if predicate(slice[i]) {
filtered = append(filtered, slice[i])
}
}
return filtered
}
IntFilter()函数接收一个[]int和一个predicate过滤函数,predicate用于决定哪些元素保留,而哪些将被删除。IntFilter()函数返回一个新的slice。
过滤一个Slice是一种常见需求,但可惜的是IntFilter()只适用于[]int. 还好,我们可以完全创建一个跟SliceIndex()相似的泛型函数。
func Filter(limit int, predicate func(int) bool, appender func(int)) {
for i := 0; i < limit; i++ {
if predicate(i) {
appender(i)
}
}
}
跟SliceIndex()函数一样,Filter()函数不知道要操作的对像,而只是基于给定的limit. Filter()函数依赖于predicate()和appender()函数,用于过滤和添加。
readings := []int{4, -3, 2, -7, 8, 19, -11, 7, 18, -6}
even := make([]int, 0, len(readings))
Filter(len(readings), func(i int) bool { return readings[i]%2 == 0 },
func(i int) { even = append(even, readings[i]) })
fmt.Println(even)
这段代码跟之前那段具有相同的功能,只是我们在Filter()函数之外创建了even slice. 第一个传递给Filter()的匿名函数——它接收一个索引位置,如果这个索引位置上的元素是偶数则返回true. 第二个匿名函数是将给定索引位置上的元素,添加到新的slice.
parts := []string{"X15", "T14", "X23", "A41", "L19", "X57", "A63"}
var Xparts []string
Filter(len(parts), func(i int) bool { return parts[i][0] == 'X' },
func(i int) { Xparts = append(Xparts, parts[i]) })
fmt.Println(Xparts)
这个例子仅仅是演示,如何将Filter应用于字符串。
var product int64 = 1
Filter(26, func(i int) bool { return i%2 != 0 },
func(i int) { product *= int64(i) })
fmt.Println(product)
最后一段filter代码演示的跟SliceIndex()函数类型,Filter()函数不仅仅用于Slice. 这里我们使用这个函数计算[1,25]类的奇数的乘积。
函数记忆
一个pure function(纯函数)是指一个函数对给定的一个参数或者多个参数都是返回相同的结果——并且不会产生副作用。如果一个纯函数的计算非常昂贵并且会经常的计算相同的参数,那么我们可以使用记忆以减少计算的开销——但会使用到更多的内存。memoization technique我们缓存计算的结果,当我们下次有相同的计算时,可以直接返回缓存的结果,而不是在一次计算。
我们在5.2节中了解到,递归的Fibonacci算法是非常昂贵的,它包含了讲多重复的计算。在这种情况下,最简单的是使用一个非递归算法,但这里我们只是展示memoization,我们将创建一个memoized Fibonacci的函数。
type memoizeFunction func(int, ...int) interface{}
var Fibonacci memoizeFunction
func init() {
Fibonacci = Memoize(func(x int, xs ...int) interface{} {
if x < 2 {
return x
}
return Fibonacci(x-1).(int) + Fibonacci(x-2).(int)
})
}
Momoize()函数我们会稍候看到它的实现,它用于记忆一个函数的结果,但是这个函数必须接返一个int, 并且返回一个interface{}. 为了方便,我们创建了一个memoizeFunction类型, 用于表示这类的函数, 然后声明了一个Fabonacci的变量,用来这种类型函数。然后在init()函数中,我们创建了一个匿名函数,用于Fibonacci的计算——并且将这个匿名函数传递给Memonize()函数。反过来,Memoize()函数返回一个memoizeFunction类型的函数,并赋值给Fibonacci变量.
在这个例子中,我们仅给Fibonacci()函数传递单个的参数。所以我们忽略其它的整型(比如,我们将忽略xs参数, 在这种情况下为空slice). 而且,当我们在对递归调用产生的结果进行求和时,我们必须unchecked type assertion将一个interface{}的值转换为int.
现在我们可以像其它函数一样使用Fibonacci(). 并且因为使用了记忆,所以不需要重复计算。
fmt.Println("Fibonacci(45) =", Fibonacci(45).(int))
Fibonacci(45) = 1134903170
在这个例子中,我们使用了一个递归的记忆Fibonacci()函数,并且输出结果。 我们在输出结果时,使用了断言将它返回的interface{}转化为int.(严格来说,我们在这里不需要转换,因为fmt package的打印函数会足够智能的做这些)。
func Memoize(function memoizeFunction) memoizeFunction {
cache := make(map[string]interface{})
return func(x int, xs ...int) interface{} {
key := fmt.Sprint(x)
for _, i := range xs {
key += fmt.Sprintf(",%d", i)
}
if value, found := cache[key]; found {
return value
}
value := function(x, xs...)
cache[key] = value
return value
}
}
我们这里使用的Memoize()函数是非常基本的。它接受一个memoizeFunction函数作为它的参数(签名为 func(int, ...int) interface{}),并且返回一个相同签名的函数。
我们使用一个map来缓存已经计算好的结果。这个map可以被这个闭包捕获。map的键是一个由所有的整型参数组成的一个以逗号分隔的字符串(Go map的键必须可以使用==和!=进行比较——字符串符合这样的要深圳市,但是slice不符合). 一旦计算出一个键,我们就查看map是否已经有这个键的值。如果有,我们不需要重复计算,只是简单的返回已经缓存的值。否则我们执行计算,并且缓存这个计算的值。
记忆化对于相对花销昂贵的, 并且又多次调用相同参数的纯函数很有用(不管是否有递归). 比如我们需要将大量的整型数字转换为罗马数字,并且有大量的数字会被重复,这就适合于使用Memoize()函数,以避免重复计算.
var RomanForDecimal memoizeFunction
func init() {
decimals := []int{1000, 900, 500, 400, 100, 90, 50, 40, 10, 9, 5, 4, 1}
romans := []string{"M", "CM", "D", "CD", "C", "XC", "L", "XL", "X",
"IX", "V", "IV", "I"}
RomanForDecimal = Memoize(func(x int, xs ...int) interface{} {
if x < 0 || x > 3999 {
panic("RomanForDecimal() only handles integers [0, 3999]")
}
var buffer bytes.Buffer
for i, decimal := range decimals {
remainder := x / decimal
x %= decimal
if remainder > 0 {
buffer.WriteString(strings.Repeat(romans[i], remainder))
}
}
return buffer.String()
})
}