[译]Go使用封装返回模式回收被goroutines占用的内存
当我们有一个后台运行的goroutines通过其内部的构造函数创建一个对象以后,我们希望这个对象即使在goroutines没有被及时关闭以后,还能及时被垃圾回收。这是不可能的因为后台运行goroutines会一直运行并且会指向这个对象上。
解决方法
我们将这个返回的对象封装一下,然后在这个对象上使用finalizer,从而达到关闭后台goroutines的目的。
举个栗子
假设我们现在有个Go的静态客户端 go-statsd-client ,它会创建一个 BufferedSender 如下:
func NewBufferedSender(addr string, flushInterval time.Duration, flushBytes int) (Sender, error) { simpleSender, err := NewSimpleSender(addr) if err != nil { return nil, err } sender := &BufferedSender{ flushBytes: flushBytes, flushInterval: flushInterval, sender: simpleSender, buffer: senderPool.Get(), shutdown: make(chan chan error), } sender.Start() return sender, nil } 复制代码
Start
方法复制创建一个gorutinues来定期刷新 BufferedSender 。
func (s *BufferedSender) Start() { // write lock to start running s.runmx.Lock() defer s.runmx.Unlock() if s.running { return } s.running = true s.bufs = make(chan *bytes.Buffer, 32) go s.run() } 复制代码
我们现在创建并且使用这个 BufferedSender 看看会发生什么
func Process() { x := statsd.NewBufferedSender("localhost:2125", time.Second, 1024) x.Inc("stat", 1, .1) } 复制代码
最开始 main gorotinues 是指向 x ,但当我们退出 Process
的时候 BufferedSender 仍然在运行,因为 Start
所启动的goruntinues没有停止。


我们相当于泄漏了 BufferedSender 的内存因为我们忘记调用 Close 来关闭它了。
解决方案
参考一下Go的缓存库 go-cache 。你会注意到 Cache
其实只是一个封装。
type Cache struct { *cache // If this is confusing, see the comment at the bottom of New() } type cache struct { defaultExpiration time.Duration items map[string]Item mu sync.RWMutex onEvicted func(string, interface{}) janitor *janitor } 复制代码
当你 new 一个 Cache 对象的时候,他将返回一个代理者,代理者指向被封装的对象 cache
,而不是返回的对象 Cache
func New(defaultExpiration, cleanupInterval time.Duration) *Cache { items := make(map[string]Item) return newCacheWithJanitor(defaultExpiration, cleanupInterval, items) } func newCacheWithJanitor(de time.Duration, ci time.Duration, m map[string]Item) *Cache { c := newCache(de, m) // This trick ensures that the janitor goroutine (which--granted it // was enabled--is running DeleteExpired on c forever) does not keep // the returned C object from being garbage collected. When it is // garbage collected, the finalizer stops the janitor goroutine, after // which c can be collected. C := &Cache{c} if ci > 0 { runJanitor(c, ci) runtime.SetFinalizer(C, stopJanitor) } return C } func runJanitor(c *cache, ci time.Duration) { j := &janitor{ Interval: ci, stop: make(chan bool), } c.janitor = j go j.Run(c) } 复制代码
参考它把我们代码改成
func Process() { x := cache.New(time.Second, time.Minute) } 复制代码


非常重要的区别的是这里的 Cache
是可以被垃圾回收的,即使 cache
对象并不能被回收。我们将GC行为器SetFinalizer设置在 cache
上。 stopJanitor
函数会通知后台运行的goruntines停止运行。
runtime.SetFinalizer(C, stopJanitor) ... ... func stopJanitor(c *Cache) { c.janitor.stop <- true } 复制代码
当后台gorutinues被停止以后,就没有东西再继续指向 cache
了。

然后它就会被垃圾回收。
什么时候使用它
这其实是取决于用你这个库的用户是怎么想的,他们是否希望能明确地创建并能关闭后台进程。Go的 http.Serve 就是一个很好的例子。注意到这里不是 func NewHTTPServer() *http.Server
,而是使用一个对象,并且用户可以在准备就绪时显式启动(或停止)服务器。
基于这个最佳实现,如果你确实想控制你的后台进程在什么时候被关闭的时候,你仍然应该暴露一个 Close
函数允许用户关闭后台gorutines来达到回收内存的目的。但是如果你认为让用户自己去调用 Close
比较麻烦,你就可以加一个finalizer的封装来确保内润以及你所创建的goruntinue能在最后被正确地回收不管有没有调用 Close