Skip to main content

Go Pointers and References

Pointers in Go are powerful constructs that provide direct access to memory addresses and enable efficient manipulation of data. Understanding pointers is crucial for writing efficient Go programs, especially when working with large data structures, implementing data sharing, and optimizing memory usage. Go's pointer system is designed to be safe and predictable while providing the flexibility needed for low-level programming. This comprehensive guide will teach you everything you need to know about Go's pointer and reference system.

Understanding Pointers in Go

What Are Pointers?

Pointers in Go are variables that store memory addresses of other variables. They provide:

  • Direct memory access - Access to the actual memory location of data
  • Memory efficiency - Avoid copying large data structures
  • Data sharing - Multiple references to the same data
  • Dynamic memory allocation - Runtime memory management
  • Reference semantics - Indirect access to values

Go's Pointer System Characteristics

Go's pointer system has several distinctive features:

Type Safety

Go pointers are type-safe, meaning a pointer to one type cannot point to another type.

No Pointer Arithmetic

Go doesn't allow pointer arithmetic for safety reasons.

Garbage Collection

Go's garbage collector automatically manages memory, preventing memory leaks.

Nil Pointers

Go pointers can be nil, and dereferencing a nil pointer causes a runtime panic.

Pointer Declaration and Basic Operations

Pointer Declaration

The * Operator

The * operator is used to declare pointer types and dereference pointers.

The & Operator

The & operator is used to get the address of a variable.

The new Built-in Function

The new function allocates memory for a new value and returns a pointer to it.

package main

import "fmt"

func main() {
// Pointer declaration and basic operations examples
fmt.Println("Pointer declaration and basic operations examples:")

// Basic variable
var x int = 42
fmt.Printf("Variable x: %d\n", x)
// Output: Variable x: 42

// Get address of variable using & operator
var ptr *int = &x
fmt.Printf("Pointer to x: %p\n", ptr)
// Output: Pointer to x: 0xc000014080 (address will vary)

// Dereference pointer using * operator
fmt.Printf("Value pointed to by ptr: %d\n", *ptr)
// Output: Value pointed to by ptr: 42

// Modify value through pointer
*ptr = 100
fmt.Printf("Modified x through pointer: %d\n", x)
// Output: Modified x through pointer: 100

// Pointer declaration with different types
var str string = "Hello, World!"
var strPtr *string = &str
fmt.Printf("String: %s\n", str)
fmt.Printf("Pointer to string: %p\n", strPtr)
fmt.Printf("Value through pointer: %s\n", *strPtr)
// Output:
// String: Hello, World!
// Pointer to string: 0xc000010240
// Value through pointer: Hello, World!

// Modify string through pointer
*strPtr = "Modified through pointer"
fmt.Printf("Modified string: %s\n", str)
// Output: Modified string: Modified through pointer

// Using new function to create pointers
var intPtr *int = new(int)
fmt.Printf("New int pointer: %p\n", intPtr)
fmt.Printf("Initial value: %d\n", *intPtr)
// Output:
// New int pointer: 0xc000014088
// Initial value: 0

// Set value through new pointer
*intPtr = 200
fmt.Printf("Value set through new pointer: %d\n", *intPtr)
// Output: Value set through new pointer: 200

// Pointer to struct
type Person struct {
Name string
Age int
}

person := Person{Name: "Alice", Age: 30}
var personPtr *Person = &person
fmt.Printf("Person: %+v\n", person)
fmt.Printf("Person pointer: %p\n", personPtr)
fmt.Printf("Person through pointer: %+v\n", *personPtr)
// Output:
// Person: {Name:Alice Age:30}
// Person pointer: 0xc0000102a0
// Person through pointer: {Name:Alice Age:30}

// Modify struct through pointer
(*personPtr).Name = "Alice Smith"
(*personPtr).Age = 31
fmt.Printf("Modified person: %+v\n", person)
// Output: Modified person: {Name:Alice Smith Age:31}
}

Pointer Operations and Dereferencing

Dereferencing Pointers

Dereferencing a pointer means accessing the value it points to.

Pointer Assignment

Pointers can be assigned to other pointers of the same type.

package main

import "fmt"

func main() {
// Pointer operations and dereferencing examples
fmt.Println("Pointer operations and dereferencing examples:")

// Create variables
a := 10
b := 20

// Create pointers
var ptrA *int = &a
var ptrB *int = &b

fmt.Printf("a = %d, b = %d\n", a, b)
fmt.Printf("ptrA points to: %d, ptrB points to: %d\n", *ptrA, *ptrB)
// Output:
// a = 10, b = 20
// ptrA points to: 10, ptrB points to: 20

// Pointer assignment
ptrA = ptrB
fmt.Printf("After ptrA = ptrB:\n")
fmt.Printf("ptrA points to: %d, ptrB points to: %d\n", *ptrA, *ptrB)
fmt.Printf("a = %d, b = %d\n", a, b)
// Output:
// After ptrA = ptrB:
// ptrA points to: 20, ptrB points to: 20
// a = 10, b = 20

// Modify value through ptrA (which now points to b)
*ptrA = 30
fmt.Printf("After *ptrA = 30:\n")
fmt.Printf("ptrA points to: %d, ptrB points to: %d\n", *ptrA, *ptrB)
fmt.Printf("a = %d, b = %d\n", a, b)
// Output:
// After *ptrA = 30:
// ptrA points to: 30, ptrB points to: 30
// a = 10, b = 30

// Pointer comparison
fmt.Printf("ptrA == ptrB: %t\n", ptrA == ptrB)
fmt.Printf("ptrA != nil: %t\n", ptrA != nil)
// Output:
// ptrA == ptrB: true
// ptrA != nil: true

// Pointer to pointer
var ptrToPtr **int = &ptrA
fmt.Printf("Pointer to pointer: %p\n", ptrToPtr)
fmt.Printf("Value through pointer to pointer: %d\n", **ptrToPtr)
// Output:
// Pointer to pointer: 0xc000012028
// Value through pointer to pointer: 30

// Modify through pointer to pointer
**ptrToPtr = 40
fmt.Printf("After **ptrToPtr = 40:\n")
fmt.Printf("a = %d, b = %d\n", a, b)
// Output:
// After **ptrToPtr = 40:
// a = 10, b = 40
}

Pointer Types and Type Safety

Type-Safe Pointers

Pointer Type Declarations

Go ensures type safety by requiring pointers to be declared with specific types.

Type Conversion with Pointers

Pointers cannot be converted between different types without explicit conversion.

package main

import "fmt"

func main() {
// Pointer types and type safety examples
fmt.Println("Pointer types and type safety examples:")

// Different pointer types
var intPtr *int
var stringPtr *string
var floatPtr *float64
var boolPtr *bool

fmt.Printf("intPtr type: %T\n", intPtr)
fmt.Printf("stringPtr type: %T\n", stringPtr)
fmt.Printf("floatPtr type: %T\n", floatPtr)
fmt.Printf("boolPtr type: %T\n", boolPtr)
// Output:
// intPtr type: *int
// stringPtr type: *string
// floatPtr type: *float64
// boolPtr type: *bool

// Create values and assign pointers
var i int = 42
var s string = "Hello"
var f float64 = 3.14
var b bool = true

intPtr = &i
stringPtr = &s
floatPtr = &f
boolPtr = &b

fmt.Printf("intPtr points to: %d\n", *intPtr)
fmt.Printf("stringPtr points to: %s\n", *stringPtr)
fmt.Printf("floatPtr points to: %.2f\n", *floatPtr)
fmt.Printf("boolPtr points to: %t\n", *boolPtr)
// Output:
// intPtr points to: 42
// stringPtr points to: Hello
// floatPtr points to: 3.14
// boolPtr points to: true

// Type safety demonstration
// intPtr = &s // This would cause a compilation error
// stringPtr = &i // This would cause a compilation error

// Pointer to struct with different fields
type Point struct {
X, Y int
}

type Circle struct {
Center Point
Radius int
}

point := Point{X: 10, Y: 20}
circle := Circle{Center: point, Radius: 5}

var pointPtr *Point = &point
var circlePtr *Circle = &circle

fmt.Printf("Point through pointer: %+v\n", *pointPtr)
fmt.Printf("Circle through pointer: %+v\n", *circlePtr)
// Output:
// Point through pointer: {X:10 Y:20}
// Circle through pointer: {{X:10 Y:20} 5}

// Access struct fields through pointer
fmt.Printf("Point X through pointer: %d\n", (*pointPtr).X)
fmt.Printf("Point Y through pointer: %d\n", (*pointPtr).Y)
// Output:
// Point X through pointer: 10
// Point Y through pointer: 20

// Shorthand for struct field access through pointer
fmt.Printf("Point X (shorthand): %d\n", pointPtr.X)
fmt.Printf("Point Y (shorthand): %d\n", pointPtr.Y)
// Output:
// Point X (shorthand): 10
// Point Y (shorthand): 20

// Modify struct fields through pointer
pointPtr.X = 100
pointPtr.Y = 200
fmt.Printf("Modified point: %+v\n", point)
// Output: Modified point: {X:100 Y:200}
}

Pointer Functions and Parameters

Pointers as Function Parameters

Pass by Reference

Pointers allow functions to modify the original values passed to them.

Memory Efficiency

Passing pointers to large structures is more efficient than copying.

package main

import "fmt"

func main() {
// Pointer functions and parameters examples
fmt.Println("Pointer functions and parameters examples:")

// Function that takes a pointer parameter
func increment(ptr *int) {
*ptr++
}

// Function that takes a value parameter
func incrementByValue(val int) int {
return val + 1
}

// Test with pointer parameter
x := 10
fmt.Printf("Before increment: x = %d\n", x)
increment(&x)
fmt.Printf("After increment: x = %d\n", x)
// Output:
// Before increment: x = 10
// After increment: x = 11

// Test with value parameter
y := 20
fmt.Printf("Before incrementByValue: y = %d\n", y)
result := incrementByValue(y)
fmt.Printf("After incrementByValue: y = %d, result = %d\n", y, result)
// Output:
// Before incrementByValue: y = 20
// After incrementByValue: y = 20, result = 21

// Function that modifies a struct through pointer
type Person struct {
Name string
Age int
}

func updatePerson(p *Person, name string, age int) {
p.Name = name
p.Age = age
}

person := Person{Name: "Alice", Age: 25}
fmt.Printf("Before update: %+v\n", person)
updatePerson(&person, "Alice Smith", 26)
fmt.Printf("After update: %+v\n", person)
// Output:
// Before update: {Name:Alice Age:25}
// After update: {Name:Alice Smith Age:26}

// Function that returns a pointer
func createPerson(name string, age int) *Person {
return &Person{Name: name, Age: age}
}

newPerson := createPerson("Bob", 30)
fmt.Printf("Created person: %+v\n", *newPerson)
// Output: Created person: {Name:Bob Age:30}

// Function that swaps values using pointers
func swap(a, b *int) {
*a, *b = *b, *a
}

a, b := 100, 200
fmt.Printf("Before swap: a = %d, b = %d\n", a, b)
swap(&a, &b)
fmt.Printf("After swap: a = %d, b = %d\n", a, b)
// Output:
// Before swap: a = 100, b = 200
// After swap: a = 200, b = 100

// Function that returns multiple pointers
func getMinMax(nums []int) (min, max *int) {
if len(nums) == 0 {
return nil, nil
}

min, max = &nums[0], &nums[0]
for i := 1; i < len(nums); i++ {
if nums[i] < *min {
min = &nums[i]
}
if nums[i] > *max {
max = &nums[i]
}
}
return min, max
}

numbers := []int{3, 7, 2, 9, 1, 5}
minPtr, maxPtr := getMinMax(numbers)
if minPtr != nil && maxPtr != nil {
fmt.Printf("Numbers: %v\n", numbers)
fmt.Printf("Minimum: %d, Maximum: %d\n", *minPtr, *maxPtr)
}
// Output:
// Numbers: [3 7 2 9 1 5]
// Minimum: 1, Maximum: 9
}

Nil Pointers and Safety

Nil Pointer Handling

Nil Pointer Detection

Go provides ways to check if a pointer is nil before dereferencing.

Safe Pointer Operations

Always check for nil pointers before dereferencing to avoid runtime panics.

package main

import "fmt"

func main() {
// Nil pointers and safety examples
fmt.Println("Nil pointers and safety examples:")

// Nil pointer declaration
var nilPtr *int
fmt.Printf("nilPtr is nil: %t\n", nilPtr == nil)
fmt.Printf("nilPtr value: %v\n", nilPtr)
// Output:
// nilPtr is nil: true
// nilPtr value: <nil>

// Attempting to dereference nil pointer causes panic
// fmt.Printf("Dereferencing nil pointer: %d\n", *nilPtr) // This would panic

// Safe nil pointer check
if nilPtr != nil {
fmt.Printf("Value: %d\n", *nilPtr)
} else {
fmt.Println("Pointer is nil, cannot dereference")
}
// Output: Pointer is nil, cannot dereference

// Initialize pointer with new
var validPtr *int = new(int)
*validPtr = 42
fmt.Printf("validPtr is nil: %t\n", validPtr == nil)
fmt.Printf("validPtr value: %d\n", *validPtr)
// Output:
// validPtr is nil: false
// validPtr value: 42

// Function that safely handles nil pointers
func safeIncrement(ptr *int) {
if ptr != nil {
*ptr++
fmt.Printf("Incremented to: %d\n", *ptr)
} else {
fmt.Println("Cannot increment: pointer is nil")
}
}

safeIncrement(nilPtr) // Safe call with nil pointer
safeIncrement(validPtr) // Safe call with valid pointer
// Output:
// Cannot increment: pointer is nil
// Incremented to: 43

// Function that returns nil pointer
func findValue(slice []int, target int) *int {
for i := range slice {
if slice[i] == target {
return &slice[i]
}
}
return nil
}

numbers := []int{1, 2, 3, 4, 5}

// Search for existing value
found := findValue(numbers, 3)
if found != nil {
fmt.Printf("Found value: %d\n", *found)
} else {
fmt.Println("Value not found")
}
// Output: Found value: 3

// Search for non-existing value
notFound := findValue(numbers, 6)
if notFound != nil {
fmt.Printf("Found value: %d\n", *notFound)
} else {
fmt.Println("Value not found")
}
// Output: Value not found

// Struct with pointer fields
type Node struct {
Value int
Next *Node
}

// Create linked list
node1 := &Node{Value: 1}
node2 := &Node{Value: 2}
node3 := &Node{Value: 3}

node1.Next = node2
node2.Next = node3
node3.Next = nil // End of list

// Traverse linked list safely
fmt.Println("Linked list traversal:")
current := node1
for current != nil {
fmt.Printf("Node value: %d\n", current.Value)
current = current.Next
}
// Output:
// Linked list traversal:
// Node value: 1
// Node value: 2
// Node value: 3

// Function that safely traverses linked list
func printList(head *Node) {
if head == nil {
fmt.Println("List is empty")
return
}

current := head
for current != nil {
fmt.Printf("Value: %d", current.Value)
if current.Next != nil {
fmt.Print(" -> ")
} else {
fmt.Print(" -> nil")
}
current = current.Next
}
fmt.Println()
}

printList(node1)
printList(nil)
// Output:
// Value: 1 -> Value: 2 -> Value: 3 -> nil
// List is empty
}

Pointer Arrays and Slices

Pointers to Arrays and Slices

Array Pointers

Pointers can point to arrays, enabling efficient array manipulation.

Slice Pointers

Pointers to slices are useful for modifying slice headers.

package main

import "fmt"

func main() {
// Pointer arrays and slices examples
fmt.Println("Pointer arrays and slices examples:")

// Array and pointer to array
arr := [5]int{1, 2, 3, 4, 5}
var arrPtr *[5]int = &arr

fmt.Printf("Array: %v\n", arr)
fmt.Printf("Array through pointer: %v\n", *arrPtr)
// Output:
// Array: [1 2 3 4 5]
// Array through pointer: [1 2 3 4 5]

// Modify array through pointer
(*arrPtr)[0] = 100
arrPtr[1] = 200 // Shorthand notation
fmt.Printf("Modified array: %v\n", arr)
// Output: Modified array: [100 200 3 4 5]

// Function that takes array pointer
func doubleArray(ptr *[5]int) {
for i := range ptr {
ptr[i] *= 2
}
}

doubleArray(&arr)
fmt.Printf("Doubled array: %v\n", arr)
// Output: Doubled array: [200 400 6 8 10]

// Slice and pointer to slice
slice := []int{10, 20, 30, 40, 50}
var slicePtr *[]int = &slice

fmt.Printf("Slice: %v\n", slice)
fmt.Printf("Slice through pointer: %v\n", *slicePtr)
// Output:
// Slice: [10 20 30 40 50]
// Slice through pointer: [10 20 30 40 50]

// Modify slice through pointer
(*slicePtr)[0] = 1000
fmt.Printf("Modified slice: %v\n", slice)
// Output: Modified slice: [1000 20 30 40 50]

// Function that modifies slice through pointer
func appendToSlice(ptr *[]int, values ...int) {
*ptr = append(*ptr, values...)
}

appendToSlice(&slice, 60, 70, 80)
fmt.Printf("Appended slice: %v\n", slice)
// Output: Appended slice: [1000 20 30 40 50 60 70 80]

// Function that creates new slice and returns pointer
func createSlice(size int) *[]int {
slice := make([]int, size)
for i := range slice {
slice[i] = i * i
}
return &slice
}

newSlicePtr := createSlice(5)
fmt.Printf("Created slice: %v\n", *newSlicePtr)
// Output: Created slice: [0 1 4 9 16]

// Pointer to slice elements
numbers := []int{1, 2, 3, 4, 5}
var firstElement *int = &numbers[0]
var lastElement *int = &numbers[len(numbers)-1]

fmt.Printf("First element: %d\n", *firstElement)
fmt.Printf("Last element: %d\n", *lastElement)
// Output:
// First element: 1
// Last element: 5

// Modify elements through pointers
*firstElement = 100
*lastElement = 500
fmt.Printf("Modified slice: %v\n", numbers)
// Output: Modified slice: [100 2 3 4 500]

// Function that finds and returns pointer to element
func findElement(slice []int, target int) *int {
for i := range slice {
if slice[i] == target {
return &slice[i]
}
}
return nil
}

found := findElement(numbers, 3)
if found != nil {
fmt.Printf("Found element: %d\n", *found)
*found = 300 // Modify found element
fmt.Printf("Modified slice: %v\n", numbers)
}
// Output:
// Found element: 3
// Modified slice: [100 2 300 4 500]
}

Advanced Pointer Patterns

Pointer Patterns and Best Practices

Pointer Receivers in Methods

Using pointer receivers for methods that modify struct values.

Pointer Return Values

Returning pointers from functions for efficiency and modification.

package main

import "fmt"

func main() {
// Advanced pointer patterns and best practices examples
fmt.Println("Advanced pointer patterns and best practices examples:")

// Define a struct with pointer receiver methods
type Counter struct {
value int
name string
}

// Constructor that returns pointer
func NewCounter(name string) *Counter {
return &Counter{value: 0, name: name}
}

// Value receiver method (read-only)
func (c Counter) GetValue() int {
return c.value
}

// Value receiver method (read-only)
func (c Counter) GetName() string {
return c.name
}

// Value receiver method (read-only)
func (c Counter) String() string {
return fmt.Sprintf("Counter{%s: %d}", c.name, c.value)
}

// Pointer receiver method (can modify)
func (c *Counter) Increment() {
c.value++
}

// Pointer receiver method (can modify)
func (c *Counter) Decrement() {
c.value--
}

// Pointer receiver method (can modify)
func (c *Counter) SetValue(value int) {
c.value = value
}

// Pointer receiver method (can modify)
func (c *Counter) Reset() {
c.value = 0
}

// Create counter using constructor
counter := NewCounter("Main")
fmt.Printf("Created: %s\n", counter.String())
// Output: Created: Counter{Main: 0}

// Use pointer receiver methods
counter.Increment()
counter.Increment()
counter.Increment()
fmt.Printf("After incrementing: %s\n", counter.String())
// Output: After incrementing: Counter{Main: 3}

counter.Decrement()
fmt.Printf("After decrementing: %s\n", counter.String())
// Output: After decrementing: Counter{Main: 2}

counter.SetValue(100)
fmt.Printf("After setting value: %s\n", counter.String())
// Output: After setting value: Counter{Main: 100}

counter.Reset()
fmt.Printf("After reset: %s\n", counter.String())
// Output: After reset: Counter{Main: 0}

// Function that returns pointer to new struct
func createPerson(name string, age int) *Person {
return &Person{Name: name, Age: age}
}

type Person struct {
Name string
Age int
}

person := createPerson("Alice", 30)
fmt.Printf("Created person: %+v\n", *person)
// Output: Created person: {Name:Alice Age:30}

// Function that modifies struct through pointer
func updatePersonAge(p *Person, newAge int) {
p.Age = newAge
}

updatePersonAge(person, 31)
fmt.Printf("Updated person: %+v\n", *person)
// Output: Updated person: {Name:Alice Age:31}

// Pointer to interface
type Stringer interface {
String() string
}

var stringer Stringer = counter
fmt.Printf("Counter as Stringer: %s\n", stringer.String())
// Output: Counter as Stringer: Counter{Main: 0}

// Pointer to function
type Operation func(int, int) int

add := func(a, b int) int { return a + b }
multiply := func(a, b int) int { return a * b }

var opPtr *Operation = &add
result := (*opPtr)(10, 20)
fmt.Printf("Result of operation: %d\n", result)
// Output: Result of operation: 30

// Change function pointer
opPtr = &multiply
result = (*opPtr)(10, 20)
fmt.Printf("Result of operation: %d\n", result)
// Output: Result of operation: 200

// Best practices summary
fmt.Println("\nBest practices for pointers:")
fmt.Println("1. Use pointers for large structs to avoid copying")
fmt.Println("2. Use pointer receivers for methods that modify the receiver")
fmt.Println("3. Always check for nil pointers before dereferencing")
fmt.Println("4. Use new() or & operator to create pointers")
fmt.Println("5. Be consistent with pointer usage in your code")
fmt.Println("6. Use pointers to share data between functions")
fmt.Println("7. Consider using pointers for optional values")
}

What You've Learned

Congratulations! You now have a comprehensive understanding of Go's pointers and references:

Pointer Fundamentals

  • Understanding pointer declaration with * and & operators
  • Working with pointer types and type safety
  • Understanding dereferencing and pointer operations
  • Using the new built-in function for memory allocation

Pointer Operations

  • Working with pointer assignment and comparison
  • Understanding pointer arithmetic limitations
  • Using pointers to structs and arrays
  • Implementing pointer-to-pointer patterns

Pointer Functions and Parameters

  • Using pointers as function parameters
  • Implementing pass-by-reference semantics
  • Returning pointers from functions
  • Understanding memory efficiency with pointers

Nil Pointer Safety

  • Understanding nil pointer behavior
  • Implementing safe nil pointer checks
  • Handling nil pointers in functions
  • Preventing runtime panics from nil dereferencing

Advanced Pointer Patterns

  • Using pointers with arrays and slices
  • Implementing pointer receiver methods
  • Working with pointer patterns and best practices
  • Understanding pointer efficiency considerations

Key Concepts

  • * - Pointer type declaration and dereferencing operator
  • & - Address-of operator for getting variable addresses
  • new - Built-in function for allocating memory
  • Nil pointers - Safe handling of uninitialized pointers
  • Type safety - Go's type-safe pointer system

Next Steps

You now have a solid foundation in Go's pointers and references. In the next section, we'll explore struct embedding, which enables composition over inheritance and provides powerful code reuse mechanisms.

Understanding pointers is crucial for writing efficient Go programs and implementing advanced data structures. These concepts form the foundation for all the more advanced programming techniques we'll cover in the coming chapters.


Ready to learn about struct embedding? Let's explore Go's powerful embedding system and learn how to compose types effectively!