Letβs dive deep into Pointers in GoLang, one of the most important concepts in programming, especially when we need efficient memory usage or want to manipulate values directly.
A pointer is a variable that stores the memory address of another variable.
Think of it like a reference or map location to the original value.
- Efficiency: Instead of copying large data structures, you can pass pointers.
- Direct Modification: Functions can change the original value.
- Data Sharing: Multiple parts of a program can share and modify the same data.
var a int = 42
var p *int = &a
a
is anint
variable.&a
gives the address ofa
.p
is a pointer to an int (*int
), and stores the address ofa
.
fmt.Println(a) // 42
fmt.Println(&a) // e.g., 0xc000018090
fmt.Println(p) // same as &a
fmt.Println(*p) // 42 (value at the address)
The *
operator is used to dereference a pointer β i.e., get the value at the memory address:
*p = 100
fmt.Println(a) // 100
This modifies the original variable a
through its pointer.
Without Pointers:
func increment(x int) {
x = x + 1
}
The original value remains unchanged.
With Pointers:
func increment(x *int) {
*x = *x + 1
}
func main() {
num := 10
increment(&num)
fmt.Println(num) // 11
}
β
Now num
is actually modified!
*int
: Pointer to an integer*string
: Pointer to a string*struct
: Pointer to a struct- etc.
Pointers in Go can be nil
(i.e., not pointing to anything):
var ptr *int
fmt.Println(ptr) // <nil>
Trying to dereference a nil
pointer will cause a runtime panic, so always check:
if ptr != nil {
fmt.Println(*ptr)
}
Go provides a built-in way to allocate memory:
ptr := new(int)
*ptr = 5
fmt.Println(*ptr) // 5
-
new(int)
returns a pointer to a newly allocated zero-valued int. -
Equivalent to:
var x int ptr := &x
type Person struct {
name string
age int
}
func updateAge(p *Person) {
p.age += 1
}
func main() {
skyy := Person{name: "Skyy", age: 29}
updateAge(&skyy)
fmt.Println(skyy.age) // 30
}
Even with dot notation:
p := &Person{"Skyy", 29}
p.age = 30 // Go automatically dereferences the pointer!
- Slices, maps, and channels are reference types.
- You donβt need pointers to modify them because they already refer to underlying data.
- But for arrays or structs, pointers are often needed.
Concept | Explanation |
---|---|
*T |
Pointer to type T |
&x |
Address of variable x |
*p |
Value stored at pointer p |
nil |
Default zero value of a pointer |
new(T) |
Allocates memory for type T and returns a pointer to it |
Now.. Let's explore more real-world use cases of pointers in Go, especially where they shine and offer powerful benefits in terms of performance and flexibility.
Go passes arguments by value by default. So if we want to modify a variable inside a function, we must pass its pointer.
func double(n *int) {
*n = *n * 2
}
func main() {
x := 10
double(&x)
fmt.Println(x) // 20
}
- Updating counters, configurations, or object states within reusable functions.
If we pass structs (especially large ones) to functions by value, it copies the entire struct. Using pointers avoids that overhead.
type User struct {
name string
age int
}
func updateName(u *User, newName string) {
u.name = newName
}
- Updating database models, request structs, or any large data types without copying them.
Pointers are essential to building dynamic structures like:
type Node struct {
value int
next *Node
}
- Building custom data structures: linked lists, trees, graphs, etc.
Sometimes instead of returning large structs, we return pointers to avoid duplication.
func createUser() *User {
return &User{name: "Skyy", age: 29}
}
- Lightweight memory management, working with APIs, database fetches.
Even though slices and maps are reference types, you may still need to pass a pointer to a slice when replacing the entire slice inside a function.
func reset(slice *[]int) {
*slice = []int{} // replaces original slice
}
- Resetting or reallocating slices or maps from a function.
Using pointers to share a single state between different parts of your program.
type Config struct {
AppName string
}
var appConfig *Config
func initConfig() {
appConfig = &Config{AppName: "GoBank"}
}
- Global config, shared state, singleton pattern in services.
Use pointer receivers when methods should modify the object:
func (u *User) IncrementAge() {
u.age++
}
- When we want method calls like
user.IncrementAge()
to change the original data.
Pointer receivers allow method chaining:
func (b *Builder) SetName(name string) *Builder {
b.name = name
return b
}
- Fluent APIs, builders, chained methods.
Pointers can be used to represent optional fields in structs, useful in APIs:
type UpdateUserRequest struct {
Name *string
Age *int
}
- If
Name
is nil, the client didnβt intend to update it. - If
Name
is not nil, we update it.
- Partial updates (PATCH APIs), optional query params, form inputs.
You can allocate zero-valued memory using new()
and store it in a pointer.
n := new(int)
*n = 42
- When you donβt need a name for a value but still need a pointer.
Use Case | Why Use Pointers? |
---|---|
Function argument modification | Allows updating values outside the function |
Working with structs | Prevents unnecessary copying of large data |
Data structures (Linked list, Tree) | Enables dynamic connections between elements |
Efficient return of large objects | Avoids copying data when returning from functions |
Modifying slices/maps inside functions | Enables total replacement of referenced data |
Shared config/state across program | Enables global access to mutable data |
Mutating method receivers | Lets methods update object state directly |
Optional fields in JSON APIs | Nil = not provided by client |
Builder patterns | Enables method chaining |
Low-level memory control (new ) |
Direct memory allocation |