1. Why pointers are needed and why there are so many legends around them
If you’ve ever heard the phrase “pointers are scary,” you’re not alone: for many programmers, the word “pointer” triggers a C/C++ flashback with mysterious crashes and “corrupted memory.” But in Go, the idea is much calmer and more down-to-earth: a pointer is simply a value that stores the address of another value.
We’ll look at pointers as a tool for two things: (1) letting a function change a specific variable “from the outside,” and (2) learning to understand code where addresses matter (including that very fmt.Scan(&x)).
Let’s introduce a basic vocabulary, without mysticism:
- Value — for example the number 10.
- Variable — a “box” that contains a value (for example x := 10).
- Address — the “coordinates of the box” in memory.
- Pointer — a variable that contains the address of another variable.
To keep this from turning into philosophy, there will be lots of small examples next.
2. The *T type: how to read the “star” in a type
When you see the type *T, it’s important to pause for a second and read it out loud correctly: “pointer to T”. It’s not “multiplication by T” and not a “decorative star,” but part of the type.
For example:
- int — a regular integer.
- *int — a pointer to an integer, i.e., a value that stores an address where an int lives.
- string — a string.
- *string — the address of a string (more precisely, the address of the variable that stores the string value).
A useful habit for beginners is to always keep a mini-table in mind:
| Notation | How to read | What’s stored inside |
|---|---|---|
|
a value of type T | the value itself (10, "hi", …) |
|
a pointer to T | an address where a T lives |
One more important detail: a pointer is also a value. You can assign it, compare it to nil, and pass it to functions. It’s just that the “contents” of that value is an address.
3. The & operator: how to get the address of a variable
Now to the main “magic” from previous days: &x. This operator is called “address-of”. It takes the address of an addressable value and returns a pointer.
The simplest example:
package main
import "fmt"
func main() {
x := 10
p := &x // p has type *int
fmt.Println(x) // 10
_ = p
}
An important thing happens here: the variable p does not “copy x,” and it does not “watch x in some magical way.” It simply stores the address of x.
Addressable value: where you can apply &
You can’t stick & just anywhere. It works only where there is a “real memory cell” whose address makes sense. Usually that’s:
- a variable (x := 10, so &x is allowed),
- a slice element (&nums[i] is allowed),
- some other things we’ll encounter later (for example, struct fields — but we’re not touching structs right now).
And here’s what you cannot apply & to (and that’s good, because the compiler will stop you):
package main
func main() {
x := 10
_ = &(x + 1) // not allowed: (x + 1) is a temporary expression; it has no “cell”
_ = &10 // not allowed: a literal is not an addressable value
_ = x
}
The idea is simple: x is a box, it has an address. x + 1 is “computed on the fly,” there’s no box, so there’s no address.
4. Dereferencing *p: how to read and how to change the value by address
Now the second “star” that confuses everyone: *p in an expression. This is dereferencing. If p stores an address, then *p means: “take the value that lives at that address.”
First, reading:
package main
import "fmt"
func main() {
x := 10
p := &x
fmt.Println(*p) // 10
}
Now the most interesting part: dereferencing can appear to the left of =, meaning you can write a new value to that address:
package main
import "fmt"
func main() {
x := 10
p := &x
*p = 42
fmt.Println(x) // 42
}
This is the key idea: we’re not “changing the pointer,” we’re changing the data it points to.
A good analogy: p is a note with an apartment address. *p is what’s inside the apartment. You can read (what’s there now), and you can bring in a new couch (overwrite the value).
5. The nil pointer: “points nowhere,” and why it matters
Every type in Go has a zero value — the default value. For pointers, the zero value is nil. This is a special value that means: “there is no address; it points nowhere”.
Example:
package main
import "fmt"
func main() {
var p *int // zero value => nil
fmt.Println(p == nil) // true
}
And here comes the most important safety rule: if you try to dereference nil, the program will crash with a panic (panic). This is not a “compile error,” but an abnormal termination at runtime.
package main
import "fmt"
func main() {
var p *int // nil
fmt.Println(p == nil) // true
fmt.Println(*p) // panic: invalid memory address or nil pointer dereference
}
It sounds scary, but it’s actually a useful rule: you can’t “read data from nowhere.”
The practical consequence is very simple: if, by the logic of your program, a pointer can be nil, you must check p != nil before dereferencing.
6. Addresses in output and a memory sketch
How to “see” an address: fmt.Printf("%p", p)
Beginners often lack a “sense of reality” for what’s happening: we say “address,” but what is it? In Go, you can print a pointer value (an address) using the "%p" format.
package main
import "fmt"
func main() {
x := 7
p := &x
fmt.Printf("p=%p\n", p) // p=0x.... (the address will be different)
fmt.Printf("*p=%d\n", *p) // *p=7
}
It’s important not to mix these up:
- "%p" — prints an address (i.e., the pointer value),
- "%d", "%v", etc. — print a value, so for a pointer you typically print *p, not p.
And yes, the address will be different each time — that’s normal. We’re not trying to memorize addresses like friends’ phone numbers; "%p" is a tool for understanding and diagnostics.
Mini sketch: what lives where (and who points to whom)
Sometimes one picture replaces five paragraphs. Imagine this situation:
x := 10
p := &x
Schematically, it can be represented like this:
┌───────────┐ ┌───────────┐
│ variable │ │ variable │
│ x │ │ p │
│ value │ │ value │
│ 10 │ │ address x │
└───────────┘ └───────────┘
▲ │
└─────────────────┘
p points to x
And dereferencing *p is “follow the arrow and take what’s inside x.”
7. Pointers in functions: an example with a balance
Here’s an important point: we’re not learning syntax just for syntax. Pointers start being useful when we have functions.
Let’s continue our learning style of a “small CLI app” that does something clear: let it be a mini “Piggy Bank.” We have a balance (balance), and we want to call a deposit function to add to the balance.
Depositing via a pointer
package main
import "fmt"
func deposit(balance *int, amount int) {
*balance += amount
}
func main() {
balance := 0
deposit(&balance, 50)
fmt.Println(balance) // 50
}
Why is it reasonable to use a pointer here? Because deposit receives the address of a specific variable balance and changes it “in place.” Without a pointer, you’d have to return a new value and assign it back. That approach is also fine, but today our goal is to understand pointer mechanics.
A dangerous moment: what happens if you pass nil
If you call deposit(nil, 50), then inside you’ll execute *balance += amount, but balance is nil. This will lead to a panic. That’s why real code often adds a guard:
package main
import "fmt"
func deposit(balance *int, amount int) {
if balance == nil {
return
}
*balance += amount
}
func main() {
var p *int // nil
deposit(p, 50)
fmt.Println(p == nil) // true
}
Notice: if a pointer can be nil, the function must either check, or have a contract of “never nil.” Which option to choose depends on the task, but you need to be aware of the fact itself.
8. Why fmt.Scan(&x) requires &x
We promised we’d unpack the old “magic” from input. Here’s the moment of truth.
When you write:
fmt.Scan(&x)
it reads like this: “the Scan function must write the value it reads into the variable x.” To be able to do that, it needs the address of x. Because if we passed just x, then Scan would receive a copy of the value (for example 0) and… where should it write the input? Into a copy that then disappears? That’s useless.
Look at an analogy (simplified) — what a function that “writes” a number might look like:
package main
import "fmt"
func setTo100(p *int) {
*p = 100
}
func main() {
x := 0
setTo100(&x)
fmt.Println(x) // 100
}
Here setTo100 does the same thing mechanically as Scan does: it receives an address and writes to it.
9. Examples for reading code and addressability
It’s very useful to learn to read pointers “with your eyes,” because you’ll encounter them in real code later.
Two pointers to one variable
package main
import "fmt"
func main() {
x := 5
p1 := &x
p2 := &x
*p1 = 9
fmt.Println(*p2) // 9
}
This is normal: different pointers can point to the same location. If you changed the value through one, the second will see the change.
A pointer is not a value
package main
import "fmt"
func main() {
x := 5
p := &x
fmt.Println(p == &x) // true (most likely true, because it's the same address)
fmt.Println(*p == x) // true (the values are equal too)
}
The first comparison compares addresses; the second compares data. Those are different levels.
You can take an address: a slice example
You already know slices, and this is a great example of “addressability” without introducing new topics.
package main
import "fmt"
func main() {
nums := []int{10, 20, 30}
p := &nums[1]
*p = 99
fmt.Println(nums) // [10 99 30]
}
Here nums[1] is a real element in memory, it has an address, so &nums[1] is legal. But &(nums[1] + 1) is not allowed, because that’s already a temporary expression.
10. Common mistakes when working with &, *T, and nil
Mistake #1: mixing up *T (a type) and *p (an operation).
The most common confusion for beginners is the “star in the type” versus the “star in the expression.” In the type *int, the star means “pointer to int.” In the expression *p, the star means “dereference the pointer p.” To keep your brain from boiling, it helps to say out loud: “star in the type” and “star in the code.”
Mistake #2: dereferencing nil and getting a panic.
nil is a normal default value for a pointer, and there’s nothing bad about that. It becomes bad when code does *p without checking p != nil. If, by the program’s logic, p can be nil, you must either check, or guarantee initialization in advance and treat nil as a contract violation.
Mistake #3: trying to take an address of something that should not have an address.
Constructs like &10, &(x+1), or &f() don’t compile — and that’s not Go being picky, it’s protection against meaningless addresses. You can take an address of a “live cell”: a variable or a collection element, but not a computed-on-the-fly result.
Mistake #4: printing a pointer like a regular number and then being surprised.
If you do fmt.Println(p), you’ll see something like 0xc0000120a8. Beginners sometimes think: “Wow, that’s a huge number, so that must be the value stored there!” No, that’s the address. For addresses, use "%p"; for data, print *p (after checking for nil, if needed).
Mistake #5: trying to “fix everything with pointers” just because they exist.
Pointers are a tool, not a universal seasoning “for flavor.” Sometimes it’s simpler to return a value from a function than to pass *int. Sometimes slices and maps already give you the desired behavior of mutating data without pointers. The important thing is not to turn code into a “forest of * and &,” where it becomes hard to understand who is changing what.
GO TO FULL VERSION