Go Bitwise Flags and Bitmasks: Configuration Pattern Guide

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 | Blue0b00000101 (purple)

Check with AND: color & Blue != 0 → "is blue set?"

Go bitwise operators for flags

OperatorNameUse for flags
|ORCombine flags: Red | Blue
&ANDCheck if flag is set: cfg & TLS != 0
^XORToggle a flag on/off
&^AND NOTClear 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.

graph LR A["DumpHeaders<br/>0001"] -->|"OR"| C["0001 | 1000<br/>= 1001"] B["DumpResponses<br/>1000"] -->|"OR"| C C -->|"AND"| D["1001 & 1001<br/>= 1001 ✓"]

Why use this instead of a struct of bools?

BitmaskStruct of bools
t.dump & (A|B) != 0t.dump.A || t.dump.B
Single integer, easily serializedMultiple fields
Pass many flags in one argPass whole struct
Config files store one numberHarder to store compactly
Standard in C/Unix heritageMore 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:

  1. You check flags frequently in hot paths — one integer comparison is cheaper than multiple field lookups
  2. You need to pass flags across API boundaries — one int is easier than a struct in many C-interop situations
  3. You want compact serialization — a single integer in a database column or config file
  4. 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).

FAQ

Q What are bitwise flags in Go?
A Bitwise flags store multiple boolean options in a single integer, where each bit represents a different option. Use bitwise OR to combine flags, AND to check them.
Q How does iota work with bit flags?
A `1 << iota` generates powers of 2 (1, 2, 4, 8...) automatically. iota increments by 1 for each const, and `<<` shifts the bit left, giving each flag its own unique bit position.
Q How do I check if multiple flags are set?
A Use `!= 0` to check "any of these flags": `if cfg & (FlagA | FlagB) != 0`. Use `== mask` to check "all of these flags": `if cfg & (FlagA | FlagB) == (FlagA | FlagB)`.
Q Why not just use a struct with bool fields?
A A struct is more readable but harder to serialize, pass around, and check multiple flags at once. Bitmasks win when you need compact representation or frequent flag combinations.
Q Can I use this with JSON or YAML config files?
A Yes. Store the integer value in config, then parse it into your flag type. You can also define human-readable names with a map[string]int for friendlier configs.
Q What happens if I accidentally use the same bit twice?
A If you manually define values instead of using iota with bit shifts, you might overlap bits. Always use 1 << iota to guarantee unique bits.
Q Is this pattern still relevant in 2026?
A Yes. Bitwise operations are still a single CPU instruction. The pattern is widely used in systems programming, networking, and anywhere you need efficient flag checking.
About the Author

Asaduzzaman Pavel

Software Engineer who actually enjoys the friction of well-architected systems. 15+ years building high-performance backends and infrastructure that handles real-world chaos at scale.

Open to new opportunities

Comments

  • Sign in with GitHub to comment
  • Keep it respectful and on-topic
  • No spam or self-promotion