golang积累-记忆闭包

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进行了封装。对于这部分代码,可以理解为:

  1. memoize就是一个独立的运行区域,
  2. 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))
}

此时,由于用到了递归,感觉会复杂一些。原理其实没变,

  1. Memoize中的cache是在Fibonacci初始化的时候就已经创建好了,也就是下边这行代码出现的时候:

    Fibonacci = Memoize(func(x int, xs …int) interface{} {

  2. 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的高阶函数要难理解。这或许算是个例吧!

你可能感兴趣的:(golang,golang,闭包)