Integer Literals
How to write integer constants in Go using decimal, binary, octal, and hexadecimal forms, digit separators for readability, and how untyped integer literals interact with Go's type system.
An integer literal is a sequence of digits that represents a fixed integer value right in your source code. Go supports four ways to write integer literals — decimal, binary, octal, and hexadecimal — and lets you insert underscores to make long numbers easier to scan. Every integer literal is an untyped constant, which means it doesn’t have a fixed type like int64 or uint32 when you write it; the compiler decides the final type based on how you use the literal.
Integer Literal Forms
You can write an integer literal in any of four bases. The prefix at the start of the literal tells the compiler which base to interpret. All forms accept optional _ digits separators (since Go 1.13), which we’ll cover in the next section.
A decimal literal has no prefix. It may start with a non‑zero digit (1‑9) followed by any number of decimal digits and underscores, or it may be just 0. A literal like 0123 is not a decimal literal — it’s the older octal form, which we’ll explain under Octal.
42
1_200
0
A literal without a prefix is always decimal. The 0 prefix without an x, o, O, b, or B is interpreted as an octal literal. To avoid confusion, prefer the explicit 0o form for octal numbers in new code.
Digit Separators for Readability
Underscores inside an integer literal let you group digits the same way commas separate thousands in prose. The Go compiler strips them out, so they change nothing about the value. This feature was added in Go 1.13.
You can place an underscore between any two successive digits, or immediately after the base prefix (0x, 0b, 0o). A single underscore may not appear at the very beginning or very end of the literal, and you cannot use more than one underscore in a row.
// Grouping by thousands
oneBillion := 1_000_000_000
// Grouping hexadecimal bytes
color := 0xFF_00_AA
// Separator right after the prefix — this is allowed
mask := 0b_1010_1111
Underscores exist only for humans:
The compiler sees 1_000 and 1000 as exactly the same integer. There is no performance cost, no change in binary representation, and no way to “get the underscores back” once the program compiles.
These placements are all compile‑time errors:
_42(leading underscore — treated as an identifier, not a literal)42_(trailing underscore)0_xFF(underscore immediately after the0xprefix? Actually0_xFFis invalid because the underscore follows0x? The spec says: after base prefix or between successive digits.0_xFFputs underscore after0and beforex, not after prefix;0x_FFis allowed. Let's clarify:0x_FFis allowed (underscore after prefix). So invalid:42_,_42,4__2,0_42(which isn't a valid octal;0_42is actually a mistake: the leading zero may be interpreted as octal, but_42is not a digit. We'll list clear errors.)
Compile errors from invalid underscore placement:
_42— leading underscore (the compiler sees an identifier, not an integer).42_— trailing underscore.4__2— more than one underscore in a row.0_42— the underscore comes right after the0but before the octal digits; this is neither a valid decimal nor octal literal.
A common pitfall: 0x_ is not a valid hex literal because you must have at least one hex digit after the prefix. The underscore after the prefix is allowed only when digits follow.
How Integer Literals Interact with Go’s Type System
All integer literals are untyped constants. An untyped constant does not yet have a concrete numeric type like int32 or uint64 — it exists at compile time as an arbitrary‑precision integer that can be as large as the Go spec allows. This design solves two problems at once: you don’t have to write a type suffix on every number, and the compiler can catch overflow errors before the program ever runs.
When you use an integer literal in a place that expects a specific type — a variable declaration, a function argument, a return statement, or a binary expression with a typed operand — the compiler assigns the literal that type, if the literal’s value fits. If the value is too large to fit in the target type, compilation fails.
// Implicit type assignment from context
var counter int = 10 // literal 10 becomes int
var small uint8 = 200 // 200 fits in uint8, okay
var overflow uint8 = 300 // compile error: 300 overflows uint8
// Used in an expression with a typed variable
var x int64 = 5
y := 10 * x // 10 takes int64 type because x is int64
// Return statement determines the type
func total() uint64 {
return (1 << 64) - 1 // 1 becomes uint64; shift and subtract happen at compile time
}
Compile‑time overflow detection:
Unlike some languages that silently wrap, Go refuses to compile an integer literal that overflows the target type. You’ll see an error like “constant 300 overflows uint8”. This gives you absolute certainty that a literal assignment is safe.
If an integer literal appears somewhere that doesn’t force a specific type — for example, as a short‑variable‑declaration on its own — it takes its default type, which is int.
n := 42 // n is of type int
m := 1 << 62 // m is also int, and must fit in whatever int size the target platform uses
The default type being int means that on a 32‑bit platform, 1 << 62 will not compile because int is only 32 bits there. The same literal 1 << 62 works on a 64‑bit platform. That’s a deliberate trade‑off: untyped constants provide arbitrary precision only until they are forced into a type.
Arbitrary Precision at Compile Time
Even though the default type is int, the literal itself, before it is assigned a concrete type, can represent numbers far beyond any Go integer type. This enables expressions like (1 << 64) - 1 in a uint64 context. The 1 is first treated as an untyped constant with arbitrary precision, shifted left 64 times, and then the subtraction happens — all at compile time — before the result is checked against uint64.
const huge = 1 << 100 // perfectly valid as an untyped constant expression
// fmt.Println(huge) // compile error if you try to pass it to a function that expects int
This is why you can perform large‑scale bit‑twiddling with literals without intermediate typed variables: the constant arithmetic happens in an arbitrary‑precision “constant space” that is not limited by the CPU’s register size.
Common Mistakes and Misconceptions
Treating a literal with a leading zero as decimal.
A number like 0123 is the octal representation of decimal 83, not 123. To get decimal 123 with leading zeros for alignment, you cannot do that with integer literals; you’d format the number at print time instead.
Thinking the literal’s type is fixed.
A frequent newcomer expectation is that 1 is an int and 1.0 is a float64. In Go, 1 is untyped; it becomes int only when needed. The same literal can morph into int64, uint8, or stay untyped in constant expressions — the compiler picks the type based on usage.
Placing underscores in invalid spots.
Trying to write _42 or 42_ creates an identifier or a syntax error, not a readable number. Group digits between them, never at the edges.
Forgetting that int size is platform‑dependent.
A literal that fits in a 64‑bit int might overflow a 32‑bit int at compile time. If you write a library that deals with values near 2^31, explicitly use int64 or uint64 to keep behavior consistent across platforms.
Shifting beyond the target type width:
1 << 65 as a standalone expression is fine until you try to assign it to a 64‑bit type. The compiler will reject it. But if you never force a concrete type, the constant remains valid. In constant propagation through untyped expressions, this can be surprising.
Summary
Integer literals in Go are more than just numbers you type — they’re untyped, arbitrary‑precision constants that the compiler evaluates and type‑checks before your program runs. You can write them in decimal, binary, octal, or hexadecimal, and use underscores to make large constants readable. Because Go enforces compile‑time overflow checks, you can trust that a literal assignment is always safe for the target type.
What makes integer literals powerful is their type‑free nature: the same 1 can become an int, a uint64, or remain a constant inside a massive expression, all without you having to cast or annotate.