Skip to content
gohacks.dev
Basics

Typed and Untyped Constants in Go and Why It Matters

Untyped constants have exact values and no fixed type until used, which is why 5 works as an int or a float64 and why 1.5 times time.Second fails to compile.

6 min read
Summarize in AI
Flat illustration of an editor window full of code.

Go's constants are an odd corner of the language, and a good one. They are evaluated at compile time with exact arithmetic, they have no size until you give them one, and they adapt to the types around them in a way that variables do not. That is why time.Sleep(5 * time.Second) compiles with no conversion, and why the line right after it, with a variable in the place of 5, does not. This article runs the rules, explains the precision, shows the error messages and says what to do about the common surprises. It follows constants and iota, underlies type conversion, and sits in the Go basics path.

Tested with Go 1.25.6, macOS on Apple Silicon (arm64)

Untyped means no type yet

A constant declared with no type, const a = 5, is untyped. There are six kinds of untyped constant, and each has a default type that applies when something needs a concrete type and nothing else decides:

main.goGo
package main

import (
	"fmt"
	"math"
)

const (
	a = 5
	b = 5.0
	c = 'x'
	d = "s"
	e = 1 + 2i
	f = true
)

func main() {
	fmt.Printf("%T %T %T %T %T %T\n", a, b, c, d, e, f)

	x := 5
	y := 5.0
	var z float32 = 5
	fmt.Printf("%T %T %T\n", x, y, z)

	fmt.Println(math.MaxInt64, math.Pi)
}
Text
int float64 int32 string complex128 bool
int float64 float32
9223372036854775807 3.141592653589793

The first line shows the defaults: int, float64, int32 (the type of a rune), string, complex128 and bool. In the next line x := 5 uses the default, y := 5.0 gets a float64, and var z float32 = 5 makes the same literal 5 a float32 because the declaration asks for it. The same untyped 5 was an int, a float64 and a float32 in three places, and that is the feature. Constants such as math.MaxInt64 and math.Pi in the standard library are untyped, which is why they fit into whatever you assign them to, provided the value fits.

Exact arithmetic at compile time

An untyped constant is not a 64-bit number. The spec requires compilers to represent integer constants with at least 256 bits and floating point constants with a mantissa of at least 256 bits. Arithmetic on them is exact:

main.goGo
package main

import "fmt"

const big = 1 << 100
const third = 1.0 / 3

func main() {
	fmt.Println(big>>98, third*3 == 1)

	x := third
	fmt.Println(x*3 == 1, x*3)

	fmt.Println(1/2, 1/2.0, 7/2, 7/2.0, 7.0/2)

	const huge = 1e400 / 1e399
	fmt.Println(huge)
}
Text
4 true
true 1
0 0.5 3 3.5 3.5
10

1 << 100 is far outside any integer type, and big >> 98 is 4, computed exactly. third * 3 == 1 is true as a constant expression, because the compiler holds one third with enough digits that tripling it gives one. Once the value is in a variable, x := third converts it to a float64, and the comparison happens in binary floating point, where it is also true here by luck of rounding, and many other such comparisons are not, as the floating point article shows. The last line is the clearest sign of exactness: 1e400 does not fit in any float64, whose maximum is about 1.8e308, and dividing it by 1e399 gives exactly 10.

The division line shows the other half of the rule. A division of two integer constants is integer division, so 1/2 is 0 and 7/2 is 3. If either operand is a floating point constant, the whole expression is exact: 1/2.0 is 0.5. It looks like a trivia question, and it is a real bug source in code like ratio := 1 / 3 * x, where 1 / 3 is zero.

When the value does not fit

An untyped constant can be any size, and the compile error appears when you try to put it somewhere too small. The compiler's message includes the value:

main.goGo
package main

import "fmt"

const big = 1 << 100

func main() {
	var x int = big
	var y uint8 = 256
	var f float32 = 1e40
	const typed int8 = 100
	z := typed * 2
	fmt.Println(x, y, f, z)
}
Text
# example.test/lab
./main.go:8:14: cannot use big (untyped int constant 1267650600228229401496703205376) as int value in variable declaration (overflows)
./main.go:9:16: cannot use 256 (untyped int constant) as uint8 value in variable declaration (overflows)
./main.go:10:18: cannot use 1e40 (untyped float constant 1e+40) as float32 value in variable declaration (overflows)
./main.go:12:7: typed * 2 (constant 200 of type int8) overflows int8

Each error says what did not fit and where: cannot use big (untyped int constant 1267650600228229401496703205376) as int value in variable declaration (overflows), the 256 against uint8, 1e40 against float32, and the typed constant. The last is the difference between typed and untyped. const typed int8 = 100 has a type, so the arithmetic typed * 2 is checked against int8 at compile time, and 200 does not fit: typed * 2 (constant 200 of type int8) overflows int8. The same expression with an untyped 100 would be an exact 200, valid until it is stored in an int8. This is the reason to leave constants untyped by default, and to give them a type only when you want the stricter checking, such as in enums built with a named type.

The time.Second trap

The rule that surprises people daily is in code that multiplies a duration by a number. time.Second is a typed constant of type time.Duration, which is an int64. A whole number adapts to it, and a fractional one does not:

main.goGo
package main

import (
	"fmt"
	"time"
)

func main() {
	fmt.Println(time.Second * 2)
	fmt.Println(1.5 * time.Second)
	fmt.Println(time.Second * 1.5)
	fmt.Println(500 * time.Millisecond)
}
Text
# example.test/lab
./main.go:10:14: 1.5 (untyped float constant) truncated to int64
./main.go:11:28: 1.5 (untyped float constant) truncated to int64

The first line is fine and the last would be, but the middle two are rejected with 1.5 (untyped float constant) truncated to int64. The untyped 1.5 tries to become a Duration, which is an integer type, and cannot represent it. The fix is to write the amount in a smaller unit, 1500 * time.Millisecond, or to compute with floats and convert, time.Duration(1.5 * float64(time.Second)). Compare this with multiplying a duration by a variable, which fails for a different reason, the types mismatching.

Why this design

The rule that constants stay untyped until needed removes a great deal of noise that other statically typed languages have. You do not write 5.0f or 5L or (int64)5 to say what a literal is, and x * 2 works whether x is an int, a float32 or a time.Duration. The cost is the occasional message about truncation or overflow, which is the compiler doing the checking you would otherwise have to do by hand. Go's blog post on constants explains the motivation, and the idea is the one in the specification's section on constants: constants are values, not objects with a size, and a type is assigned when a value is used.

Practical rules

Leave constants untyped unless the type documents something. A const MaxRetries = 5 can be compared with an int or a uint8 or used as an array length, and const MaxRetries int = 5 limits it to int. Give a constant a type when you are defining a set of related values with a distinct type, such as type Level int with Debug Level = iota, or when a precise type avoids an ambiguous conversion in a generic call. Use math.MaxInt64 and friends without conversion, and convert to the destination type at the assignment, where the compiler can check the value fits. And when you read an error that mentions an "untyped constant", the fix is to look at what the constant was being put into: it is almost always a type that is too small, or an integer type being given a fraction.

Share

Keep reading

Get new articles by email

We send one email when a new article is published. No spam.

Unsubscribe in one click. No spam, ever.