一、函数使用入门
概述
在 Go 语言中,函数主要有三种类型:
- 普通函数
- 匿名函数(闭包)
- 类方法
函数定义
Go语言中定义函数使用func
关键字,具体格式如下:
func 函数名(参数)(返回值){ 函数体 }
Go 普通函数的基本组成包括:关键字 func
、函数名、参数列表、返回值、函数体和返回语句。作为强类型语言,无论是参数还是返回值,在定义函数时,都要声明其类型,这里我们用一个最简单的加法函数来进行详细说明
func add(a, b int) int { return a + b }
如果函数的参数列表中包含若干个类型相同的参数,比如上面例子中的 a
和 b
,则可以在参数列表中省略前面变量的类型声明,只保留最后一个
func add(a, b int) int { // ... }
函数调用
调用同一个包定义的函数
func main() { fmt.Println(add(1, 2)) // 3 }
调用其他包定义的函数
如果是在不同的包中,需要先导入了该函数所在的包,然后才能调用该函数,比如,我们将 add
函数放到单独的 mymath
包中(函数名首字母需要改为大写)
package mymath func Add(a, b int) int { return a + b }
然后我们可以这样在 main
包中调用 Add
函数:
package main import ( "fmt" "mymath" ) func main() { fmt.Println(mymath.Add(1, 2)) // 3 }
在调用其他包定义的函数时,只有函数名首字母大写的函数才可以被访问
Go 语言中没有 public
、protected
、private
之类的关键字,它是通过首字母的大小写来区分可见性的:首字母小写的函数只能在同一个包中访问,首字母大写的函数才可以在其他包中调用,Go 文件中定义的全局变量也是如此
二、参数
传递参数
按值传参
Go 语言默认使用按值传参来传递参数,也就是传递参数值的一个副本:函数接收到传递进来的参数后,会将参数值拷贝给声明该参数的变量,如果在函数体中有对参数值做修改,实际上修改的是形参值,不影响到实际传递进来的参数值,以add
函数为例
func add(a, b int) int { a *= 2 b *= 3 return a + b } func main() { x, y := 1, 2 z := add(x, y) fmt.Printf("add(%d, %d) = %d\n", x, y, z) }
当我们把 x
、y
变量作为参数传递到 add
函数时,这两个变量会拷贝出一个副本赋值给 a
、b
变量作为参数,因此,在 add
函数中修改 a
、b
变量的值并不会影响原变量 x
、y
的值
引用传参
想要实现在函数中修改形参值可以同时修改实参值,需要通过引用传参来完成,此时传递给函数的参数是一个指针,而指针代表的是实参的内存地址,修改指针引用的值即修改变量内存地址中存储的值,所以实参的值也会被修改(&获取变量所在的内存地址,*
获取指针指向内存地址存储的变量值)
func add(a, b *int) int { *a *= 2 *b *= 3 return *a + *b } func main() { x, y := 1, 2 z := add(&x, &y) fmt.Printf("add(%d, %d) = %d\n", x, y, z) }
变长参数
变长参数指的是函数参数的数量不确定,可以按照需要传递任意数量的参数到指定函数,合适地使用变长参数
基本定义和传值
只需要在参数类型前加上 ...
前缀,就可以将该参数声明为变长参数
// 函数 myfunc() 接受任意数量的参数,这些参数的类型全部是 int func myfunc(numbers ...int) { for _, number := range numbers { fmt.Println(number) } }
变长参数还支持传递一个 []int
类型的切片,传递切片时需要在末尾加上 ...
作为标识,表示对应的参数类型是变长参数
slice := []int{1, 2, 3, 4, 5} myfunc(slice...) myfunc(slice[1:3]...) //...type 格式的类型只能作为函数的参数类型存在,并且必须是函数的最后一个参数
任意类型的变长参数(泛型)
指定变长参数类型为 interface{}就可以
支持传递任意类型的值作为变长参数
Go 语言标准库中 fmt.Printf()
的函数原型
func Printf(format string, args ...interface{}) { // ... }
参照其实现来自定义一个支持任意类型的变长参数函数
func myPrintf(args ...interface{}) { for _, arg := range args { switch reflect.TypeOf(arg).Kind() { case reflect.Int: fmt.Println(arg, "is an int value.") case reflect.String: fmt.Printf("\"%s\" is a string value.\n", arg) case reflect.Array: fmt.Println(arg, "is an array type.") default: fmt.Println(arg, "is an unknown type.") } } } func main() { myPrintf(1, "1", [1]int{1}, true) }
多返回值
func add(a, b *int) (int, error) { if *a < 0 || *b < 0 { err := errors.New("只支持非负整数相加") return 0, err } *a *= 2 *b *= 3 return *a + *b, nil } func main() { x, y := -1, 2 z, err := add(&x, &y) if err != nil { fmt.Println(err.Error()) return } fmt.Printf("add(%d, %d) = %d\n", x, y, z) }
通过 error
指定多返回一个表示错误信息的、类型为 error
的返回值,函数的多个返回值之间可以通过逗号分隔,并且在最外面通过圆括号包起来
命名返回值
对返回值进行变量命名,而不必每次都按照指定的返回值格式返回多个变量
func calc(x, y int) (sum, sub int) {
sum = x + y
sub = x - y
return
}
三、匿名函数与闭包
匿名函数的定义和使用
匿名函数的定义格式如下
func(参数)(返回值){ 函数体 } func(a, b int) int { return a + b }
Go 匿名函数也可以赋值给一个变量或者直接执行
// 1、将匿名函数赋值给变量 add := func(a, b int) int { return a + b } // 调用匿名函数 add fmt.Println(add(1, 2)) // 2、定义时直接调用匿名函数 func(a, b int) { fmt.Println(a + b) } (1, 2)
匿名函数的常见使用场景
保证局部变量的安全性
匿名函数内部声明的局部变量无法从外部修改,从而确保了安全性
var j int = 1 f := func() { var i int = 1 fmt.Printf("i, j: %d, %d\n", i, j) } f() j += 2 f()
将匿名函数作为函数参数
add := func(a, b int) int { return a + b } // 将函数类型作为参数 func(call func(int, int) int) { fmt.Println(call(1, 2)) }(add)
将匿名函数作为函数返回值
// 将函数作为返回值类型 func deferAdd(a, b int) func() int { return func() int { return a + b } } func main() { ... // 此时返回的是匿名函数 addFunc := deferAdd(1, 2) // 这里才会真正执行加法操作 fmt.Println(addFunc()) }
四、通过高阶函数实现装饰器模式
装饰器模式
装饰器模式(Decorator)是一种软件设计模式,其应用场景是为某个已经存在的功能模块(类或者函数)添加一些「装饰」功能,而又不会侵入和修改原有的功能模块
通过高阶函数实现装饰器模式
编写基本功能模块
package main import "fmt" func multiply(a, b int) int { return a * b } func main() { a := 2 b := 8 c := multiply(a, b) fmt.Printf("%d x %d = %d\n", a, b, c) }
想要在不修改现有 multiply
函数代码的前提下计算乘法运算的执行时间,显然,这可以引入装饰器模式来实现
装饰器模式实现
package main import ( "fmt" "time" ) // 为函数类型设置别名提高代码可读性 type MultiPlyFunc func(int, int) int // 乘法运算函数 func multiply(a, b int) int { return a * b } // 通过高阶函数在不侵入原有函数实现的前提下计算乘法函数执行时间 func execTime(f MultiPlyFunc) MultiPlyFunc { return func(a, b int) int { start := time.Now() // 起始时间 c := f(a, b) // 执行乘法运算函数 end := time.Since(start) // 函数执行完毕耗时 fmt.Printf("--- 执行耗时: %v ---\n", end) return c // 返回计算结果 } } func main() { a := 2 b := 8 // 通过修饰器调用乘法函数,返回的是一个匿名函数 decorator := execTime(multiply) // 执行修饰器返回函数 c := decorator(a, b) fmt.Printf("%d x %d = %d\n", a, b, c) }
代码分析
1.通过 type 语句为匿名函数类型设置了别名 MultiPlyFunc,后续用这个类型别名来声明对应的函数类型参数和返回值 2.装饰器模式实现代码 execTime 函数 2.1在返回的 MultiPlyFunc 类型匿名函数体中,真正执行乘法运算函数 f 前,先通过 time.Now() 获取当前系统时间,并将其赋值给 start 变量; 2.2然后执行 f 函数,将返回值赋值给变量 c; 2.3接下来,通过 time.Since(start) 计算从 start 到现在经过的时间,也就是 f 函数执行耗时,将结果赋值给 end 变量并打印出来; 2.4最后返回 f 函数运行结果 c 作为最终返回值。 核心思路就是在被修饰的功能模块(这里是外部传入的乘法函数 f)执行前后加上一些额外的业务逻辑,而又不影响原有功能模块的执行 在 main 函数中调用乘法函数 multiply 时,如果要应用装饰器,需要通过装饰器 execTime 包裹,装饰器返回的是个匿名函数,所以需要再度调用才能真正执行
对比位运算与算术运算的性能
为了更好地体现装饰器模式的优势,我们还可以在此基础上实现一个比较位运算和算术运算性能的程序:
package main import ( "fmt" "time" ) // 为函数类型设置别名提高代码可读性 type MultiPlyFunc func(int, int) int // 乘法运算函数1(算术运算) func multiply1(a, b int) int { return a * b } // 乘法运算函数2(位运算) func multiply2(a, b int) int { return a << b } // 通过高阶函数在不侵入原有函数实现的前提下计算乘法函数执行时间 func execTime(f MultiPlyFunc) MultiPlyFunc { return func(a, b int) int { start := time.Now() // 起始时间 c := f(a, b) // 执行乘法运算函数 end := time.Since(start) // 函数执行完毕耗时 fmt.Printf("--- 执行耗时: %v ---\n", end) return c // 返回计算结果 } } func main() { a := 2 b := 8 fmt.Println("算术运算:") decorator1 := execTime(multiply1) c := decorator1(a, b) fmt.Printf("%d x %d = %d\n", a, b, c) fmt.Println("位运算:") decorator2 := execTime(multiply2) a = 1 b = 4 c = decorator2(a, b) fmt.Printf("%d << %d = %d\n", a, b, c) }
原有的代码逻辑不需要做任何变动,只需要新增一个位运算版乘法实现函数 multiply2
,然后套上装饰器函数 execTime
计算耗时,最后与算术运算版乘法实现函数 multiply1
耗时做下对比即可
五、递归函数及性能调优
递归函数及编写思路
所谓递归函数指的是在函数内部调用函数自身的函数,从数学解题思路来说,递归就是把一个大问题拆分成多个小问题,再各个击破,在实际开发过程中,某个问题满足以下条件就可以通过递归函数来解决:
- 一个问题的解可以被拆分成多个子问题的解
- 拆分前的原问题与拆分后的子问题除了数据规模不同,求解思路完全一样
- 子问题存在递归终止条件
需要注意的是,编写递归函数时,这个递归一定要有终止条件,否则就会无限调用下去,直到内存溢出。所以我们可以归纳出递归函数的编写思路:抽象出递归模型(可以被复用到子问题的模式/公式),同时找到终止条件。‘’
通过斐波那契数列求解演示
对应的递归函数 fibonacci
实现如下
func fibonacci(n int) int { if n == 1 { return 0 } if n == 2 { return 1 } return fibonacci(n-1) + fibonacci(n-2) } func main() { n := 5 num := fibonacci(n) fmt.Printf("The %dth number of fibonacci sequence is %d\n", n, num) }
递归函数性能优化
递归函数是层层递归嵌套执行的,如果层级不深,比如上面这种,很快就会返回结果,但如果传入一个更大的序号,比如 50,就会明显感觉到延迟。
为了更直观地对比耗时差异,我们参照上篇教程实现一个斐波那契函数版的耗时计算函数 fibonacciExecTime
,然后通过装饰器模式调用 fibonacci
函数:
package main import ( "fmt" "time" ) type FibonacciFunc func(int) int // 通过递归函数实现斐波那契数列 func fibonacci(n int) int { // 终止条件 if n == 1 { return 0 } if n == 2 { return 1 } // 递归公式 return fibonacci(n-1) + fibonacci(n-2) } // 斐波那契函数执行耗时计算 func fibonacciExecTime(f FibonacciFunc) FibonacciFunc { return func(n int) int { start := time.Now() // 起始时间 num := f(n) // 执行斐波那契函数 end := time.Since(start) // 函数执行完毕耗时 fmt.Printf("--- 执行耗时: %v ---\n", end) return num // 返回计算结果 } } func main() { n1 := 5 f := fibonacciExecTime(fibonacci) r1 := f(n1) fmt.Printf("The %dth number of fibonacci sequence is %d\n", n1, r1) n2 := 50 r2 := f(n2) fmt.Printf("The %dth number of fibonacci sequence is %d\n", n2, r2) }
可以看到,虽然 5 和 50 从序号上看只相差了 10 倍,但是最终体现在递归函数的执行时间上,却是不止十倍百倍的巨大差别
通过内存缓存技术优化递归函数性能
通过缓存中间计算结果来避免重复计算,从而提升递归函数的性能
代码实现
const MAX = 50 var fibs [MAX]int // 缓存中间结果的递归函数优化版 func fibonacci2(n int) int { if n == 1 { return 0 } if n == 2 { return 1 } index := n - 1 if fibs[index] != 0 { return fibs[index] } num := fibonacci2(n-1) + fibonacci2(n-2) fibs[index] = num return num }
func main() { n1 := 5 f1 := fibonacciExecTime(fibonacci) r1 := f1(n1) fmt.Printf("The %dth number of fibonacci sequence is %d\n", n1, r1) n2 := 50 r2 := f1(n2) fmt.Printf("The %dth number of fibonacci sequence is %d\n", n2, r2) f2 := fibonacciExecTime(fibonacci2) r3 := f2(n2) fmt.Printf("The %dth number of fibonacci sequence is %d\n", n2, r3) }
通过预定义数组 fibs
保存已经计算过的斐波那契序号对应的数值,这样下次要获取对应序号的斐波那契值时会直接返回而不是调用一次递归函数进行计算
通过尾递归优化递归函数性能
尾递归优化是函数式编程的重要特性之一,在了解尾递归优化前,我们先来看看什么是尾递归。
在计算机科学里,尾调用是指一个函数的最后一个动作是调用一个函数(只能是一个函数调用,不能有其他操作,比如函数相加、乘以常量等):
func f(x int) int { ... return g(x); }
这种情况下称该调用位置为尾位置,若这个函数在尾位置调用自身,则称这种情况为尾递归,它是尾调用的一种特殊情形。尾调用的一个重要特性是它不是在函数调用栈上添加一个新的堆栈帧 —— 而是更新它,尾递归自然也继承了这一特性,这就使得原来层层递进的调用栈变成了线性结构,因而可以极大优化内存占用,提升程序性能,这就是尾递归优化技术。
以计算斐波那契数列的递归函数为例,简单来说,就是处于函数尾部的递归调用前面的中间状态都不需要再保存了,这可以节省很大的内存空间,在此之前的代码实现中,递归调用 fibonacci(n-1)
时,还有 fibonacci(n-2)
没有执行,因此需要保存前面的中间状态,内存开销很大。
尾递归的实现需要重构之前的递归函数,确保最后一步只调用自身,要做到这一点,就要把所有用到的内部变量/中间状态变成函数参数,以 fibonacci
函数为例,就是 fibonacci(n-1)
和 fibonacci(n-2)
,我们可以这样实现尾递归函数 fibonacciTail
:
func fibonacciTail(n, first, second int) int { if n < 2 { return first } return fibonacciTail(n-1, second, first+second) }
当前 first
+ second
的和赋值给下次调用的 second
参数,当前 second
值赋值给下次调用的 first
参数,就等同于实现了 F(n) = F(n-1) + F(n-2)
的效果,循环往复,不断累加,直到 n
值等于 1(F(1) = 0,无需继续迭代下去),则返回 first
的值,也就是最终的 F(n)
的值。
简单来说,就是把原来通过递归调用计算结果转化为通过外部传递参数初始化,再传递给下次尾递归调用不断累加,这样就可以保证 fibonacciTail
调用始终是线性结构的更新,不需要开辟新的堆栈保存中间函数调用。
但是从语义上看这个新的斐波那契函数有点怪,我们可以在外面套一层:
func fibonacci3(n int) int { return fibonacciTail(n, 0, 1) // F(1) = 0, F(2) = 1 }
这样,就可以像之前一样调用 fibonacci3
计算在斐波那契数列中序号 n
的值了:
func fibonacciTail(n, first, second int) int { if n < 2 { return first } return fibonacciTail(n-1, second, first+second) } func fibonacci3(n int) int { return fibonacciTail(n, 0, 1) // F(1) = 0, F(2) = 1 } func main() { n1 := 5 f1 := fibonacciExecTime(fibonacci) r1 := f1(n1) fmt.Printf("The %dth number of fibonacci sequence is %d\n", n1, r1) n2 := 50 r2 := f1(n2) fmt.Printf("The %dth number of fibonacci sequence is %d\n", n2, r2) f2 := fibonacciExecTime(fibonacci2) r3 := f2(n2) fmt.Printf("The %dth number of fibonacci sequence is %d\n", n2, r3) f3 := fibonacciExecTime(fibonacci3) r4 := f3(n2) fmt.Printf("The %dth number of fibonacci sequence is %d\n", n2, r4) }
六、 Map-Reduce-Filter 模式处理集合元素
从处理集合元素聊起
日常开发过程中,要处理数组、切片、字典等集合类型,常规做法都是循环迭代进行处理。比如将一个字典类型用户切片中的所有年龄属性值提取出来,然后求和,常规实现是通过循环遍历所有切片,然后从用户字典键值对中提取出年龄字段值,再依次进行累加,最后返回计算结果
package main import ( "fmt" "strconv" ) func ageSum(users []map[string]string) int { var sum int for _, user := range users { num, _ := strconv.Atoi(user["age"]) sum += num } return sum } func main() { var users = []map[string]string{ { "name": "张三", "age": "18", }, { "name": "李四", "age": "22", }, { "name": "王五", "age": "20", }, } fmt.Printf("用户年龄累加结果: %d\n", ageSum(users)) }
针对简单的单个场景,这么实现没什么问题,但这是典型的面向过程思维,而且代码几乎没有什么复用性可言:每次处理类似的问题都要编写同样的代码模板,比如计算其他字段值,或者修改类型转化逻辑,都要重新编写实现代码
引入 Map-Reduce
Map-Reduce 并不是一个整体,而是要分两步实现:Map 和 Reduce,这个示例也正好符合 Map-Reduce 模型:先将字典类型切片转化为一个字符串类型切片(Map,字面意思就是一一映射),再将转化后的切片元素转化为整型后累加起来(Reduce,字面意思就是将多个集合元素通过迭代处理减少为一个)。
为此,我们先要实现 Map 映射转化函数:
func mapToString(items []map[string]string, f func(map[string]string) string) []string { newSlice := make([]string, len(items)) for _, item := range items { newSlice = append(newSlice, f(item)) } return newSlice }
再编写 Reduce 求和函数
func fieldSum(items []string, f func(string) int) int { var sum int for _, item := range items{ sum += f(item) } return sum }
通过 Map-Reduce 重构后没有什么硬编码,类型转化和字段获取逻辑都封装到两个函数支持的函数类型参数中实现了,在 main
函数中编写新的调用代码如下:
ageSlice := mapToString(users, func(user map[string]string) string { return user["age"] }) sum := fieldSum(ageSlice, func(age string) int { intAge, _ := strconv.Atoi(age) return intAge }) fmt.Printf("用户年龄累加结果: %d\n", sum)
Filter 函数
有的时候,为了让 Map-Reduce 代码更加健壮(排除无效的字段值),或者只对指定范围的数据进行统计计算,还可以在 Map-Reduce 基础上引入 Filter(过滤器),对集合元素进行过滤
上面的代码中新增一个 Filter 函数
func itemsFilter(items []map[string]string, f func(map[string]string) bool) []map[string]string { newSlice := make([]map[string]string, len(items)) for _, item := range items { if f(item) { newSlice = append(newSlice, item) } } return newSlice }
在 main
函数中应用 Filter 函数对无效用户年龄进行过滤,或者排除指定范围年龄:
func main() { var users = []map[string]string{ { "name": "张三", "age": "18", }, { "name": "李四", "age": "22", }, { "name": "王五", "age": "20", }, { "name": "赵六", "age": "-10", }, { "name": "孙七", "age": "60", }, { "name": "周八", "age": "10", }, } //fmt.Printf("用户年龄累加结果: %d\n", ageSum(users)) validUsers := itemsFilter(users, func(user map[string]string) bool { age, ok := user["age"] if !ok { return false } intAge, err := strconv.Atoi(age) if err != nil { return false } if intAge < 18 || intAge > 35 { return false } return true }) ageSlice := mapToString(validUsers, func(user map[string]string) string { return user["age"] }) sum := fieldSum(ageSlice, func(age string) int { intAge, _ := strconv.Atoi(age) return intAge }) fmt.Printf("用户年龄累加结果: %d\n", sum) }
七、基于管道技术实现函数的流式调用
通过管道重构 Map-Reduce-Filter 代码
上面的代码分别调用Map-Reduce-Filter 这三个函数显得很繁琐,不够优雅,今天,我们正好可以通过管道模式实现这三个函数的流式调用
package main import ( "log" ) type user struct { name string age int } func filterAge(users []user) interface{} { var slice []user for _, u := range users { if u.age >= 18 && u.age <= 35 { slice = append(slice, u) } } return slice } func mapAgeToSlice(users []user) interface{} { var slice []int for _, u := range users { slice = append(slice, u.age) } return slice } func sumAge(users []user, pipes ...func([]user) interface{}) int { var ages []int var sum int for _, f := range pipes { result := f(users) switch result.(type) { case []user: users = result.([]user) case []int: ages = result.([]int) } } if len(ages) == 0 { log.Fatalln("没有在管道中加入 mapAgeToSlice 方法") } for _, age := range ages { sum += age } return sum }
将 Filter 和 Map 函数中的闭包函数取消掉了,改为直接在代码中实现,以便精简代码,为了便于通过管道统一声明 Filter 和 Map 函数,将他们的返回值声明成了空接口 interface{}
表示可以返回任何类型。
接下来重点来看 Reduce 函数 sumAge
的实现,这里,我们将其第二个参数声明为了变长参数类型,表示支持传递多个处理函数,这些处理器函数按照声明的先后顺序依次调用,由于这些处理函数的返回值类型被声明为了空接口,所以需要在运行时动态对它们的返回值类型做检测,并赋值给指定变量,以便程序可以按照我们期望的路径执行下去,而不会因为类型问题报错退出
for _, f := range pipes { result := f(users) switch result.(type) { case []user: users = result.([]user) case []int: ages = result.([]int) } }
最后一个处理函数的结果 ages
整型切片将作为 Reduce 函数求和逻辑的数据源
流式调用 Map-Reduce-Filter 函数
我们在 main
函数中通过管道组合 Map-Reduce-Filter 功能模块,实现这些函数的流式调用:func main() {
var users = []user{ { name: "张三", age: 18, }, { name: "李四", age: 22, }, { name: "王五", age: 20, }, { name: "赵六", age: -10, }, { name: "孙七", age: 60, }, { name: "周八", age: 10, }, } sum := sumAge(users, filterAge, mapAgeToSlice) log.Printf("用户年龄累加结果: %d\n", sum) }
通过管道,我们可以更优雅地实现 Filter->Map->Reduce 的流式调用。此外,管道技术在 HTTP 请求处理中间件中也有广泛的应用,后面介绍 Web 编程时会提到