Table of contents
Part of nurago, a collection of independent Go packages for backend services.
import "github.com/tecnickcom/nurago/pkg/enumbitmap"
Two functions. StringsToBitMap packs a set of names into one integer; BitMapToStrings unpacks it.
00000001 = 1 = FIRST
00000010 = 2 = SECOND
00000100 = 4 = THIRD
00001000 = 8 = FOURTH
00001001 = 9 = FIRST + FOURTH
When one integer beats a join table
A set of flags on a record has two standard storage shapes. A separate table with a row per flag per record, which is correct and normalised and costs a join on every read. Or a single integer column, which is denormalised and costs nothing.
For a fixed set of flags that will never carry attributes of their own, such as permissions, feature toggles, or notification channels, the integer wins on every axis that matters: one column, one index, set membership as a bitwise AND, and set operations in the query itself. Adding a flag means defining a new constant, not migrating a table.
The cost is that the mapping between bit position and meaning now lives in code, and if it drifts from what is stored, every historical row is misread. Keeping that mapping in exactly one place is what this package is for.
The contract, stated
Three guarantees the caller must supply, none of which the type system can check:
IDs must be distinct single-bit powers of two, from 1<<0 to 1<<31. Two names sharing a bit means decoding cannot tell which was set.
An ID of 0 is unreachable. Zero has no bit set, so BitMapToStrings can never produce it. A “NONE” entry with ID 0 will encode without complaint and vanish on decode. The value 0 always decodes to an empty slice, which is the correct representation of the empty set.
A multi-bit ID breaks symmetry. An ID of 3 (bits 0 and 1) encodes fine and decodes as two separate flags.
None of these can be enforced by the type system, which is why they are documented and tested rather than validated. Test_BitMapToStrings_zeroIDUnreachable and Test_RoundTrip are in the test file for exactly this.
The 32-bit ceiling and the sign bit
The package handles bits 0 through 31 and treats values as the low 32 bits of a host int.
Bit 31 is the sign bit where int is 32 bits wide. On a 64-bit build, which is everything this library targets, int is 64 bits and bit 31 is an ordinary bit. On a 32-bit build, inputs at or above 1<<31 behave in implementation-defined ways, and the package says so rather than pretending to be portable.
Thirty-two flags is a real limit. Past it, use two columns or a different representation.
Error reporting
An unknown bit in the input to BitMapToStrings is reported with the offending mask, and an unknown name in StringsToBitMap is reported with a sentinel. Both give you enough to identify which value was wrong, rather than a bare “invalid input”. This is the path that catches a mapping that has drifted from the stored data.
Measured
| Operation | ns/op | B/op | allocs/op |
|---|---|---|---|
StringsToBitMap | 63.6 | 64 | 1 |
BitMapToStrings, sparse | 210 | 768 | 2 |
BitMapToStrings, all bits set | 430 | 768 | 2 |
Measured with go test -bench on Go 1.27.1, linux/amd64, Intel Core i7-1260P. Run go test -bench=. ./pkg/enumbitmap/ to reproduce them on your own hardware; the absolute values will differ, the ratios much less so.
Decoding costs three to seven times encoding and allocates twelve times the memory, because it returns a slice of strings against one integer. Encoding is also flat in the number of flags, while decoding scales with how many bits are actually set: the sparse and full cases differ by a factor of two on identical input size.
Store and query the integer. Decode at the edge, once, when you are about to render it.
Example
enum := map[int]string{1: "FIRST", 2: "SECOND", 4: "THIRD", 8: "FOURTH"}
s, err := enumbitmap.BitMapToStrings(enum, 9)
fmt.Println(s, err)
Full source is in example_enumbitmap_test.go. More runnable examples are on pkg.go.dev.
Dependencies
This package reaches no external module: it uses only the Go standard library.
Links
- Reference documentation
- Source code
- The same codec off a loaded cache: enumcache
- All nurago packages