本文主要介绍如何利用go的闭包机制实现一个函数缓存,并用 sync.Map 优化并发性能,weak 优化GC流程,同时通过泛型机制方便缓存的创建。
调用结果缓存
如果需要减少重复计算,节省cpu资源,可以给一个函数或者方法加上缓存层,在调用缓存时先检查是否已经有结果,如果没有,再调用具体的函数进行计算,并将计算的结果存入缓存层,在一定时间内,后续的请求会通过缓存层取值。以一个非常简单并且没有定时器的缓存为例:
type FnType func(FnParams) (FnReturns, error)
// 存储的函数所有参数
type FnParams strcut {
...
}
// 函数/方法返回值,不包括error
type FnReturns struct {
...
}
func FnCache(fn FnType) FnType {
cacheMap := make(map[FnParams]FnResult)
return func(fp FnParams) (FnResult, error) {
if res, ok := cacheMap[fp]; ok {
fmt.Println("hit cache")
return res, nil
}
fmt.Println("cache miss")
res, err := fn(fp)
if err == nil {
cacheMap[fp] = res
}
return res, err
}
}
在这里一个简单的加法函数为例, 令FnParams 为两个int变量,FnRetruns包含一个int变量,Fn 函数返回int变量的和。
type FnParams struct {
Param1 int
Param2 int
}
type FnReturns struct {
Result int
}
func Fn(fp FnParams) (FnReturns, error) {
return FnReturns{fp.Param1 + fp.Param2}, nil
}
func main() {
cachedFn := FnCache(Fn)
p1 := FnParams{1, 2}
r1, _ := cachedFn(p1)
fmt.Println(r1.Result)
_, _ = cachedFn(p1)
_, _ = cachedFn(p1)
}
运行程序,显示如下结构,可以看到,后面的调用都命中了缓存,
$ go run main.go
cache miss
3
hit cache
hit cache
如果希望添加 TTL/Expiration 机制的话可以在 FnReturns 的基础上创建结构 cacheEntry 包含过期时间
type cacheEntry struct {
result FnReturns
expiration time.Time
}
并对 FnCache 的缓存命中判断部分进行修改函数进行修改
if entry, ok := cacheMap[fp]; ok {
if time.Now().Before(entry.expiration) {
fmt.Println("hit cache")
return entry.result, nil
}
// Remove expired entry.
delete(cacheMap, fp)
}
generic type 泛型
Go 在 1.18 版本添加了对于 generic type 的支持,并在 1.24 版本添加了对于 generic type alias
泛型语法
通过泛型能够定义一个新的类型,泛型的定义需要包含新的类型名,类型参数列表(type parameters),其中每一项都由类型名称(与底层类型的列表相对应)和限制(constraint)构成,以及底层类型(UnderlyingType)。对于类型的定义如下:
type TypeName[<type parameters>] UnderlyingType
现在就可以对函数类型 FnType 以及缓存map的类型 FnCacheMapType 进行定义:
type FnType[FnParams comparable, FnReturns any] func(FnParams) (FnReturns, error)
其中 FnParams 和 FnReturns 后面的字段都是限制(constraint)字段,comparable 代表可以比较的,因为要作为字典的键,any 则是任意的,即任何类型都行,因为需要作为字典的值。
在函数中使用泛型时,则需要在函数名和参数列表之间加上类型参数列表,如下:
func Function[<type parameters>](<parameters>) (<parameters>)
利用泛型语法让 FnCache 更加灵活,对 FnCache 进行以下修改:
func FnCache[FnParams comparable, FnReturns any](fn FnType[FnParams, FnReturns]) FnType[FnParams, FnReturns] {
cacheMap := make(map[FnParams]FnReturns)
}
泛型别名
泛型别名可以为已有的泛型附上具体的类型,并创建别名,语法规则如下:
type AliasName[<type parameters>] = ExistingTypeExpression
仍然以两数相加为例,这里的泛型被附上的具体的类型,作为一个别名:
type Fn1 struct {
i1 int
i2 int
}
type Fn2 struct {
r int
}
// AddFnType并不是一个新的泛型
type AddFnType = FnType[Fn1, Fn2]
weak pointer 弱指针
weak 标准库实现了 weak pointer。weak pointer 像正常的指针一样可以引用对象,但是允许对象被Go Runtime的GC程序清理掉。
使用弱指针能够让内存管理更为灵活,如果内存不足,runtime在触发GC时会自动将这些缓存对象清楚,以确保程序的正常运行,而不是等到缓存的内容过期。
通过 weak.Make[T any](ptr *T) 可以创建 weak.Pointer[T],并通过 Value() 方法返回指针(具有强引用的),如果内存已经被GC清理,则返回 nil。
func getIntPointer(val int) *int {
return &val
}
func main() {
a := getIntPointer(2)
b := a
fmt.Println("a:", a)
weakPtr := weak.Make(b)
p1 := weakPtr.Value()
fmt.Println("初始的weak pointer: ", p1)
a = nil
// 触发GC,清理a,b
runtime.GC()
p2 := weakPtr.Value()
fmt.Println("GC之后的weak pointer: ", p2)
}
返回结果
a: 0xc000010140
weak pointer: 0xc000010140
weak pointer: <nil>
但是如果不触发GC
// 不发触发GC
// runtime.GC()
注释掉runtime.GC()
a: 0xc000010140
初始的weak pointer: 0xc000010140
未GC的weak pointer: 0xc000010140
现在可以修改 FnCache 函数使用 weak.Pointer 更高效地管理缓存。
修改cacheMap的定义,使用 weak pointer。
cacheMap := make(map[FnParams]weak.Pointer[FnReturns])
通过 weakp.Value() 获取强引用的指针,如果缓存的内容已经被清除(返回 nil ),则不使用。
if weakp, ok := cacheMap[fp]; ok {
fmt.Println("hit cache")
res := weakp.Value()
if res != nil {
return *res, nil
}
}
如果缓存未命中或者缓存内容已时效,则需要获取 fn 的返回结果,并创建 weak.Point
res, err := fn(fp)
if err == nil {
weakp := weak.Make(&res)
cacheMap[fp] = weakp
}
通过 sync.Map 处理并发
sync.Map 提供了一个内置的并发安全的map实现,不需要再添加额外的锁,同时对读密集型的任务非常高效。sync.Map 在内部维护两张map:一张只读map,能够无锁地进行读取,如果在这里找不到需要的键,则会到脏表中查找;一张脏map,记录所有的写入,每个一段时间或者已经有足够多的更新,脏map就会合并到只读map中。
为了能够让 FnCache 使用 sync.Map,除了修改 cacheMap 的类型为sync.Map以外,还需要修改其存储和读取的流程
通过 cacheMap 的 Load() 方法读取弱指针:
if v, ok := cacheMap.Load(fp); ok {
if weakp, ok := v.(weak.Pointer[FnReturns]); ok {
fmt.Println("hit cache")
if res := weakp.Value(); res != nil {
return *res, nil
}
}
}
通过 Store 方法存入弱指针:
weakp := weak.Make(&res)
cacheMap.Store(fp, weakp)
完整代码
package main
import (
"fmt"
"sync"
"weak"
)
type FnType[FnParams comparable, FnReturns any] func(FnParams) (FnReturns, error)
func FnCache[FnParams comparable, FnReturns any](fn FnType[FnParams, FnReturns]) FnType[FnParams, FnReturns] {
var cacheMap sync.Map // keys: FnParams, values: weak.Pointer[FnReturns]
return func(fp FnParams) (FnReturns, error) {
// Try to load the cached weak pointer.
if v, ok := cacheMap.Load(fp); ok {
if weakp, ok := v.(weak.Pointer[FnReturns]); ok {
fmt.Println("hit cache")
if res := weakp.Value(); res != nil {
return *res, nil
}
}
}
fmt.Println("cache miss")
// Call the underlying function.
res, err := fn(fp)
if err == nil {
// Create a weak pointer for the result and store it.
weakp := weak.Make(&res)
cacheMap.Store(fp, weakp)
}
return res, err
}
}
type Fn1 struct {
i1 int
i2 int
}
type Fn2 struct {
r int
}
type AddFnType = FnType[Fn1, Fn2]
func AddFn(fp Fn1) (Fn2, error) {
var r int
r = fp.i1 + fp.i2
return Fn2{r: r}, nil
}
func main() {
cachedFn := FnCache(AddFn)
fn1 := Fn1{
i1: 1,
i2: 2,
}
r1, _ := cachedFn(fn1)
fmt.Println(r1.r)
_, _ = cachedFn(fn1)
_, _ = cachedFn(fn1)
}