Back to Blog

The Go const Keyword: Untyped Constants, iota and Enums

How Go's const keyword works: grouped declarations, what can be a constant, typed vs untyped constants, iota, enums with String methods and stringer, and const vs var.

The Go const Keyword: Untyped Constants, iota and Enums

The const keyword declares a name for a value the compiler knows at build time, such as a timeout, a header name or a size limit. A Go constant can only hold a boolean, a number, a string or a rune, and it can never change while the program runs. Most constants are untyped, which means they have no fixed type until you use them, so const maxBodyBytes = 1 << 20 works as an int, an int64 or a float64 without a conversion. Inside a const block, the predeclared identifier iota counts up from 0, and that is how Go writes enums (Go spec).

TL;DR

  • const name = value declares one constant. A const ( ... ) block declares several, and most Go code groups related constants that way.
  • Constants can be booleans, numbers, strings or runes. Slices, maps, structs and the result of a function call like time.Now() can't be constants.
  • An untyped constant like 5 or "GET" takes the type its context needs. 5 * time.Second compiles for that reason, while timeout * time.Second with an int variable does not.
  • Untyped constants are exact. The compiler does their arithmetic with arbitrary precision and only complains when a value doesn't fit the type it ends up in.
  • iota starts at 0 in every const block and goes up by one per line. _ skips a value, iota + 1 starts at 1 and 1 << iota builds bit flags.
  • Go has no enum keyword. An enum is a named type, a const block with iota and usually a String() method, which the stringer tool can generate.
  • You can't take the address of a constant or assign to one. Values computed at runtime, and lookup tables like maps, belong in a var.

How do you declare a constant in Go?

Write const, a name and a value. For several related constants, use a grouped block with parentheses:

example.gogo
package main

import (
	"fmt"
	"net/http"
	"time"
)

const maxBodyBytes = 1 << 20 // 1 MiB

const (
	readTimeout  = 5 * time.Second
	writeTimeout = 10 * time.Second
	idleTimeout  = 2 * time.Minute
)

const (
	headerRequestID  = "X-Request-ID"
	headerRetryAfter = "Retry-After"
)

func main() {
	srv := &http.Server{
		Addr:         ":8080",
		ReadTimeout:  readTimeout,
		WriteTimeout: writeTimeout,
		IdleTimeout:  idleTimeout,
	}
	fmt.Println(srv.ReadTimeout, srv.WriteTimeout, srv.IdleTimeout)
}

This prints 5s 10s 2m0s. For plain values like these, the grouping is only for readers. Each name in the block is an independent constant, and the block keeps the server's timeouts in one place where a reviewer can compare them.

Constants follow the same scope and visibility rules as variables. A constant declared at package level is visible in every file of the package, and it is exported if its name starts with a capital letter, as in http.StatusNotFound or time.RFC3339. The Go package keyword covers how exported names work across packages. A constant declared inside a function only exists in that function. Go style uses mixed caps for constants like any other name, so the idiomatic spelling is maxBodyBytes or MaxBodyBytes, not MAX_BODY_BYTES.

A const block has one rule that a var block doesn't. When a line has a name but no type and no value, it repeats the type and expression of the line before it. On its own that only gives several names the same value, which is rarely what you want. Combined with iota, it lets one expression number a whole list of constants.

What values can be constants in Go?

Only booleans, numbers, strings and runes. The number kinds are integers, floating-point and complex numbers, and a rune is an integer that stands for a Unicode code point. Anything else fails to compile, including a value that is fixed in practice but has to be computed at runtime:

example.gogo
package main

import "time"

const startedAt = time.Now()
example.texttext
./main.go:5:19: time.Now() (value of struct type time.Time) is not constant

Composite values are rejected the same way. const allowedMethods = []string{"GET", "POST"} fails with []string{…} (value of type []string) is not constant. Slices, maps, structs, pointers, channels and functions can't be constants, even when every element is a literal.

Named types built on a number, string or boolean are allowed. time.Duration is defined as an int64, so const pollInterval = 500 * time.Millisecond is a constant of type time.Duration with the value 500ms. The same goes for your own types, like type LogLevel int, which is what enums build on.

A few built-in functions also produce constants when their argument allows it. len of an array is a constant because an array's length is part of its type. len("Retry-After") on a string constant is a constant too. unsafe.Sizeof, unsafe.Alignof and unsafe.Offsetof are constants whenever the argument's type has a fixed size, so const headerSize = unsafe.Sizeof(int64(0)) compiles and has the value 8. len of a slice is not a constant, since a slice's length is only known at runtime.

What is the difference between typed and untyped constants in Go?

A typed constant has a type in its declaration. An untyped constant doesn't, and it takes whatever type the code around it needs:

example.gogo
const maxRetries int32 = 3   // typed: always an int32
const maxBodyBytes = 1 << 20 // untyped: becomes whatever type it is used as

The typed one follows the normal assignment rules, so it can't go into an int without a conversion:

example.gogo
var attempts int = maxRetries
example.texttext
./main.go:6:21: cannot use maxRetries (constant 3 of type int32) as int value in variable declaration

The untyped one fits into var limit int64 = maxBodyBytes, var limit int = maxBodyBytes and float64(maxBodyBytes) alike. The standard library relies on that flexibility and leaves most of its numeric constants untyped, like math.MaxInt64 and math.Pi.

An untyped constant still has a default type. It is used when nothing else decides the type, as in x := 1.5 or when the constant is passed to fmt.Println:

Untyped constantDefault type
true, falsebool
42, 1 << 20int
1.5, 1e9float64
'a'rune (an alias for int32)
2icomplex128
"GET"string

Why does 5 * time.Second compile but timeout * time.Second fails?

Because 5 is an untyped constant and timeout is a variable. Go never converts between numeric types on its own, but an untyped constant can take any numeric type its value fits in. In 5 * time.Second, the 5 becomes a time.Duration, and the result is a time.Duration of five seconds.

A variable already has a type, so the same multiplication fails when the number comes from configuration:

example.gogo
package main

import (
	"net/http"
	"os"
	"strconv"
	"time"
)

func main() {
	timeout, _ := strconv.Atoi(os.Getenv("TIMEOUT_SECONDS"))
	client := &http.Client{Timeout: timeout * time.Second}
	_ = client
}
example.texttext
./main.go:12:34: invalid operation: timeout * time.Second (mismatched types int and "time".Duration)

timeout is an int, and time.Second is a time.Duration. The fix is an explicit conversion, time.Duration(timeout) * time.Second. An untyped constant with the same value works without one, so const retryDelay = 3 followed by retryDelay * time.Second gives 3s.

How precise are untyped constants?

Untyped numeric constants are exact. The spec requires compilers to represent integer constants with at least 256 bits and to keep floating-point constants accurate to a mantissa of at least 256 bits, and the standard Go compiler allows up to 512 bits. math.Pi is written out with more digits than a float64 can hold, and it is only rounded when it lands in a float64 or float32 variable.

That precision lets you write intermediate values that are too big for any Go type, as long as the final result fits. const maxOffset = 1 << 100 compiles, and maxOffset >> 98 is the constant 4. The error only comes when the value has to become a concrete type:

example.gogo
package main

import "fmt"

const maxOffset = 1 << 100

func main() {
	fmt.Println(maxOffset)
}
example.texttext
./main.go:8:14: cannot use maxOffset (untyped int constant 1267650600228229401496703205376) as int value in argument to fmt.Println (overflows)

Because the compiler knows every constant value, these range checks happen at build time instead of wrapping around silently at runtime. A few more examples:

CodeCompiler error
var uploadLimit int32 = 1 << 40cannot use 1 << 40 (untyped int constant 1099511627776) as int32 value in variable declaration (overflows)
const maxConns uint8 = 300cannot use 300 (untyped int constant) as uint8 value in constant declaration (overflows)
var workers int = 2.5cannot use 2.5 (untyped float constant) as int value in variable declaration (truncated)

The check looks only at the value. var workers int = 2.0 compiles, because 2.0 is an untyped float constant whose value is a whole number.

What is iota in Go?

iota is a predeclared identifier that only means something inside a const declaration. It is 0 for the first constant spec in a const block and goes up by one with each spec after it. A spec is one line that declares names, so blank lines and comments don't count. Combined with the implicit repetition of the previous expression, a type and one iota are enough to number a whole list:

example.gogo
type LogLevel int

const (
	LevelDebug LogLevel = iota // 0
	LevelInfo                  // 1
	LevelWarn                  // 2
	LevelError                 // 3
)

LevelInfo, LevelWarn and LevelError have no type or value of their own, so each one repeats LogLevel = iota with the next value of iota. Every constant in the list has the type LogLevel.

How iota counts:

  1. iota resets to 0 at the start of every const block. Two separate blocks both start at 0.
  2. It counts specs, not names. Two constants declared on the same line see the same iota.
  3. A line that uses _ still counts, so the blank identifier skips a value.
  4. iota is an untyped integer constant, so it works in any constant expression, like iota + 1 or 1 << iota.

How do you start iota at 1 or skip a value?

Add one to it, or throw the first value away with _. Starting at 1 keeps the zero value free to mean "not set":

example.gogo
type Priority int

const (
	PriorityLow    Priority = iota + 1 // 1
	PriorityNormal                     // 2
	PriorityHigh                       // 3
)

Skipping with _ fits cases where the first value is meaningless. The size units below skip iota == 0, since 1 << 0 would be 1 byte:

example.gogo
type ByteSize int64

const (
	_           = iota // skip 0
	KB ByteSize = 1 << (10 * iota)
	MB
	GB
)

KB is 1 << 10, which is 1024, MB is 1 << 20 and GB is 1 << 30. Each line repeats ByteSize = 1 << (10 * iota) with a larger iota.

How do you define bit flags with iota?

Shift 1 left by iota. Each constant then gets its own bit, and you combine them with |:

example.gogo
package main

import "fmt"

type Permission uint8

const (
	PermRead   Permission = 1 << iota // 1
	PermWrite                         // 2
	PermDelete                        // 4
)

func main() {
	perm := PermRead | PermWrite
	fmt.Println(perm&PermWrite != 0)  // true
	fmt.Println(perm&PermDelete != 0) // false
}

The standard library uses the same pattern. log.Ldate, log.Ltime and log.Lshortfile are 1 << iota flags that you combine in log.SetFlags(log.LstdFlags | log.Lshortfile).

Does Go have enums?

Not as a separate language feature. There is no enum keyword. Go builds enums from three parts it already has: a named type, a const block with iota and a String() method so the values print as names instead of numbers:

example.gogo
package main

import "fmt"

type LogLevel int

const (
	LevelDebug LogLevel = iota
	LevelInfo
	LevelWarn
	LevelError
)

func (l LogLevel) String() string {
	switch l {
	case LevelDebug:
		return "debug"
	case LevelInfo:
		return "info"
	case LevelWarn:
		return "warn"
	case LevelError:
		return "error"
	default:
		return fmt.Sprintf("LogLevel(%d)", int(l))
	}
}

func main() {
	fmt.Println(LevelWarn, LogLevel(9))
	fmt.Printf("level=%v\n", LevelError)
}

This prints warn LogLevel(9) and then level=error. fmt calls String() when it prints a value with Println, %v or %s. The default case turns a value outside the list, like LogLevel(9), into something readable.

How do you generate String methods with stringer?

Writing that switch by hand gets tedious, and it goes stale when someone adds a constant and forgets the method. The stringer tool from the Go team generates it. Add a go:generate directive next to the type and run go generate:

example.gogo
package main

import "fmt"

//go:generate go run golang.org/x/tools/cmd/stringer@latest -type=OrderStatus -trimprefix=OrderStatus

type OrderStatus int

const (
	OrderStatusUnknown OrderStatus = iota
	OrderStatusPending
	OrderStatusPaid
	OrderStatusShipped
	OrderStatusRefunded
)

func main() {
	fmt.Println(OrderStatusPaid, OrderStatus(42))
}

go generate writes orderstatus_string.go with a String() method, and the program prints Paid OrderStatus(42). -trimprefix drops the shared prefix from the names. The generated file also contains a small compile-time check. When an existing constant's value changes and the file hasn't been regenerated, the build fails with an error like invalid argument: index 4 out of bounds [0:1]. A constant added at the end of the list is not caught, and it prints as OrderStatus(5) until you run go generate again, so many teams run it in CI and fail the build when the generated files change.

Why should an enum's zero value be Unknown?

Because every Go variable starts at its zero value, and for an int-based enum that is 0. An Order struct decoded from JSON without a status field, or declared with var o Order, has status 0. If 0 is OrderStatusPending, a missing value looks like a real pending order. Putting an explicit OrderStatusUnknown first, or starting at iota + 1, makes the zero value mean "not set" so the code can reject it.

Does Go check that a switch covers every enum value?

No. An enum type is still an ordinary integer type, so the compiler doesn't know which values are valid. var level LogLevel = 9 compiles, since 9 is an untyped constant that fits in an int. A switch that handles only some of the values also compiles without a warning.

Teams that want that check add the exhaustive linter, which golangci-lint includes. It reports a switch on an enum type that misses one of the type's constants. A default case, like the one in the String() method above, handles values outside the list at runtime.

When should you use const vs var in Go?

Use const for values that are known when you write the code and never change: timeouts, limits, header names, enum values. Use var for anything computed at runtime, read from configuration or built from a composite type.

You can't take the address of a constant, since a constant isn't stored in a variable:

example.gogo
p := &maxBodyBytes
example.texttext
./main.go:6:8: invalid operation: cannot take address of maxBodyBytes (untyped int constant 1048576)

This comes up with APIs that take pointers to optional fields. Since Go 1.26, new accepts an expression, so new(maxBodyBytes) returns an *int pointing to a copy of the value. On older versions, copy the constant into a variable first and take its address.

You can't assign to a constant either. maxBodyBytes = 2 << 20 fails with cannot assign to maxBodyBytes (neither addressable nor a map index expression).

Unused constants compile. An unused local variable is an error, declared and not used: retries, but an unused constant, even inside a function, is accepted. Linters like staticcheck still report unused unexported constants at package level.

Lookup tables are variables. A map from status to label can't be a constant, so declare it with var at package level, or return it from a function when you don't want other code to modify it:

example.gogo
var orderStatusLabels = map[OrderStatus]string{
	OrderStatusPending:  "Awaiting payment",
	OrderStatusPaid:     "Paid",
	OrderStatusShipped:  "On the way",
	OrderStatusRefunded: "Refunded",
}

A package-level var can be changed by any code in the package, which is the trade-off for being able to hold any type. When the values are fixed and simple, a constant gives you compile-time checks and no risk of changes at runtime. The Go var keyword covers variables, zero values and := in depth.

Where LevelUpGo fits

LevelUpGo teaches Go through exercises that run real Go code in the browser. Go Basics introduces const alongside variables and types. Go Language Deep Dives has a lesson on iota and const patterns where you build an enum, bit flags and byte size units. The Training Ground has short standalone exercises for practicing outside a course. For the other 24 reserved words, see Go keywords: all 25 explained.

FAQ

Is const a keyword in Go?

Yes. const is one of Go's 25 reserved keywords, so you can't use it as a variable, function or type name. It starts a constant declaration, either a single const name = value or a grouped const ( ... ) block.

Can a slice or map be a constant in Go?

No. Constants can only be booleans, numbers, strings and runes, or named types built on them. const allowedMethods = []string{"GET", "POST"} fails with is not constant. Use a package-level var for slices and maps, or a function that returns a fresh copy when callers must not modify the shared one.

What is iota in Go?

iota is a predeclared identifier that numbers the lines of a const block. It is 0 on the first line, goes up by one on each following line and resets in every new block. With a named type and implicit repetition, it gives each constant in a list its own value, which is how Go writes enums and bit flags.

What is the difference between typed and untyped constants in Go?

A typed constant, like const maxRetries int32 = 3, has a fixed type and follows the normal assignment rules. An untyped constant, like const maxRetries = 3, has no type until it is used, so it can become an int, an int64, a float64 or a time.Duration. Untyped constants also keep exact, arbitrary-precision values until they are converted.

Does Go have enums?

Go has no enum keyword, but it has enums in practice. You declare a named type such as type OrderStatus int, list its values in a const block with iota and add a String() method, often generated by stringer. The compiler doesn't restrict the type to the listed values, so validate input and use the exhaustive linter if you need every switch to cover every value.

Sources

Write Go like a senior engineer

Interactive lessons in your browser. The first ones are free.

Try a free lessonOr create a free account