Go
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Functions and Methods
-
Section 4: Concurrency
-
Section 5: Packages and Tooling
-
Section 6: More Standard Library
-
Section 7: Building Services
-
Section 8: Advanced Go
-
Section 9: Go in the Cloud-Native Ecosystem
-
Section 10: Data Structures and Algorithms in Go
-
Section 11: Testing and Deployment
-
Section 12: Practical Projects
-
Section 13: More Standard Library Practice
-
Section 14: More Practice Projects
-
Section 15: Design Patterns in Go
-
Section 16: Interview Practice
-
Section 17: Package fmt In Depth
-
Section 18: Package strings and strconv
-
Section 19: Package os and io
-
Section 20: Package time
-
Section 21: Package sort and container
-
Section 22: Package encoding
-
Section 23: Package net/http In Depth
-
Section 24: Package context
-
Section 25: Package regexp and bytes
-
Section 26: Package errors In Depth
-
Section 27: Package crypto and hash
-
Section 28: Package flag and log
-
Section 29: Package sync In Depth
-
Section 30: More Practice Exercises
-
Section 31: Go Modules and Workspaces In Depth
-
Section 32: Generics Deep Dive (Go 1.18+)
-
Section 33: Testing Package In Depth
-
Section 34: More Interview and Whiteboard Practice
-
Section 35: Package math and unicode
-
Section 36: Package path and filepath
-
Section 37: Package database/sql
-
Section 38: Package text/template and html/template
-
Section 39: Package archive and compress
-
Section 40: Lower-Level net Package
-
Section 41: Package runtime and reflect
-
Section 42: Package embed and io/fs
-
Section 43: Go Assembly and CGO Basics
-
Section 44: Building CLIs and TUIs
-
Section 45: Go Performance Tuning
-
Section 46: More Real-World Projects
-
Section 47: Go in Production
-
Section 48: Go Design Patterns
-
Section 49: Go Interfaces Deep Dive
-
Section 50: Final Practice and Review
97: Building a Simple In-Memory Cache with TTL
I've had a few projects lately where I needed to avoid hitting a slow database for data that doesn't change every second. A simple in-memory cache usually does the trick. But if you just throw everything into a map, you're essentially building a memory leak. You need a way for data to expire—a Time-To-Live (TTL).
The Map that Never Forgets
My first instinct is always the simplest: a map. Let's say we're caching user session tokens. I'll start with a basic structure and a way to set values.
type Cache map[string]string
func (c Cache) Set(key, value string) {
c[key] = value
}
func (c Cache) Get(key string) (string, bool) {
val, ok := c[key]
return val, ok
}
This works great for five minutes. But if this is a production server running for three months, that map just grows and grows. Even if a user logs out or their session should have ended hours ago, the data stays there. We need a way to mark when a piece of data is "stale."
Making Data Perishable
Instead of storing just the string, I'll wrap it in a struct that carries a timestamp. If I can record when the item expires, I can just check that time whenever I try to retrieve it.
type item struct {
value string
expiresAt time.Time
}
type Cache map[string]item
func (c Cache) Set(key, value string, ttl time.Duration) {
c[key] = item{
value: value,
expiresAt: time.Now().Add(ttl),
}
}
Now, the Get method can't just return the value. It has to be smarter. If the current time is past the expiresAt time, the data is useless to us.
func (c Cache) Get(key string) (string, bool) {
it, ok := c[key]
if !ok {
return "", false
}
if time.Now().After(it.expiresAt) {
return "", false // Treat it as if it's not there
}
return it.value, true
}
This solves the "wrong data" problem, but it doesn't solve the "memory" problem. The expired item is still sitting in the map; we're just ignoring it. If we have a million expired sessions, they're still eating RAM.
Cleaning Up the Mess
To actually free up memory, we need a "janitor"—something that runs in the background and scrubs the map of expired entries. I'll use a goroutine and a time.Ticker for this. But wait, as soon as I try to do this, I run into a wall: Go maps are not thread-safe.
If my janitor is deleting keys while my main application is reading them, the program will panic with a concurrent map read and map write. I need a sync.RWMutex to coordinate access.
type Cache struct {
mu sync.RWMutex
items map[string]item
}
func NewCache(cleanupInterval time.Duration) *Cache {
c := &Cache{
items: make(map[string]item),
}
go func() {
for range time.Tick(cleanupInterval) {
c.DeleteExpired()
}
}()
return c
}
func (c *Cache) DeleteExpired() {
c.mu.Lock()
defer c.mu.Unlock()
for k, v := range c.items {
if time.Now().After(v.expiresAt) {
delete(c.items, k)
}
}
}
I'm using Lock() in the janitor because it's modifying the map. For the Get method, I'll use RLock(), which allows multiple readers to access the cache simultaneously as long as no one is writing to it. This is much more efficient than a full lock.
The Final Polish
Let's put it all together. I'll refine the Set and Get methods to use the mutex. I've noticed that in high-traffic apps, locking the entire map during a full cleanup can cause a "stop-the-world" feel for the API requests. In a real-world scenario, you might clean up in batches, but for our simple cache, this structure is a massive leap forward from the basic map.
func (c *Cache) Set(key, value string, ttl time.Duration) {
c.mu.Lock()
defer c.mu.Unlock()
c.items[key] = item{
value: value,
expiresAt: time.Now().Add(ttl),
}
}
func (c *Cache) Get(key string) (string, bool) {
c.mu.RLock()
defer c.mu.RUnlock()
it, ok := c.items[key]
if !ok || time.Now().After(it.expiresAt) {
return "", false
}
return it.value, true
}
Now we have a system that is thread-safe, prevents stale data from being returned, and automatically manages its own memory footprint. It's not Redis, but for a local service, it's often all you need.
📋 Practical Task
Exercise: Implementing a Manual Purge for the Session Cache
Your task is to extend the Cache implementation from this lesson. While the background janitor handles automatic expiration, there are times when you need to force a cleanup—for example, during a system shutdown or a manual admin trigger.
Requirements:
- Add a method called
Purge()to theCachestruct. - The
Purge()method should immediately remove all items from the cache, regardless of their expiration time. - Ensure the method is thread-safe using the existing mutex.
- Write a small
mainfunction that:- Initializes a cache.
- Adds three different items with varying TTLs.
- Calls
Purge(). - Attempts to
Getone of the items to verify it is gone.
There are no comments for now.