prod/users.json.I was building a simple Go server and kept users in a map. I wanted to save them to disk, but a full database felt like too much, so I just wrote the map to a JSON file. It worked, but I wanted the same simplicity with durability. A map in memory, stored as JSON on disk, without losing records if the process crashes.
Install MeDB:
go get github.com/antonmedv/medb
package main
import (
"fmt"
"github.com/antonmedv/medb"
)
type User struct {
Name string `json:"name"`
Age int `json:"age"`
}
func main() {
db, err := medb.Open("data")
if err != nil {
panic(err)
}
defer func() {
if err := db.Close(); err != nil {
panic(err)
}
}()
users := medb.C[User](db, "users")
if err := users.Set("ada", User{Name: "Ada", Age: 36}); err != nil {
panic(err)
}
user, err := users.Get("ada")
if err != nil {
panic(err)
}
fmt.Printf("%s is %d years old\n", user.Name, user.Age)
}
This creates a users collection backed by data/users.json. Collections are typed views over JSON data, so use a compatible Go type whenever you reopen the same collection.
Collection names may contain nested paths:
users := medb.C[User](db, "prod/eu/users")
This collection is stored as data/prod/eu/users.json.
Use Update when the new value depends on the current document:
err := users.Update("ada", func(user User) (User, error) {
user.Age++
return user, nil
})
if err != nil {
panic(err)
}
Update prevents read-modify-write races between goroutines. Keep the callback short and do not call other methods on the same database from inside it.
id := medb.NewID()
if err := users.Set(id, User{Name: "Grace", Age: 45}); err != nil {
panic(err)
}
fmt.Println(users.Has(id))
fmt.Println(users.Count())
for id, user := range users.All() {
fmt.Printf("%s: %s\n", id, user.Name)
}
NewID returns a random 128-bit hexadecimal identifier. All iterates over a stable snapshot of the collection, ordered by document ID.
Delete one document:
if err := users.Delete("ada"); err != nil {
panic(err)
}
List and remove entire collections:
for _, name := range db.Collections() {
fmt.Println(name)
}
if err := db.Drop("users"); err != nil {
panic(err)
}
Deleting a missing document or collection is a no-op. Successful Set, Update, Delete, and Drop calls are durable when they return.
MeDB exposes sentinel errors that can be checked with errors.Is:
user, err := users.Get("missing")
switch {
case errors.Is(err, medb.ErrNotFound):
fmt.Println("user not found")
case err != nil:
panic(err)
default:
fmt.Println(user)
}
Other sentinel errors include:
ErrLocked when another process has already opened the database directory.ErrClosed when an operation is attempted after Close.ErrTooLarge when a document exceeds the configured size limit.ErrDirSync when a filesystem directory change cannot be made durable.MeDB is safe to use from multiple goroutines, but a database directory can be opened by only one process at a time.
Database options can control document size and when JSON snapshots are written:
db, err := medb.Open(
"data",
medb.WithMaxDocSize(4<<20), // 4 MiB per document
medb.WithFlushBytes(16<<20), // snapshot at 16 MiB of WAL
medb.WithFlushInterval(time.Second), // snapshot changed collections every second
)
if err != nil {
panic(err)
}
WithFlushBytes and WithFlushInterval control snapshot timing, not write durability. Every successful change is synced to the write-ahead log before returning.
Go 1.27 and newer also support the equivalent method syntax:
users := db.C[User]("users")
The package-level medb.C function remains available on every supported Go version.
Rewriting the entire JSON file on every update gets more expensive as the collection grows. MeDB writes a small log entry instead.
fsync—about 4 ms on this machine. That is why the
rewrite is faster below roughly 20k documents when it does not use fsync. Add fsync, and it reaches the same limit
as MeDB.fsync and sustains about
7.8k writes/s at every collection size. It overtakes the rewrite at roughly 440 documents and is 187× faster at 100k.Get
must unmarshal the stored JSON.Measured on an Apple M4 Pro with APFS, where fsync uses F_FULLFSYNC. See the bench.
76 commits
Hacker News (2)
Go
96.5%
Python
3.5%
prod/users.json.I was building a simple Go server and kept users in a map. I wanted to save them to disk, but a full database felt like too much, so I just wrote the map to a JSON file. It worked, but I wanted the same simplicity with durability. A map in memory, stored as JSON on disk, without losing records if the process crashes.
Install MeDB:
go get github.com/antonmedv/medb
package main
import (
"fmt"
"github.com/antonmedv/medb"
)
type User struct {
Name string `json:"name"`
Age int `json:"age"`
}
func main() {
db, err := medb.Open("data")
if err != nil {
panic(err)
}
defer func() {
if err := db.Close(); err != nil {
panic(err)
}
}()
users := medb.C[User](db, "users")
if err := users.Set("ada", User{Name: "Ada", Age: 36}); err != nil {
panic(err)
}
user, err := users.Get("ada")
if err != nil {
panic(err)
}
fmt.Printf("%s is %d years old\n", user.Name, user.Age)
}
This creates a users collection backed by data/users.json. Collections are typed views over JSON data, so use a compatible Go type whenever you reopen the same collection.
Collection names may contain nested paths:
users := medb.C[User](db, "prod/eu/users")
This collection is stored as data/prod/eu/users.json.
Use Update when the new value depends on the current document:
err := users.Update("ada", func(user User) (User, error) {
user.Age++
return user, nil
})
if err != nil {
panic(err)
}
Update prevents read-modify-write races between goroutines. Keep the callback short and do not call other methods on the same database from inside it.
id := medb.NewID()
if err := users.Set(id, User{Name: "Grace", Age: 45}); err != nil {
panic(err)
}
fmt.Println(users.Has(id))
fmt.Println(users.Count())
for id, user := range users.All() {
fmt.Printf("%s: %s\n", id, user.Name)
}
NewID returns a random 128-bit hexadecimal identifier. All iterates over a stable snapshot of the collection, ordered by document ID.
Delete one document:
if err := users.Delete("ada"); err != nil {
panic(err)
}
List and remove entire collections:
for _, name := range db.Collections() {
fmt.Println(name)
}
if err := db.Drop("users"); err != nil {
panic(err)
}
Deleting a missing document or collection is a no-op. Successful Set, Update, Delete, and Drop calls are durable when they return.
MeDB exposes sentinel errors that can be checked with errors.Is:
user, err := users.Get("missing")
switch {
case errors.Is(err, medb.ErrNotFound):
fmt.Println("user not found")
case err != nil:
panic(err)
default:
fmt.Println(user)
}
Other sentinel errors include:
ErrLocked when another process has already opened the database directory.ErrClosed when an operation is attempted after Close.ErrTooLarge when a document exceeds the configured size limit.ErrDirSync when a filesystem directory change cannot be made durable.MeDB is safe to use from multiple goroutines, but a database directory can be opened by only one process at a time.
Database options can control document size and when JSON snapshots are written:
db, err := medb.Open(
"data",
medb.WithMaxDocSize(4<<20), // 4 MiB per document
medb.WithFlushBytes(16<<20), // snapshot at 16 MiB of WAL
medb.WithFlushInterval(time.Second), // snapshot changed collections every second
)
if err != nil {
panic(err)
}
WithFlushBytes and WithFlushInterval control snapshot timing, not write durability. Every successful change is synced to the write-ahead log before returning.
Go 1.27 and newer also support the equivalent method syntax:
users := db.C[User]("users")
The package-level medb.C function remains available on every supported Go version.
Rewriting the entire JSON file on every update gets more expensive as the collection grows. MeDB writes a small log entry instead.
fsync—about 4 ms on this machine. That is why the
rewrite is faster below roughly 20k documents when it does not use fsync. Add fsync, and it reaches the same limit
as MeDB.fsync and sustains about
7.8k writes/s at every collection size. It overtakes the rewrite at roughly 440 documents and is 187× faster at 100k.Get
must unmarshal the stored JSON.Measured on an Apple M4 Pro with APFS, where fsync uses F_FULLFSYNC. See the bench.
Hacker News (2)
76 commits
Go
96.5%
Python
3.5%