Default Types of Untyped Constants
How Go assigns a default type to untyped constants when no specific type is given and how that affects variable declarations and assignments
An untyped constant in Go is a value that has not yet been given a fixed numeric or string type. It exists in a flexible space where it can be used with any type that can represent it. But when the compiler reaches a place where a concrete type must exist — a short variable declaration, an argument passed to interface{}, a map key — the constant needs a type. Go supplies one automatically from a small, predictable set of default types.
This mechanism is the reason you can write x := 5 and get an int, or pass 3.14 to fmt.Println without a cast. It also creates a handful of subtle traps when the default type does not match what you intended.
How Untyped Constants Work Without a Fixed Type
Before looking at default types, it helps to recall what an untyped constant actually is. In Go, every literal value — numbers like 42, 3.14, 'A', true, "hello" — starts as an untyped constant. Named constants declared without an explicit type also remain untyped:
const a = 100 // untyped integer constant
const b = 2.718 // untyped floating-point constant
const c = "gopher" // untyped string constant
These constants are not int, float64, or string. They are values that can be used wherever a type-compatible target is expected. That is why you can assign a to an int64, a float64, or even a custom type based on int — as long as the constant’s value fits.
type Count int
var n Count = a // works: untyped 100 fits in Count
If a were a typed int, this assignment would fail with a type mismatch. The absence of a type gives untyped constants their power.
But the compiler cannot keep values floating in a type-less void forever. At some point, a variable must have a concrete type, an interface value must store a concrete dynamic type, or a function signature demands one. That is where the default type steps in.
The Default Type for Each Constant Kind
Every untyped constant belongs to one of six kinds: boolean, integer, rune, floating-point, complex, or string. Each kind has a well-defined default type that Go uses when no other type is implied by context.
| Constant Kind | Example Literal | Default Type |
|---|---|---|
| Boolean | true | bool |
| Integer | 42 | int |
| Rune | 'A' | rune (int32) |
| Floating-point | 3.14 | float64 |
| Complex | 1 + 2i | complex128 |
| String | "hello" | string |
A rune literal’s default type is rune, which is an alias for int32. This means c := 'A' gives c the type rune, not byte or int.
Where the default type table comes from:
The Go language specification defines these default types explicitly. They are not inferred dynamically at compile time — they are a fixed part of the language. The consistency across all Go programs means you can rely on 3.14 always defaulting to float64, never float32.
When the Default Type Is Used
The default type is applied in situations where the compiler must pick a type but has no other hint. Three common scenarios make this visible:
1. Short variable declarations with :=
i := 100 // i is int
f := 2.5 // f is float64
s := "text" // s is string
No type annotation exists, so the untyped constant on the right dictates the type via its default.
2. var declarations without an explicit type
var x = 42 // x is int
var y = 3.14 // y is float64
The same logic as := — the constant provides the type for the new variable.
3. Passing constants to interface{} parameters
Functions like fmt.Println accept interface{} arguments. When you pass an untyped constant, Go packages it into an interface value, and the concrete type stored is the constant’s default type.
func inspect(v interface{}) {
fmt.Printf("value: %v, type: %T\n", v, v)
}
func main() {
inspect(42) // type: int
inspect(3.14) // type: float64
inspect('G') // type: int32 (rune)
inspect("gopher") // type: string
inspect(true) // type: bool
}
The output confirms that even though 42 could fit in many integer types, the interface holds int.
There is another context where a type is required — assigning to a typed variable. But in that case, the target type is already known, so the constant is implicitly converted to that type rather than using the default. The default type only appears when no target type exists.
Default Types Are Not Implicit Conversions
A frequent point of confusion: the default type is not a conversion rule. It is a fallback. When you write:
var seconds int64 = 60
The untyped constant 60 does not become an int first and then get converted. The compiler sees that seconds requires int64, checks that 60 is representable in int64, and directly assigns it. No default type is involved.
The default type would only appear if you wrote:
s := 60
Now there is no target type, so s becomes int. The distinction is critical because it explains why untyped constants feel so fluid in one place and suddenly produce a type mismatch in another.
Default type can surprise you when type inference meets later assignments:
A variable declared with := using an untyped integer becomes int. That int cannot be assigned to an int64 variable later without an explicit conversion, even though the original constant could have been assigned directly.
How This Differs from Typed Constants
A typed constant already has a fixed type and behaves like any other typed value. It cannot be assigned to a differently typed variable without a conversion, and it never triggers a default type fallback.
const untyped = 10 // untyped integer constant
const typedInt int = 10 // typed int constant
type Count int
var a Count = untyped // OK: untyped 10 fits in Count
// var b Count = typedInt // compile error: cannot use typedInt (type int) as type Count
The typed constant typedInt is int, not Count. The untyped constant untyped has no type, so it can be used anywhere the value 10 makes sense — Count, int64, float64, and so on.
The default type only becomes relevant if you ask Go to infer a type for an untyped constant. For typedInt, the type is already there; no inference happens.
Real-World Consequences and Common Mistakes
Understanding default types prevents two classes of bugs that catch even experienced developers.
Mistake 1: Assuming := gives the specific integer width you want
package main
import "fmt"
func main() {
value := 42 // value is int
var bigVal int64 = 100
// bigVal = value // error: cannot use value (type int) as type int64
bigVal = int64(value) // explicit conversion required
fmt.Println(bigVal)
}
Many developers assume that because their system is 64-bit, value will be int64. It won’t — int and int64 are distinct types in Go, even when they have the same underlying size. The constant 42 could have been assigned to bigVal directly, but once it is captured in a variable with :=, the type is locked.
Unnecessary type conversions that hide bugs:
Using int64(value) works, but if value ever exceeds the range of int64, silent truncation or wrapping may occur. Prefer explicit types from the start when the width matters: var value int64 = 42.
Mistake 2: Floating-point defaults to float64, not float32
ratio := 0.5 // ratio is float64
var half float32 = 0.5 // OK: 0.5 is untyped and converts to float32
// half = ratio // error: cannot use ratio (type float64) as type float32
If you are working with a library that expects float32 (graphics, some machine learning libraries), the default float64 inference will force you to add conversions everywhere. The habit of typing variables explicitly — var ratio float32 = 0.5 — eliminates the churn.
Mistake 3: Rune literals become rune, not byte
ch := 'A' // ch is rune (int32)
var b byte = 'B' // OK: untyped rune constant converts to byte
// b = ch // error: cannot use ch (type rune) as type byte
When processing ASCII-only text, you might expect ch to be a byte. It isn’t. The default type for a rune literal is rune. Explicit typing, var ch byte = 'A', avoids surprises when passing the value to functions that expect byte.
Let untyped constants work for you:
When you declare package-level configuration constants, keep them untyped:
const DefaultPort = 8080
const DefaultTimeout = 30
This allows callers to use them with any compatible type — int, int64, time.Duration — without casts. The default type only kicks in if someone writes port := DefaultPort without further context. Most of the time, the untyped nature gives you free flexibility.
The Mental Model
Think of untyped constants as numbers (or strings, or booleans) scribbled on a whiteboard. They have a value, but until you assign them to a labelled box (a typed variable), they don’t carry a type label. The default type is the label Go puts on the box when you point to the whiteboard and say “put that in a new box” without specifying which label to use. If you already have a labelled box, Go just checks whether the value fits and puts it there directly — no default label needed.
This model explains why var x int64 = 42 works without the default type ever appearing: the box is already labelled int64, and 42 fits.
Summary
Default types for untyped constants are a fallback mechanism that keeps Go’s type inference predictable. They make short variable declarations possible and give interface conversions a concrete type to store. The key takeaways:
- Every untyped constant has a default type based solely on its kind —
intfor integers,float64for floating-point,stringfor strings, and so on. - The default type is only used when the compiler must infer a type from nothing. Assignments to typed variables bypass it entirely through implicit conversion.
- Typed constants do not have a default type — they already possess a specific type and follow the normal type-compatibility rules.
- The most common trap is using
:=with numeric constants and ending up withintorfloat64when you needed a different width, creating conversion friction downstream.
When you understand when and why the default type is applied, you can intentionally keep constants untyped for maximum flexibility, and type variables explicitly where precision or interface compatibility demands it.