Table of contents
- Why Deal with Addresses
- & — Getting the Address
- * — The Value at an Address
- Modifying the Original from a Function via Pointer
- new — Creating a Pointer
- Struct Pointers
- nil Pointers
- When to Use Pointers
Why Deal with Addresses
Go passes values by copy by default. When you pass a variable to a function, a copy is made, and no matter how much the function changes it, the original is unaffected.
package main
import "fmt"
func tryChange(x int) {
x = 100
}
func main() {
n := 42
tryChange(n)
fmt.Println(n) // 42 — unchanged
}
The x that tryChange receives is a copy of n. Setting it to 100 inside the function has no effect on the original n. For a small integer this is fine, but copying a large struct every time can be wasteful in terms of memory and performance. There are also cases where you need to modify the original directly.
This is where pointers come in.
& — Getting the Address
The & operator gets the memory address of a variable.
package main
import "fmt"
func main() {
x := 42
p := &x // p holds the address of x
fmt.Println(x) // 42
fmt.Println(p) // 0xc0000b6010 (memory address, varies per run)
fmt.Printf("type: %T\n", p) // *int
}
The type of p is *int. It means “a pointer to an int” — it holds the location where the value is stored, not the value itself.
* — The Value at an Address
The * operator accesses the actual value that a pointer points to. This is called dereferencing.
package main
import "fmt"
func main() {
x := 42
p := &x
fmt.Println(*p) // 42 — the value p points to
*p = 100 // Write 100 to the location p points to
fmt.Println(x) // 100 — the original changed!
}
*p = 100 writes 100 directly to the memory location that p points to. Since p and x look at the same memory, x’s value changes too. The principle is the same as C pointers, but Go is much safer since there’s no pointer arithmetic.
Here’s a visualization of how & and * connect to memory.
flowchart LR
subgraph Stack[Stack / variable space]
X["x: int<br/>value = 42"]
P["p: *int<br/>value = &x"]
end
P -->|reference| X
Op1["&x (get address)"] -.-> P
Op2["*p (dereference)"] -.-> X
Modifying the Original from a Function via Pointer
Let’s redo the failed tryChange with a pointer version.
package main
import "fmt"
func changeByPointer(p *int) {
*p = 100
}
func main() {
n := 42
changeByPointer(&n)
fmt.Println(n) // 100 — it changed this time
}
We pass the address with &n, and inside the function, dereference with *p to modify the value. This is a clear illustration of the difference between pass-by-value and pass-by-pointer.
Here’s a visual comparison of the two approaches from the caller’s perspective.
flowchart TB
subgraph ValCall[Pass by value]
N1["main: n = 42"] -->|copy| X1["tryChange: x = 42"]
X1 --> X2["x = 100<br/>(only the copy changes)"]
N1 -.no change.- End1["n is still 42"]
end
subgraph PtrCall[Pass by pointer]
N2["main: n = 42"] -->|"&n"| P1["changeByPointer: p = &n"]
P1 --> P2["*p = 100<br/>(write to original location)"]
N2 -.shared reference.- End2["n = 100"]
end
In summary:
| Passing method | What’s copied | Can modify original |
|---|---|---|
By value (func f(x int)) | The value itself | No |
By pointer (func f(p *int)) | The address (8 bytes) | Yes |
new — Creating a Pointer
The new function allocates the zero value of a specified type and returns a pointer to it.
package main
import "fmt"
func main() {
p := new(int) // *int, initialized to 0
fmt.Println(*p) // 0
*p = 42
fmt.Println(*p) // 42
s := new(string) // *string, initialized to ""
fmt.Println(*s) // (empty string)
}
new(int) allocates memory for an int, initializes it to 0, and returns its address. In practice, the &T{} pattern is used more often than new because you can set initial values right when creating the struct.
// Using new
p := new(Config)
p.Port = 8080
// Using &T{} — more idiomatic
p := &Config{Port: 8080}
Struct Pointers
Structs bundle multiple fields, so they can be large. Copying the entire struct every time you pass it to a function is inefficient, which is why structs are often handled via pointers.
package main
import "fmt"
type User struct {
Name string
Age int
}
func birthday(u *User) {
u.Age++ // Same as (*u).Age++, but Go auto-dereferences
}
func main() {
u := &User{Name: "Gopher", Age: 10}
birthday(u)
fmt.Printf("%s: %d years old\n", u.Name, u.Age) // Gopher: 11 years old
}
In Go, when accessing fields through a struct pointer, you can write u.Age instead of (*u).Age. The compiler handles the dereferencing automatically. It’s as if C’s -> operator is unified into . in Go, making the syntax cleaner.
Pointers are also commonly used in method receivers.
func (u *User) SetName(name string) {
u.Name = name // Modify the original
}
func (u User) GetName() string {
return u.Name // Read from a copy — no change to original
}
Use a pointer receiver (*User) when modifying the original and a value receiver (User) for read-only. However, the Go community convention is that if even one method uses a pointer receiver, all methods should use pointer receivers for consistency.
nil Pointers
When a pointer is declared without initialization, its default value is nil.
package main
import "fmt"
func main() {
var p *int
fmt.Println(p) // <nil>
if p != nil {
fmt.Println(*p) // Dereferencing nil without checking causes panic
}
}
Dereferencing a nil pointer causes a runtime panic. To write defensive code, develop the habit of nil-checking first in functions that receive pointers.
func printUser(u *User) {
if u == nil {
fmt.Println("no user information")
return
}
fmt.Printf("%s (%d years old)\n", u.Name, u.Age)
}
When to Use Pointers
You don’t need to use pointers everywhere. Here are some guidelines.
Use pointers when:
- A function needs to modify the original value
- The struct is large or copying cost is a concern
- nil is a meaningful value (e.g., representing “no configuration”)
Use values directly when:
- Small structs (2-3 fields or fewer, composed of basic types)
- Data that doesn’t need modification
- Avoiding shared state in concurrent environments
Go’s pointers are much safer compared to C. There’s no pointer arithmetic, and the garbage collector handles memory reclamation, so there are no dangling pointer issues. That said, mistakes like nil dereference still happen, so whenever you use a pointer, always think first: “Could this pointer be nil?”
The essence of pointers is simple. & gets an address, and * accesses the value at that address. Pointers appear when you need to overcome the limitations of pass-by-value to modify the original or avoid copying large data. They’re especially common with structs and method receivers, so once you’re comfortable with these patterns, your speed reading Go code will increase dramatically.
The next part covers Go’s killer feature: goroutines and channels. We’ll explore how concurrent programming becomes natural in Go.




Loading comments...