I use BoltDb in a few projects of mine because it’s lightweight and easily embedded in Go. However, everytime I write something that uses BoltDB I find myself writing a ton of boilerplate to complete the same tasks in every project.

For example to create a bucket you need something like this:

package main

import (
  "go.etcd.io/bbolt"
  "log"
  "fmt"
)

func main() {
  db, err := bbolt.Open("mydb.db", 0664, nil)
    if err != nil {
      log.Println(err)
    }
   
   if err := db.Update(func(tx *bbolt.Tx) error {
       _, err := tx.CreateBucketIfNotExists([]byte("testing"))
          if err != nil {
              return fmt.Errorf("error creating bucket: %s", err)
          }

       return nil

   }); err != nil {

   log.Println(err)

   }
}

All of this ends up in a lot of reused, boilerplate that could be trimmed down.

I decided to write a simple client that covers simple use cases for db operations. This doesn’t cover every use case by any means, it’s just normal CRUD operations. The client embeds a BoltDB so any use cases not covered in the client can still be executed on that DB.

To create a bucket all you need is:

package main

import (
	"gitlab.com/hooksie1/bclient"
)

func main() {
	client := bclient.NewClient()
	client.NewDB("test.db")
	
	bucket := bclient.NewBucket("mybucket")
	client.Write(bucket)
}

Similarly to create a kv pair, all you need is:

package main

import (
	"gitlab.com/hooksie1/bclient"
)

func main() {
	client := bclient.NewClient()
	client.NewDB("test.db")
	
	kv := bclient.NewKV().SetBucket("mybucket").
		SetKey("mykey").SetValue("myvalue")
	
	client.Write(kv)

Return values are a little harder. BoltDB executes transactions in a function. So you execute db.Update(func(tx *bbolt.Tx) error {}). The abstractions fit nicely for writes, but for reads you also need to return some value(s).

The client initially just had a type named boltTxn which was just that function transaction definition. However, since the function is defined and run later, setting a value in the transaction means you need a way to return the function AND the value at the same time. The function is returned and then run in db.View(boltTxn). So to handle this, the transaction is now stored in a struct along with the return value which is of type interface{}. This way the value can be defined in the function call itself and stored in the struct when the function is executed. Then depending on the caller, type assertion can be used to return the correct type.

Right now this is only leveraged when reading all of the key/values in a bucket. If you only need to retrieve a single KV pair, you just define a KV pair with the bucket and key name and the read() method will fill in the value.

For example:

kv := bclient.NewKV().SetBucket("mybucket").SetKey("mykey")
// kv.Value is empty

client.Read(kv)

fmt.Println(kv.Value)
// prints out value returned from read.

This ended up being a bigger learning experience than I imagined. It helped a lot of pieces I’ve half understood for a while fit together and make the picture more clear for how I should be writing code. A big shoutout to my friend Jess who was a huge help in figuring certain things out in this client.