Storage
m.DB is a persistent key-value store, saved to disk and surviving restarts.
Owner and key
Every value lives under an owner — use m.Name() so your data stays yours:
m.DB.SetString(m.Name(), "last_city", "Moscow")
city := m.DB.GetString(m.Name(), "last_city", "")Getters take a default returned when the key is missing, so there is no "does it exist" dance.
Typed access
m.DB.SetString(m.Name(), "city", "Moscow")
m.DB.SetInt(m.Name(), "count", 42)
m.DB.SetInt64(m.Name(), "user_id", 123456789)
m.DB.SetBool(m.Name(), "enabled", true)
m.DB.SetStringSlice(m.Name(), "cities", []string{"Moscow", "Kazan"})
m.DB.SetInt64Slice(m.Name(), "admins", []int64{1, 2, 3})
m.DB.SetStringMap(m.Name(), "labels", map[string]string{"ru": "Москва"})
m.DB.SetAnyMap(m.Name(), "state", map[string]any{"n": 1})Each has a matching getter with a default:
city := m.DB.GetString(m.Name(), "city", "Moscow")
count := m.DB.GetInt(m.Name(), "count", 0)
enabled := m.DB.GetBool(m.Name(), "enabled", false)
cities := m.DB.GetStringSlice(m.Name(), "cities", nil)Handle write errors
Writes hit the disk and can fail. Ignoring the error means silently losing data:
if err := m.DB.SetString(m.Name(), "city", city); err != nil {
return fmt.Errorf("save city: %w", err)
}Reads cannot fail — that is what the default is for.
Per-chat and per-user data
Compose the key:
func chatKey(chatID int64) string {
return "chat:" + strconv.FormatInt(chatID, 10)
}
m.DB.SetString(m.Name(), chatKey(msg.ChatID), "Moscow")For many small values under one key, a map is cheaper than many keys:
cities := m.DB.GetStringMap(m.Name(), "cities_by_chat", nil)
if cities == nil {
cities = map[string]string{}
}
cities[strconv.FormatInt(msg.ChatID, 10)] = city
if err := m.DB.SetStringMap(m.Name(), "cities_by_chat", cities); err != nil {
return fmt.Errorf("save cities: %w", err)
}What you can store
Only JSON-serialisable values: strings, numbers, bools, and slices and maps of those. Storing anything else returns an error rather than corrupting the file.
Getters return a copy, so mutating the returned map does not change what is stored — write it back to persist.
Cost
Each write serialises the database and rewrites the file. That is fine at ordinary rates, but do not write once per incoming message in a watcher. Batch in memory and flush periodically:
type Counter struct {
goroku.Base
mu sync.Mutex
counts map[int64]int
}
func (m *Counter) flush() error {
m.mu.Lock()
snapshot := make(map[string]int, len(m.counts))
for id, n := range m.counts {
snapshot[strconv.FormatInt(id, 10)] = n
}
m.mu.Unlock()
return m.DB.SetStringMapInt(m.Name(), "counts", snapshot)
}Secrets
Values that must never appear in a backup belong in a config field marked Secret: true — see Configuration. Plain DB values are included in backups verbatim.