Day 7 - Pointers
Before going to variables first let us understand variables and how they store values.
What Are Variables, and How Do They Store Values?
A variable is a named storage location in memory that holds a value.
Each variable is stored at a specific memory address, usually represented in hexadecimal format (e.g., 0xFFAAF123).
Since memory addresses are hard to remember, we assign human-friendly names (variables) to them.
When we access a variable, we are indirectly accessing the value stored at its memory address.
What Is a Pointer?
A pointer is a special variable used to store the memory address of another variable rather than the actual value.
Pointers help us:
- Access the memory location of a variable
- Read or modify the actual value stored at that address
- Improve performance by avoiding unnecessary copies (especially for structs and arrays)
Pointer Operators
| Operator | Meaning | Usage |
& | Address operator | Returns memory address of a variable |
* | Dereference operator | Used to access the value at the given memory address |
Declaring and Initializing Pointers
var s *string // Declaration of a pointer to a string
var a = 34
addr := &a // 'addr' stores the address of variable 'a'
var ptr *int = addr // Initializing pointer 'ptr' with memory address of 'a'
Dereferencing a Pointer
package main
import "fmt"
func main() {
a := 10
addrOfA := &a
fmt.Println("Memory address of 'a':", addrOfA)
// Dereferencing the pointer
fmt.Println("Value stored at", addrOfA, "is", *addrOfA)
}
Output:
Memory address of 'a': 0xc00000a0d8
Value stored at 0xc00000a0d8 is 10
Passing by Value vs Passing by Reference
package main
import "fmt"
// increment increments the value stored at the given memory address
func increment(a *int) {
*a = *a + 1
}
// incrementNormal increments only a copy of the value, not the original
func incrementNormal(a int) {
a = a + 1
}
type Person struct {
name string
age int
address string
}
func changeAge(p *Person) {
p.age = 23
}
func main() {
a := 10
ptrA := &a
fmt.Println("Memory address of 'a':", ptrA)
fmt.Println("Value stored at", ptrA, "is", *ptrA)
// Passing by reference (pointer)
counter := 0
increment(&counter)
fmt.Println("After increment() - counter:", counter)
// Passing by value (copy)
counter2 := 0
incrementNormal(counter2)
fmt.Println("After incrementNormal() - counter2:", counter2)
// Struct example
p1 := Person{name: "John", age: 20, address: "California, USA"}
changeAge(&p1)
fmt.Println("Updated Person:", p1)
}
Output:
Memory address of 'a': 0xc00000a0d8
Value stored at 0xc00000a0d8 is 10
After increment() - counter: 1
After incrementNormal() - counter2: 0
Updated Person: {John 23 California, USA}
Reference-Like Types (No need to send pointers)
| Type | Behavior |
Slices ([]T) | Contains a pointer to an underlying array; passing shares data |
Maps (map[K]V) | Handled internally by reference |
Channels (chan T) | Reference type enabling shared communication |
| Functions | Stored and passed by reference |
| Interfaces | Holds a reference to the underlying value |
Value Types (Copied on assignment/passing)
| Type | Pointer Recommended? | Reason |
| Structs | Usually | To avoid copying large data & allow mutation |
Arrays ([n]T) | Yes | Full array is copied by default |
Basic types (int, string, bool, float64) | ❌ Usually no | Lightweight and copied quickly |
Example: Slice Passed Without Pointer (Still Affected)
package main
import "fmt"
func modifySlice(slice []int) {
slice[0] = -1
}
func main() {
slice := []int{1, 2, 3, 4, 5}
modifySlice(slice)
fmt.Println(slice)
}
Output:```
[-1 2 3 4 5]
### Example: Array Passed Without Pointer (Not Affected Unless Pointer is Used)
```go
package main
import "fmt"
func modifyArray(arr [4]int) {
arr[0] = -1
}
func modifyArrayUsingPtr(arr *[4]int) {
arr[0] = -1
}
func main() {
arr := [4]int{2, 3, 4, 5}
modifyArray(arr)
fmt.Println(arr)
modifyArrayUsingPtr(&arr)
fmt.Println(arr)
}
Output:
[2 3 4 5]
[-1 3 4 5]
When to Use Pointers in Go (Golang)
Pointers in Go allow you to pass references to variables rather than copying their values. This is especially useful in several real-world scenarios.
1. Large Structs (Performance Optimization)
When a struct is large, passing it by value leads to copying all its fields every time it’s passed to a function — which is inefficient. Using pointers avoids copying by passing a reference instead.
package main
import "fmt"
type Employee struct {
Name string
Age int
Address string
Position string
Salary float64
}
// Passing by pointer avoids copying the entire struct
func UpdatePosition(e *Employee, newPosition string) {
e.Position = newPosition
}
func main() {
emp := Employee{"John", 30, "NY", "Developer", 60000}
UpdatePosition(&emp, "Senior Developer")
fmt.Println(emp.Position) // Output: Senior Developer
}
Since we're using a pointer (*Employee), we modify the original struct without copying it.
2. Mutability (Modify Original Data)
If you need to modify a variable or struct inside a function, you must pass a pointer. Passing by value creates a copy, and changes won't affect the original.
3. Representing Null / Optional Values
Go does not support null for structs or primitive values directly, but pointers can hold a nil value to indicate absence. This is useful when something is optional or may not exist. You can see this struct
type Person struct {
Name string
Age int
Office *string // Optional field using pointer
}
func main() {
office := "Bangalore"
person1 := Person{"Alice", 25, &office} // Has an office
person2 := Person{"Bob", 28, nil} // Office unknown
fmt.Println(person1.Office) // Output: address reference
fmt.Println(person2.Office) // Output: <nil>
}
Drawbacks of Using Pointers
| Drawback | Description |
| Performance Overhead | Passing pointers can trigger heap allocation via escape analysis, which is slower than stack allocation. |
| GC Pressure | Heap allocations lead to increased garbage collection (GC) activity, causing latency in performance-critical systems. |
| Nil Pointer Dereference Risk | Dereferencing a nil pointer causes runtime panics unless explicitly checked, requiring defensive programming. |
| Increased Code Complexity | Pointers add mental overhead, making code harder to read, understand, and maintain—especially for beginners. |
| Risk of Unintended Side Effects | Since pointers allow shared mutation of data, multiple functions can unknowingly modify the same value, causing hard-to-track bugs. |
Key Takeaways
- Use pointers when you want to modify the original variable or avoid copying large data.
- Slices, maps, functions, channels, and interfaces behave like references — changes reflect in the original.
- Arrays and structs are copied unless passed by pointer.