Frontend Labs
⋯

26. Terminal project: an LRU cache library and CLI cache server

Goal: implement the design from projects/lru_cache.md as generic, reusable Go — then wrap it in a tiny TCP server so it's a real, runnable service.

// lru/lru.go
package lru

import "sync"

type node[K comparable, V any] struct {
	key        K
	value      V
	prev, next *node[K, V]
}

type Cache[K comparable, V any] struct {
	mu       sync.Mutex
	capacity int
	items    map[K]*node[K, V]
	head, tail *node[K, V] // head = most recently used
}

func New[K comparable, V any](capacity int) *Cache[K, V] {
	if capacity <= 0 {
		panic("lru: capacity must be positive")
	}
	return &Cache[K, V]{
		capacity: capacity,
		items:    make(map[K]*node[K, V], capacity),
	}
}

func (c *Cache[K, V]) unlink(n *node[K, V]) {
	if n.prev != nil {
		n.prev.next = n.next
	} else {
		c.head = n.next
	}
	if n.next != nil {
		n.next.prev = n.prev
	} else {
		c.tail = n.prev
	}
}

func (c *Cache[K, V]) pushFront(n *node[K, V]) {
	n.prev, n.next = nil, c.head
	if c.head != nil {
		c.head.prev = n
	}
	c.head = n
	if c.tail == nil {
		c.tail = n
	}
}

func (c *Cache[K, V]) Get(key K) (V, bool) {
	c.mu.Lock()
	defer c.mu.Unlock()
	n, ok := c.items[key]
	if !ok {
		var zero V
		return zero, false
	}
	c.unlink(n)
	c.pushFront(n)
	return n.value, true
}

func (c *Cache[K, V]) Put(key K, value V) {
	c.mu.Lock()
	defer c.mu.Unlock()
	if n, ok := c.items[key]; ok {
		n.value = value
		c.unlink(n)
		c.pushFront(n)
		return
	}
	n := &node[K, V]{key: key, value: value}
	c.items[key] = n
	c.pushFront(n)
	if len(c.items) > c.capacity {
		lru := c.tail
		c.unlink(lru)
		delete(c.items, lru.key)
	}
}

func (c *Cache[K, V]) Len() int {
	c.mu.Lock()
	defer c.mu.Unlock()
	return len(c.items)
}
// lru/lru_test.go
package lru

import "testing"

func TestEviction(t *testing.T) {
	c := New[string, int](2)
	c.Put("a", 1)
	c.Put("b", 2)
	c.Put("c", 3) // evicts "a", the least recently used

	if _, ok := c.Get("a"); ok {
		t.Fatal("expected 'a' to be evicted")
	}
	if v, ok := c.Get("b"); !ok || v != 2 {
		t.Fatalf("got (%v, %v), want (2, true)", v, ok)
	}
}

func TestGetPromotesToMostRecentlyUsed(t *testing.T) {
	c := New[string, int](2)
	c.Put("a", 1)
	c.Put("b", 2)
	c.Get("a")     // promotes "a"; "b" is now least recently used
	c.Put("c", 3) // evicts "b", not "a"

	if _, ok := c.Get("b"); ok {
		t.Fatal("expected 'b' to be evicted")
	}
	if _, ok := c.Get("a"); !ok {
		t.Fatal("expected 'a' to survive")
	}
}
go test -race ./lru/...

Now wrap it in a minimal line-protocol TCP server (GET key, PUT key value) — the same pattern that memcached and Redis expose, minus the distributed part:

// cmd/cacheserver/main.go
package main

import (
	"bufio"
	"fmt"
	"log"
	"net"
	"strings"

	"example.com/cacheserver/lru"
)

func main() {
	cache := lru.New[string, string](1000)
	ln, err := net.Listen("tcp", ":9090")
	if err != nil {
		log.Fatal(err)
	}
	log.Println("cache server listening on :9090")
	for {
		conn, err := ln.Accept()
		if err != nil {
			log.Println("accept error:", err)
			continue
		}
		go handleConn(conn, cache)
	}
}

func handleConn(conn net.Conn, cache *lru.Cache[string, string]) {
	defer conn.Close()
	scanner := bufio.NewScanner(conn)
	for scanner.Scan() {
		fields := strings.Fields(scanner.Text())
		if len(fields) == 0 {
			continue
		}
		switch strings.ToUpper(fields[0]) {
		case "GET":
			if len(fields) != 2 {
				fmt.Fprintln(conn, "ERR usage: GET key")
				continue
			}
			if v, ok := cache.Get(fields[1]); ok {
				fmt.Fprintln(conn, "OK", v)
			} else {
				fmt.Fprintln(conn, "MISS")
			}
		case "PUT":
			if len(fields) < 3 {
				fmt.Fprintln(conn, "ERR usage: PUT key value")
				continue
			}
			cache.Put(fields[1], strings.Join(fields[2:], " "))
			fmt.Fprintln(conn, "OK")
		default:
			fmt.Fprintln(conn, "ERR unknown command")
		}
	}
}
go run ./cmd/cacheserver &
nc localhost 9090
PUT session:42 alice
GET session:42
GET missing

Each connection gets its own goroutine (go handleConn(...)); the shared *lru.Cache is safe under concurrent access because every method locks internally — this is the "one goroutine per connection, shared synchronized state" pattern that describes most simple Go TCP servers, extended fully in Chapter 38.


Generated from real-life-golang-guide.md · 8 Oct 2026 16:36