Numeric Types
An in-depth guide to Go's numeric data types covering signed and unsigned integers, floating-point numbers, complex numbers, and type aliases byte and rune
Go gives you precise, fixed-size numeric types that map directly to what the hardware understands. This isn’t an afterthought — it’s a deliberate design choice that lets you control memory layout, avoid accidental precision loss, and catch type mismatches at compile time. In this document we’ll examine every built‑in numeric type, how to choose among them, and the common mistakes that can turn a small type choice into a subtle bug.
Integer Types
Go’s integer types are split into signed and unsigned families, each with a specific bit width. The language also offers two platform‑adaptive types (int and uint) and two named aliases (byte and rune) that clarify intent.
Signed Integers
A signed integer can hold negative numbers, zero, and positive numbers. The range is determined by the number of bits, with one bit used for the sign.
| Type | Size | Range |
|---|---|---|
| int8 | 1 byte | -128 to 127 |
| int16 | 2 bytes | -32,768 to 32,767 |
| int32 | 4 bytes | -2,147,483,648 to 2,147,483,647 |
| int64 | 8 bytes | -9,223,372,036,854,775,808 to 9,223,372,036,854,775,807 |
| int | 4 or 8 bytes (platform) | Same as int32 on 32‑bit, int64 on 64‑bit |
The following program declares a few signed integers and prints their maximum possible values using the math package.
package main
import (
"fmt"
"math"
)
func main() {
var a int8 = 100
var b int16 = 10000
var c int32 = 100000
var d int64 = 1000000000
var e int = 42 // platform-dependent size
fmt.Println("Values:", a, b, c, d, e)
fmt.Printf("Max int8: %d\n", math.MaxInt8)
fmt.Printf("Max int16: %d\n", math.MaxInt16)
fmt.Printf("Max int32: %d\n", math.MaxInt32)
fmt.Printf("Max int64: %d\n", math.MaxInt64)
}
The output confirms the declared values and shows the hard limits. On a 64‑bit system, e will occupy 8 bytes and have the same range as int64. These limits are enforced at compile time when you assign a literal that overflows, but arithmetic overflow at runtime happens silently.
Integer overflow wraps around silently:
If you increment a signed integer past its maximum value, the result wraps to the smallest negative value. No panic, no exception — the value just flips. For example, math.MaxInt8 + 1 is -128. This is a common source of logic errors that can survive testing for a long time.
Unsigned Integers
Unsigned integers store only non‑negative values, doubling the positive range for the same number of bits.
| Type | Size | Range |
|---|---|---|
| uint8 | 1 byte | 0 to 255 |
| uint16 | 2 bytes | 0 to 65,535 |
| uint32 | 4 bytes | 0 to 4,294,967,295 |
| uint64 | 8 bytes | 0 to 18,446,744,073,709,551,615 |
| uint | 4 or 8 bytes (platform) | Same as uint32 on 32‑bit, uint64 on 64‑bit |
| uintptr | large enough to store a pointer | Platform‑dependent; used for low‑level memory manipulation |
You will often see uint8 used when working with raw bytes or network protocols. The following snippet demonstrates a few unsigned variables and reveals their types with %T.
package main
import "fmt"
func main() {
var port uint16 = 8080
var mask uint32 = 0xFF00FF00
var count uint = 100
fmt.Printf("port: %v (%T)\n", port, port)
fmt.Printf("mask: %#x (%T)\n", mask, mask)
fmt.Printf("count: %v (%T)\n", count, count)
}
Using uint for general non‑negative values can backfire:
The empty loop for i := uint(len(slice)-1); i >= 0; i-- never terminates because i can never become negative — when it underflows, it wraps to a huge positive number. Prefer int for counters and length comparisons unless you have a specific reason (like bitwise operations or protocol fields) that demands an unsigned type.
Platform‑Dependent Integers: int and uint
The types int and uint adapt to the word size of the CPU your program is compiled for. On a 64‑bit system they are 64 bits wide; on a 32‑bit system they are 32 bits wide. This is the integer type Go infers when you write a := 42.
You can verify the actual size using unsafe.Sizeof, though this is meant for diagnostics, not production code.
package main
import (
"fmt"
"unsafe"
)
func main() {
var x int = 1
fmt.Printf("int size: %d bytes\n", unsafe.Sizeof(x))
}
Go consistently uses int for lengths and capacities:
The built‑in functions len() and cap() return int, not an unsigned type. This is intentional: it keeps loop logic safe from the underflow trap we described earlier.
byte and rune — Aliases with Meaning
Go does not have a dedicated “character” type. Instead, it uses two aliases that make the code self‑documenting:
- byte is an alias for
uint8. Use it when you are working with raw bytes — reading from a file, handling ASCII text, or building binary protocols. - rune is an alias for
int32. A rune holds a single Unicode code point, which is the correct way to represent a character that might be outside the ASCII range.
package main
import "fmt"
func main() {
var b byte = 'A' // ASCII letter A
var r rune = '🚀' // Unicode rocket emoji
fmt.Printf("byte: %v (type %T)\n", b, b)
fmt.Printf("rune: %v (type %T)\n", r, r)
fmt.Printf("rune literal: %c\n", r)
}
'A' is a rune literal, but because it fits in a byte, Go allows the assignment without an explicit conversion. The rocket emoji requires the full 4 bytes of an int32, so assigning it to a byte would overflow at compile time.
Floating‑Point Types
Floating‑point numbers represent values with a fractional component. Go provides two sizes that follow the IEEE‑754 standard.
| Type | Size | Approximate precision |
|---|---|---|
| float32 | 4 bytes | ~6‑7 decimal digits |
| float64 | 8 bytes | ~15‑17 decimal digits |
The default type for a floating‑point literal is float64. That means a := 3.14 gives you a float64, which is almost always the correct choice — it offers better precision and the performance difference is negligible on modern hardware.
package main
import "fmt"
func main() {
var pi float64 = 3.141592653589793
var tiny float32 = 0.0000000001
fmt.Printf("pi: %.15f (%T)\n", pi, pi)
fmt.Printf("tiny: %.20f (%T)\n", tiny, tiny)
sum := pi + 1.0
fmt.Printf("sum: %f\n", sum)
}
Note the difference in output for tiny. A float32 cannot represent the value with high accuracy, so the printed digits quickly deviate from the literal you wrote. This is not a bug — it’s the nature of finite‑precision floating‑point.
Don’t use float32 for accumulating large sums or financial calculations:
With only about 7 decimal digits of precision, rounding errors add up. If you need exact decimal arithmetic (money, billing), use integers (cents) or a decimal library. Go’s float64 helps, but it is still binary floating‑point.
The standard arithmetic operators (+, -, *, /) work as expected. Division by zero for floating‑point returns +Inf or NaN rather than panicking, which is consistent with IEEE‑754 but can surprise newcomers.
Complex Numbers
Go includes first‑class complex number types, eliminating the need for external libraries in many scientific or signal‑processing scenarios.
| Type | Real / Imaginary parts |
|---|---|
| complex64 | float32 |
| complex128 | float64 |
You can create a complex value with the built‑in complex() function or with a literal using the i suffix.
package main
import "fmt"
func main() {
c1 := complex(3.0, 4.0) // 3 + 4i
c2 := 1.5 + 2.5i // literal syntax
fmt.Println("c1:", c1)
fmt.Println("c2:", c2)
// Extract real and imaginary parts
fmt.Printf("real(c2): %f, imag(c2): %f\n", real(c2), imag(c2))
sum := c1 + c2
product := c1 * c2
fmt.Println("sum:", sum, "product:", product)
}
All usual arithmetic operators work with complex numbers. The functions real() and imag() are built into the language, not imported from a package.
Complex numbers are first‑class citizens:
Go’s specification defines complex types natively. You can pass them to functions, return them, and use them in expressions without any third‑party dependency. This makes numerical code more portable and reduces the risk of version conflicts.
Choosing the Right Numeric Type
The sheer number of choices can feel overwhelming. Here is a practical guide that covers most day‑to‑day programming:
- Use int for general whole numbers — loop counters, slice indices, counts.
- Use int64 or uint64 when you need a guaranteed size, such as timestamps or hash values.
- Use byte when handling raw binary data or ASCII characters.
- Use rune when processing Unicode text character by character.
- Use float64 for any floating‑point math unless you are dealing with massive datasets where the 50% memory saving of
float32truly matters and you have profiled the precision loss. - Use complex128 for complex arithmetic; you rarely need
complex64.
No implicit numeric conversions:
Go will not let you add an int and an int32 directly. The compiler rejects the expression with a clear error. This prevents whole classes of bugs where a value silently changes width or signedness.
Common Pitfalls
Beyond the overflow and underflow issues already shown, there are a few patterns that trip up even experienced developers.
- Assuming int is always 64 bits. If your code is ever compiled for a 32‑bit platform,
intbecomes 4 bytes. Any value that fits in aninton your dev machine may overflow in production. - Comparing floating‑point numbers with
==. Due to rounding,0.1 + 0.2 == 0.3is false. Use approximate comparisons with a small epsilon, or multiply by a power of ten and work with integers. - Using uint for “this value will never be negative”. As shown earlier, that decision creates a constant risk of underflow in loops and comparisons.
- Storing a non‑ASCII character in a byte.
'€'is a rune that overflows a byte; the compiler catches it, but if you read such a character from a string and store it in abyte, you’ll get a truncated value.
You have a solid foundation once these rules become second nature:
When you find yourself reaching for int and float64 by default, using byte/rune for text, and explicitly sizing only when you have a performance or protocol requirement, you’ve internalized Go’s numeric type philosophy. From here, the next step is mastering type conversions and constants.
Summary
Go’s numeric type system is a mirror of the hardware — small, fast, and absolutely explicit. Every type exists because real systems care about bit width and signedness, and Go refuses to hide those concerns behind automatic promotions. This forces you to think about your data, but the payoff is code that behaves predictably under load, across architectures, and over time.
The most important take‑away is that int and float64 are the safe defaults for most applications, while the sized types and complex numbers are precise tools for specialised work.