JavaRush /Courses /Go SELF /Enum and flags in Go: practice and reinforcement

Enum and flags in Go: practice and reinforcement

Go SELF
Level 8, Lesson 5
Available

1. Introduction

When a program is small, it feels normal to write if status == 2. But a week goes by (or one call with your team lead), and you no longer remember: “Is 2 done or in progress?”. And if you have twenty such “2” values across the project, you end up playing “guess the meaning by the code author’s face”. Enums and flags are a simple technique that turns numbers into clear names and sharply reduces the chance of mistakes.

Let’s imagine we’re building a mini console program TaskInspector. For now it can do very little: we enter a task status and some “flags” (for example, an important task or one with a deadline), and the program prints a clear description. Today we’re not doing architecture, not doing files, not doing collections—we’re simply learning to write and read code where numbers don’t look like a cipher.

Enum vs flags: these are different models

Enums and flags are similar in that both use constants, and often even iota. But their meaning is fundamentally different:

  • Enum — “choose exactly one option from a set”.
  • Flags — “choose any set of attributes at the same time”.

If you mix these models up, the code may compile and even “sometimes work”, but it will be painful for everyone to read—including you a month later.

A short table to lock in the difference:

Model The question it answers What the check looks like Example
Enum “Which single state is it in right now?” == (equality to a single value) task status: new / in_progress / done
Flags “Which attributes are enabled?” & with a mask and comparison to 0 is the task urgent? has a deadline? blocked?

2. The enum pattern in Go

Your own type + constants + readable checks

When you write type Status int, you create a new type, not a “renamed int”. This is convenient: the compiler won’t let you accidentally compare a task status with something unrelated (for example, number of attempts or the price of a shawarma).

Minimal skeleton of an enum-like status type:

package main

import "fmt"

func main() {
	type Status int

	const (
		StatusNew Status = iota
		StatusInProgress
		StatusDone
	)

	var s Status = StatusDone
	fmt.Println(s == StatusDone) // true
}

Notice: we compare s == StatusDone, not s == 2. The condition reads as “status equals Done”, not “status equals 2 (why 2?)”.

Human-readable output: “number inside, word outside”

An enum almost always stores a number—that’s normal and even useful (compact). But a human needs text. That’s why next to an enum you typically have a small “translator”: logic that converts an enum value to a string.

If you print the status like this: fmt.Println(s), you’ll get 2. Formally correct, but “human-useless”. In learning examples (and in many real ones) the simplest approach is a mapping via switch:

package main

import "fmt"

func main() {
	type Status int

	const (
		StatusNew Status = iota
		StatusInProgress
		StatusDone
	)

	s := StatusInProgress

	text := "unknown"
	switch s {
	case StatusNew:
		text = "new"
	case StatusInProgress:
		text = "in_progress"
	case StatusDone:
		text = "done"
	}

	fmt.Println(text) // in_progress
}

What matters here is that we have a “Plan B”—"unknown". In real programs, data can be messy (especially if it comes from outside), and “unknown” isn’t a sign of weakness; it’s a sign that you don’t take the world at face value (and you’re right not to).

3. Getting to know functions

You’ve already seen that it’s easy to accumulate a lot of logic in main: switch, a bunch of if, building strings… And very quickly main turns into a “bedsheet” that’s hard to make sense of. Functions are a way to move a piece of logic into a separate block with a name.

What a function is in simple words

A function is a “mini-program inside a program”:

  • it has an input (parameters),
  • an output (what it returns),
  • and a body (the code that runs).

For example, we want to turn a status into text. You can write that directly in main, but it’s better to name the rule separately: statusText.

How to read a function declaration

Look at this line:

func statusText(s Status) string
Function signature: input and output

It is read like this:

  • func — declare a function;
  • statusText — the function name;
  • (s Status) — input: one parameter s of type Status;
  • string after the parentheses — output: the function returns a string.

That is: “The function statusText takes a Status and returns a string”.

Return: “return a result and end the function”

The return statement does two things at once:

  1. returns a value to the calling code;
  2. ends function execution (no further code in that function runs).

You’ve already seen return as “exit main early”. In functions the meaning is the same, except now we usually return a result.

For example, in a switch it’s convenient to return a string directly from the needed branch:

func statusText(s Status) string {
	switch s {
	case StatusNew:
		return "new"
	case StatusInProgress:
		return "in_progress"
	case StatusDone:
		return "done"
	default:
		return "unknown"
	}
}

Notice: there is no “shared variable text” that we mutate. Instead, each branch returns the answer immediately. This is one of the most readable styles in Go: less state, fewer chances to mess up.

How a function is called (and where the result goes)

A call looks like “name + parentheses”:

t := statusText(StatusDone)
fmt.Println(t)

We call statusText(...), get a string, and then pass it to fmt.Println. You can also write it more compactly:

fmt.Println(statusText(StatusDone))

Helper functions

Here is what your program will look like once you move the status-to-text conversion into a separate function statusText. Even if you don’t feel confident with functions yet, you can treat this code as a “black box: put in a status—get a string”.

package main

import "fmt"

type Status int

const (
	StatusNew Status = iota
	StatusInProgress
	StatusDone
)

func statusText(s Status) string {
	switch s {
	case StatusNew:
		return "new"
	case StatusInProgress:
		return "in_progress"
	case StatusDone:
		return "done"
	default:
		return "unknown"
	}
}

func main() {
	fmt.Println(statusText(StatusDone)) // done
}

Such “translators” show up constantly: “code → text”, “flag → yes/no”, “type → name”. Their value is that they centralize the rule: if you decide that StatusInProgress should print as "in-progress", you change it in one place.

A small note: comments with expected output are a useful habit for learning and self-checking.

4. The flags pattern in Go

A set of attributes in one number: each flag is a separate bit

For flags, the key idea is: each flag is a separate bit. That’s why flags are almost always defined as 1 << iota, not just iota.

If you define flags as 0, 1, 2, 3, that will be an enum, not flags (and you’ll set yourself up for a logical trap).

Let a task have attributes: Urgent, HasDeadline, Blocked. We’ll introduce a TaskFlags type based on uint and define flags as powers of two:

package main

import "fmt"

type TaskFlags uint

const (
	FlagUrgent TaskFlags = 1 << iota
	FlagHasDeadline
	FlagBlocked
)

func main() {
	flags := FlagUrgent | FlagBlocked
	fmt.Println(flags&FlagHasDeadline != 0) // false
}

Two things are important here:

  • enabling flags is done via | (OR): this means “add an attribute”;
  • checking is done via & (AND) and comparison to zero: this means “does it intersect with the mask?”.

If you try to check a flag like this: flags == FlagUrgent, you break the meaning of flags. Because flags can contain multiple flags, and equality to a single flag will only be true in a special case (when exactly one flag is set).

Helper functions for flags: readability matters more than “I already remember &”

Bitwise operations are honest, but visually “spiky”: flags = flags &^ FlagBlocked looks like an incantation. To make the code read like text, people often create small helper functions: hasFlag, addFlag, removeFlag.

This is not “speeding up the program” (the compiler is fast anyway), it’s speeding up a human reading the code.

Minimal helper for checking:

package main

import "fmt"

type TaskFlags uint

const (
	FlagUrgent TaskFlags = 1 << iota
	FlagHasDeadline
	FlagBlocked
)

func hasFlag(all, f TaskFlags) bool { return all&f != 0 }

func main() {
	flags := FlagUrgent | FlagBlocked
	fmt.Println(hasFlag(flags, FlagBlocked)) // true
}

Now if hasFlag(flags, FlagBlocked) reads fine even for someone who doesn’t keep bitwise operators in their head (for example, you on a Monday morning).

Human-readable output for flags: not “13”, but “urgent|blocked”

When you print flags as a number, you get something like 5 or 13. That’s honest, but it doesn’t explain which attributes are enabled. That’s why next to flags you often have a “string builder”: it converts a set of flags into a clear label.

We won’t do auto-generation or “smart magic”; we’ll assemble a string with simple if statements. Yes, it’s a bit of manual work, but it’s maximally transparent.

package main

import "fmt"

type TaskFlags uint

const (
	FlagUrgent TaskFlags = 1 << iota
	FlagHasDeadline
	FlagBlocked
)

func flagsText(f TaskFlags) string {
	out := ""
	if f&FlagUrgent != 0 {
		out += "urgent|"
	}
	if f&FlagHasDeadline != 0 {
		out += "deadline|"
	}
	if f&FlagBlocked != 0 {
		out += "blocked|"
	}
	if out == "" {
		return "none"
	}
	return out[:len(out)-1] // remove the last '|'
}

func main() {
	fmt.Println(flagsText(FlagUrgent | FlagBlocked)) // urgent|blocked
}

There’s a small detail here that looks a bit “tricky”: out[:len(out)-1]. We cut off the last delimiter "|". In larger programs you might handle this differently, but for a small example this approach demonstrates the idea of “building a string piece by piece” well.

5. Mini app: TaskInspector

Now let’s put everything together into one scenario. The user enters two numbers: statusCode and flagsCode. We convert them into Status and TaskFlags, then print the status text and the decoded flags.

This is not a “real task tracker” yet, but it’s already a useful building block: we learned not to keep meaning in our heads, but to keep it in the code—via constants and helper functions.

package main

import "fmt"

type Status int
type TaskFlags uint

const (
	StatusNew Status = iota
	StatusInProgress
	StatusDone
)

const (
	FlagUrgent TaskFlags = 1 << iota
	FlagHasDeadline
	FlagBlocked
)

func statusText(s Status) string {
	switch s {
	case StatusNew:
		return "new"
	case StatusInProgress:
		return "in_progress"
	case StatusDone:
		return "done"
	default:
		return "unknown"
	}
}

func flagsText(f TaskFlags) string {
	out := ""
	if f&FlagUrgent != 0 {
		out += "urgent "
	}
	if f&FlagHasDeadline != 0 {
		out += "deadline "
	}
	if f&FlagBlocked != 0 {
		out += "blocked "
	}
	if out == "" {
		return "none"
	}
	return out
}

func main() {
	var statusCode int
	var flagsCode uint
	fmt.Scan(&statusCode, &flagsCode)

	s := Status(statusCode)
	f := TaskFlags(flagsCode)

	fmt.Println(statusText(s)) // example: in_progress
	fmt.Println(flagsText(f))  // example: urgent blocked
}

If you run this and enter, for example, 1 5, then 1 will be StatusInProgress, and 5 in binary is 101, meaning FlagUrgent (bit 0) + FlagBlocked (bit 2). And the program will print “in_progress” and “urgent blocked”.

6. Typical mistakes

Mistake #1: mixing up enums and flags and checking flags with ==.
This is one of the most common problems: a developer sees “named constants” and starts comparing everything with equality. But flags are a set, and the check must go through a mask: flags&FlagX != 0. Equality is appropriate only for enums, where the meaning is a single value.

Mistake #2: defining flags as iota instead of 1 << iota.
If you write FlagRead = iota, you get values 0, 1, 2, 3…, and it stops being a bit mask. This “almost works” until you start combining attributes, and then you suddenly discover that “3” does not mean what you thought it meant.

Mistake #3: printing enum/flags “as a number” and being surprised that nobody understands anything.
The computer understands that 2 means done, but the user, the QA engineer, and you yourself a week later need text. For enums, do a switch into a string; for flags, parse the flag set into a label. This is not decoration; it’s practical diagnostics.

Mistake #4: not having a default branch in the enum translator.
Even if “our data is always correct”, reality loves surprises. If you don’t handle an unknown value, you’ll get either an empty string, or weird output, or incorrect logic. default: return "unknown" is a simple safety net.

Mistake #5: writing bitwise logic “straight” across the whole codebase and not using helper functions where they improve readability.
Bitwise expressions are fine by themselves, but when they repeat, the code starts to look like a math exam. Small helper functions like hasFlag and string “translators” don’t add magic—they add meaningful names, which reduces the chance of mistakes.

1
Task
Go SELF, level 8, lesson 5
Locked
Traffic Light Translator
Traffic Light Translator
1
Task
Go SELF, level 8, lesson 5
Locked
Order Finality
Order Finality
1
Task
Go SELF, level 8, lesson 5
Locked
File permissions
File permissions
1
Task
Go SELF, level 8, lesson 5
Locked
Task Inspector
Task Inspector
1
Survey/quiz
Constants and bit operations, 8 level, 5 lesson
Unavailable
Constants and bit operations
Constants, iota, and bit operations
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION