This tutorial is for Go beginners who know variables and functions and want a practical mental model for pointers: what * and & mean, how to read and change values through pointers, how that interacts with function calls and methods, and when slices and maps make extra pointers unnecessary. It ends with decision tables and common mistakes. For how methods differ from package-level functions, see Go methods vs functions; for functions in general, see functions in Go and variable scope. For structs in depth, see structs in Go.
Tested with Go 1.24 on Linux.
Quick answer: what a pointer is
A pointer in Go is a variable that stores the memory address of another value. Use & to take the address of a variable; use * in a type like *int to mean “pointer to int,” and use * in front of a pointer expression to read or write the value at that address. The zero value of any pointer type is nil, which must not be dereferenced.
What is a pointer?
A normal variable holds a value directly. A pointer variable holds where that value lives in memory so you can read or update it through that address.
A pointer does not duplicate the whole value by default.
It stores the location where the value can be found.| Idea | Meaning |
|---|---|
| Value variable | Holds the data |
| Pointer variable | Holds the address of the data |
& |
Address of a value |
* on a type |
Pointer type, for example *int |
* on a pointer |
Dereference: follow the address to the value |
Address and dereference operators
| Use | Syntax | Meaning |
|---|---|---|
| Address of | &x |
Pointer to x (type *T if x is T) |
| Pointer type | *T |
“Pointer to T” in a declaration |
| Dereference | *p |
The T that p points at (read or assign) |
The symbol * is overloaded in a helpful way: in var p *int it is part of the type; in *p it means “the value p points to.”
Pointer type and zero value
A pointer type is written *T for any type T: *int, *string, *MyStruct, and so on. An uninitialized pointer variable has the zero value nil, meaning it does not point at any valid value yet.
Get value from a pointer (dereference)
Reading the value stored at the address is called dereferencing. Assigning through a pointer updates the original variable.
package main
import "fmt"
func main() {
x := 42
p := &x // p has type *int
fmt.Println(p) // address (hex, varies by run)
fmt.Println(*p) // read through pointer: 42
*p = 100 // write through pointer
fmt.Println(x) // x is now 100
s := "hello"
ps := &s
fmt.Printf("%q\n", *ps)
}You should see an address line, then 42, then 100, then "hello". The first fmt.Println(p) prints the pointer’s address representation, not the integer 42.
Pointers to different types
The same rules apply to other types: take &, dereference with *, and the pointer’s static type must match the value.
package main
import "fmt"
func main() {
username := "Go developer"
age := 12
ok := true
height := 5.5
up := &username
ap := &age
bp := &ok
hp := &height
fmt.Printf("string ptr %v -> %q\n", up, *up)
fmt.Printf("int ptr %v -> %d\n", ap, *ap)
fmt.Printf("bool ptr %v -> %t\n", bp, *bp)
fmt.Printf("float64 ptr %v -> %g\n", hp, *hp)
}You should see four lines, each showing a pointer value and the dereferenced contents.
Pointer to struct
Struct fields can be read or written through a pointer. Go lets you write p.Field even when p is *MyStruct; that is shorthand for (*p).Field.
type User struct {
Name string
Age int
}
u := User{Name: "Ada", Age: 36}
p := &u
p.Name = "Augusta" // same as (*p).NameFor large structs, passing a pointer avoids copying the whole struct on every call. See structs in Go for layout and embedding.
Passing values vs passing pointers to functions
Go passes every argument by value. If you pass an int, the function gets a copy; changes inside the function do not affect the caller’s variable. If you pass a pointer, the function still gets a copy, but the copy is the same address, so the callee can change the pointed-to value with *p.
| Callee receives | What the function can change |
|---|---|
| Plain value | Only its local copy |
| Pointer | The caller’s data through *p |
Wording that matches the language spec: Go is pass-by-value; a copied pointer still aliases the original storage.
package main
import "fmt"
func bump(n *int) {
*n++
}
func useless(n int) {
n++
}
func main() {
x := 1
bump(&x)
fmt.Println("after bump:", x)
y := 1
useless(y)
fmt.Println("after useless:", y)
}You should see after bump: 2 and after useless: 1.
Pointer receivers in methods
Methods can use a value receiver (t T) or a pointer receiver (t *T). Use a pointer receiver when the method must mutate the receiver, when copying the receiver would be expensive, or when the type contains a sync.Mutex or similar field that must not be copied. Use a value receiver when the type is small, immutable in practice, and methods only read fields.
| Situation | Typical choice |
|---|---|
| Method changes receiver fields | Pointer receiver *T |
| Large struct | Pointer receiver |
Contains sync.Mutex |
Pointer receiver |
| Small value-like type, read-only methods | Value receiver T |
The method set of T and *T differs: types with only pointer-receiver methods may require a pointer value to match some interfaces. The compiler often inserts & for addressable values when you call a pointer-receiver method on a value. Details are in the language spec on selectors.
package main
import "log"
type Cat struct {
name string
}
func (c Cat) Walk() {
log.Println("Animal name:", c.name)
}
func (c *Cat) rename(s string) {
c.name = s
}
func main() {
pc := &Cat{name: "Tom"}
vc := Cat{name: "Katie"}
pc.rename("Tommy")
pc.Walk()
vc.rename("Kate") // addressable: compiler uses (&vc).rename
vc.Walk()
}You should see two log lines with Tommy and two with Kate.
Nil pointers and new
A nil pointer does not point at valid memory. Dereferencing it causes a panic at runtime, so guard with if p != nil (or design APIs so nil is impossible on the hot path).
package main
import "fmt"
func main() {
var p *string
if p == nil {
fmt.Println("nil pointer — do not dereference")
return
}
fmt.Println(*p)
}You should see the nil message.
new(T) allocates storage for a zero-valued T and returns *T:
package main
import "fmt"
func main() {
p := new(string)
fmt.Printf("%v -> %q\n", p, *p)
}You should see a non-nil pointer and an empty string (the zero value for string).
Pointers with slices, maps, and channels
Slices, maps, and channels are already small header-like values that refer to backing data managed by the runtime. You usually do not need *[]T or *map[K]V just to mutate elements: mutating s[i] or m[k] updates the shared backing store.
| Type | Need pointer to modify contents? |
|---|---|
| Slice | No for elements; yes only if you must replace the whole slice header in the caller |
| Map | No for entries |
| Channel | Rarely; the channel value identifies the same channel |
| Struct | Often useful to avoid copies and to mutate in functions |
This program shows the usual case: functions receive a slice, map, or channel by value (the header is copied), but mutations to elements or map entries still affect the caller because the header points at the same backing store or map data. A second channel variable is just another copy of the header; it names the same channel.
package main
import "fmt"
func bumpFirst(s []int) {
if len(s) > 0 {
s[0] = 99
}
}
func setScore(m map[string]int, k string, v int) {
m[k] = v
}
func grow(s []int) {
s = append(s, 2)
fmt.Println("inside grow:", len(s), s)
}
func main() {
s := []int{1, 2, 3}
bumpFirst(s)
fmt.Println("slice after bumpFirst:", s)
m := map[string]int{"ada": 10}
setScore(m, "ada", 100)
setScore(m, "bob", 5)
fmt.Println("map:", m["ada"], m["bob"])
ch := make(chan int, 1)
alias := ch
ch <- 7
fmt.Println("channel via alias:", <-alias)
s2 := []int{1}
grow(s2)
fmt.Println("outside grow:", len(s2), s2)
}You should see [99 2 3], map scores 100 and 5, 7 from the channel, then inside grow: 2 [1 2] but outside grow: 1 [1]: assigning to the parameter s inside grow (including after append) does not change the caller’s slice variable—only the caller’s elements were shared. To extend a slice visible in the caller, return the new slice, pass a pointer to the slice (*[]T), or use a pointer to a struct that holds the slice field.
When to use pointers
| Situation | Why |
|---|---|
| Function or method should update caller-visible data | Shared address |
| Large struct as parameter | Avoid copying the whole value |
| Optional field | nil can mean “not set” |
| Linked structures or shared graph | Nodes refer to the same memory |
| Pointer receiver for mutation or copy avoidance | Matches common API style |
When to avoid pointers
| Situation | Better approach |
|---|---|
| Small integers, booleans, short strings | Pass by value |
| No mutation needed | Values simplify reasoning |
| Map or slice element updates | Pass the map or slice, not *map / *slice |
| Trying to look “more advanced” | Prefer the simplest type that works |
A pointer to an interface type (*io.Reader) is almost never what you want; pass io.Reader itself.
Go pointers vs C pointers
| Topic | Go |
|---|---|
| Pointer arithmetic | Not allowed (no p++ to walk memory) |
| Memory lifetime | Garbage collector reclaims unreachable values |
nil |
Valid zero value for pointers |
| Typing | *T only points to T |
Go keeps pointers for aliasing and data structures, not for manual memory walking.
Mistakes to avoid
Confusing & and *: & creates a pointer from a value; * follows a pointer to a value (or declares *T).
Dereferencing nil: always validate or guarantee non-nil before *p.
Assuming pointers always speed things up: indirection and harder-to-read code have a cost; measure if performance matters.
Using pointers everywhere: small values are often clearer as values.
Expecting C-style pass-by-reference: Go copies everything, including pointer bits; the aliasing behavior follows from copying addresses.
Pointer-to-interface confusion: use the interface value, not *interface{}.
Go pointer cheat sheet
| Goal | What to use |
|---|---|
Take address of x |
&x |
Type “pointer to T” |
*T |
| Read or write pointed-to value | *p |
| No target yet | nil |
Allocate zero value, get *T |
new(T) |
Callee mutates caller’s int / struct |
Pass *T, use *p in callee |
| Method mutates receiver | Pointer receiver *T |
| Large struct argument | Prefer *MyStruct |
| Optional field | *string, *int, or use a wrapper type |
| Modify slice element | Slice value is enough |
| Walk memory with pointer math | Not supported |
Summary
Pointers in Go are typed addresses: *T for the type, & for address-of, and unary * to read or write through the pointer. Go passes arguments by value, including pointer values, which still lets functions and methods mutate shared data. Check for nil before dereferencing; new gives a non-nil pointer to a zero value. Prefer value receivers for small immutable types and pointer receivers when you mutate or avoid large copies. Slices, maps, and channels already indirect to their backing data, so extra pointers are usually for APIs, not for ordinary element updates. Go does not offer C-style pointer arithmetic.

