How I rewrote Redis in less than 70 lines with Golang
Published on May 16, 2025 in Software engineering
Redis is at the heart of any good bloatware. You have probably heard many times: “We used Redis to manage concurrent access” or other explanations of this kind.
In fact, in 9 out of 10 cases, applications that use Redis don’t need it. Not only do they not need it, but they would gain in simplicity and performance by not using it.
You also have also probably heard that managing concurrent access to data is a complicated task, and it’s better to not reinvent the wheel.
So, let’s see what it really is. In this example, I’ll show you how to use a Golang map to store data with a limited lifetime, just like you might do in Redis.
We will also use an RWMutex to allow read access by several goroutines at the same time. Of course, this code is concurent safe and only one write access is possible at any given time.
Redis is a tool with many features and my title was a bit clickbait. Of course, I’m not going to recreate all of Redis. I’m just going to show you that for most uses it’s very easy to do without it.
Even if it is minimalist, this implementation allows you to store any complex type without serialization. And in modern apps, serialization/deserialization is often the bottleneck that has replaced I/O issues.
I’m not even talking about the performance gain of directly managing everything in memory in the same program.
Here’s the code:
package myredis
import (
"sync"
"time"
)
type record struct {
value interface{}
lastAccess int64
ttl int64
}
type database struct {
store map[string]*record
mu sync.RWMutex
}
func NewDatabase() *database {
db := &database{
store: make(map[string]*record),
}
// Start a goroutine to periodically check for expired keys and remove them
go func() {
for {
time.Sleep(time.Second) // Check every second but it can be adjusted
now := time.Now().Unix()
db.mu.Lock()
for key, item := range db.store {
if now-item.lastAccess > item.ttl {
delete(db.store, key)
}
}
db.mu.Unlock()
}
}()
return db
}
func (db *database) Set(key string, value interface{}, ttl int64) {
db.mu.Lock()
defer db.mu.Unlock()
db.store[key] = &record{
value: value,
lastAccess: time.Now().Unix(),
ttl: ttl,
}
}
func (db *database) Get(key string) (interface{}, bool) {
db.mu.RLock()
defer db.mu.RUnlock()
if d, ok := db.store[key]; ok {
d.lastAccess = time.Now().Unix()
return d.value, true
}
return nil, false
}
func (db *database) Delete(key string) {
db.mu.Lock()
defer db.mu.Unlock()
delete(db.store, key)
}
As you can see, this code is extremely simple and does not require extensive explanations.
In the NewDatabase function, an initial size could be specified when allocating the map. If your database has a specific memory requirement, setting it at creation will avoid multiple reallocations and improve performance.
This implementation has few features, but with it, you can easily add all you need, for example the use of wilcard…
And here’s how you can use your new key-value database.
package main
import (
"time"
"myproject/myredis"
)
func main() {
// Create a new Redis-like database
db := myredis.NewDatabase()
// Set a key-value pair with an expiration timeout of 1 second
db.Set("name", "Alice", 1)
// Set another key-value pair with a longer expiration timeout
db.Set("age", 30, 5)
// Note: The values can be of any type, but you will need to use type assertion to retrieve them
// Get the values
name, _ := db.Get("name")
age, _ := db.Get("age")
// Print the values with type assertion
println(name.(string)) // Output: Alice
println(age.(int)) // Output: 30
// Simulate a 3-second wait
time.Sleep(3 * time.Second)
// Try to get an expired key
if value, ok := db.Get("name"); ok {
println(value.(string))
} else {
println("Key 'name' not found") // Output: Key 'name' not found
}
// Try to get a non-expired key
if value, ok := db.Get("age"); ok {
println(value.(int)) // Output: 30
} else {
println("Key 'age' not found")
}
}
Of course, Redis is a great tool and sometimes it is necessary to use it. But as you’ve just seen, it is not the ultimate solution to all problems, and it’s often much easier to do without it.
Don’t miss my upcoming posts — hit the follow button on my LinkedIn profile