DEV Community

Asaduzzaman Pavel
Asaduzzaman Pavel

Posted on • Originally published at iampavel.dev

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
}
Enter fullscreen mode Exit fullscreen mode

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
)
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

Combine with OR: Red | Blue0b00000101 (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)
)
Enter fullscreen mode Exit fullscreen mode

Bitwise OR (|) — stack flags together:

fs.DumpHeaders | fs.DumpResponses
// 0001
// 1000
// ---- OR
// 1001  → both flags set
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

If t.dump had neither flag:

//  t.dump = 0100  (only DumpRequests)
//  mask   = 1001
//           ---- AND
//           0000  → zero → condition is FALSE
Enter fullscreen mode Exit fullscreen mode

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:

  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
    // ...
)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Clearing a flag:

cfg.Dump &^= fs.DumpHeaders  // AND NOT
Enter fullscreen mode Exit fullscreen mode

Toggling a flag:

cfg.Dump ^= fs.DumpHeaders  // XOR
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Or mixing flag types accidentally:

type LogFlags int
const Verbose LogFlags = 1 << iota

// This compiles but is semantically wrong:
dump := fs.DumpHeaders | int(Verbose)
Enter fullscreen mode Exit fullscreen mode

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)
}
Enter fullscreen mode Exit fullscreen mode

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)
}
Enter fullscreen mode Exit fullscreen mode

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)
}
Enter fullscreen mode Exit fullscreen mode

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)