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:

CharacteristicMemcachedRedis
Data ModelSimple 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-threadingYes (very good at using multi-core CPUs)Single-threaded (per core for data processing)
Horizontal ScalabilityClient-side Consistent HashingBuilt-in Redis Cluster (Master-Replica)
Main Use CasesVery large-scale RAM-based instant cachingCaching, 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.ErrCacheMiss isn’t a critical error — check with errors.Is to distinguish a miss from a real error.
  • GetMulti to fetch many keys in one round trip — far more efficient than a Get loop.
  • Add to set only if it doesn’t exist (idempotent create); Replace only if it already exists.
  • Increment/Decrement for atomic counters — the value must be a number in string form.
  • CAS (Gets + CompareAndSwap) for optimistic locking — retry on ErrCASConflict.
  • 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 0 means no expiry — but items can still be evicted when memory is full (LRU).
  • mc.Timeout must 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.

← Previous: Redis   Next: Gin →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact