Pointers in Go

Pointers are a fundamental concept in Go that allow you to work directly with memory addresses. Understanding pointers is crucial for efficient programming, especially when dealing with large data structures or when you need to modify values in functions. This tutorial covers pointer basics, syntax, and common use cases in Go.

What are Pointers?

A pointer is a variable that stores the memory address of another variable. Instead of holding a value directly, it “points to” where the value is stored in memory.

var x int = 42          // x holds the value 42
var p *int = &x         // p holds the address of x
fmt.Println(p)          // Prints memory address, like 0xc0000140b0
fmt.Println(*p)         // Prints 42 (dereferencing)

Pointer Declaration and Initialization

Declaring Pointers

var p *int      // Pointer to int, initially nil
var s *string  // Pointer to string
var arr *[3]int // Pointer to array

Initializing Pointers

// Method 1: Using the address-of operator &
x := 42
p := &x

// Method 2: Using new() function
p2 := new(int)
*p2 = 100

// Method 3: Pointer to struct
type Person struct {
    name string
    age  int
}

person := Person{"Alice", 30}
ptr := &person

Dereferencing Pointers

Dereferencing means accessing the value that the pointer points to, using the * operator.

x := 42
p := &x

fmt.Println(x)   // 42
fmt.Println(*p)  // 42 (same value)

*p = 100         // Change value through pointer
fmt.Println(x)   // 100 (x was modified)

Pointers and Functions

Passing by Value (Default)

func modifyValue(x int) {
    x = 100  // This only changes the local copy
}

func main() {
    x := 42
    modifyValue(x)
    fmt.Println(x)  // Still 42
}

Passing by Reference (Using Pointers)

func modifyValue(ptr *int) {
    *ptr = 100  // This changes the original value
}

func main() {
    x := 42
    modifyValue(&x)
    fmt.Println(x)  // Now 100
}

Pointers with Different Types

Pointers to Slices

func modifySlice(s *[]int) {
    *s = append(*s, 4, 5, 6)
}

func main() {
    numbers := []int{1, 2, 3}
    modifySlice(&numbers)
    fmt.Println(numbers)  // [1 2 3 4 5 6]
}

Pointers to Maps

func addEntry(m *map[string]int, key string, value int) {
    (*m)[key] = value
}

func main() {
    scores := make(map[string]int)
    addEntry(&scores, "Alice", 95)
    fmt.Println(scores)  // map[Alice:95]
}

Pointers to Structs

type Rectangle struct {
    width, height float64
}

func (r *Rectangle) scale(factor float64) {
    r.width *= factor
    r.height *= factor
}

func main() {
    rect := Rectangle{10, 20}
    rect.scale(2)
    fmt.Println(rect)  // {20 30}
}

Nil Pointers

Pointers that don’t point to anything are nil.

var p *int
fmt.Println(p)        // <nil>
fmt.Println(p == nil) // true

// Dereferencing nil pointer causes panic
// fmt.Println(*p)  // Runtime panic!

// Safe dereferencing
if p != nil {
    fmt.Println(*p)
}

Pointer Arithmetic (Not Allowed in Go)

Unlike C/C++, Go doesn’t allow pointer arithmetic for safety reasons.

// This doesn't work in Go
p := &x
p++  // Compiler error!

The new() Function

new() allocates memory and returns a pointer to the zero value of the type.

p := new(int)     // Equivalent to var p *int = new(int)
fmt.Println(*p)   // 0

s := new(string)  // Pointer to empty string
fmt.Println(*s)   // ""

arr := new([3]int) // Pointer to zero-valued array
fmt.Println(*arr)  // [0 0 0]

Pointer Receivers in Methods

When to use pointer receivers vs value receivers.

type Counter struct {
    count int
}

// Value receiver - doesn't modify original
func (c Counter) increment() {
    c.count++
}

// Pointer receiver - modifies original
func (c *Counter) incrementPtr() {
    c.count++
}

func main() {
    c := Counter{0}
    
    c.increment()
    fmt.Println(c.count)  // 0 (unchanged)
    
    c.incrementPtr()
    fmt.Println(c.count)  // 1 (changed)
}

Common Patterns

Returning Pointers from Functions

func createPerson(name string, age int) *Person {
    return &Person{name: name, age: age}
}

func main() {
    p := createPerson("Bob", 25)
    fmt.Println(p.name)  // Bob
}

Avoiding Large Struct Copies

type LargeStruct struct {
    data [1000]int
}

// Pass by value (expensive)
func processLargeStruct(s LargeStruct) {
    // Function gets a copy of the entire struct
}

// Pass by pointer (efficient)
func processLargeStructPtr(s *LargeStruct) {
    // Function only gets the pointer
}

Linked List Implementation

type Node struct {
    value int
    next  *Node
}

func main() {
    // Create nodes
    head := &Node{value: 1}
    head.next = &Node{value: 2}
    head.next.next = &Node{value: 3}
    
    // Traverse
    current := head
    for current != nil {
        fmt.Println(current.value)
        current = current.next
    }
}

Memory Management

Go has automatic garbage collection, so you don’t need to manually free memory like in C/C++.

func createData() *[]int {
    data := []int{1, 2, 3}
    return &data  // Pointer to local variable is OK
}

func main() {
    p := createData()
    fmt.Println(*p)  // [1 2 3] - data still accessible
}

Common Mistakes

Forgetting to Dereference

x := 42
p := &x
fmt.Println(p)   // Prints address
fmt.Println(*p)  // Prints 42

Modifying Copies

type Config struct {
    debug bool
}

func enableDebug(c Config) {  // Value parameter
    c.debug = true
}

func main() {
    c := Config{debug: false}
    enableDebug(c)
    fmt.Println(c.debug)  // false (unchanged)
}

Returning Pointers to Local Variables

func badFunction() *int {
    x := 42
    return &x  // Don't do this!
}

func goodFunction() *int {
    x := 42
    return &x  // This is actually OK in Go
}

Best Practices

  1. Use pointers when you need to modify the original value in functions
  2. Use pointers for large structs to avoid expensive copies
  3. Use value receivers for small structs or when you don’t need to modify
  4. Check for nil pointers before dereferencing
  5. Be consistent with pointer vs value receivers in methods
  6. Document when functions expect pointers in comments

Complete Example

package main

import "fmt"

type BankAccount struct {
    owner   string
    balance float64
}

func (b *BankAccount) deposit(amount float64) {
    if amount > 0 {
        b.balance += amount
        fmt.Printf("Deposited $%.2f. New balance: $%.2f\n", amount, b.balance)
    }
}

func (b *BankAccount) withdraw(amount float64) bool {
    if amount > 0 && amount <= b.balance {
        b.balance -= amount
        fmt.Printf("Withdrew $%.2f. New balance: $%.2f\n", amount, b.balance)
        return true
    }
    fmt.Println("Insufficient funds or invalid amount")
    return false
}

func (b *BankAccount) getBalance() float64 {
    return b.balance
}

func createAccount(owner string, initialDeposit float64) *BankAccount {
    account := &BankAccount{owner: owner, balance: 0}
    account.deposit(initialDeposit)
    return account
}

func main() {
    // Create account
    account := createAccount("Alice", 1000.0)
    
    // Perform transactions
    account.deposit(500.0)
    account.withdraw(200.0)
    account.withdraw(2000.0)  // Should fail
    
    fmt.Printf("Final balance for %s: $%.2f\n", account.owner, account.getBalance())
    
    // Demonstrate pointer concepts
    balancePtr := &account.balance
    *balancePtr += 100  // Direct modification through pointer
    fmt.Printf("After bonus: $%.2f\n", account.balance)
}

Summary

Pointers in Go provide:

  • Direct memory access for efficient operations
  • Ability to modify values in functions
  • Reduced copying for large data structures
  • Method receivers for object-oriented programming

Key operators:

  • & - Address of (create pointer)
  • * - Dereference (access value)

While pointers are powerful, Go’s design encourages using them judiciously, with value semantics being the default.


External Resources:

Related Tutorials:

  • Learn about Go data types here to understand what types can have pointers.
  • Check out Go functions here to see how pointers work with function parameters.
Last updated on