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.
Asaduzzaman Pavel

About the Author

Asaduzzaman Pavel is a Software Engineer who actually enjoys the friction of a well-architected system. He has over 15 years of experience building high-performance backends and infrastructure that can actually handle the real-world chaos of scale.

Currently looking for new opportunities to build something amazing.