I was adding my eighth boolean toggle to a config struct — EnableCompression, SkipValidation, LogQueries, and so on. Eight fields, eight if cfg.Field checks, eight more lines in my YAML. The boilerplate was starting to annoy me.
Then I replaced them with this:
type ConfigFlags int
const (
EnableCompression ConfigFlags = 1 << iota // 1 (0001)
SkipValidation // 2 (0010)
LogQueries // 4 (0100)
// next one is 8 (1000), then 16 (00010000)...
)
cfg := EnableCompression | LogQueries
if cfg & (EnableCompression | LogQueries) != 0 {
// do stuff
}
One integer. One check for any combination. Bitwise operators — OR (|) to combine, AND (&) to check, XOR (^) to toggle — handle the logic. And iota generates the bit positions automatically.
What bit flags look like
Each bit in a byte represents a separate option. An RGB color example:
const (
Red = 0b00000001 // bit 0
Green = 0b00000010 // bit 1
Blue = 0b00000100 // bit 2
)
Visually, each bit position controls one color:
bit: 7 6 5 4 3 2 1 0
│ │ │ │ │ │ │ │
value: 0 0 0 0 0 0 0 0
│ │ │
│ │ └─ Red (1 << 0)
│ └─── Green (1 << 1)
└───── Blue (1 << 2)
Combine with OR: Red | Blue → 0b00000101 (purple)
Check with AND: color & Blue != 0 → "is blue set?"
Go bitwise operators for flags
| Operator | Name | Use for flags |
|---|---|---|
| `\ | ` | OR |
& |
AND | Check if flag is set: cfg & TLS != 0
|
^ |
XOR | Toggle a flag on/off |
&^ |
AND NOT | Clear a flag: cfg &^= TLS
|
These four operators handle everything you need for flag manipulation.
How it works
Each flag is a power of 2, so each occupies exactly one bit:
const (
DumpHeaders = 1 << iota // 0001 (1)
DumpBodies // 0010 (2)
DumpRequests // 0100 (4)
DumpResponses // 1000 (8)
)
Bitwise OR (|) — stack flags together:
fs.DumpHeaders | fs.DumpResponses
// 0001
// 1000
// ---- OR
// 1001 → both flags set
Bitwise AND (&) — check if a flag is set:
t.dump & (fs.DumpHeaders | fs.DumpResponses) != 0
// t.dump = 1001 (has DumpHeaders + DumpResponses)
// mask = 1001
// ---- AND
// 1001 → non-zero → condition is TRUE
If t.dump had neither flag:
// t.dump = 0100 (only DumpRequests)
// mask = 1001
// ---- AND
// 0000 → zero → condition is FALSE
The condition checks "is at least one of these flags set?" — a single CPU instruction, no branches, no allocations.
Why use this instead of a struct of bools?
| Bitmask | Struct of bools |
|---|---|
| `t.dump & (A\ | B) != 0` |
| Single integer, easily serialized | Multiple fields |
| Pass many flags in one arg | Pass whole struct |
| Config files store one number | Harder to store compactly |
| Standard in C/Unix heritage | More Go-idiomatic sometimes |
Most of the time a struct of bools is fine. If you're writing a web service with twelve config options and you only check them at startup, just use a struct. I reach for bitmasks when:
- You check flags frequently in hot paths — one integer comparison is cheaper than multiple field lookups
-
You need to pass flags across API boundaries — one
intis easier than a struct in many C-interop situations - You want compact serialization — a single integer in a database column or config file
- You're modeling Unix-style file permissions or socket options — where the underlying system already uses bitmasks
The iota pattern for bit flags in Go
In rclone, fs.DumpFlags is defined using iota with bit shifts:
type DumpFlags int
const (
DumpHeaders DumpFlags = 1 << iota // 1
DumpBodies // 2
DumpRequests // 4
DumpResponses // 8
DumpAuth // 16
DumpFilters // 32
// ...
)
1 << iota is the idiomatic way to do this in Go. iota increments by 1 for each constant in the block, and 1 << n shifts the bit left by n positions. So you get 1, 2, 4, 8, 16, 32 — each a unique bit, never overlapping.
I used to write these out manually as 1, 2, 4, 8 until I had a bug where I typo'd 16 as 6 and spent an hour wondering why my flags weren't working. The iota pattern prevents that entirely.
How to set, clear, and toggle flags
Setting a flag:
cfg.Dump |= fs.DumpHeaders
Clearing a flag:
cfg.Dump &^= fs.DumpHeaders // AND NOT
Toggling a flag:
cfg.Dump ^= fs.DumpHeaders // XOR
Checking if all flags in a mask are set:
if cfg.Dump & (fs.DumpHeaders | fs.DumpBodies) == (fs.DumpHeaders | fs.DumpBodies) {
// has BOTH headers AND bodies enabled
}
That last one is a common mistake. != 0 checks "any of these." == mask checks "all of these."
When I actually use this
I don't reach for bitmasks in everyday application code. If I'm building a CRUD API, I'll use a struct with bool fields and not feel bad about it. But I do use bitmasks when:
- Modeling file permissions (read/write/execute bits)
- gRPC/HTTP middleware option sets that get checked per-request
- Database query builder flags (distinct, for_update, etc.)
- Anytime I'm wrapping a C library or syscalls that already use this convention
The Go standard library uses this pattern for file permissions (os.O_RDONLY, os.O_WRONLY, os.O_RDWR, os.O_APPEND, etc.). The syscall package is basically a museum of bitmasks. So it's not some archaic C-ism we left behind. It's still the right tool when you need compact, efficient sets of boolean options.
The one gotcha
Bitmasks are type-unsafe in practice. Nothing stops you from writing:
var f fs.DumpFlags = 999 // garbage bits set
Or mixing flag types accidentally:
type LogFlags int
const Verbose LogFlags = 1 << iota
// This compiles but is semantically wrong:
dump := fs.DumpHeaders | int(Verbose)
Go's type system won't save you here. If you need type safety, wrap the operations in methods or use a struct with an internal bitmask field. I usually just add a Has(flag DumpFlags) bool method to centralize the bitwise logic.
Is it worth learning?
Yeah. You might not use it weekly, but you'll encounter it in any serious systems code. Understanding &, |, ^, and &^ lets you read code like rclone's transport layer without your eyes glazing over. And honestly, there's something satisfying about packing twelve booleans into a single uint16 and knowing exactly which bits mean what.
Just don't be the person who uses bitmasks for a three-option config struct. That's not clever, it's just obnoxious.
Using flags in a struct
I usually wrap the bitmask in a struct with methods so the rest of my code doesn't have to think about bit math:
package main
import "fmt"
type ConnFlags int
const (
TLS ConnFlags = 1 << iota
Compress
RetryFailed
LogQueries
)
type Connection struct {
Host string
Port int
Flags ConnFlags
}
// Has checks if a specific flag is set
func (c *Connection) Has(flag ConnFlags) bool {
return c.Flags&flag != 0
}
// HasAny checks if at least one of the flags in the mask is set
func (c *Connection) HasAny(flags ConnFlags) bool {
return c.Flags&flags != 0
}
// HasAll checks if all flags in the mask are set
// Usage: HasAll(TLS | Compress) — checks BOTH are enabled
func (c *Connection) HasAll(mask ConnFlags) bool {
return c.Flags&mask == mask
}
// Set enables a flag
func (c *Connection) Set(flag ConnFlags) {
c.Flags |= flag
}
// Clear disables a flag
func (c *Connection) Clear(flag ConnFlags) {
c.Flags &^= flag
}
func main() {
// Initialize with flags
conn := Connection{
Host: "db.example.com",
Port: 5432,
Flags: TLS | Compress | LogQueries,
}
// Check individual flags
if conn.Has(TLS) {
fmt.Println("TLS enabled")
}
// Check combinations
if conn.HasAny(TLS | RetryFailed) {
fmt.Println("Secure or retry-capable connection")
}
// TLS | Compress creates a mask with both bits set
// HasAll checks if conn.Flags contains BOTH bits
if conn.HasAll(TLS | Compress) {
fmt.Println("Fully encrypted and compressed")
}
// Modify flags
conn.Set(RetryFailed)
conn.Clear(LogQueries)
fmt.Printf("Final flags: %d (binary: %b)\n", conn.Flags, conn.Flags)
}
The rest of your code calls conn.Has(TLS) instead of conn.Flags&TLS != 0.
JSON marshal and unmarshal for flags
Raw integers in JSON configs are unreadable. Fix it with custom marshaling:
package main
import (
"encoding/json"
"fmt"
)
type FeatureFlags int
const (
DarkMode FeatureFlags = 1 << iota
BetaFeatures
OfflineMode
Analytics
)
// String returns human-readable names for debugging
func (f FeatureFlags) String() string {
var names []string
if f&DarkMode != 0 {
names = append(names, "dark_mode")
}
if f&BetaFeatures != 0 {
names = append(names, "beta_features")
}
if f&OfflineMode != 0 {
names = append(names, "offline_mode")
}
if f&Analytics != 0 {
names = append(names, "analytics")
}
if len(names) == 0 {
return "none"
}
return fmt.Sprintf("%v", names)
}
type Config struct {
Name string `json:"name"`
Flags FeatureFlags `json:"flags"`
}
// MarshalJSON outputs flags as string slice for readability
func (c Config) MarshalJSON() ([]byte, error) {
return json.Marshal(struct {
Name string `json:"name"`
Flags []string `json:"flags"`
}{
Name: c.Name,
Flags: parseFlagNames(c.Flags),
})
}
// UnmarshalJSON accepts either []string or integer format
func (c *Config) UnmarshalJSON(data []byte) error {
// Try []string first
var strCfg struct {
Name string `json:"name"`
Flags []string `json:"flags"`
}
if err := json.Unmarshal(data, &strCfg); err == nil {
c.Name = strCfg.Name
c.Flags = 0
for _, f := range strCfg.Flags {
switch f {
case "dark_mode":
c.Flags |= DarkMode
case "beta_features":
c.Flags |= BetaFeatures
case "offline_mode":
c.Flags |= OfflineMode
case "analytics":
c.Flags |= Analytics
}
}
return nil
}
// Fall back to integer format (for backward compatibility with hand-written
// configs that use raw numbers instead of string slices)
var intCfg struct {
Name string `json:"name"`
Flags FeatureFlags `json:"flags"`
}
if err := json.Unmarshal(data, &intCfg); err != nil {
return err
}
c.Name = intCfg.Name
c.Flags = intCfg.Flags
return nil
}
func parseFlagNames(f FeatureFlags) []string {
var names []string
if f&DarkMode != 0 {
names = append(names, "dark_mode")
}
if f&BetaFeatures != 0 {
names = append(names, "beta_features")
}
if f&OfflineMode != 0 {
names = append(names, "offline_mode")
}
if f&Analytics != 0 {
names = append(names, "analytics")
}
return names
}
func main() {
// Marshal to JSON (errors intentionally ignored for example)
cfg := Config{
Name: "my-app",
Flags: DarkMode | OfflineMode,
}
data, _ := json.MarshalIndent(cfg, "", " ")
fmt.Println(string(data))
// Unmarshal back (errors intentionally ignored for example)
var restored Config
_ = json.Unmarshal(data, &restored)
fmt.Printf("Restored: %+v\n", restored)
}
Compact storage internally, readable JSON externally. The UnmarshalJSON accepts both formats, so legacy integer configs still work.
A complete working example
Flags work well in HTTP clients where they influence both construction and runtime behavior:
package main
import (
"fmt"
"net/http"
"time"
)
// RequestFlags control request behavior
type RequestFlags int
const (
FollowRedirects RequestFlags = 1 << iota
SkipTLSVerify
RetryOnError
LogRequestBody
)
type HTTPClient struct {
Timeout time.Duration
Flags RequestFlags
client *http.Client
}
func NewHTTPClient(timeout time.Duration, flags RequestFlags) *HTTPClient {
transport := &http.Transport{
TLSClientConfig: nil,
}
if flags&SkipTLSVerify != 0 {
// In production you'd set InsecureSkipVerify here
fmt.Println("Note: TLS verification would be disabled")
}
var checkRedirect func(req *http.Request, via []*http.Request) error
if flags&FollowRedirects == 0 {
checkRedirect = func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
}
}
return &HTTPClient{
Timeout: timeout,
Flags: flags,
client: &http.Client{
Timeout: timeout,
Transport: transport,
CheckRedirect: checkRedirect,
},
}
}
func (h *HTTPClient) ShouldRetry() bool {
return h.Flags&RetryOnError != 0
}
func (h *HTTPClient) ShouldLogBody() bool {
return h.Flags&LogRequestBody != 0
}
func main() {
client := NewHTTPClient(
30*time.Second,
FollowRedirects|RetryOnError,
)
fmt.Printf("Client created with flags: %b\n", client.Flags)
fmt.Printf("Should retry: %v\n", client.ShouldRetry())
fmt.Printf("Should log body: %v\n", client.ShouldLogBody())
fmt.Printf("Follows redirects: %v\n", client.Flags&FollowRedirects != 0)
}
Flags work at two stages: they configure the client during initialization (FollowRedirects sets up the redirect handler) and drive runtime decisions (RetryOnError checked before each retry).
Top comments (0)