Pointers
Go's pointers allow you to work directly with memory addresses. Unlike C/C++ pointers, Go has no pointer arithmetic, making them safe, while the garbage collector manages memory. Understanding pointers helps reduce value copy costs, modify variables outside functions, and write efficient struct methods.
What is a Pointer?β
A pointer is a variable that stores the memory address of another variable. Go uses two operators:
&(address-of operator): obtains the memory address of a variable*(dereference operator): accesses the value the pointer points to
package main
import "fmt"
func main() {
x := 42
// &x: get x's memory address, store in pointer variable p
p := &x
fmt.Println("x value:", x) // 42
fmt.Println("x address:", &x) // 0xc0000b4008 (varies)
fmt.Println("p value:", p) // 0xc0000b4008 (address)
fmt.Println("p dereferenced:", *p) // 42
// Change x's value through *p
*p = 100
fmt.Println("x after change:", x) // 100
fmt.Println("*p after change:", *p) // 100
}
Pointer Type Declarationβ
Pointer types are declared as *TypeName.
package main
import "fmt"
func main() {
var p1 *int // pointer to int (nil)
var p2 *string // pointer to string (nil)
var p3 *float64 // pointer to float64 (nil)
fmt.Println(p1, p2, p3) // <nil> <nil> <nil>
// Assign address to pointer
n := 10
p1 = &n
fmt.Println(*p1) // 10
s := "hello"
p2 = &s
fmt.Println(*p2) // hello
// Check pointer type
fmt.Printf("p1 type: %T\n", p1) // *int
fmt.Printf("p2 type: %T\n", p2) // *string
}
nil Pointersβ
A declared but uninitialized pointer holds the nil value. Dereferencing a nil pointer causes a runtime panic.
package main
import "fmt"
func safeDeref(p *int) {
// Always check for nil!
if p == nil {
fmt.Println("pointer is nil")
return
}
fmt.Println("value:", *p)
}
func main() {
var p *int
fmt.Println(p == nil) // true
safeDeref(p) // "pointer is nil"
n := 42
safeDeref(&n) // "value: 42"
// Warning: dereferencing nil pointer causes panic!
// fmt.Println(*p) // panic: runtime error: invalid memory address or nil pointer dereference
}
The new() Functionβ
new() allocates zero-initialized memory for a specified type on the heap and returns a pointer to it.
package main
import "fmt"
func main() {
// new(T) allocates T's zero value and returns *T
p := new(int)
fmt.Println(*p) // 0 (zero value of int)
fmt.Printf("%T\n", p) // *int
*p = 100
fmt.Println(*p) // 100
// Works with structs too
type Point struct {
X, Y int
}
pt := new(Point)
fmt.Println(*pt) // {0 0}
pt.X = 10
pt.Y = 20
fmt.Println(*pt) // {10 20}
// Comparing new(int) with &int{}
p1 := new(int)
n := 0
p2 := &n
fmt.Println(*p1 == *p2) // true (both 0)
}
Using Pointers as Parametersβ
Go uses pass by value by default. To modify the original variable inside a function, pass a pointer.
package main
import "fmt"
// Pass by value: cannot modify original
func doubleByValue(n int) {
n *= 2
fmt.Println("inside function:", n)
}
// Pass by pointer: can modify original
func doubleByPointer(n *int) {
*n *= 2
fmt.Println("inside function:", *n)
}
// Swap two variables
func swap(a, b *int) {
*a, *b = *b, *a
}
func main() {
x := 10
doubleByValue(x)
fmt.Println("x after value pass:", x) // 10 (unchanged)
y := 10
doubleByPointer(&y)
fmt.Println("y after pointer pass:", y) // 20 (changed)
a, b := 3, 7
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)
}
Returning Pointers from Functionsβ
Returning a pointer to a local variable is safe in Go. The compiler allocates the variable on the heap instead of the stack (escape analysis).
package main
import "fmt"
// Returning pointer to local variable β completely safe in Go!
func newInt(n int) *int {
result := n // may move to heap when pointer is returned
return &result
}
type Counter struct {
count int
}
func newCounter() *Counter {
return &Counter{count: 0} // allocated on heap
}
func (c *Counter) Increment() {
c.count++
}
func (c *Counter) Value() int {
return c.count
}
func main() {
p := newInt(42)
fmt.Println(*p) // 42
c := newCounter()
c.Increment()
c.Increment()
c.Increment()
fmt.Println(c.Value()) // 3
// Multiple pointers sharing the same data
p1 := newInt(100)
p2 := p1 // p2 points to same address as p1
*p2 = 200
fmt.Println(*p1) // 200 (p1 also changed)
fmt.Println(*p2) // 200
}
Structs and Pointersβ
When working with struct pointers, Go automatically dereferences them, so you can use p.Field instead of (*p).Field.
package main
import "fmt"
type Person struct {
Name string
Age int
}
func birthday(p *Person) {
p.Age++ // same as (*p).Age++ β Go auto-dereferences
}
func changeName(p *Person, name string) {
p.Name = name
}
func main() {
alice := Person{Name: "Alice", Age: 29}
fmt.Println(alice) // {Alice 29}
birthday(&alice)
fmt.Println(alice) // {Alice 30}
changeName(&alice, "Alicia")
fmt.Println(alice) // {Alicia 30}
// Struct declared as pointer
bob := &Person{Name: "Bob", Age: 25}
bob.Age = 26 // auto-dereference
fmt.Println(*bob) // {Bob 26}
// Pointer comparison
p1 := &alice
p2 := &alice
fmt.Println(p1 == p2) // true (same address)
carol := Person{Name: "Carol", Age: 22}
p3 := &carol
fmt.Println(p1 == p3) // false (different address)
}
Double Pointersβ
You can have pointers to pointers. Not frequently used, but important to understand.
package main
import "fmt"
func modifyPointer(pp **int, newVal int) {
n := newVal
*pp = &n
}
func main() {
x := 42
p := &x // p points to x
pp := &p // pp points to p
fmt.Println(x) // 42
fmt.Println(*p) // 42
fmt.Println(**pp) // 42
**pp = 100
fmt.Println(x) // 100
// Modifying the pointer itself
y := 999
*pp = &y
fmt.Println(*p) // 999 (p now points to y)
fmt.Println(x) // 100 (x unchanged)
// Changing pointer through function
modifyPointer(&p, 777)
fmt.Println(*p) // 777
}
Slices, Maps, and Pointersβ
Slices and maps already contain internal pointers, so you often don't need additional pointers when passing them to functions.
package main
import "fmt"
// Modifying slice contents β reflected without a pointer
func addOne(nums []int) {
for i := range nums {
nums[i]++
}
}
// append returns a new slice, so a pointer or return value is needed
func appendElement(nums *[]int, elem int) {
*nums = append(*nums, elem)
}
// Modifying a map β reflected without a pointer
func addEntry(m map[string]int, key string, val int) {
m[key] = val
}
func main() {
// Modifying slice contents
s := []int{1, 2, 3, 4, 5}
addOne(s)
fmt.Println(s) // [2 3 4 5 6]
// append requires a pointer
appendElement(&s, 7)
fmt.Println(s) // [2 3 4 5 6 7]
// Map can be modified without a pointer
m := map[string]int{"a": 1}
addEntry(m, "b", 2)
fmt.Println(m) // map[a:1 b:2]
}
Practical Example: Linked Listβ
A linked list implementation using pointers.
package main
import "fmt"
type Node struct {
Value int
Next *Node
}
type LinkedList struct {
Head *Node
Size int
}
func (l *LinkedList) Push(val int) {
node := &Node{Value: val, Next: l.Head}
l.Head = node
l.Size++
}
func (l *LinkedList) Pop() (int, bool) {
if l.Head == nil {
return 0, false
}
val := l.Head.Value
l.Head = l.Head.Next
l.Size--
return val, true
}
func (l *LinkedList) Print() {
current := l.Head
for current != nil {
fmt.Printf("%d", current.Value)
if current.Next != nil {
fmt.Print(" -> ")
}
current = current.Next
}
fmt.Println()
}
func main() {
list := &LinkedList{}
list.Push(1)
list.Push(2)
list.Push(3)
list.Print() // 3 -> 2 -> 1
val, ok := list.Pop()
fmt.Printf("Pop: %d, ok: %v\n", val, ok) // Pop: 3, ok: true
list.Print() // 2 -> 1
fmt.Println("size:", list.Size) // size: 2
}
Key Summary
&variable: get address,*pointer: access the pointed-to value- Dereferencing nil pointer causes panic β always check for nil
new(T): returns a pointer to a zero-initialized T- Returning local variable pointers is safe in Go (escape analysis)
- Struct pointers auto-dereference with
.operator- Slices and maps already contain internal pointers