0
0
Goprogramming~15 mins

Passing values vs pointers in Go - Trade-offs & Expert Analysis

Choose your learning style9 modes available
Overview - Passing values vs pointers
What is it?
Passing values vs pointers in Go means deciding whether to send a copy of data or a reference to the original data when calling functions. Passing by value sends a copy, so changes inside the function don't affect the original. Passing by pointer sends the address, so the function can modify the original data. This choice affects how your program uses memory and behaves.
Why it matters
Without understanding passing values vs pointers, you might accidentally change data you didn't want to or waste memory copying large data. This can cause bugs or slow programs. Knowing when to use each helps write efficient, clear, and safe code that behaves as expected.
Where it fits
Before this, you should know basic Go variables, functions, and types. After this, you can learn about methods with pointer receivers, interfaces, and memory management in Go.
Mental Model
Core Idea
Passing by value sends a copy of data, while passing by pointer sends the address to the original data, allowing direct modification.
Think of it like...
It's like giving a friend a photocopy of your homework (value) versus giving them your actual homework notebook (pointer). Changes to the photocopy don't affect your original, but changes to the notebook do.
Function Call
  ├─ Pass by Value ──> [Copy of Data] ──> Function works on copy
  └─ Pass by Pointer -> [Address of Data] -> Function works on original

Memory:
  Original Data [1000] <─ Pointer holds address 1000
  Copy Data [2000] <─ Value is separate copy
Build-Up - 7 Steps
1
FoundationUnderstanding Passing by Value
🤔
Concept: Passing by value means sending a copy of the data to a function.
In Go, when you pass a variable to a function without using pointers, the function receives its own copy. Changes inside the function do not affect the original variable. Example: func changeValue(x int) { x = 10 } func main() { a := 5 changeValue(a) fmt.Println(a) // prints 5 }
Result
The original variable remains unchanged after the function call.
Understanding that passing by value protects the original data from accidental changes helps prevent bugs.
2
FoundationBasics of Passing by Pointer
🤔
Concept: Passing by pointer means sending the memory address of the data to a function.
In Go, you can pass a pointer to a variable using the & operator. The function receives the address and can modify the original data. Example: func changeValue(x *int) { *x = 10 } func main() { a := 5 changeValue(&a) fmt.Println(a) // prints 10 }
Result
The original variable is changed because the function modifies the data at the given address.
Knowing that pointers allow functions to modify original data is key to controlling side effects.
3
IntermediateWhen to Use Value vs Pointer
🤔Before reading on: Do you think passing large structs by value or pointer is more efficient? Commit to your answer.
Concept: Choosing between value and pointer depends on data size and whether you want to modify the original.
Small data like integers or booleans are cheap to copy, so passing by value is fine. Large structs or arrays are costly to copy, so passing pointers saves memory and time. Also, if you want the function to change the original data, use pointers. Example: // Large struct type Person struct { Name string Age int } func updateAge(p *Person) { p.Age = 30 } func main() { person := Person{"Alice", 25} updateAge(&person) fmt.Println(person.Age) // prints 30 }
Result
Using pointers for large data avoids expensive copying and allows modification.
Understanding efficiency and intent guides the choice between value and pointer passing.
4
IntermediatePointer Syntax and Dereferencing
🤔Before reading on: Does using a pointer variable automatically access the original data, or do you need to dereference it? Commit to your answer.
Concept: Pointers hold addresses; to access or change the original data, you must dereference them with *.
In Go, * is used to get or set the value at the pointer's address. The & operator gets the address of a variable. Example: var x int = 5 var p *int = &x // p holds address of x fmt.Println(*p) // prints 5 *p = 10 // changes x to 10 fmt.Println(x) // prints 10
Result
Dereferencing pointers lets you read or modify the original variable.
Knowing how to use & and * correctly prevents common pointer mistakes.
5
IntermediatePassing Pointers to Structs and Slices
🤔
Concept: Structs and slices behave differently with pointers; slices are reference types but structs are value types.
Structs are copied when passed by value, so use pointers to modify them. Slices hold pointers internally, so passing a slice by value still allows modifying the underlying array. Example: func modifySlice(s []int) { s[0] = 100 } func modifyStruct(p *Person) { p.Name = "Bob" } func main() { nums := []int{1, 2, 3} modifySlice(nums) fmt.Println(nums[0]) // prints 100 person := Person{"Alice", 25} modifyStruct(&person) fmt.Println(person.Name) // prints Bob }
Result
Slices can be modified without pointers; structs need pointers to modify original data.
Understanding Go's slice internals clarifies when pointers are needed.
6
AdvancedPointer Semantics in Method Receivers
🤔Before reading on: Do methods with pointer receivers modify the original struct or a copy? Commit to your answer.
Concept: Methods can have value or pointer receivers, affecting whether they modify the original struct or a copy.
If a method has a pointer receiver, it can change the struct's fields. If it has a value receiver, it works on a copy. Example: type Counter struct { Count int } func (c *Counter) Increment() { c.Count++ } func (c Counter) Reset() { c.Count = 0 } func main() { c := Counter{5} c.Increment() fmt.Println(c.Count) // prints 6 c.Reset() fmt.Println(c.Count) // still prints 6 }
Result
Pointer receiver methods modify the original; value receiver methods do not.
Knowing method receiver types helps control when structs are changed.
7
ExpertSubtle Pointer Pitfalls and Escape Analysis
🤔Before reading on: Does passing a pointer always improve performance? Commit to your answer.
Concept: Passing pointers can sometimes hurt performance due to Go's escape analysis and memory allocation behavior.
Go's compiler decides if variables escape to the heap or stay on the stack. Passing pointers may cause variables to escape, increasing garbage collection overhead. Also, small structs passed by pointer might be slower due to indirection. Example: func process(p *Person) { // p may cause heap allocation } func main() { person := Person{"Alice", 25} process(&person) } Sometimes passing by value is faster for small structs. Understanding escape analysis helps write efficient code.
Result
Pointer use can increase heap allocations and reduce performance if not used carefully.
Knowing Go's escape analysis prevents blindly using pointers and helps optimize memory use.
Under the Hood
When you pass by value, Go copies the data into a new memory location for the function. When you pass a pointer, Go copies the address (a small fixed-size value) instead. The function uses this address to access or modify the original data in memory. The compiler and runtime manage these copies and references, deciding where variables live (stack or heap) based on usage.
Why designed this way?
Go was designed for simplicity and performance. Passing by value avoids unintended side effects by default, making code safer. Pointers provide controlled access to original data when needed. This balance helps prevent bugs common in languages with unrestricted pointers, while still allowing efficient memory use.
Caller Stack Frame
┌─────────────────────┐
│ Original Variable    │
│ Value: 42           │
│ Address: 0xc0000140  │
└─────────┬───────────┘
          │
          │ Pass by Value (copy)
          ▼
Function Stack Frame
┌─────────────────────┐
│ Parameter Variable   │
│ Value: 42 (copy)    │
└─────────────────────┘

OR

Caller Stack Frame
┌─────────────────────┐
│ Original Variable    │
│ Value: 42           │
│ Address: 0xc0000140  │
└─────────┬───────────┘
          │
          │ Pass by Pointer (address)
          ▼
Function Stack Frame
┌─────────────────────┐
│ Parameter Variable   │
│ Value: 0xc0000140   │
└─────────────────────┘
          │
          ▼
Dereference to access original data
Myth Busters - 4 Common Misconceptions
Quick: Does passing a slice by value prevent modifying its elements? Commit to yes or no.
Common Belief:Passing a slice by value means the function cannot change the original slice's elements.
Tap to reveal reality
Reality:Slices are reference types; passing by value copies the slice header but not the underlying array, so the function can modify the original elements.
Why it matters:Assuming slices are fully copied can lead to unexpected data changes and bugs.
Quick: Does passing a pointer always improve performance? Commit to yes or no.
Common Belief:Using pointers always makes the program faster by avoiding copying data.
Tap to reveal reality
Reality:For small data, passing by value can be faster. Pointers add indirection and may cause heap allocations, hurting performance.
Why it matters:Blindly using pointers can degrade performance and increase garbage collection.
Quick: If a function receives a pointer, does it always modify the original data? Commit to yes or no.
Common Belief:If a function takes a pointer, it will always change the original variable.
Tap to reveal reality
Reality:The function can choose not to modify the data; passing a pointer only allows modification but doesn't force it.
Why it matters:Assuming all pointer parameters change data can cause confusion and unnecessary defensive code.
Quick: Does a method with a value receiver modify the original struct? Commit to yes or no.
Common Belief:Methods with value receivers can change the original struct's fields.
Tap to reveal reality
Reality:Value receiver methods work on copies; they cannot change the original struct's fields.
Why it matters:Misunderstanding this leads to bugs where changes seem ignored.
Expert Zone
1
Pointer receivers are necessary for methods that modify the receiver or to avoid copying large structs, but using them everywhere can reduce code clarity.
2
Escape analysis can cause variables to be allocated on the heap when pointers are passed, impacting performance in subtle ways.
3
Slices and maps are reference types internally, so passing them by value still allows modification of their contents, unlike arrays.
When NOT to use
Avoid pointers when working with small, immutable data where copying is cheap and safer. Use value receivers for methods that do not modify the receiver to keep code simple. For concurrency, avoid sharing pointers without synchronization to prevent race conditions.
Production Patterns
In real-world Go code, pointer receivers are common for structs representing entities or resources to allow efficient updates. Functions often accept pointers for large structs to reduce copying. Immutable data is passed by value to avoid side effects. Understanding escape analysis guides performance tuning in critical code.
Connections
Memory Management
Passing pointers relates directly to how memory is allocated and accessed.
Knowing how passing pointers affects memory allocation helps optimize program speed and resource use.
Functional Programming
Passing by value aligns with immutability principles in functional programming.
Understanding value passing clarifies how to write side-effect-free functions and safer code.
Human Communication
Passing values vs pointers is like sending copies of messages versus giving direct access to your notes.
This connection shows how controlling information sharing affects trust and control, similar to programming data.
Common Pitfalls
#1Modifying a copy when intending to change original data
Wrong approach:func update(x int) { x = 10 } func main() { a := 5 update(a) fmt.Println(a) // prints 5, not 10 }
Correct approach:func update(x *int) { *x = 10 } func main() { a := 5 update(&a) fmt.Println(a) // prints 10 }
Root cause:Passing by value copies data, so changes inside the function don't affect the original.
#2Assuming passing a slice by value prevents modification of elements
Wrong approach:func modify(s []int) { s[0] = 100 } func main() { nums := []int{1, 2, 3} modify(nums) fmt.Println(nums[0]) // prints 100 }
Correct approach:No change needed; this is correct behavior.
Root cause:Slices hold pointers internally, so passing them by value still allows modifying the underlying array.
#3Using pointer receivers unnecessarily for small structs
Wrong approach:func (p *Point) Distance() float64 { // method body } // Point is a small struct with two floats
Correct approach:func (p Point) Distance() float64 { // method body } // Use value receiver for small structs when no modification needed
Root cause:Unnecessary pointers add indirection and reduce code clarity without performance benefit.
Key Takeaways
Passing by value sends a copy of data, protecting the original from changes inside functions.
Passing by pointer sends the address, allowing functions to modify the original data directly.
Use value passing for small, immutable data and pointer passing for large data or when modification is needed.
Slices are reference types; passing them by value still allows modifying their elements.
Understanding Go's escape analysis helps avoid performance pitfalls when using pointers.