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 }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 )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)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 | Combine flags: Red | Blue |
& | 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) )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 setfs.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 TRUEt.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// 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 | t.dump.A || t.dump.B |
| 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 // ... )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.DumpHeaderscfg.Dump |= fs.DumpHeaders
Clearing a flag:
cfg.Dump &^= fs.DumpHeaders // AND NOTcfg.Dump &^= fs.DumpHeaders // AND NOT
Toggling a flag:
cfg.Dump ^= fs.DumpHeaders // XORcfg.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 }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 setvar 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)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) }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) }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) }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).
