Go Data Types: Complete Reference
Go provides a comprehensive set of basic types that map directly to hardware primitives. Unlike dynamically typed languages, you must declare types explicitly, and unlike C, there are no implicit...
Key Insights
- Go’s type system enforces explicit conversions between all types, even between different integer sizes, eliminating entire classes of bugs common in C-like languages
- Every type in Go has a guaranteed zero value, making uninitialized variable bugs impossible and eliminating the need for defensive nil checks in many scenarios
- Understanding the distinction between value types (arrays, structs) and reference types (slices, maps, channels) is critical for predicting memory behavior and avoiding subtle bugs
Basic Types: The Foundation
Go provides a comprehensive set of basic types that map directly to hardware primitives. Unlike dynamically typed languages, you must declare types explicitly, and unlike C, there are no implicit conversions.
Integer Types
Go offers signed integers (int8, int16, int32, int64) and unsigned variants (uint8, uint16, uint32, uint64). The int and uint types are platform-dependent—32 bits on 32-bit systems, 64 bits on 64-bit systems.
var a int8 = 127
var b int8 = 1
// a + b causes overflow, wrapping to -128
fmt.Println(a + b) // -128
// Explicit conversion required between sizes
var x int32 = 100
var y int64 = int64(x) // Must convert explicitly
Always use int unless you have a specific reason to use sized integers. Use int32 or int64 when serializing data or interfacing with APIs that specify exact sizes.
Floating-Point and Complex Numbers
Go provides float32 and float64 for floating-point arithmetic, and complex64 and complex128 for complex numbers. Default to float64—the performance difference is negligible on modern hardware, and the precision matters.
var f1 float32 = 0.1
var f2 float32 = 0.2
fmt.Println(f1 + f2 == 0.3) // false - floating point precision
// Complex numbers are first-class types
c := complex(3, 4) // 3+4i
fmt.Println(real(c), imag(c)) // 3, 4
Booleans and Strings
Booleans are true or false—no truthy/falsy values exist. Strings are immutable UTF-8 byte sequences.
s := "hello"
// s[0] = 'H' // Compilation error - strings are immutable
// Must create new string
s2 := "H" + s[1:] // "Hello"
// String zero value is empty string
var s3 string
fmt.Println(s3 == "") // true
Composite Types: Building Complexity
Arrays vs Slices
Arrays have fixed size determined at compile time. Slices are dynamic views over arrays. You’ll use slices 99% of the time.
// Array - size is part of the type
var arr [3]int // [0, 0, 0]
arr[0] = 1
// Slices - dynamic, reference underlying array
slice := []int{1, 2, 3}
slice = append(slice, 4) // Can grow
// Slices have length and capacity
s := make([]int, 3, 5) // length 3, capacity 5
fmt.Println(len(s), cap(s)) // 3, 5
Pre-allocate slices when you know the size to avoid repeated allocations:
// Bad - multiple allocations
var result []int
for i := 0; i < 1000; i++ {
result = append(result, i)
}
// Good - single allocation
result := make([]int, 0, 1000)
for i := 0; i < 1000; i++ {
result = append(result, i)
}
Maps
Maps are hash tables with reference semantics. They must be initialized before use.
// Nil map - will panic on write
var m map[string]int
// m["key"] = 1 // panic!
// Initialized map
m = make(map[string]int)
m["key"] = 1
// Check for existence with comma-ok idiom
value, exists := m["key"]
if exists {
fmt.Println(value)
}
// Delete keys
delete(m, "key")
Structs
Structs are value types—assignment copies the entire struct. Use pointers when you need reference semantics or want to avoid copying large structs.
type Person struct {
Name string
Age int
}
// Value semantics - copied
p1 := Person{Name: "Alice", Age: 30}
p2 := p1
p2.Age = 31
fmt.Println(p1.Age) // 30 - unchanged
// Struct embedding for composition
type Employee struct {
Person // Embedded - promotes Person's fields
ID int
}
e := Employee{
Person: Person{Name: "Bob", Age: 25},
ID: 100,
}
fmt.Println(e.Name) // Accesses embedded field directly
Reference Types: Indirection and Interfaces
Pointers
Go has pointers but no pointer arithmetic. The & operator takes addresses, * dereferences.
x := 42
p := &x // p is *int
*p = 43 // Modify through pointer
fmt.Println(x) // 43
// No pointer arithmetic like C
// p++ // Compilation error
Use pointers for large structs, when you need to modify the receiver, or for optional fields:
type Config struct {
Timeout *int // nil means use default
}
func (c *Config) GetTimeout() int {
if c.Timeout != nil {
return *c.Timeout
}
return 30 // default
}
Functions as Values
Functions are first-class values. You can pass them, return them, and store them in variables.
type Operation func(int, int) int
func apply(a, b int, op Operation) int {
return op(a, b)
}
add := func(x, y int) int { return x + y }
result := apply(5, 3, add) // 8
Channels
Channels are typed conduits for communication between goroutines.
ch := make(chan int) // Unbuffered
ch2 := make(chan int, 5) // Buffered, capacity 5
go func() {
ch <- 42 // Send
}()
value := <-ch // Receive
// Close and range over channel
go func() {
for i := 0; i < 5; i++ {
ch2 <- i
}
close(ch2)
}()
for v := range ch2 {
fmt.Println(v)
}
Interfaces
Interfaces are implemented implicitly—no implements keyword. Any type with matching methods satisfies the interface.
type Writer interface {
Write([]byte) (int, error)
}
type ConsoleWriter struct{}
func (cw ConsoleWriter) Write(data []byte) (int, error) {
return fmt.Print(string(data))
}
var w Writer = ConsoleWriter{} // Implicit implementation
Type Conversions and Assertions
Go requires explicit conversions between all types, even between different integer sizes.
var i int = 42
var f float64 = float64(i) // Must convert explicitly
var u uint = uint(i)
// String conversions
s := string(65) // "A" - converts rune to string
b := []byte("hello") // [104 101 108 108 111]
s2 := string(b) // "hello"
Type assertions extract concrete types from interfaces:
var i interface{} = "hello"
// Type assertion with panic on failure
s := i.(string)
// Safe type assertion with comma-ok
s, ok := i.(string)
if ok {
fmt.Println(s)
}
// Type switch for multiple types
switch v := i.(type) {
case string:
fmt.Println("string:", v)
case int:
fmt.Println("int:", v)
default:
fmt.Println("unknown type")
}
Custom Types and Aliases
Create custom types to add methods and improve type safety:
// Custom type - new type with same underlying representation
type UserID int64
func (id UserID) String() string {
return fmt.Sprintf("User-%d", id)
}
// Type alias - just another name for the same type
type MyInt = int
var uid UserID = 100
var i MyInt = 42
// uid and int64 are different types
// var x int64 = uid // Compilation error
var x int64 = int64(uid) // Must convert
Use custom types for domain concepts like IDs, units, or status codes to prevent mixing incompatible values:
type Celsius float64
type Fahrenheit float64
func (c Celsius) ToFahrenheit() Fahrenheit {
return Fahrenheit(c*9/5 + 32)
}
// Type system prevents mixing
var temp Celsius = 100
// var f Fahrenheit = temp // Compilation error
var f Fahrenheit = temp.ToFahrenheit()
Zero Values: Safety by Default
Every type in Go has a zero value—the value a variable has when declared but not explicitly initialized. This eliminates entire classes of bugs.
var i int // 0
var f float64 // 0.0
var b bool // false
var s string // ""
var p *int // nil
var slice []int // nil (but safe to use with append)
var m map[string]int // nil (must initialize before use)
var ch chan int // nil
Nil slices are safe to use with append, but nil maps panic on write:
var slice []int
slice = append(slice, 1) // Works fine
var m map[string]int
// m["key"] = 1 // panic!
m = make(map[string]int)
m["key"] = 1 // Now safe
Performance Considerations
Understanding memory layout and allocation patterns helps you write efficient Go code.
Stack vs Heap Allocation
Go’s compiler performs escape analysis to determine whether variables live on the stack or heap. Variables that don’t escape the function stay on the stack (fast). Variables that might be accessed after the function returns are heap-allocated.
func stackAlloc() int {
x := 42 // Lives on stack
return x
}
func heapAlloc() *int {
x := 42 // Escapes to heap - pointer returned
return &x
}
Struct Padding
The compiler adds padding to structs for alignment. Order fields from largest to smallest to minimize padding:
// Bad - 24 bytes due to padding
type Bad struct {
a bool // 1 byte + 7 padding
b int64 // 8 bytes
c bool // 1 byte + 7 padding
}
// Good - 16 bytes
type Good struct {
b int64 // 8 bytes
a bool // 1 byte
c bool // 1 byte + 6 padding
}
Choosing the Right Type
Use int for counters and indices—it’s the natural word size for your platform. Use specific sizes only when necessary for serialization or memory constraints. Pre-allocate slices when you know the size. Use value types (structs) for small objects and pointer types for large objects or when you need reference semantics.
The Go type system is designed to catch errors at compile time while maintaining simplicity. Master these fundamentals, and you’ll write safer, more efficient code.