【从零单排Golang】第八话:通过cache缓存模块示范interface该怎么用

和许多面向对象的编程语言一样,Golang也存在interface接口这样的概念。interface相当于是一个中间层,下游只需要关心interface实现了什么行为,利用这些行为做些业务级别事情,而上游则负责实现interface,把这些行为具象化。本文就来通过一个简单的缓存cache模块的实现,来示范一下Golanginterface该怎么用。

首先,从业务service角度而言,一个cache模块可能需要以下几种方法:

  • 获取缓存中的某个值
  • 缓存数据,加缓存时效
  • 删除缓存内容

那么这些个方法,就可以用一类叫Cacheinterface来表示:

1
2
3
4
5
6
type Cache interface {
Get(key string) (interface{}, bool)
Set(key string, value interface{})
SetExpire(key string, value interface{}, expire time.Duration)
Delete(key string)
}

其中,Get方法返回一个interface{}value,以及是否存在的bool标识;SetSetExpire表示无时限跟有时限的缓存行为;Delete表示删除缓存内容。整块Cache的接口定义也非常明显。

这样写有什么好处?如果你是下游业务服务的话,你只需要这样写就可以了。这里给一个同package下的测试用例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
func TestCache(t *testing.T) {
k, v := "hello", "world"
// Current()的实现,在下文慢慢解释
var curCache Cache = Current()

// set & get & delete
curCache.Set(k, v)
cached, ok := curCache.Get(k)
if !ok {
t.Fatalf("cannot cache %s:%s", k, v)
} else {
t.Logf("got cached %s:%v (type: %s)", k, cached, reflect.TypeOf(cached).Name())
}
curCache.Delete(k)
_, ok = curCache.Get(k)
if ok {
t.Fatalf("cannot delete %s:%s", k, v)
} else {
t.Logf("delete cached %s:%s", k, v)
}

// set expire
curCache.SetExpire(k, v, 1*time.Second)
cached, ok = curCache.Get(k)
if !ok {
t.Fatalf("cannot cache %s:%s", k, v)
} else {
t.Logf("got cached %s:%v (type: %s)", k, cached, reflect.TypeOf(cached).Name())
}
time.Sleep(3 * time.Second)
_, ok = curCache.Get(k)
if ok {
t.Fatalf("cannot expire %s:%s", k, v)
} else {
t.Logf("expired %s:%s", k, v)
}
}

可以看到,我们指定的缓存对象curCache标识为一个Cache,是个接口定义,这样标识起来的话,下面的代码就可以正常使用GetSet之类的方法了。而更重要的是,下面的代码,不会因为Cache的具体实现变化而有所变化。举个例子,你有10个开源的缓存库,想定时切换Current() Cache背后的缓存对象实现,就算你再怎么换,只要用到缓存的代码标注缓存对象为Cache这个interface,并且interface的定义没有变化,那么使用缓存的代码就不需要动。这样,就彻底实现了缓存提供方和使用方的解耦,开发效率也会噌噌噌的上去。

既然提到了提供方Provider的概念,那在缓存的实现上,就可以走依赖注入控制反转的模式。假设某个Web服务有个本地缓存模块,在实现上,就可以考虑提供多个Cache接口的实现,同时在配置里指定默认的一种。这里,就以go-cache为例,做一个实现案例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
import (
"github.com/patrickmn/go-cache"
"time"
)

const (
GoCacheDefaultExpiration = 10 * time.Minute
GoCacheCleanupInterval = 15 * time.Minute
)

type GoCache struct {
c *cache.Cache

defaultExpiration time.Duration
cleanupInterval time.Duration
}

func (g *GoCache) Get(key string) (interface{}, bool) {
return g.c.Get(key)
}

func (g *GoCache) Set(key string, value interface{}) {
g.c.Set(key, value, GoCacheDefaultExpiration)
}

func (g *GoCache) SetExpire(key string, value interface{}, expire time.Duration) {
if expire < 0 {
expire = g.defaultExpiration
}
if expire > g.cleanupInterval {
expire = g.cleanupInterval
}
g.c.Set(key, value, expire)
}

func (g *GoCache) Delete(key string) {
g.c.Delete(key)
}

func NewGoCache() *GoCache {
return &GoCache{
c: cache.New(GoCacheDefaultExpiration, GoCacheCleanupInterval),

defaultExpiration: GoCacheDefaultExpiration,
cleanupInterval: GoCacheCleanupInterval,
}
}

当我们定义一个GoCachestruct,实现了Cache接口定义的所有行为,那么GoCache的实例,在Golang里,就能够被标识为一个Cache接口实例。NewGoCache方法,不仅是提供了一个GoCache的实例,而在业务层面,更是提供了一个Cache实例。因此,我们可以简单用一个map来管理所有的Cache的构造器,从而标识不同的缓存实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
func provideGoCache() Cache {
return NewGoCache()
}

var cacheProviders = map[string]Cache{
"go-cache": provideGoCache(),
}

const (
DefaultCacheProvider = "go-cache"
)

func Get(provider string) Cache {
c, ok := cacheProviders[provider]
if !ok {
return nil
}
return c
}

func Default() Cache {
return Get(DefaultCacheProvider)
}

// 上文提到的样例代码,就用了这个方法拿到go-cache实现的Cache接口实例
func Current() Cache {
return Default()
}

显而易见,通过这样的一个代码组织,不论是go-cache,抑或是其它的Cache实现,都可以集中管理并灵活取用。这,便是interfaceGolang编程中给我们带来的便利了。

版权声明
本文为博客HiKariのTechLab原创文章,转载请标明出处,谢谢~~~