Basic Types
Every value in a Go program has a type, and that type is fixed the moment the value is created — it never silently changes while the program runs. Go’s basic types are the small set of built-in scalar types (booleans, numbers, and strings) that every other type, from slices to structs, is ultimately built out of. Understanding exactly how they behave — their zero values, their sizes, and the rules around converting between them — is the foundation everything else in Go rests on.
Overview: How Types Work in Go
Go is statically typed: the type of every variable is determined at compile time, either because you write it explicitly or because the compiler infers it from the value you assign. This is different from dynamically typed languages like Python or JavaScript, where a variable can hold an integer one moment and a string the next. In Go, once a variable is declared as an int, it holds an int for its entire lifetime. This is not just a style choice — it lets the compiler catch a huge class of bugs (passing a string where a number is expected, for example) before the program ever runs, and it lets the compiler generate faster code because it always knows exactly how many bytes a value occupies and how to interpret them.
Go’s numeric types come in families. The signed integers are int8, int16, int32, int64, and the general-purpose int; the unsigned integers mirror them as uint8, uint16, uint32, uint64, and uint, plus uintptr for holding raw pointer addresses. The plain int and uint types are platform-dependent in the language specification (32 or 64 bits), but on every mainstream platform you’ll actually target today they are 64 bits wide, and int is the type you should reach for by default. The floating-point types are float32 and float64 (IEEE 754 numbers); prefer float64 unless you have a specific memory-layout reason not to, since it’s what the math library and most numeric literals assume. There are also two rarely-used complex number types, complex64 and complex128.
Two type names are aliases rather than distinct types: byte is an alias for uint8, and rune is an alias for int32. This matters because Go strings are not arrays of characters — a string is an immutable, read-only sequence of bytes that is conventionally (but not enforced to be) valid UTF-8 text. When you range over a string, Go decodes the UTF-8 bytes for you and hands you rune values, one per Unicode code point, which is why rune exists: a single character like “é” or “漢” can take more than one byte, so “one character” and “one byte” are not the same thing in Go.
The Basic Types at a Glance
| Category | Types | Notes |
|---|---|---|
| Boolean | bool |
Only true or false; zero value is false |
| Signed integers | int, int8, int16, int32, int64 |
int is the default choice; effectively 64-bit on modern platforms |
| Unsigned integers | uint, uint8, uint16, uint32, uint64, uintptr |
byte is an alias for uint8 |
| Floating point | float32, float64 |
float64 is the default choice |
| Complex | complex64, complex128 |
Rare outside scientific code |
| Text | string |
Immutable byte sequence, conventionally UTF-8 |
| Character | rune (alias for int32) |
One Unicode code point |
Zero Values
Unlike C, where an uninitialized local variable holds whatever garbage bits happened to be in memory, Go always zero-initializes every variable you declare without an explicit value. This eliminates an entire category of undefined-behavior bugs.
| Type | Zero value |
|---|---|
bool |
false |
| Any numeric type | 0 (or 0.0 for floats) |
string |
\"\" (the empty string, not nil) |
| pointers, slices, maps, channels, funcs, interfaces | nil |
Syntax
Go gives you a few interchangeable ways to declare a typed value:
var name Type = value // explicit type and initial value
var name Type // no initializer: the zero value is used
name := value // short declaration; type is inferred from value
const name Type = value // constant; value must be known at compile time
var name Type = value— the most explicit form; use it when you want the type spelled out for clarity, or when the zero value isn’t what you want.var name Type— declaresnamewith its type’s zero value, useful when you’ll assign to it later.name := value— the short variable declaration; the compiler infers the type from the right-hand side. Only legal inside a function body, never at package scope.const name Type = value— a compile-time constant. TheTypecan be omitted for an untyped constant, which adapts to whatever context it’s used in (see Example 3).
Examples
Example 1: Declaring Variables and Inspecting Zero Values
package main
import "fmt"
func main() {
var age int = 30
var price float64 = 19.99
var initial byte = 'G'
var isActive bool = true
var name string = "Gopher"
var count int
var ratio float64
var flag bool
var label string
fmt.Println(age, price, initial, isActive, name)
fmt.Println(count, ratio, flag, label)
fmt.Printf("%T %T %T %T\n", age, price, isActive, name)
}
Output:
30 19.99 71 true Gopher
0 0 false
int float64 bool string
The first line prints the values we explicitly initialized. Note that initial is a byte holding the character literal 'G', which is really just the number 71 (its ASCII code) — fmt.Println has no way to know you “meant” a character, so it prints the number. The second line shows the zero values: 0 for the int, 0 for the float, false for the bool, and an empty string for label (which still gets a leading space from Println‘s separator, even though it prints nothing visible). The third line uses the %T verb to print each value’s dynamic type, confirming what the compiler inferred.
Example 2: Converting Between Types
package main
import (
"fmt"
"strconv"
)
func main() {
var i int = 42
var f float64 = float64(i) / 4
fmt.Println("Divided:", f)
age := 25
ageText := strconv.Itoa(age)
fmt.Println("Age as text:", ageText)
parsed, err := strconv.Atoi("100")
if err != nil {
fmt.Println("conversion failed:", err)
return
}
fmt.Println("Parsed value:", parsed+1)
var counter uint8 = 250
counter += 10
fmt.Println("Wrapped counter:", counter)
}
Output:
Divided: 10.5
Age as text: 25
Parsed value: 101
Wrapped counter: 4
Go never converts numeric types for you implicitly, even between an int and a float64 — you must write float64(i) explicitly, which is why the division produces 10.5 instead of truncating. Converting a number to its text representation is not a type conversion at all; it requires the strconv package (strconv.Itoa to go from int to string, strconv.Atoi to parse back, both of which can fail, which is why Atoi returns an error you must check). Finally, notice that uint8 has a maximum value of 255; adding 10 to 250 doesn’t panic or produce a bigger type — it silently wraps around using modular arithmetic, landing on 4.
Example 3: Constants and iota
package main
import "fmt"
type Weekday int
const (
Sunday Weekday = iota
Monday
Tuesday
Wednesday
Thursday
Friday
Saturday
)
const Pi = 3.14159
func main() {
fmt.Println(Sunday, Monday, Saturday)
var radius float64 = 2.0
area := Pi * radius * radius
fmt.Println("Area:", area)
fmt.Println("Wednesday index:", Wednesday)
}
Output:
0 1 6
Area: 12.56636
Wednesday index: 3
Inside a const block, iota starts at 0 and increases by one on every line, which is Go’s idiomatic way to build a set of related named constants without writing out each number by hand. Pi, on the other hand, is declared with no type at all — it’s an untyped constant. Untyped constants don’t commit to a concrete type until they’re used; in Pi * radius * radius, Pi automatically behaves as a float64 because that’s what radius is. This is why you can write const Pi = 3.14159 once and use it seamlessly with both float32 and float64 variables elsewhere in a program.
How It Works Step by Step
When the compiler encounters x := value, it evaluates the type of value first (a literal like 42 defaults to int, 3.14 defaults to float64, "hi" defaults to string) and permanently binds that type to x in its symbol table — from that point on, any attempt to assign a value of a different type to x is a compile error, not a runtime one. This is why Go programs that compile at all tend to have far fewer “wrong type at runtime” bugs than dynamically typed ones.
For a plain var x Type with no initializer, the compiler reserves storage sized for Type and fills it with zero bits before your code runs at all — this happens as part of program startup, before main executes for package-level variables, or at the point of declaration for locals. Every basic type’s zero value happens to correspond to all-zero bits: 0 for numbers, false for booleans, and an empty (zero-length, nil-pointer) header for strings, which is a big part of why Go can zero-initialize so cheaply and uniformly.
Explicit conversions like float64(i) or int32(bigValue) are evaluated at the point they appear: the compiler emits the appropriate CPU instruction (an integer-to-float conversion, a truncation, a sign extension) right there, at runtime, every time that line executes — there’s no hidden coercion happening anywhere else in the program, which makes it easy to grep a codebase for every place a conversion (and thus a potential precision loss) occurs.
Common Mistakes
Mistake 1: Converting a Number to a String with string()
age := 25
text := string(age) // WRONG: converts 25 to the rune with code point 25, not the text "25"
fmt.Println(text) // prints an unprintable control character, not "25"
Because string conversion from an integer treats the number as a Unicode code point (like converting a rune to text), string(25) does not give you the digits “25” — it gives you the single, unprintable control character U+0019. Modern go vet will even warn about this. The fix is to use the strconv package, which is built for exactly this:
package main
import (
"fmt"
"strconv"
)
func main() {
age := 25
text := strconv.Itoa(age)
fmt.Println(text)
}
Output:
25
Mistake 2: Forgetting That Integer Division Truncates
total := 7
count := 2
average := total / count
fmt.Println(average) // prints 3, not 3.5 -- integer division truncates toward zero
When both operands of / are integers, Go performs integer division and discards the remainder — there’s no automatic promotion to a floating-point result the way some languages do. If you want a fractional answer, at least one operand needs to be a float before the division happens:
package main
import "fmt"
func main() {
total := 7
count := 2
average := float64(total) / float64(count)
fmt.Println(average)
}
Output:
3.5
Mistake 3: Comparing Floats with ==
a := 0.1 + 0.2
if a == 0.3 {
fmt.Println("equal")
} else {
fmt.Println("not equal") // this branch runs because of floating-point rounding
}
Floating-point numbers can’t represent most decimal fractions exactly, so 0.1 + 0.2 produces a float64 that is extremely close to, but not bit-for-bit equal to, the float64 that 0.3 compiles to. Comparing floats for exact equality is almost always a bug; compare the absolute difference against a small tolerance instead:
package main
import (
"fmt"
"math"
)
func main() {
a := 0.1 + 0.2
const epsilon = 1e-9
if math.Abs(a-0.3) < epsilon {
fmt.Println("equal")
} else {
fmt.Println("not equal")
}
}
Output:
equal
Best Practices
- Default to
intfor whole numbers andfloat64for decimals unless you have a specific reason (a binary file format, a memory-constrained struct) to pick a smaller width. - Never write a comparison like
x == yfor two floats; compare the difference against a small epsilon instead. - Always check the
errorreturned bystrconv.Atoi,strconv.ParseFloat, and similar parsing functions — malformed input is a normal, expected condition, not an exceptional one. - Reach for
strconvwhenever you need to move between a number and its textual representation; never rely on a rawstring()conversion of an integer for that. - Use unsigned integer types (
uint,uint8, …) sparingly — only when a value can truly never be negative (like a count or a size) and you specifically want the wraparound or the extra positive range, since accidental underflow (e.g. subtracting past zero) is an easy source of bugs. - Prefer untyped constants (
const Pi = 3.14159) over explicitly typed ones when the constant should work naturally with several numeric types.
Practice Exercises
- Write a program that declares an
int, afloat64, and astringusing:=, then prints each one’s type using%T. - Write a function that takes two
intparameters and returns their average as afloat64, being careful to convert before dividing. Call it with7and3and confirm you get3.5, not3. - Declare a
constblock usingiotato represent the seasons (Spring,Summer,Fall,Winter) starting at1instead of0. Hint: you can adjust the starting value with an expression likeiota + 1on the first constant.
Summary
- Go is statically typed: every variable’s type is fixed at compile time, either written explicitly or inferred with
:=. - The basic types are
bool, the signed and unsigned integer families,float32/float64,complex64/complex128, andstring;byteandruneare aliases foruint8andint32. - Every variable is automatically zero-initialized if you don’t supply a value —
0,false, or""depending on its type. - Type conversions between numeric types must be explicit (
float64(i)); converting a number to text requiresstrconv, not a rawstring()conversion. - Integer division truncates; unsigned integers wrap on overflow instead of panicking; floats should never be compared with
==. - Untyped constants, and the
iotaidentifier inside aconstblock, are Go’s idiomatic tools for defining families of related values.
