go语言中,作为一等类型的函数,是可以作为值来传递和使用。而闭包,则是函数和环境变量的结合。将函数作为参数,利用闭包的特性,可以用简洁的代码提供实用的功能。
之前提到call通过wg组合,来规避同一时刻同样的耗时操作导致系统崩溃。【golang积累-Call回调模式】,这个在Groupcache【github】的代码中用于同样数据在惰性加载的时候,对数据库的过热请求。具体代码参见:【singleflight.go】。
但如果不是同一时刻的访问冲突,而是one by one一次一次的重复处理,这时我们通常为了避免重复计算,尤其是耗时且又通用的计算处理、数据库查询,就会考虑cache。通常会非常亲切的在很多地方类似的代码:
cache:=make(map[string]interface{})
//...
if _,founded:=cache[key];founded{
//do something
}else{
v:=function(key)
cache[key]=v
}
如果业务中使用场景较多,可考虑封装到高阶函数中,在函数内部封装cache进行过滤。
memcache函数的基本形式
//需要被cache结果的函数
type memoizeFunction func(int, ...int) int
//封装cache的高阶函数,每次运算都会先查找cache,如果没有则计算
func Memoize(function memoizeFunction)memoizeFunction{
//封装了的cache
cache:=make(map[string]int)
return func(x int,xs ...int)int{
//1、将函数的输入参数展开并合并为字符串,作为cache的key。对于参数按顺序的情况非常实用。
key:=fmt.Sprint(x)
for _,i:=range xs{
key+=fmt.Sprintf(",%d",i)
}
//2、在cache中查找
if value,found:=cache[key];found{
return value
}
//3、没有缓存,则计算,并将结果那入到cache中
value:=function(x,xs...)
cache[key]=value
return value
}
}
//具体的业务方法非常耗时的计算
var caculate = Memoize(func(x int, xs ...int) int {
//通过sleep模拟耗时1秒的内部处理
time.Sleep(time.Second)
//随机返回一个结果
return rand.Intn(10)
})
func main() {
//模拟计算100次,实际只有前10次是真实计算,后边都会cache出结果。
for i := 0; i < 100; i++ {
caculate(i % 10)
}
}
代码中,Memoize这个函数,其实有类似于result pool的作用。 每次只需要修改caculate内部的具体代码即可。其是否已被cache还是重新计算都被Memoize进行了封装。对于这部分代码,可以理解为:
- memoize就是一个独立的运行区域,
- caculate通过Memoize的返回值定位,访问只由它可见的cache,可以把第19行代码转换为匿名函数,就很清晰:
//...
//3、没有缓存,则计算,并将结果那入到cache中
value:=func(x,xs...int)int{//<------此处开始,用匿名函数展开caculate的函数代码
time.Sleep(time.Second)
return rand.Intn(10)
}(x,xs...)
cache[key]=value //<--------对匿名函数而言,cache是外部公用的
return value
memcache函数的扩展
现实中,前面代码还有几个缺陷:
1. 返回值是整形,限制很大。
2. 输入值是整形,不适合其他形式。
由于go的语法特征,对返回值可以改为interface{},根据具体业务再进行转换。而输入值,则根据情况斟酌是否转换。
在有些资料中,提到斐波拉契函数的计算优化,就用到了cache来规避多次递归。代码如下:
package main
import "fmt"
// 将结果形式扩展为interface{}
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)
})
}
func Memoize(function memoizeFunction) memoizeFunction {
//封装了的cache
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
}
//没有缓存,则计算,并将结果那入到cache中
value := function(x, xs...)
cache[key] = value
return value
}
}
func main() {
fmt.Println("Fibonacci(45)=", Fibonacci(45))
}
此时,由于用到了递归,感觉会复杂一些。原理其实没变,
Memoize中的cache是在Fibonacci初始化的时候就已经创建好了,也就是下边这行代码出现的时候:
Fibonacci = Memoize(func(x int, xs …int) interface{} {
Fibonacci递归的时候,仅仅就是从key := fmt.Sprint(x)开始执行,这与传统的递归调用是相通的。
其他
代码中的key转换,实际上是有序的
key:=fmt.Sprint(x)
for _,i:=range xs{
key+=fmt.Sprintf(",%d",i)
}
如果入参是无序集合,而集合元素的顺序确不同,此时key并不相同,无法直接定位结果,依然会重新计算。考虑将入参改为interface{},并进行排序处理应该可以优化。
cache是函数内部私有
如果同一个文件中,即使多个函数使用Memoize也不用担心相同key的冲突,因为每个函数都会有一个内部的cache,这是闭包函数的特点。
简言之,通过封装cache的记忆闭包,结合call模式,将会大幅提升系统的性能和健壮性。本质上,就是利用语法替代了一些模式上的应用。当然,从代码风格上,个人认为golang的记忆闭包,比scala的高阶函数要难理解。这或许算是个例吧!
有疑问加站长微信联系(非本文作者)