Go, unlike some other languages like C/C++, handles pointers in a more restricted
and safer way. While you can use pointers to directly manipulate memory addresses,
Go removes features like pointer arithmetic to prevent common pitfalls and enhance
memory safety.
Here's a detailed breakdown of pointers in Go:
Pointers in Go: Detailed Notes
I. What is a Pointer?
A pointer is a variable that stores the memory address of another variable. Instead
of holding a direct value1 (like an int or string), it holds the location in memory where
that value is stored.
Analogy: Think of a variable as a house, and its value as the family living inside. A
pointer is like a street address that tells you exactly where that house is located.
II. Why Use Pointers?
1. Passing by Reference (Implicitly): When you pass variables to functions in Go,
they are typically passed by value (a copy is made). Pointers allow you to pass a
variable's memory address, enabling the function to modify the original
variable directly, rather than just a copy. This is crucial for:
• Modifying function arguments.
• Implementing methods on custom types (receivers).
2. Efficiency: For large data structures (like structs or arrays), passing a pointer is
more efficient than passing a full copy of the entire structure. You're just
passing a small memory address, not duplicating potentially megabytes of
data.
3. Returning Multiple Values (Less Common now): While Go allows functions to
return multiple values, historically, pointers were sometimes used to achieve a
similar effect (e.g., passing pointers to empty variables for the function to fill).
4. Linked Data Structures: Pointers are fundamental for building data structures
like linked lists, trees, and graphs, where elements need to reference other
elements in memory.
5. Representing Optional/Nullable Values: Although nil for pointers serves this
purpose, it's worth noting that a pointer to a struct or primitive can be nil,
indicating the absence of a value.
III. Declaring and Initializing Pointers
You use the * (asterisk) symbol to denote a pointer type.
Syntax: var variableName *Type
Example:
Go
var p *int // Declares a pointer 'p' that can point to an integer.
// By default, it's initialized to nil (zero value for pointers).
To get the memory address of a variable, use the & (ampersand) operator, known as
the "address-of" operator.
Go
name := "Alice" // A string variable
var ptrName *string = &name // ptrName now holds the memory address of 'name'
// Using short variable declaration (type inference):
ptrName2 := &name // This is the idiomatic way in Go
IV. Dereferencing Pointers
To access the value that a pointer points to, you use the * (asterisk) operator again,
this time as the "dereference" or "indirection" operator.
Syntax: *pointerVariable
Example:
Go
value := 10
ptr := &value
fmt.Println("Value of 'value':", value) // Output: 10
fmt.Println("Address of 'value':", &value) // Output: 0xc0000140a0 (example address)
fmt.Println("Value of 'ptr' (address):", ptr) // Output: 0xc0000140a0 (same address)
fmt.Println("Value pointed to by 'ptr':", *ptr) // Output: 10 (dereferencing)
// Modifying the value through the pointer
*ptr = 20
fmt.Println("New value of 'value':", value) // Output: 20 (original variable changed)
V. The nil Pointer
The zero value for a pointer type is nil. A nil pointer does not point to any valid
memory address.
Go
var p *int
fmt.Println(p) // Output: <nil>
if p == nil {
fmt.Println("p is a nil pointer")
}
Attempting to dereference a nil pointer will cause a runtime panic:
Go
// var p *int
// fmt.Println(*p) // This will cause a runtime error (panic: runtime error: invalid memory address or nil
pointer dereference)
Always check for nil before dereferencing if there's a possibility the pointer might not
be pointing to a valid address.
VI. Pointers to Structs
Pointers are very commonly used with structs, especially when passing structs to
functions, as it avoids copying large data structures.
Go
type Person struct {
Name string
Age int
}
func main() {
p1 := Person{Name: "Bob", Age: 30}
pPtr := &p1 // pPtr is a pointer to a Person struct
fmt.Println("Original Name:", p1.Name) // Output: Original Name: Bob
// Accessing fields through a pointer to a struct
// Go automatically dereferences for field access: (*pPtr).Name is equivalent to pPtr.Name
fmt.Println("Name via pointer:", pPtr.Name) // Output: Name via pointer: Bob
// Modifying a field through the pointer
pPtr.Name = "Robert" // Automatically dereferenced
fmt.Println("New Name:", p1.Name) // Output: New Name: Robert (original struct changed)
// Explicit dereferencing also works:
(*pPtr).Age = 31
fmt.Println("New Age:", p1.Age) // Output: New Age: 31
}
Important Note: Go provides a syntactic sugar for accessing fields of a struct
through a pointer. If pPtr is a pointer to a struct, pPtr.FieldName is a shorthand for
(*pPtr).FieldName.
VII. Pointers and Function Arguments
This is one of the most common and important uses of pointers in Go.
Go
func modifyValue(x int) { // x is passed by value (a copy)
x = 100
}
func modifyValueByPointer(x *int) { // x is a pointer to an int
*x = 100 // Dereference x to modify the original value
}
func main() {
num := 50
fmt.Println("Before modifyValue:", num) // Output: 50
modifyValue(num)
fmt.Println("After modifyValue:", num) // Output: 50 (num remains unchanged)
fmt.Println("Before modifyValueByPointer:", num) // Output: 50
modifyValueByPointer(&num) // Pass the address of num
fmt.Println("After modifyValueByPointer:", num) // Output: 100 (num is changed!)
// For structs
user := Person{Name: "Alice", Age: 25}
fmt.Println("Before modification:", user)
// Function to modify a struct by pointer
changeAge := func(p *Person, newAge int) {
p.Age = newAge
}
changeAge(&user, 26) // Pass the address of user
fmt.Println("After modification:", user) // Output: {Alice 26}
}
VIII. No Pointer Arithmetic
Unlike C/C++, Go does not allow pointer arithmetic. You cannot add or subtract
integers from a pointer to move to adjacent memory locations. This is a deliberate
design choice to improve memory safety and simplify the language.
Go
// p := &someInt
// p++ // This is NOT allowed in Go
IX. Pointers to Pointers (Double Pointers)
Go supports pointers to pointers, though they are less common in everyday code.
Go
val := 10
p1 := &val // p1 is a *int, points to val
p2 := &p1 // p2 is a **int, points to p1 (which points to val)
fmt.Println("Value of val:", val) // 10
fmt.Println("Value of p1:", p1) // address of val
fmt.Println("Value of p2:", p2) // address of p1
fmt.Println("Dereference p1:", *p1) // 10
fmt.Println("Dereference p2 (once):", *p2) // address of val (the value of p1)
fmt.Println("Dereference p2 (twice):", **p2) // 10 (the value of val)
**p2 = 20 // Changes val to 20
fmt.Println("New value of val:", val) // 20
X. new() Function for Pointers
Go provides a built-in new() function that allocates memory for a new variable of a
given type and returns a pointer to its zero value.
Go
p := new(int) // p is a *int, points to a newly allocated int, initialized to 0
fmt.Println(*p) // Output: 0
*p = 42
fmt.Println(*p) // Output: 42
s := new(string) // s is a *string, points to a new string, initialized to ""
fmt.Println(*s) // Output: ""
This is equivalent to:
Go
var i int
p := &i // if you want to initialize i to its zero value then get its address
Or for structs:
Go
type Point struct {
X, Y int
}
p := new(Point) // p is a *Point, points to a new Point {0, 0}
fmt.Println(p.X, p.Y) // Output: 0 0
Using new() is less common for simple types; usually, you declare the variable directly
and take its address. It's more often seen when you need to allocate memory for a
new instance of a custom type on the heap and get a pointer to it immediately.
XI. Pointers vs. Values in Method Receivers
This is a fundamental concept in Go's object-oriented capabilities (though Go isn't
strictly OO).
• Value Receiver: func (p Person) MyMethod() { ... }
• The method operates on a copy of the receiver.
• Changes made to p inside MyMethod will not affect the original variable.
• Generally used for methods that only read the receiver's state.
• Pointer Receiver: func (p *Person) MyMethod() { ... }
• The method operates on the original receiver through its pointer.
• Changes made to p (or *p) inside MyMethod will affect the original
variable.
• Essential for methods that modify the receiver's state, or for large
structs to avoid copying.
Example:
Go
type Counter struct {
Count int
}
// Value receiver - does not modify original
func (c Counter) IncrementValue() {
c.Count++
fmt.Println("Inside IncrementValue (copy):", c.Count)
}
// Pointer receiver - modifies original
func (c *Counter) IncrementPointer() {
c.Count++ // Go automatically dereferences: (*c).Count++
fmt.Println("Inside IncrementPointer (original):", c.Count)
}
func main() {
c1 := Counter{Count: 0}
c1.IncrementValue()
fmt.Println("After IncrementValue:", c1.Count) // Output: After IncrementValue: 0
c2 := Counter{Count: 0}
c2.IncrementPointer() // Automatically takes address: (&c2).IncrementPointer()
fmt.Println("After IncrementPointer:", c2.Count) // Output: After IncrementPointer: 1
}
XII. When to Pass a Pointer vs. a Value
Pass by Value (default):
• Small, primitive types (int, bool, float, string - even though strings are
immutable, they are passed by value).
• When you don't want the function to modify the original variable.
• Arrays (full copy).
Pass by Pointer:
• Large structs or other complex data structures (to avoid expensive copying).
• When the function needs to modify the original variable.
• When a method needs to modify the receiver.
• When you need to represent an optional or "nullable" value (e.g., *int could be
nil).
Conclusion
Pointers in Go are a powerful feature, but their usage is more controlled than in
some other languages. They are essential for modifying variables outside the current
function scope, for optimizing memory usage with large data structures, and for
defining methods that alter the state of their receiver. By understanding the & and *
operators, and the implications of value vs. pointer semantics, you can effectively
leverage pointers for clean, efficient, and safe Go code.