Memcached #
Memcached is a very simple and very fast distributed memory caching system. Compared to the feature-rich Redis (persistence, data structures, pub/sub), Memcached is far more minimal — it only stores key-value strings with an expiration time. This simplicity is its strength: Memcached is very lightweight, easy to scale horizontally, and its read/write performance stays consistent even under very high loads. Go uses the github.com/bradfitz/gomemcache/memcache library.
Architecture Comparison: Memcached vs Redis #
Before deciding, let’s compare the two caching technologies:
| Characteristic | Memcached | Redis |
|---|---|---|
| Data Model | Simple Key-Value (Text/Blob) | Rich Data Structures (String, Hash, List, Set, ZSet, etc.) |
| Durability (Persistence) | Not supported (RAM only / Volatile) | Supported (RDB snapshots and Append-Only File AOF) |
| Multi-threading | Yes (very good at using multi-core CPUs) | Single-threaded (per core for data processing) |
| Horizontal Scalability | Client-side Consistent Hashing | Built-in Redis Cluster (Master-Replica) |
| Main Use Cases | Very large-scale RAM-based instant caching | Caching, Message Broker, fast DB, Leaderboards |
The Client-Side Consistent Hashing Mechanism #
Memcached is stateless and each server node doesn’t communicate with the others. Storage load distribution (sharding) is entirely computed on the Go client side automatically:
flowchart TD
Client["Go Application (gomemcache client)"] -->|"Compute MD5 Hash of Key: 'product_456'"| Hashing{"Hash Ring / Hash key"}
Hashing -->|"Routed to Node 1"| Server1["Cache Server 1<br/>(10.0.1.10:11211)"]
Hashing -->|"Routed to Node 2"| Server2["Cache Server 2<br/>(10.0.1.11:11211)"]
Hashing -->|"Routed to Node 3"| Server3["Cache Server 3<br/>(10.0.1.12:11211)"]Installation #
go get github.com/bradfitz/gomemcache/memcache
Connecting to Memcached #
import "github.com/bradfitz/gomemcache/memcache"
func newMemcacheClient(servers ...string) *memcache.Client {
mc := memcache.New(servers...)
// Timeout settings
mc.Timeout = 100 * time.Millisecond
// Number of idle connections per server
mc.MaxIdleConns = 100
return mc
}
func main() {
// Single server
mc := newMemcacheClient("localhost:11211")
// Multi-server (the client automatically uses consistent hashing)
mcCluster := newMemcacheClient(
"cache-1:11211",
"cache-2:11211",
"cache-3:11211",
)
_ = mcCluster
// Test the connection
if err := mc.Ping(); err != nil {
log.Fatal("Memcached ping:", err)
}
fmt.Println("✓ Connected to Memcached")
}
Basic Operations #
Set — Storing Items #
// Set with an expiration (in seconds, 0 = never expires)
err := mc.Set(&memcache.Item{
Key: "product:1",
Value: []byte(`{"id":1,"name":"Laptop","price":15000000}`),
Expiration: 3600, // 1 hour
})
// Example helper for JSON marshaling
func setJSON(mc *memcache.Client, key string, value interface{}, expiry int32) error {
data, err := json.Marshal(value)
if err != nil {
return err
}
return mc.Set(&memcache.Item{
Key: key,
Value: data,
Expiration: expiry,
})
}
Get — Reading Items #
item, err := mc.Get("product:1")
if err == memcache.ErrCacheMiss {
fmt.Println("Cache miss — fetch from the database")
// fetch from the DB, then set the cache
} else if err != nil {
log.Fatal("Get error:", err)
} else {
fmt.Println("Cache hit:", string(item.Value))
}
// Helper for JSON unmarshaling
func getJSON(mc *memcache.Client, key string, dest interface{}) error {
item, err := mc.Get(key)
if err != nil {
return err // including ErrCacheMiss
}
return json.Unmarshal(item.Value, dest)
}
// GetMulti — fetch many keys at once (one round trip)
items, err := mc.GetMulti([]string{"product:1", "product:2", "product:3"})
if err != nil {
log.Fatal(err)
}
for key, item := range items {
fmt.Printf("%s: %s\n", key, string(item.Value))
}
Add and Replace #
// Add — set only if the key does NOT exist (errors if it does)
err := mc.Add(&memcache.Item{
Key: "lock:resource",
Value: []byte("worker-1"),
Expiration: 30,
})
if err == memcache.ErrNotStored {
fmt.Println("The lock is already held by another process")
}
// Replace — set only if the key ALREADY exists (errors if it doesn't)
err = mc.Replace(&memcache.Item{
Key: "product:1",
Value: []byte(`{"updated":true}`),
Expiration: 3600,
})
Delete #
// Delete one key
err := mc.Delete("product:1")
if err == memcache.ErrCacheMiss {
fmt.Println("Key doesn't exist, nothing to delete")
}
// DeleteAll — flush all cache (BE CAREFUL in production!)
err = mc.DeleteAll()
Increment and Decrement #
// INCR — atomic increment (the value must be a number in string form)
mc.Set(&memcache.Item{Key: "counter", Value: []byte("0"), Expiration: 3600})
newVal, err := mc.Increment("counter", 1)
fmt.Println("Counter:", newVal) // 1
mc.Increment("counter", 5) // add 5
// DECR
newVal, err = mc.Decrement("counter", 2)
fmt.Println("Counter after decr:", newVal) // 4
CAS — Check-And-Set (Optimistic Locking) #
CAS prevents race conditions during updates — an update only succeeds if the value hasn’t changed since it was read:
func updateWithCAS(mc *memcache.Client, key string, updateFn func([]byte) []byte) error {
for retries := 0; retries < 3; retries++ {
// Get with a CAS token
item, err := mc.Gets(key) // Gets (not Get) returns a CAS token
if err == memcache.ErrCacheMiss {
return fmt.Errorf("key not found: %s", key)
}
if err != nil {
return err
}
// Modify the value
newValue := updateFn(item.Value)
// CAS — update only if the value hasn't changed since Gets
item.Value = newValue
err = mc.CompareAndSwap(item)
if err == nil {
return nil // success
}
if err == memcache.ErrCASConflict {
// The value changed since it was read — try again
log.Printf("CAS conflict, retry %d", retries+1)
time.Sleep(time.Duration(retries+1) * 10 * time.Millisecond)
continue
}
return err
}
return errors.New("too many CAS conflicts")
}
// Example CAS usage to update a product view counter
func incrementViewCount(mc *memcache.Client, productID int) error {
key := fmt.Sprintf("views:product:%d", productID)
return updateWithCAS(mc, key, func(current []byte) []byte {
count := 0
fmt.Sscanf(string(current), "%d", &count)
return []byte(fmt.Sprintf("%d", count+1))
})
}
Struct Serialization with Compression #
To store complex structs with compression (saving memory):
import (
"bytes"
"compress/gzip"
"encoding/gob"
)
// Encode a struct to gob + gzip
func encodeCompressed(v interface{}) ([]byte, error) {
var buf bytes.Buffer
gz := gzip.NewWriter(&buf)
enc := gob.NewEncoder(gz)
if err := enc.Encode(v); err != nil {
return nil, err
}
if err := gz.Close(); err != nil {
return nil, err
}
return buf.Bytes(), nil
}
// Decode gzip + gob into a struct
func decodeCompressed(data []byte, v interface{}) error {
gz, err := gzip.NewReader(bytes.NewReader(data))
if err != nil {
return err
}
defer gz.Close()
return gob.NewDecoder(gz).Decode(v)
}
type ProductDetail struct {
ID int
Name string
Description string // can be very long
Images []string
Specs map[string]string
Reviews []Review
}
func cacheProductDetail(mc *memcache.Client, p ProductDetail) error {
data, err := encodeCompressed(p)
if err != nil {
return err
}
return mc.Set(&memcache.Item{
Key: fmt.Sprintf("product_detail:%d", p.ID),
Value: data,
Expiration: 1800, // 30 minutes
})
}
func getProductDetail(mc *memcache.Client, id int) (*ProductDetail, error) {
item, err := mc.Get(fmt.Sprintf("product_detail:%d", id))
if err != nil {
return nil, err
}
var p ProductDetail
if err := decodeCompressed(item.Value, &p); err != nil {
return nil, err
}
return &p, nil
}
The Cache-Aside Pattern #
Cache-Aside is the most common caching pattern — the application manages the cache manually:
type ProductCache struct {
mc *memcache.Client
ttl int32
}
func NewProductCache(mc *memcache.Client, ttl int32) *ProductCache {
return &ProductCache{mc: mc, ttl: ttl}
}
func (c *ProductCache) Get(id int) (*Product, error) {
key := fmt.Sprintf("product:%d", id)
var p Product
if err := getJSON(c.mc, key, &p); err == nil {
return &p, nil // cache hit
}
return nil, memcache.ErrCacheMiss
}
func (c *ProductCache) Set(p *Product) error {
return setJSON(c.mc, fmt.Sprintf("product:%d", p.ID), p, c.ttl)
}
func (c *ProductCache) Invalidate(id int) {
c.mc.Delete(fmt.Sprintf("product:%d", id))
}
// A service that uses the cache
type ProductService struct {
cache *ProductCache
db *sql.DB
}
func (s *ProductService) GetProduct(ctx context.Context, id int) (*Product, error) {
// 1. Check the cache
if p, err := s.cache.Get(id); err == nil {
return p, nil
}
// 2. Cache miss — fetch from the database
var p Product
err := s.db.QueryRowContext(ctx,
"SELECT id, name, price, stock, category FROM products WHERE id = ?", id,
).Scan(&p.ID, &p.Name, &p.Price, &p.Stock, &p.Category)
if errors.Is(err, sql.ErrNoRows) {
return nil, ErrNotFound
}
if err != nil {
return nil, err
}
// 3. Save to the cache for the next request
s.cache.Set(&p)
return &p, nil
}
func (s *ProductService) UpdateProduct(ctx context.Context, p *Product) error {
// Update the database
_, err := s.db.ExecContext(ctx,
"UPDATE products SET name=?, price=?, stock=? WHERE id=?",
p.Name, p.Price, p.Stock, p.ID)
if err != nil {
return err
}
// Invalidate the cache
s.cache.Invalidate(p.ID)
return nil
}
Complete Example Program #
package main
import (
"encoding/json"
"errors"
"fmt"
"log"
"time"
"github.com/bradfitz/gomemcache/memcache"
)
type Product struct {
ID int `json:"id"`
Name string `json:"name"`
Price float64 `json:"price"`
Stock int `json:"stock"`
Category string `json:"category"`
}
type Review struct {
UserID string
Rating int
Comment string
}
// Cache helper
func setJSON(mc *memcache.Client, key string, value interface{}, expiry int32) error {
data, _ := json.Marshal(value)
return mc.Set(&memcache.Item{Key: key, Value: data, Expiration: expiry})
}
func getJSON(mc *memcache.Client, key string, dest interface{}) error {
item, err := mc.Get(key)
if err != nil {
return err
}
return json.Unmarshal(item.Value, dest)
}
// Simulated database
var fakeDB = map[int]Product{
1: {1, "Pro Laptop 14", 15_000_000, 10, "electronics"},
2: {2, "Wireless Mouse", 350_000, 50, "electronics"},
3: {3, "Mech Keyboard", 1_500_000, 25, "electronics"},
}
var dbQueryCount int
func fetchFromDB(id int) (*Product, error) {
dbQueryCount++
time.Sleep(50 * time.Millisecond) // simulate DB latency
p, ok := fakeDB[id]
if !ok {
return nil, errors.New("not found")
}
return &p, nil
}
func getProductWithCache(mc *memcache.Client, id int) (*Product, error) {
key := fmt.Sprintf("product:%d", id)
var p Product
if err := getJSON(mc, key, &p); err == nil {
fmt.Printf(" [HIT] product:%d\n", id)
return &p, nil
}
fmt.Printf(" [MISS] product:%d — query DB\n", id)
result, err := fetchFromDB(id)
if err != nil {
return nil, err
}
setJSON(mc, key, result, 300) // cache for 5 minutes
return result, nil
}
func main() {
mc := memcache.New("localhost:11211")
mc.Timeout = 100 * time.Millisecond
if err := mc.Ping(); err != nil {
log.Fatal("Memcached not available:", err)
}
fmt.Println("✓ Connected to Memcached\n")
// Clean the cache for a fresh demo
mc.DeleteAll()
// Cache-Aside demo
fmt.Println("=== Cache-Aside Pattern ===")
fmt.Println("\nRound 1 — all cache misses:")
for _, id := range []int{1, 2, 3} {
p, err := getProductWithCache(mc, id)
if err != nil {
log.Println(err)
} else {
fmt.Printf(" → %s (Rp%.0f)\n", p.Name, p.Price)
}
}
fmt.Println("\nRound 2 — all cache hits:")
for _, id := range []int{1, 2, 3} {
p, _ := getProductWithCache(mc, id)
fmt.Printf(" → %s\n", p.Name)
}
fmt.Printf("\nTotal DB queries: %d (out of 6 requests)\n", dbQueryCount)
// Increment demo
fmt.Println("\n=== Atomic Counter ===")
mc.Set(&memcache.Item{Key: "pageviews:home", Value: []byte("0"), Expiration: 3600})
for i := 0; i < 5; i++ {
val, _ := mc.Increment("pageviews:home", 1)
fmt.Printf(" Pageview #%d\n", val)
}
// GetMulti demo
fmt.Println("\n=== GetMulti (1 round trip for 3 keys) ===")
keys := []string{"product:1", "product:2", "product:3"}
items, err := mc.GetMulti(keys)
if err != nil {
log.Println(err)
} else {
fmt.Printf(" Found %d of %d keys\n", len(items), len(keys))
for k, item := range items {
var p Product
json.Unmarshal(item.Value, &p)
fmt.Printf(" %s → %s\n", k, p.Name)
}
}
// TTL demo — item expires after 2 seconds
fmt.Println("\n=== Expiration Demo ===")
mc.Set(&memcache.Item{Key: "temp:data", Value: []byte("temporary value"), Expiration: 2})
item, _ := mc.Get("temp:data")
fmt.Printf(" Before expiration: %q\n", string(item.Value))
fmt.Println(" Waiting 3 seconds...")
time.Sleep(3 * time.Second)
_, err = mc.Get("temp:data")
if errors.Is(err, memcache.ErrCacheMiss) {
fmt.Println(" After expiration: cache miss ✓")
}
// CAS demo
fmt.Println("\n=== CAS (Check-And-Set) ===")
mc.Set(&memcache.Item{Key: "stock:1", Value: []byte("100"), Expiration: 3600})
// Simulate two goroutines reading and updating concurrently
item1, _ := mc.Gets("stock:1")
item2, _ := mc.Gets("stock:1")
// The first update succeeds
item1.Value = []byte("95")
err = mc.CompareAndSwap(item1)
fmt.Printf(" Update 1 (reduce by 5): %v\n", err)
// The second update fails because the data has changed
item2.Value = []byte("90")
err = mc.CompareAndSwap(item2)
fmt.Printf(" Update 2 (reduce by 10): %v (should be ErrCASConflict)\n", err)
final, _ := mc.Get("stock:1")
fmt.Printf(" Final value: %s (should be 95)\n", string(final.Value))
}
Summary #
memcache.ErrCacheMissisn’t a critical error — check witherrors.Isto distinguish a miss from a real error.GetMultito fetch many keys in one round trip — far more efficient than aGetloop.Addto set only if it doesn’t exist (idempotent create);Replaceonly if it already exists.Increment/Decrementfor atomic counters — the value must be a number in string form.- CAS (
Gets+CompareAndSwap) for optimistic locking — retry onErrCASConflict.- Multi-server with automatic consistent hashing —
memcache.New("s1:11211", "s2:11211", "s3:11211").- Compression (gzip + gob) to save memory when storing large structs.
- Expiration
0means no expiry — but items can still be evicted when memory is full (LRU).mc.Timeoutmust be configured to prevent goroutines from hanging when the Memcached server is unresponsive.- Memcached has no persistence — don’t store data that can’t be re-fetched from its original source.