πŸ”’ Chapter 4 of 20

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.

⏱ β€”πŸ“Š Beginner–Intermediate

🎯 Learning Objectives

βœ… After this chapter you will:
  • 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:

CategoryTypesSemantics
Numericint, int8–int64, uint, uint8–uint64, float32, float64, complex64, complex128Value
BooleanboolValue
StringstringValue (immutable, but internally a pointer+length)
Rune/Byterune (=int32), byte (=uint8)Value
Compositearray, structValue
Referenceslice, map, channel, pointer, function, interfaceReference (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.

TypeSizeRangeWhen to use
int88 bits (1 byte)-128 to 127Rare β€” very tight memory constraints
int1616 bits (2 bytes)-32,768 to 32,767Rare β€” binary protocols
int32 / rune32 bits (4 bytes)-2.1B to 2.1BUnicode code points (rune)
int6464 bits (8 bytes)-9.2Γ—10¹⁸ to 9.2Γ—10¹⁸Timestamps, large counters, file offsets
intPlatform (32 or 64 bits)Depends on platformGeneral purpose β€” prefer this for most integers
uint8 / byte8 bits0 to 255Raw bytes, pixel values, binary data
uint1616 bits0 to 65,535Port numbers, image dimensions
uint3232 bits0 to 4.3BIPv4 addresses, 32-bit checksums
uint6464 bits0 to 1.8Γ—10¹⁹Large unsigned counters, bitfields
uintPlatform (32 or 64 bits)DependsArray indices (be careful with underflow!)
uintptrPlatformEnough for pointerUnsafe pointer arithmetic only
go
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)
}
βœ… Output
int8 max: 127 int16 max: 32767 int32 max: 2147483647 int64 max: 9223372036854775807 int8 overflow: -128 int size: 64 bits bin=170 oct=493 hex=16711850
⚠️ Integer Overflow is Silent

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.

go
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)
}
βœ… Output
float32: 3.14159274101257324219 float64: 3.14159265358979323846 false 0.30000000000000004 true Inf: +Inf, -Inf: -Inf, NaN: NaN NaN == NaN: false IsNaN: true SmallestNonzero: 5.000000e-324 MaxFloat64: 1.797693e+308

βœ… Boolean Type

go
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
}
βœ… Output
a = true, b = false false true false bool size: 1 byte(s)

πŸ“ 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.

πŸ“Š String Internal Layout
String header (on stack, 16 bytes on 64-bit):
header[0]ptr β†’ "Hello" bytespointer to UTF-8 data
header[8]5length in bytes
String data (on heap or read-only data segment):
0x???072 'H'101 'e'108 'l'108 'l'111 'o'
go
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!
}
βœ… Output
bytes: 13 runes: 9 s[0] = 72 (H) s[7] = 228 Byte-by-byte (wrong for Unicode): [0]=72 [1]=101 [2]=108 [3]=108 [4]=111 [5]=44 [6]=32 [7]=228 [8]=184 [9]=150 [10]=231 [11]=149 [12]=140 Rune-by-rune (correct): [0]=H(U+0048) [1]=e(U+0065) [2]=l(U+006C) [3]=l(U+006C) [4]=o(U+006F) [5]=,(U+002C) [6]= (U+0020) [7]=δΈ–(U+4E16) [10]=η•Œ(U+754C) xxxxx

Unicode and UTF-8

πŸ’‘ Unicode vs UTF-8 vs Rune vs Byte
  • 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). Use rune when you care about characters.
  • Byte: A Go type alias for uint8. Represents a single byte of raw data. Use byte when you care about bytes.
go
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
}
βœ… Output
rune: δΈ–, code point: U+4E16, value: 19990 byte: A, value: 65 First byte: 72 First rune: H Last rune: η•Œ true true true true 65 true

πŸ”„ Type Conversion

Go has no implicit type conversion. Every conversion must be explicit. This prevents bugs from accidental type widening/narrowing.

go
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
}
βœ… Output
42 42 42 -24 value out of int8 range! 3 -3 42 <nil> 42 3.14 <nil> A 65 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 TypesReference Types
int, float, bool, string, rune, byteslice, map, channel
array, structpointer, func, interface
Copying creates an independent copyCopying shares the underlying data
Modifying copy doesn't affect originalModifying via one reference affects all
go
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!
}
βœ… Output
a: 42 b: 100 arr1: [1 2 3] arr2: [99 2 3] s1: [99 2 3] s2: [99 2 3] s1 after copy: [99 2 3] s3: [1 2 3] m1: map[a:1 b:2]

🎀 Interview Questions

1
What is the difference between len(s) and utf8.RuneCountInString(s) for a string?
β–Ά
Detailed Answer

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.

2
Why is 0.1 + 0.2 != 0.3 in Go? How do you handle floating-point equality?
β–Ά
Detailed Answer

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.

3
What is the difference between a rune and a byte in Go?
β–Ά
Detailed Answer

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

🧩
Data Types
5 questions
1. What does len("δΈ–η•Œ") return?
2. What does fmt.Println(0.1 + 0.2 == 0.3) print?
3. What underlying type is rune an alias for?
4. Why does var x int = 5; var y float64 = x cause a compile error?
5. After s1 := []int{1,2,3}; s2 := s1; s2[0] = 99, what is s1[0]?
β€”

πŸ“Œ Cheat Sheet

πŸ“Œ Data Types β€” Quick Reference
Default integer
int (platform size)
Default float
float64
Character type
rune = int32
Raw byte
byte = uint8
String bytes
len(s) β†’ byte count
String chars
utf8.RuneCountInString(s)
Iterate chars
for i, r := range s { }
String→int
strconv.Atoi(s)
Int→string
strconv.Itoa(n)
Float compare
math.Abs(a-b) < 1e-9
Type convert
float64(i), int(f)
Builder (efficient)
var sb strings.Builder; sb.WriteString()