Bitwise in Go: Doing More with Less

A Go gopher wearing headphones points at a grid of buttons and tiles showing bits set to 0 and 1.
AI-generated illustration (Gemini). The Go gopher was designed by Renée French and is licensed under CC BY 4.0.

Hey there! Did you know you can stash a ton of boolean values, states, and features inside just one integer? By giving each property its own bit, you can save space and simplify your code. Let's look at how to pull this off in Go, and compare it against the usual way of cramming booleans and strings into a struct.

Where You've Seen This Before: File Permissions

You've probably seen this concept in action before. When you run ls -l in your terminal, you get something that looks like this:

-rwxr-xr-x  1 user  staff  4096 Sep  9 10:12 build

Those ten characters at the start actually represent a single integer! The owner, the group, and everyone else each get three bits: read, write, and execute. rwxr-xr-x translates to 111 101 101 in binary. In octal, that's 755—yep, the exact same number you type when you run chmod 755.

Opening a file in Go uses this exact same trick:

os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o644)

See those pipe characters (|)? They combine three separate flags into one argument. Under the hood, Go uses an AND operation (flags & os.O_CREATE != 0) to check if a specific flag is set.

The golden rule: Combine things with OR (|), test them with AND (&).

Go gives you os.FileMode for this, which is just a uint32 under the hood. Its methods use these exact bitwise operations. It's a simple, elegant pattern: use an integer, name a constant for each bit, OR them to combine, and AND them to test. Let's apply this to our own Go types!

The Standard Way

Let's pretend we're building a user account system. A user has a role, a subscription plan, some basic flags (like if they are verified), a list of active features, and an account state. Usually, we'd write a Go struct that looks a bit like this:

const (
    RoleGuest  = "guest"
    RoleMember = "member"
    RoleAdmin  = "admin"

    PlanFree = "free"
    PlanPro  = "pro"

    StateInvited   = "invited"
    StateActive    = "active"
    StateSuspended = "suspended"

    AwaitingEmail   = "email"
    AwaitingReview  = "review"
    AwaitingPayment = "payment"
)

type User struct {
    Role        string
    Plan        string
    IsVerified  bool
    IsTwoFactor bool
    IsBeta      bool
    Features    []string
    State       string
    Awaiting    string
}

Updating this user is pretty standard: you set properties, check them with an if statement, and maybe flip a boolean. But what happens when you have a complex rule? You end up writing a whole function for it:

func canExport(u User) bool {
    return (u.Role == RoleMember || u.Role == RoleAdmin) &&
        u.Plan == PlanPro &&
        slices.Contains(u.Features, "bar")
}

And what about a background worker updating users? You'd loop through, check a bunch of string fields, and update the state. The catch? If two goroutines try to update the same user at once, they'll overwrite each other unless you wrap the whole thing in a clunky mutex lock.

This works, but it's a bit messy. You're juggling three different field types (strings, bools, slices), writing custom functions for every business rule, risking runtime crashes from typos (like typing "Admin" instead of "admin"), and dealing with locking overhead.

The Cool Way

Now, let's look at the magic of doing this all with one integer. We can define a UserFlag type as a uint32 and use iota to set up our bits:

type UserFlag uint32

const (
    // Role.
    UserGuest UserFlag = 1 << iota
    UserMember
    UserAdmin

    // Plan.
    UserFree
    UserPro
    UserEnterprise

    // Account.
    UserVerified
    UserTwoFactor
    UserBeta
    UserNewsletter

    // Features.
    UserFeatureFoo
    UserFeatureBar
    UserFeatureBaz
    UserFeatureQux

    // State.
    UserInvited
    UserActive
    UserSuspended
    UserDeleted

    // Waiting for something.
    UserAwaitingEmail
    UserAwaitingReview
    UserAwaitingPayment
    // We still have eleven bits free!
)

iota is Go's handy automatic counter. When we do 1 << iota, we're taking the number 1 and shifting it to the left by that count. This gives us powers of two: 1, 2, 4, 8, 16, and so on. This ensures every single constant gets its very own, unique bit.

By creating a custom UserFlag type, we also get strict type safety. The compiler will yell at you if you try to pass a random integer or misspell UserAdmin.

How do we manipulate these? You just need three operators:

Checking multiple flags is a breeze, too. Want to see if a user has all required permissions? Compare the AND result to the required set. Just want to see if they have any of them? Check if the AND result isn't zero:

want := UserMember | UserPro
isMemberPro := flags&want == want // Do they have both member AND pro?

oneOf := UserFeatureBar | UserFeatureBaz
isBarOrBaz := flags&oneOf != 0 // Do they have bar OR baz?

Suddenly, business rules aren't messy, sprawling functions anymore. They're just values stored in a simple map!

var permissions = map[string]UserFlag{
    "export":  UserMember | UserPro | UserFeatureBar,
    "delete":  UserAdmin,
    "publish": UserBeta | UserFeatureFoo,
}

func allowed(flags UserFlag, action string) bool {
    want := permissions[action]
    return flags&want == want
}

Your background worker loop becomes a single & check, and updating the user is a neat one-liner that clears the old state and sets the new one—without accidentally changing their role or plan:

wants := UserAdmin | UserPro | UserAwaitingReview
for _, user := range users {
    if user.Flags&wants == wants {
        review(user)
    }
}

// Clear the review flag and set the payment flag
user.Flags = user.Flags&^UserAwaitingReview | UserAwaitingPayment

The Best Parts

And that's it! By letting bits do the heavy lifting, your code stays fast, strictly typed, and completely lock-free.