Eventualmente, todos los proyectos terminan con la necesidad de acelerar las respuestas del servicio o algunos cálculos pesados. La solución rápida y fácil es usar caché. Por lo general, hay Redis o Memcached, pero en realidad no los necesitamos en microservicios de instancia única. A veces es mejor usar un caché en memoria simple en su aplicación Go y hoy quiero mostrarle las principales formas de implementarlo. mapa sencillo La primera forma es una implementación explícita simple de caché. Por lo general, usa mapas y almacena estructuras allí. Además, es necesario vigilar la caducidad de las claves y el tamaño de la memoria caché. Puse todas las implementaciones en . un repositorio de GitHub package go_cache import ( "errors" "sync" "time" ) type user struct { Id int64 `json:"id"` Email string `json:"email"` } type cachedUser struct { user expireAtTimestamp int64 } type localCache struct { stop chan struct{} wg sync.WaitGroup mu sync.RWMutex users map[int64]cachedUser } func newLocalCache(cleanupInterval time.Duration) *localCache { lc := &localCache{ users: make(map[int64]cachedUser), stop: make(chan struct{}), } lc.wg.Add(1) go func(cleanupInterval time.Duration) { defer lc.wg.Done() lc.cleanupLoop(cleanupInterval) }(cleanupInterval) return lc } func (lc *localCache) cleanupLoop(interval time.Duration) { t := time.NewTicker(interval) defer t.Stop() for { select { case <-lc.stop: return case <-tC: lc.mu.Lock() for uid, cu := range lc.users { if cu.expireAtTimestamp <= time.Now().Unix() { delete(lc.users, uid) } } lc.mu.Unlock() } } } func (lc *localCache) stopCleanup() { close(lc.stop) lc.wg.Wait() } func (lc *localCache) update(u user, expireAtTimestamp int64) { lc.mu.Lock() defer lc.mu.Unlock() lc.users[u.Id] = cachedUser{ user: u, expireAtTimestamp: expireAtTimestamp, } } var ( errUserNotInCache = errors.New("the user isn't in cache") ) func (lc *localCache) read(id int64) (user, error) { lc.mu.RLock() defer lc.mu.RUnlock() cu, ok := lc.users[id] if !ok { return user{}, errUserNotInCache } return cu.user, nil } func (lc *localCache) delete(id int64) { lc.mu.Lock() defer lc.mu.Unlock() delete(lc.users, id) } Usamos la identificación de un usuario como la clave de un elemento almacenado en caché. Dado que hay un mapa, todas las actualizaciones/lecturas/eliminaciones toman tiempo O(1). ventajas implementación explícita Buen rendimiento Contras tiempo adicional para implementar un caché para cada estructura de datos tiempo extra para probar la lógica de caché tiempo extra para corregir errores Biblioteca gCache abstrae la gestión de caché e incluye varias configuraciones. Por ejemplo, simplemente puede configurar las reglas de desalojo de caché, el tamaño máximo, el TTL de vencimiento, etc. La biblioteca gCache package go_cache import ( "errors" "fmt" "github.com/bluele/gcache" "time" ) type gCache struct { users gcache.Cache } const ( cacheSize = 1_000_000 cacheTTL = 1 * time.Hour // default expiration ) func newGCache() *gCache { return &gCache{ users: gcache.New(cacheSize).Expiration(cacheTTL).ARC().Build(), } } func (gc *gCache) update(u user, expireIn time.Duration) error { return gc.users.SetWithExpire(u.Id, u, expireIn) } func (gc *gCache) read(id int64) (user, error) { val, err := gc.users.Get(id) if err != nil { if errors.Is(err, gcache.KeyNotFoundError) { return user{}, errUserNotInCache } return user{}, fmt.Errorf("get: %w", err) } return val.(user), nil } func (gc *gCache) delete(id int64) { gc.users.Remove(id) } ventajas administración de caché lista para producción interfaz única que funciona con cualquier tipo de datos diferentes políticas de desalojo de caché: LRU, LFU, ARC Contras necesita convertir manualmente un valor de caché en cada lectura que conduce a un rendimiento deficiente la biblioteca no se ha mantenido por un tiempo Biblioteca BigCache es rápida, concurrente y expulsa la caché en memoria escrita para mantener una gran cantidad de entradas sin afectar el rendimiento. BigCache retiene entradas en el montón pero omite GC para ellas. La biblioteca BigCache package go_cache import ( "encoding/json" "errors" "fmt" "github.com/allegro/bigcache" "strconv" "time" ) type bigCache struct { users *bigcache.BigCache } func newBigCache() (*bigCache, error) { bCache, err := bigcache.NewBigCache(bigcache.Config{ // number of shards (must be a power of 2) Shards: 1024, // time after which entry can be evicted LifeWindow: 1 * time.Hour, // Interval between removing expired entries (clean up). // If set to <= 0 then no action is performed. // Setting to < 1 second is counterproductive — bigcache has a one second resolution. CleanWindow: 5 * time.Minute, // rps * lifeWindow, used only in initial memory allocation MaxEntriesInWindow: 1000 * 10 * 60, // max entry size in bytes, used only in initial memory allocation MaxEntrySize: 500, // prints information about additional memory allocation Verbose: false, // cache will not allocate more memory than this limit, value in MB // if value is reached then the oldest entries can be overridden for the new ones // 0 value means no size limit HardMaxCacheSize: 256, // callback fired when the oldest entry is removed because of its expiration time or no space left // for the new entry, or because delete was called. A bitmask representing the reason will be returned. // Default value is nil which means no callback and it prevents from unwrapping the oldest entry. OnRemove: nil, // OnRemoveWithReason is a callback fired when the oldest entry is removed because of its expiration time or no space left // for the new entry, or because delete was called. A constant representing the reason will be passed through. // Default value is nil which means no callback and it prevents from unwrapping the oldest entry. // Ignored if OnRemove is specified. OnRemoveWithReason: nil, }) if err != nil { return nil, fmt.Errorf("new big cache: %w", err) } return &bigCache{ users: bCache, }, nil } func (bc *bigCache) update(u user) error { bs, err := json.Marshal(&u) if err != nil { return fmt.Errorf("marshal: %w", err) } return bc.users.Set(userKey(u.Id), bs) } func userKey(id int64) string { return strconv.FormatInt(id, 10) } func (bc *bigCache) read(id int64) (user, error) { bs, err := bc.users.Get(userKey(id)) if err != nil { if errors.Is(err, bigcache.ErrEntryNotFound) { return user{}, errUserNotInCache } return user{}, fmt.Errorf("get: %w", err) } var u user err = json.Unmarshal(bs, &u) if err != nil { return user{}, fmt.Errorf("unmarshal: %w", err) } return u, nil } func (bc *bigCache) delete(id int64) { bc.users.Delete(userKey(id)) } Usamos JSON para codificar/decodificar valores, pero existe la oportunidad de usar cualquier formato de datos. Por ejemplo, uno de los formatos binarios, Protobuf, puede aumentar significativamente el rendimiento. ventajas administración de caché lista para producción la rica configuración de caché mantenido el caché no activa GC, por lo tanto, excelente rendimiento en caché de gran tamaño Contras necesita implementar su codificador/decodificador de valores Puntos de referencia goos: darwin goarch: amd64 pkg: go-cache cpu: Intel(R) Core(TM) i5-8257U CPU @ 1.40GHz Benchmark_bigCache Benchmark_bigCache-8 1751281 688.0 ns/op 390 B/op 6 allocs/op Benchmark_gCache Benchmark_gCache-8 772846 1699 ns/op 373 B/op 8 allocs/op Benchmark_localCache Benchmark_localCache-8 1534795 756.6 ns/op 135 B/op 0 allocs/op PASS ok go-cache 6.044s BigCache es el caché más rápido. gCache pierde rendimiento debido a la necesidad de emitir valores desde interface{} Conclusión Investigamos diferentes formas de caché en memoria en Golang. Recuerde que no existe la mejor solución, y depende de cada caso. Utilice el artículo para comparar las soluciones y decidir cuál se ajusta a las necesidades de su proyecto.