Data Types in Go
A complete guide to every built-in type in Go β how they're stored in memory, their sizes, their zero values, and the critical distinction between value types and reference types.
π― Learning Objectives
- Know every Go built-in type and its memory size
- Understand the difference between value types and reference types
- Work with strings, runes, bytes, Unicode, and UTF-8 correctly
- Perform safe type conversions and understand when implicit conversion fails
- Explain integer overflow and floating-point precision issues
- Choose the right numeric type for any use case
π Go's Type System
Go is strongly and statically typed. Every value has a type known at compile time. Types are divided into:
| Category | Types | Semantics |
|---|---|---|
| Numeric | int, int8βint64, uint, uint8βuint64, float32, float64, complex64, complex128 | Value |
| Boolean | bool | Value |
| String | string | Value (immutable, but internally a pointer+length) |
| Rune/Byte | rune (=int32), byte (=uint8) | Value |
| Composite | array, struct | Value |
| Reference | slice, map, channel, pointer, function, interface | Reference (header is value, underlying data is shared) |
π’ Integer Types
Go has 11 integer types. Choosing the right one matters for memory, performance, and overflow safety.
| Type | Size | Range | When to use |
|---|---|---|---|
int8 | 8 bits (1 byte) | -128 to 127 | Rare β very tight memory constraints |
int16 | 16 bits (2 bytes) | -32,768 to 32,767 | Rare β binary protocols |
int32 / rune | 32 bits (4 bytes) | -2.1B to 2.1B | Unicode code points (rune) |
int64 | 64 bits (8 bytes) | -9.2Γ10ΒΉβΈ to 9.2Γ10ΒΉβΈ | Timestamps, large counters, file offsets |
int | Platform (32 or 64 bits) | Depends on platform | General purpose β prefer this for most integers |
uint8 / byte | 8 bits | 0 to 255 | Raw bytes, pixel values, binary data |
uint16 | 16 bits | 0 to 65,535 | Port numbers, image dimensions |
uint32 | 32 bits | 0 to 4.3B | IPv4 addresses, 32-bit checksums |
uint64 | 64 bits | 0 to 1.8Γ10ΒΉβΉ | Large unsigned counters, bitfields |
uint | Platform (32 or 64 bits) | Depends | Array indices (be careful with underflow!) |
uintptr | Platform | Enough for pointer | Unsafe pointer arithmetic only |
package main
import (
"fmt"
"math"
)
func main() {
// Integer sizes and limits
var i8 int8 = math.MaxInt8 // 127
var i16 int16 = math.MaxInt16 // 32767
var i32 int32 = math.MaxInt32 // 2147483647
var i64 int64 = math.MaxInt64 // 9223372036854775807
fmt.Printf("int8 max: %d\n", i8)
fmt.Printf("int16 max: %d\n", i16)
fmt.Printf("int32 max: %d\n", i32)
fmt.Printf("int64 max: %d\n", i64)
// Integer overflow β wraps around (no panic)
var x int8 = 127
x++ // overflows: 127 + 1 = -128 (wraps to most negative value)
fmt.Println("int8 overflow:", x) // -128
// Checking platform int size
var n int = 0
fmt.Printf("int size: %d bits\n", 8*int(^n>>1+1)) // typically 64 on modern systems
// Binary, octal, hex literals (Go 1.13+)
bin := 0b1010_1010 // binary: 170
oct := 0o755 // octal: 493
hex := 0xFF_00_AA // hex: 16711850
fmt.Printf("bin=%d oct=%d hex=%d\n", bin, oct, hex)
}Go integers overflow silently β they wrap around without panicking. If you need overflow detection, use the math/big package or check before the operation using constants like math.MaxInt32.
π Floating-Point Types
Go has two IEEE 754 floating-point types. float64 is the default and almost always the right choice.
package main
import (
"fmt"
"math"
)
func main() {
var f32 float32 = 3.14159265358979323846
var f64 float64 = 3.14159265358979323846
// float32 loses precision after ~7 significant digits
fmt.Printf("float32: %.20f\n", f32) // 3.14159274101257324219 (imprecise!)
fmt.Printf("float64: %.20f\n", f64) // 3.14159265358979323846 (much more precise)
// Floating-point comparison β never use ==
a := 0.1 + 0.2 // Not exactly 0.3 in IEEE 754!
fmt.Println(a == 0.3) // false!
fmt.Println(a) // 0.30000000000000004
// Correct comparison: use an epsilon
const epsilon = 1e-9
fmt.Println(math.Abs(a-0.3) < epsilon) // true
// Special values
inf := math.Inf(1) // +Infinity
negInf := math.Inf(-1) // -Infinity
nan := math.NaN() // Not a Number
fmt.Printf("Inf: %v, -Inf: %v, NaN: %v\n", inf, negInf, nan)
fmt.Println("NaN == NaN:", nan == nan) // false! (IEEE 754 rule)
fmt.Println("IsNaN:", math.IsNaN(nan)) // true (use this)
// float64 range
fmt.Printf("SmallestNonzero: %e\n", math.SmallestNonzeroFloat64)
fmt.Printf("MaxFloat64: %e\n", math.MaxFloat64)
}β Boolean Type
package main
import "fmt"
func main() {
var a bool = true
var b bool // zero value: false
fmt.Printf("a = %v, b = %v\n", a, b)
// Boolean operators
fmt.Println(true && false) // false (AND β both must be true)
fmt.Println(true || false) // true (OR β at least one must be true)
fmt.Println(!true) // false (NOT)
// Short-circuit evaluation
// If left side of && is false, right side is NOT evaluated
// If left side of || is true, right side is NOT evaluated
x := 0
if x != 0 && 10/x > 2 { // 10/x is never evaluated when x==0 β no panic!
fmt.Println("safe")
}
// In Go, there is NO automatic boolean conversion:
// if 1 { } β compile error!
// if "string" { } β compile error!
// You must be explicit: if x != 0 { }
// Boolean size
fmt.Printf("bool size: %d byte(s)\n", 1) // 1 byte in practice
}π Strings, Bytes & Runes
Go strings are one of the most nuanced types. Understanding them thoroughly separates competent Go developers from experts.
String Internals
A Go string is a read-only byte slice. Internally it's a struct with two fields: a pointer to the bytes and a length. Strings are immutable β you cannot modify individual bytes.
package main
import (
"fmt"
"strings"
"unicode/utf8"
)
func main() {
s := "Hello, δΈη" // A string with ASCII and Chinese characters
// len() returns BYTE count, not character count!
fmt.Println("bytes:", len(s)) // 13 (not 9!)
fmt.Println("runes:", utf8.RuneCountInString(s)) // 9 characters
// Indexing gives BYTES, not characters
fmt.Printf("s[0] = %d (%c)\n", s[0], s[0]) // 72 (H)
fmt.Printf("s[7] = %d\n", s[7]) // 228 (first byte of δΈ)
// Correct iteration β use range for Unicode:
fmt.Println("\nByte-by-byte (wrong for Unicode):")
for i := 0; i < len(s); i++ {
fmt.Printf("[%d]=%d ", i, s[i])
}
fmt.Println("\n\nRune-by-rune (correct):")
for i, r := range s {
fmt.Printf("[%d]=%c(U+%04X) ", i, r, r)
}
fmt.Println()
// String operations
fmt.Println(strings.ToUpper("hello")) // HELLO
fmt.Println(strings.Contains("hello", "ell")) // true
fmt.Println(strings.Replace("aaa", "a", "b", 2)) // bba
fmt.Println(strings.Split("a,b,c", ",")) // [a b c]
fmt.Println(strings.TrimSpace(" hello ")) // hello
fmt.Println(strings.HasPrefix("hello", "he")) // true
// String concatenation β the efficient way
var sb strings.Builder
for i := 0; i < 5; i++ {
sb.WriteString("x") // O(1) amortised β no allocations
}
fmt.Println(sb.String()) // xxxxx
// β οΈ Avoid: s1 + s2 in a loop β creates O(nΒ²) allocations!
}Unicode and UTF-8
- Unicode: A standard that assigns a unique number (code point) to every character in every human writing system. "Hello" β U+0048 U+0065 U+006C U+006C U+006F
- UTF-8: An encoding that represents Unicode code points as 1β4 bytes. ASCII characters (U+0000βU+007F) use 1 byte. This is what Go strings store.
- Rune: A Go type alias for
int32. Represents a single Unicode code point (character). Userunewhen you care about characters. - Byte: A Go type alias for
uint8. Represents a single byte of raw data. Usebytewhen you care about bytes.
package main
import (
"fmt"
"unicode"
)
func main() {
// Rune literals
var r rune = 'δΈ'
fmt.Printf("rune: %c, code point: U+%04X, value: %d\n", r, r, r)
// rune: δΈ, code point: U+4E16, value: 19990
// Byte literals
var b byte = 'A'
fmt.Printf("byte: %c, value: %d\n", b, b)
// byte: A, value: 65
// Converting between string, []byte, []rune
s := "Hello, δΈη"
// String β []byte: each element is a byte (not character!)
bytes := []byte(s)
fmt.Println("First byte:", bytes[0]) // 72
// String β []rune: each element is a character
runes := []rune(s)
fmt.Println("First rune:", string(runes[0])) // H
fmt.Println("Last rune:", string(runes[len(runes)-1])) // η
// []rune β string
s2 := string(runes)
fmt.Println(s2 == s) // true
// Unicode character properties
fmt.Println(unicode.IsLetter('A')) // true
fmt.Println(unicode.IsDigit('5')) // true
fmt.Println(unicode.IsSpace(' ')) // true
fmt.Println(unicode.ToUpper('a')) // 65 (='A')
fmt.Println(unicode.IsLetter('δΈ')) // true
}π Type Conversion
Go has no implicit type conversion. Every conversion must be explicit. This prevents bugs from accidental type widening/narrowing.
package main
import (
"fmt"
"strconv"
)
func main() {
// Numeric conversions
var i int = 42
var f float64 = float64(i) // int β float64
var u uint = uint(f) // float64 β uint (truncates, not rounds!)
fmt.Println(i, f, u) // 42 42 42
// Narrowing conversion β data loss possible
var big int = 1000
var small int8 = int8(big) // 1000 > MaxInt8(127) β overflow!
fmt.Println(small) // -24 (unexpected!)
// Safe conversion with bounds check
if big >= -128 && big <= 127 {
small = int8(big)
} else {
fmt.Println("value out of int8 range!")
}
// float to int β truncates (does NOT round)
fmt.Println(int(3.9)) // 3 (not 4!)
fmt.Println(int(-3.9)) // -3 (not -4!)
// String conversions β use strconv package, NOT type casting
n, err := strconv.Atoi("42") // string β int
fmt.Println(n, err) // 42
s := strconv.Itoa(42) // int β string
fmt.Println(s) // "42"
f2, err2 := strconv.ParseFloat("3.14", 64)
fmt.Println(f2, err2) // 3.14
// β WRONG: type conversion does NOT convert number to string!
wrong := string(65) // gives 'A' (the rune), not "65"!
fmt.Println(wrong) // A (not "65"!)
// β
RIGHT:
right := strconv.Itoa(65)
fmt.Println(right) // 65
// Byte/rune conversions
r := 'A' // rune (int32)
b := byte(r) // rune β byte
s2 := string(r) // rune β string (the character)
fmt.Println(r, b, s2) // 65 65 A
} π¦ Value Types vs Reference Types
This distinction is fundamental β it affects how variables are copied, passed to functions, and shared between goroutines.
| Value Types | Reference Types |
|---|---|
| int, float, bool, string, rune, byte | slice, map, channel |
| array, struct | pointer, func, interface |
| Copying creates an independent copy | Copying shares the underlying data |
| Modifying copy doesn't affect original | Modifying via one reference affects all |
package main
import "fmt"
func main() {
// VALUE TYPE: int β copy is independent
a := 42
b := a // b is a COPY of a
b = 100
fmt.Println("a:", a, "b:", b) // a: 42 b: 100 β a unchanged
// VALUE TYPE: array β copy is independent
arr1 := [3]int{1, 2, 3}
arr2 := arr1 // full copy of all elements
arr2[0] = 99
fmt.Println("arr1:", arr1) // [1 2 3] β unchanged
fmt.Println("arr2:", arr2) // [99 2 3]
// REFERENCE TYPE: slice β shares underlying array
s1 := []int{1, 2, 3}
s2 := s1 // s2's header is copied, but both point to SAME array
s2[0] = 99
fmt.Println("s1:", s1) // [99 2 3] β CHANGED! They share data
fmt.Println("s2:", s2) // [99 2 3]
// To get an independent slice copy, use copy():
s3 := make([]int, len(s1))
copy(s3, s1)
s3[0] = 1
fmt.Println("s1 after copy:", s1) // [99 2 3] β unchanged
fmt.Println("s3:", s3) // [1 2 3] β independent
// REFERENCE TYPE: map β shares underlying hash table
m1 := map[string]int{"a": 1}
m2 := m1 // m2 points to same hash table
m2["b"] = 2
fmt.Println("m1:", m1) // map[a:1 b:2] β CHANGED via m2!
}π€ Interview Questions
len(s) and utf8.RuneCountInString(s) for a string?len(s) returns the number of bytes in the string β not the number of characters. For pure ASCII strings they're equal. For strings containing multi-byte UTF-8 characters (like Chinese, emoji, Arabic), len(s) returns more than the number of visible characters.
utf8.RuneCountInString(s) returns the number of runes (Unicode code points / characters). This is what users mean when they say "string length."
Example: "δΈη" β len = 6 (3 bytes per character), RuneCountInString = 2 (2 characters).
Why interviewers ask: This distinguishes developers who truly understand Go strings from those who only know the API. Wrong answer (using len() for character count) causes real bugs in internationalised applications.
0.1 + 0.2 != 0.3 in Go? How do you handle floating-point equality?Floating-point numbers in IEEE 754 are binary fractions. 0.1 cannot be represented exactly in binary (like 1/3 cannot be represented exactly in decimal). When you write 0.1 in Go, you get the closest representable binary value, which is slightly off.
When you add 0.1 + 0.2, the rounding errors accumulate, giving 0.30000000000000004 β not exactly 0.3.
Solution: Never compare floats with ==. Instead, check if the absolute difference is smaller than an epsilon (tolerance): math.Abs(a-b) < 1e-9. The epsilon value depends on the precision you need.
For financial calculations, use integer arithmetic (store cents as int64) or a decimal library like github.com/shopspring/decimal.
rune and a byte in Go?byte is an alias for uint8 β an 8-bit unsigned integer (0β255). It represents a single byte of raw binary data. UTF-8 characters can take 1β4 bytes each.
rune is an alias for int32 β a 32-bit integer that can hold any Unicode code point (0β0x10FFFF). It represents a single Unicode character regardless of how many bytes it occupies in UTF-8.
Use byte when working with binary data, network protocols, or ASCII-only text. Use rune when working with user-visible characters in internationalised text.
π§ͺ Quiz
len("δΈη") return?fmt.Println(0.1 + 0.2 == 0.3) print?rune an alias for?var x int = 5; var y float64 = x cause a compile error?s1 := []int{1,2,3}; s2 := s1; s2[0] = 99, what is s1[0]?π Cheat Sheet
int (platform size)float64rune = int32byte = uint8len(s) β byte countutf8.RuneCountInString(s)for i, r := range s { }strconv.Atoi(s)strconv.Itoa(n)math.Abs(a-b) < 1e-9float64(i), int(f)var sb strings.Builder; sb.WriteString()