Skip to main content

Constants and Enumerations in Go

Published: May 8, 2026 Updated: August 29, 2026 Larry Qu 7 min read

Constants in Go serve two distinct purposes: preventing magic numbers scattered through code, and creating enumeration types that the compiler can enforce. The iota keyword makes the second use case particularly elegant — you get auto-incrementing integer values, bit flag patterns, and byte-size constants without maintaining the numbers manually.

Understanding the difference between typed and untyped constants is also important — it affects what assignments are legal and how the type system protects you.

Declaring Constants

Constants are declared with const, either individually or in a grouped block. Unlike variables, they are evaluated at compile time and can appear in any expression:

const Pi = 3.14159
const MaxRetries = 5
const ServiceName = "order-service"

// Grouped declaration — common for related constants
const (
    ReadTimeout  = 15 * time.Second
    WriteTimeout = 15 * time.Second
    IdleTimeout  = 60 * time.Second
)

// Computed from other constants — resolved at compile time
const (
    KB = 1024
    MB = 1024 * KB
    GB = 1024 * MB
)

Constants can be any basic type: booleans, integers, floats, complex numbers, or strings. They cannot be slices, maps, or structs.

Typed vs Untyped Constants

This distinction is one of Go’s subtleties. An untyped constant has a “kind” but no fixed type — it’s compatible with any type of the same kind within range:

const untypedX = 42       // untyped integer constant
var a int     = untypedX  // OK
var b int64   = untypedX  // OK — untyped constant converts freely
var c float64 = untypedX  // OK

const typedY int = 42
var d int     = typedY    // OK
var e int64   = typedY    // ❌ compile error: cannot use typedY (int) as int64

Untyped constants are more flexible — they let the compiler assign the most appropriate type at each use site. Typed constants are stricter and participate in type checking exactly like variables.

For enumerations, you almost always want typed constants. Typing prevents mixing incompatible enum values:

type Direction int
type Color int

const (
    North Direction = iota
    South; East; West
)
const (
    Red Color = iota
    Green; Blue
)

var d Direction = North
d = Red  // ❌ compile error: cannot use Red (Color) as Direction

Without the custom types, North and Red would both be plain int and the assignment would silently compile.

iota: Automatic Enumeration

iota is a counter that starts at 0 and increments by 1 for each constant in a const block. It resets to 0 at the start of each new const block:

type Weekday int

const (
    Sunday Weekday = iota  // 0
    Monday                 // 1
    Tuesday                // 2
    Wednesday              // 3
    Thursday               // 4
    Friday                 // 5
    Saturday               // 6
)

The value iota appears in only needs to be stated once — subsequent lines in the block repeat the same expression with the incremented iota. This is why Monday through Saturday have no explicit expression.

Skipping Values with _

Use the blank identifier to skip a value in the sequence:

type LogLevel int

const (
    _        LogLevel = iota // skip 0 — "unset" values won't accidentally match
    Debug                    // 1
    Info                     // 2
    Warning                  // 3
    Error                    // 4
    Fatal                    // 5
)

Skipping 0 is a useful pattern: if someone declares a LogLevel variable without initializing it, the zero value won’t silently mean Debug — it won’t match any named level.

iota in Expressions: Bit Flags

iota becomes powerful in expressions. The bit-shift pattern is idiomatic for permission flags:

type Permission uint

const (
    Read    Permission = 1 << iota  // 1 << 0 = 1 (binary: 001)
    Write                           // 1 << 1 = 2 (binary: 010)
    Execute                         // 1 << 2 = 4 (binary: 100)
    Admin                           // 1 << 3 = 8 (binary: 1000)
)

// Combine permissions with bitwise OR
userPerms := Read | Write  // 3 (binary: 011)

// Check a permission with bitwise AND
if userPerms&Read != 0 {
    fmt.Println("can read")
}
if userPerms&Execute == 0 {
    fmt.Println("cannot execute")
}

// Grant a permission
userPerms |= Execute  // add Execute

// Revoke a permission
userPerms &^= Write   // remove Write (&^ is bit-clear / AND NOT)

This pattern packs multiple boolean flags into a single integer, which is efficient for storage and comparison. The os package uses exactly this for file mode bits (os.FileMode).

Byte-Size Constants

Another common iota pattern:

const (
    _  = iota             // skip 0
    KB = 1 << (10 * iota) // 1 << 10 = 1024
    MB                    // 1 << 20 = 1,048,576
    GB                    // 1 << 30 = 1,073,741,824
    TB                    // 1 << 40 = 1,099,511,627,776
)

Adding String() to Enumerations

By default, printing a typed integer enum shows the raw number. Implement fmt.Stringer to get readable output — and this also improves log messages, error strings, and debugging:

type Status int

const (
    StatusPending Status = iota
    StatusActive
    StatusCompleted
    StatusCancelled
)

func (s Status) String() string {
    switch s {
    case StatusPending:   return "pending"
    case StatusActive:    return "active"
    case StatusCompleted: return "completed"
    case StatusCancelled: return "cancelled"
    default:              return fmt.Sprintf("Status(%d)", int(s))
}

fmt.Println(StatusActive)  // active — fmt calls String() automatically

The default case that returns Status(N) is important: if someone adds a new constant to the enum but forgets to update String(), you get a clear indication like Status(5) rather than a silent zero value or panic.

For large enums, go generate with stringer tool generates this automatically:

go install golang.org/x/tools/cmd/stringer@latest
//go:generate stringer -type=Status
type Status int

Running go generate creates a status_string.go file with the full String() method.

JSON Marshaling for Enums

By default, JSON encoding writes the integer value. Implement MarshalJSON/UnmarshalJSON to serialize as strings:

func (s Status) MarshalJSON() ([]byte, error) {
    return json.Marshal(s.String())
}

func (s *Status) UnmarshalJSON(b []byte) error {
    var str string
    if err := json.Unmarshal(b, &str); err != nil {
        return err
    }
    switch str {
    case "pending":   *s = StatusPending
    case "active":    *s = StatusActive
    case "completed": *s = StatusCompleted
    case "cancelled": *s = StatusCancelled
    default:
        return fmt.Errorf("unknown status %q", str)
    }
    return nil
}

This way, your API sends "status": "active" instead of "status": 1 — far more readable and stable across code changes.

Sentinel Error Values

Constants also appear as sentinel error values — named errors at package scope that callers check with errors.Is:

var (
    ErrNotFound   = errors.New("not found")
    ErrPermission = errors.New("permission denied")
    ErrTimeout    = errors.New("operation timed out")
)

func findUser(id string) (*User, error) {
    if id == "" {
        return nil, ErrNotFound
    }
    // ...
}

// Caller can check without parsing strings
if errors.Is(err, ErrNotFound) {
    http.Error(w, "not found", 404)
}

Note these are var declarations, not consterrors.New returns a pointer, and pointer constants aren’t allowed in Go. The package-level var achieves the same sentinel purpose.

What Not to Do

Don’t use int directly for enum parameters. Without a named type, the compiler can’t prevent passing the wrong enum to a function:

// ❌ Any int can be passed — no type safety
func setDirection(d int) { ... }
setDirection(42) // compiles, likely wrong

// ✅ Only Direction values can be passed
func setDirection(d Direction) { ... }
setDirection(42)     // compile error
setDirection(North)  // correct

Don’t hardcode magic numbers. Repeated integers with no name are maintenance hazards:

// ❌ What does 3 mean?
if user.Status == 3 { ... }

// ✅ Self-documenting
if user.Status == StatusCompleted { ... }

Summary

  • Untyped constants convert freely to compatible types; typed constants enforce exact types — use typed enums for safety
  • iota starts at 0 per const block, increments by 1 per line; skip 0 with _ to avoid zero-value confusion
  • Bit flags with 1 << iota pack multiple boolean flags into a single integer efficiently
  • Implement String() string on every enum type — it costs little and pays off in every log message and error string
  • Use //go:generate stringer for large enums rather than hand-writing the switch
  • Sentinel errors are var, not consterrors.New returns a pointer

Resources

Comments

👍 Was this article helpful?