Baike.dev
All toolsOnline toolsAI codingTrendingOpen sourceNewsSubmit
Log in
< Back to tools
B

bolthold

> 编程语言
Open source

BoltHold is an embeddable NoSQL store for Go types built on BoltDB

657 stars0 likes0 views
WebsiteGitHub

About

BoltHold is an embeddable NoSQL store for Go types built on BoltDB

BoltHold

BoltHold is a simple querying and indexing layer on top of a Bolt DB instance. For a similar library built on Badger see BadgerHold.

The goal is to create a simple, higher level interface on top of Bolt DB that simplifies dealing with Go Types and finding data, but exposes the underlying Bolt DB for customizing as you wish. By default the encoding used is Gob, so feel free to use the GobEncoder/Decoder interface for faster serialization. Or, alternately, you can use any serialization you want by supplying encode / decode funcs to the Options struct on Open.

One Go Type will have one bucket, and multiple index buckets in a BoltDB file, so you can store multiple Go Types in the same database.

Why not just use Bolt DB directly?

I love BoltDB, and I've used it in several projects. However, I find myself writing the same code over and over again, for encoding and decoding objects and searching through data. I figure formalizing how I've been using BoltDB and including tests and benchmarks will, at a minimum, be useful to me. Maybe it'll be useful to others as well.

Indexes

Indexes allow you to skip checking any records that don't meet your index criteria. If you have 1000 records and only 10 of them are of the Division you want to deal with, then you don't need to check to see if the other 990 records match your query criteria if you create an index on the Division field. The downside of an index is added disk reads and writes on every write operation. For read heavy operations datasets, indexes can be very useful.

In every BoltHold store, there will be a reserved bucket _indexes which will be used to hold indexes that point back to another bucket's Key system. Indexes will be defined by setting the boltholdIndex struct tag on a field in a type.

go
type Person struct {
	Name string
	Division string `boltholdIndex:"Division"`
}

This means that there will be an index created for Division that will contain the set of unique divisions, and the main record keys they refer to. More information on how indexes work can be found here

Optionally, you can implement the Storer interface, to specify your own indexes, rather than using the boltholdIndex struct tag.

Slice Indexes

When you create an index on a slice of items, by default it may not do what you expect. Consider the following records:

ID Name Categories
1 John red, green, blue
2 Bill red, purple
3 Jane red, orange
4 Brian red, purple

You may expect your Categories index to look like the following:

Categories ID
red 1, 2, 3, 4
green 1
blue 1
purple 2, 4
orange 3

But they'll actually look like this:

Categories ID
red, green, blue 1
red, purple 2, 4
red, orange 3

So if you did a query like this:

go
bh.Where("Categories").Contains("red").Index("Categories")

It'll work, but you'll be reading more records than you'd expect. You'd only "save reads" on values where the list of categories exactly match.

If instead you want to index each individual item in the slice, you can use the struct tag boltholdSliceIndex. It will then individually index each item in the slice, and potentially give you performance benefits if you have a lot of overlap with individual items in your sliced fields.

Be sure to benchmark both a regular index and a sliced index to see which performs better for your specific dataset.

Queries

Queries are chain-able constructs that filters out any data that doesn't match it's criteria. An index will be used if the .Index() chain is called, otherwise bolthold won't use any index.

Queries will look like this:

go
s.Find(&result, bolthold.Where("FieldName").Eq(value).And("AnotherField").Lt(AnotherValue).Or(bolthold.Where("FieldName").Eq(anotherValue)))

Fields must be exported, and thus always need to start with an upper-case letter. Available operators include:

  • Equal - Where("field").Eq(value)
  • Not Equal - Where("field").Ne(value)
  • Greater Than - Where("field").Gt(value)
  • Less Than - Where("field").Lt(value)
  • Less than or Equal To - Where("field").Le(value)
  • Greater Than or Equal To - Where("field").Ge(value)
  • In - Where("field").In(val1, val2, val3)
  • IsNil - Where("field").IsNil()
  • Regular Expression - Where("field").RegExp(regexp.MustCompile("ea"))
  • Matches Function
    • Where("field").MatchFunc(func(ra *RecordAccess) (bool, error)) // see RecordAccess Type
    • Where("field").MatchFunc(func(m *MyType) (bool, error))
    • Where("field").MatchFunc(func(field string) (bool, error))
  • Skip - Where("field").Eq(value).Skip(10)
  • Limit - Where("field").Eq(value).Limit(10)
  • SortBy - Where("field").Eq(value).SortBy("field1", "field2")
  • Reverse - Where("field").Eq(value).SortBy("field").Reverse()
  • Index - Where("field").Eq(value).Index("indexName")
  • Not - Where("field").Not().In(val1, val2, val3)
  • Contains - Where("field").Contains(val1)
  • ContainsAll - Where("field").Contains(val1, val2, val3)
  • ContainsAny - Where("field").Contains(val1, val2, val3)
  • HasKey - Where("field").HasKey(val1) // to test if a Map value has a key

An empty / zero value query matches against all records, because it has no critiera. You can then use Skip and Limit to page through all records in your dataset:

go
q := &bolthold.Query{}

err := store.Find(&result, q.Skip(10).Limit(50))

If you want to run a query's criteria against the Key value, you can use the bolthold.Key constant:

go
store.Find(&result, bolthold.Where(bolthold.Key).Ne(value))

You can access nested structure fields in queries like this:

go
type Repo struct {
  Name string
  Contact ContactPerson
}

type ContactPerson struct {
  Name string
}

store.Find(&repo, bolthold.Where("Contact.Name").Eq("some-name")

Instead of passing in a specific value to compare against in a query, you can compare against another field in the same struct. Consider the following struct:

go
type Person struct {
	Name string
	Birth time.Time
	Death time.Time
}

If you wanted to find any invalid records where a Person's death was before their birth, you could do the following:

go
store.Find(&result, bolthold.Where("Death").Lt(bolthold.Field("Birth")))

Queries can be used in more than just selecting data. You can delete or update data that matches a query.

Using the example above, if you wanted to remove all of the invalid records where Death < Birth:

go
// you must pass in a sample type, so BoltHold knows which bucket to use and what indexes to update
store.DeleteMatching(&Person{}, bolthold.Where("Death").Lt(bolthold.Field("Birth")))

Or if you wanted to update all the invalid records to flip/flop the Birth and Death dates:

go
store.UpdateMatching(&Person{}, bolthold.Where("Death").Lt(bolthold.Field("Birth")), func(record interface{}) error {
	update, ok := record.(*Person) // record will always be a pointer
	if !ok {
		return fmt.Errorf("Record isn't the correct type!  Wanted Person, got %T", record)
	}

	update.Birth, update.Death = update.Death, update.Birth

	return nil
})

If you simply want to count the number of records returned by a query use the Count method:

go
// need to pass in empty datatype so bolthold knows what type to count
count, err := store.Count(&Person{}, bolthold.Where("Death").Lt(bolthold.Field("Birth")))

Keys in Structs

A common scenario is to store the bolthold Key in the same struct that is stored in the boltDB value. You can automatically populate a record's Key in a struct by using the boltholdKey struct tag when running Find queries.

Another common scenario is to insert data with an auto-incrementing key assigned by the database. When performing an Insert, if the type of the key matches the type of the boltholdKey tagged field, the data is passed in by reference, and the field's current value is the zero-value for that type, then it is set on the data before insertion.

go
type Employee struct {
	ID string `boltholdKey:"ID"`  // the tagName isn't required, but some linters will complain without it
	FirstName string
	LastName string
	Division string
	Hired time.Time
}

Bolthold assumes only one of such struct tags exists. If a value already exists in the key field, it will be overwritten.

If you want to insert an auto-incrementing Key you can pass the bolthold.NextSequence() func as the Key value.

go
err := store.Insert(bolthold.NextSequence(), data)

The key value will be a uint64.

If you want to know the value of the auto-incrementing Key that was generated using bolthold.NextSequence(), then make sure to pass your data by reference and that the boltholdKey tagged field is of type uint64.

go
err := store.Insert(bolthold.NextSequence(), &data)

Slices in Structs and Queries

When querying slice fields in structs you can use the Contains, ContainsAll and ContainsAny criterion.

go
val := struct {
    Set []string
}{
    Set: []string{"1", "2", "3"},
}
bh.Where("Set").Contains("1") // true
bh.Where("Set").ContainsAll("1", "3") // true
bh.Where("Set").ContainsAll("1", "3", "4") // false
bh.Where("Set").ContainsAny("1", "7", "4") // true

The In, ContainsAll and ContainsAny critierion accept a slice of interface{} values. This means you can build your queries by passing in your values as arguments:

where := bolthold.Where("Id").In("1", "2", "3")

However if you have an existing slice of values to test against, you can't pass in that slice because it is not of type []interface{}.

go
t := []string{"1", "2", "3", "4"}
where := bolthold.Where("Id").In(t...) // compile error

Instead you need to copy your slice into another slice of empty interfaces:

go
t := []string{"1", "2", "3", "4"}
s := make([]interface{}, len(t))
for i, v := range t {
    s[i] = v
}
where := bolthold.Where("Id").In(s...)

You can use the helper function bolthold.Slice which does exactly that.

go
t := []string{"1", "2", "3", "4"}
where := bolthold.Where("Id").In(bolthold.Slice(t)...)

Unique Constraints

You can create a unique constraint on a given field by using the boltholdUnique:"ConstraintName" struct tag:

go
type User struct {
  Name string
  Email string `boltholdUnique:"UniqueEmail"` // this field will be indexed with a unique constraint
}

The example above will only allow one record of type User to exist with a given Email field. Any insert, update or upsert that would violate that constraint will fail and return the bolthold.ErrUniqueExists error.

ForEach

When working with large datasets, you may not want to have to store the entire dataset in memory. It's be much more efficient to work with a single record at a time rather than grab all the records and loop through them, which is what cursors are used for in databases. In BoltHold you can accomplish the same thing by calling ForEach:

go
err := store.ForEach(boltholdWhere("Id").Gt(4), func(record *Item) error {
	// do stuff with record

	// if you return an error, then the query will stop iterating through records

	return nil
})

Aggregate Queries

Aggregate queries are queries that group results by a field. For example, lets say you had a collection of employees:

go
type Employee struct {
	FirstName string
	LastName string
	Division

Issues· 0 open

View all issuesOpen on GitHub

No open issues yet, or sync has not completed.

> Tags

Goboltdbbucketgogolang

No comments yet. Be the first to share.

> Details

PublishedAug 1, 2026
UpdatedSep 17, 2026
Category编程语言
PricingOpen source

> Related tools

T
TypeScript
JavaScript 的超集,为前端与全栈提供静态类型
P
Python
通用编程语言,广泛用于 Web、数据与 AI
G
Go
Google 推出的简洁高效系统语言