Question

Go Pointers vs Values in Parameters and Return Values

gopointers

Question

In Go, there are several ways to return a struct or work with it through function parameters. For example:

type MyStruct struct {
    Val int
}

func myfunc() MyStruct {
    return MyStruct{Val: 1}
}

func myfunc() *MyStruct {
    return &MyStruct{Val: 1}
}

func myfunc(s *MyStruct) {
    s.Val = 1
}

I understand the basic differences:

  • Returning MyStruct gives back a copy of the struct.
  • Returning *MyStruct gives back a pointer to a struct created inside the function.
  • Accepting *MyStruct as a parameter lets the function modify an existing struct.

I have seen all of these patterns used in real Go code, and I want to understand the best practices behind them. When should each style be used?

For example:

  • Returning a value might be fine for small structs.
  • Returning a pointer might make sense for larger structs or when shared mutation is needed.
  • Passing in a pointer might help reuse memory by modifying an existing struct instance.

Are these good rules of thumb? What are the practical best practices in Go?

I have the same question for slices:

func myfunc() []MyStruct {
    return []MyStruct{{Val: 1}}
}

func myfunc() []*MyStruct {
    return []*MyStruct{{Val: 1}}
}

func myfunc(s *[]MyStruct) {
    *s = []MyStruct{{Val: 1}}
}

func myfunc(s *[]*MyStruct) {
    *s = []*MyStruct{{Val: 1}}
}

Again, what are the best practices here?

I know that a slice already contains a reference to underlying data, so returning a pointer to a slice is usually unnecessary. But in normal Go code, when should I:

  • return a slice of struct values,
  • return a slice of pointers to structs, or
  • pass a pointer to a slice into a function?

I would like to understand the idiomatic Go approach and the trade-offs involved.

Short Answer

By the end of this page, you will understand how Go handles values, pointers, and slices in function parameters and return values. You will learn when to return a struct value, when to return a pointer, when to modify data through parameters, and how to choose between []T and []*T in idiomatic Go code.

Concept

In Go, the choice between values and pointers is mostly about ownership, mutation, clarity, and cost.

Struct values vs pointers

A struct value like MyStruct is copied when passed to a function or returned from one. That copy is independent from the original.

A pointer like *MyStruct points to one shared struct value. If multiple parts of your program hold that pointer, they all refer to the same underlying data.

This leads to a practical rule:

  • Use values when the data is small, self-contained, and should behave independently.
  • Use pointers when the function needs to modify the original, when copying would be undesirable, or when nil is a meaningful state.

Slices are small headers, not raw arrays

A slice in Go is a small descriptor containing:

  • a pointer to an underlying array
  • a length
  • a capacity

So a slice itself is already cheap to pass around. Passing []T does not copy all elements. It copies only the slice header.

That means:

  • Passing []T is usually fine.
  • Returning []T is usually fine.
  • Passing *[]T is uncommon and usually only needed if the function must replace the slice itself and the caller must observe that exact new slice header.

Why this matters

This decision affects:

  • performance
  • readability
  • safety
  • API design
  • mutation behavior

Idiomatic Go generally prefers the simplest thing that works clearly. That often means:

  • return values, not output parameters
  • use value types unless mutation or shared identity is needed
  • use slices of values unless pointers are truly useful

Important nuance: avoid premature optimization

Many beginners assume pointers are always faster. In Go, that is not always true. Pointers can increase complexity, make mutation harder to reason about, add heap allocations, and hurt cache locality.

So the usual advice is:

  1. Start with value semantics.
  2. Switch to pointers when you have a clear reason.
  3. Measure performance if optimization matters.

Mental Model

Think of a struct value as a printed form and a pointer as a house address.

  • If you hand someone a printed form, they get their own copy. They can write on it, but your copy does not change.
  • If you hand someone a house address, they can go to the same house you use and change what is inside.

A slice is like a note that says:

  • where the storage starts
  • how many items are currently used
  • how much room is available

When you pass a slice, you are copying that note, not copying all the items. But both notes may still refer to the same underlying storage.

So:

  • T means “here is your own item”
  • *T means “here is access to the shared item”
  • []T means “here is a lightweight view of a list”
  • []*T means “here is a list of references to shared items”
Take Quiz

Syntax and Examples

Returning a struct value

package main

import "fmt"

type User struct {
    Name string
    Age  int
}

func NewUser() User {
    return User{Name: "Ava", Age: 30}
}

func main() {
    u := NewUser()
    fmt.Println(u)
}

Use this when:

  • the struct is reasonably small
  • copying is acceptable
  • you want value semantics
  • nil is not needed

Returning a pointer to a struct

package main

import "fmt"

type User struct {
    Name string
    Age  int
}

func NewUser() *User {
    return &User{Name: "Ava", Age: 30}
}

func main() {
    u := NewUser()
    u.Age = 31
    fmt.Println(*u)
}

Use this when:

  • the caller should mutate the same object
  • the struct is large or contains synchronization primitives
  • nil is a useful result
  • shared identity matters

Mutating through a pointer parameter

package main

import "fmt"

type Counter struct {
    Value int
}

func Increment(c *Counter) {
    c.Value++
}

func main() {
    c := Counter{Value: 10}
    Increment(&c)
    fmt.Println(c.Value)
}

Use this when the function's job is to modify an existing value.

Returning a slice of values

package main

import "fmt"

type User struct {
    Name string
}

func LoadUsers() []User {
    return []User{{Name: "Ava"}, {Name: "Liam"}}
}

func main() {
    users := LoadUsers()
    fmt.Println(users)
}

This is idiomatic and common.

Returning a slice of pointers

package main

import "fmt"

type User struct {
    Name string
}

func LoadUsers() []*User {
    return []*User{{Name: "Ava"}, {Name: "Liam"}}
}

func main() {
    users := LoadUsers()
    users[0].Name = "Emma"
    fmt.Println(users[0].Name)
}

Use this only when each element needs shared identity, mutation, or avoiding expensive copies.

Passing a slice normally

package main

import "fmt"

func Fill(nums []int) {
    for i := range nums {
        nums[i] = i * 10
    }
}

func main() {
    nums := make([]int, 5)
    Fill(nums)
    fmt.Println(nums)
}

Even though nums is passed by value, both the caller and function refer to the same underlying array, so element changes are visible.

Passing a pointer to a slice

package main

import "fmt"

func Replace(nums *[]int) {
    *nums = []int{1, 2, 3}
}

func main() {
    nums := []int{9, 9, 9}
    Replace(&nums)
    fmt.Println(nums)
}

This is only needed when you want to replace the caller's slice header itself.

Step by Step Execution

Consider this example:

package main

import "fmt"

type Item struct {
    Value int
}

func changeValue(i Item) {
    i.Value = 99
}

func changePointer(i *Item) {
    i.Value = 99
}

func main() {
    a := Item{Value: 1}
    b := Item{Value: 1}

    changeValue(a)
    changePointer(&b)

    fmt.Println(a.Value)
    fmt.Println(b.Value)
}

What happens step by step

  1. a := Item{Value: 1} creates a struct value.
  2. b := Item{Value: 1} creates another struct value.
  3. changeValue(a) passes a copy of a into the function.
  4. Inside changeValue, i.Value = 99 changes only the local copy.
  5. Back in main, a.Value is still 1.
  6. changePointer(&b) passes the address of b.
  7. Inside changePointer, i.Value = 99 changes the original struct through the pointer.
  8. Back in main, b.Value is now 99.

Output

1
99

Now compare with slices:

package main

import "fmt"

func updateFirst(nums []int) {
    nums[0] = 100
}

func replaceSlice(nums []int) {
    nums = []int{7, 8, 9}
}

func main() {
    a := []int{1, 2, 3}

    updateFirst(a)
    fmt.Println(a)

    replaceSlice(a)
    fmt.Println(a)
}

Step by step

  1. a is a slice header pointing to an underlying array.
  2. updateFirst(a) gets a copy of the slice header.
  3. That copied header still points to the same underlying array.
  4. nums[0] = 100 changes the shared array, so the caller sees the change.
  5. replaceSlice(a) also gets a copy of the slice header.
  6. Inside the function, nums = []int{7, 8, 9} makes nums point somewhere else.
  7. The caller's slice header is unchanged.
  8. So a remains the original slice, except for the earlier element update.

Output

[100 2 3]
[100 2 3]

Real World Use Cases

Return struct values

Common for:

  • configuration values
  • timestamps or coordinates
  • small domain objects
  • result objects that should not be shared implicitly

Example:

func DefaultConfig() Config {
    return Config{Port: 8080, Debug: false}
}

Return struct pointers

Common for:

  • objects with methods that mutate state
  • database models shared across layers
  • large structs
  • optional results where nil means "not found"

Example:

func FindUser(id int) *User {
    // return nil if not found
    return &User{ID: id, Name: "Ava"}
}

Use pointer parameters

Common for:

  • decoders and unmarshallers
  • functions that fill or update existing values
  • performance-sensitive code that reuses buffers

Example:

func DecodeUser(data []byte, u *User) error {
    // fill u from data
    return nil
}

Return []T

Common for:

  • API responses
  • query results
  • transformed data
  • lists of simple records

Example:

func ActiveUsers() []User {
    return []User{{Name: "Ava"}, {Name: "Noah"}}
}

Return []*T

Common for:

  • objects shared across components
  • mutable graph/tree structures
  • large objects where copying each element is undesirable

Example:

func LoadNodes() []*Node {
    return []*Node{{ID: 1}, {ID: 2}}
}

Use *[]T rarely

Common only when:

  • a helper must replace the caller's slice directly
  • APIs follow a fill-in-place style
  • you are working with custom scanning or decoding patterns

In everyday Go code, returning the new slice is usually clearer.

Take Quiz

Real Codebase Usage

In real Go codebases, developers usually follow a few practical patterns.

1. Prefer returning values instead of output parameters

This is usually clearer:

func BuildUser() User {
    return User{Name: "Ava"}
}

Instead of:

func BuildUser(u *User) {
    u.Name = "Ava"
}

The output-parameter style is mostly used when:

  • mutation is the point of the API
  • avoiding allocation matters
  • matching an interface or framework pattern

2. Use pointer receivers consistently when needed

If a struct's methods mutate state or the struct is large, methods often use pointer receivers:

type Counter struct {
    Value int
}

func (c *Counter) Inc() {
    c.Value++
}

If some methods use pointer receivers, it is often best to keep the method set consistent.

3. Prefer []T for collections by default

A slice of values is often simpler:

  • fewer heap objects
  • better memory locality
  • fewer nil element concerns
  • easier reasoning
func ListProducts() []Product {
    return []Product{{ID: 1}, {ID: 2}}
}

4. Use []*T only when identity matters

Use pointers in a slice when elements need to be shared or independently mutated elsewhere.

Examples:

  • AST nodes
  • linked structures
  • cached objects
  • objects with internal locks or large state

5. Guard clauses and nil handling

When using pointers, code often needs nil checks:

func SetName(u *User, name string) {
    if u == nil {
        return
    }
    u.Name = name
}

This is extra complexity that value types avoid.

6. Returning slices instead of modifying *[]T

In most codebases, this is preferred:

func FilterEven(nums []int) []int {
    out := make([]int, 0)
    for _, n := range nums {
        if n%2 == 0 {
            out = append(out, n)
        }
    }
    return out
}

Instead of:

func FilterEven(nums []int, out *[]int) {
    *out = (*out)[:0]
    for _, n := range nums {
        if n%2 == 0 {
            *out = append(*out, n)
        }
    }
}

The second pattern can be useful in optimized code, but the first is usually more idiomatic.

Common Mistakes

Mistake 1: Assuming pointers are always faster

Pointers can cause:

  • more heap allocations
  • more shared mutable state
  • worse cache locality
  • more nil checks

Do not choose pointers automatically.

Mistake 2: Using *[]T when []T is enough

Broken idea:

func Add(nums *[]int) {
    (*nums)[0] = 10
}

A pointer is unnecessary if you only want to modify elements.

Better:

func Add(nums []int) {
    nums[0] = 10
}

Mistake 3: Using []*T without needing shared identity

This adds complexity:

  • each element can be nil
  • more allocations
  • harder memory behavior

Prefer []T unless you need pointer semantics.

Mistake 4: Forgetting that reassigning a slice inside a function does not update the caller

Broken expectation:

func reset(nums []int) {
    nums = []int{1, 2, 3}
}

The caller's slice does not change.

If you want a new slice, return it:

func reset() []int {
    return []int{1, 2, 3}
}

Or, if truly necessary, pass *[]int.

Mistake 5: Returning pointers for tiny immutable-like structs

This often makes code harder to use:

func PointZero() *Point {
    return &Point{X: 0, Y: 0}
}

Often simpler:

func PointZero() Point {
    return Point{X: 0, Y: 0}
}

Mistake 6: Mixing value and pointer semantics inconsistently

If one part of your API returns values and another expects mutation through pointers, the code may become confusing. Try to keep one clear model for each type.

Mistake 7: Writing invalid slice-of-pointer literals

Broken code:

func myfunc() []*MyStruct {
    return []MyStruct{&MyStruct{Val: 1}}
}

The element type does not match.

Correct:

func myfunc() []*MyStruct {
    return []*MyStruct{{Val: 1}}
}

Comparisons

ChoiceWhat it meansUsually good whenDownsides
Tpass or return a copysmall structs, clear ownership, no shared mutationcopying may cost more for large structs
*Tpass or return shared accessmutation, large structs, nil needed, identity mattersnil handling, shared state, possible extra allocations
[]Tslice header copied, elements shared through underlying arraymost collections in Goelement copies happen when appending values
[]*Tslice of references to structsshared mutable elements, large or identity-based objectsmore allocations, nil elements, worse locality
*[]Tpointer to the slice header itselfrare cases where callee must replace caller's sliceless idiomatic, harder to read

[]T vs []*T

OptionPrefer whenExample
[]Telements are small to medium, mostly independent, no shared identity neededrecords, DTOs, config entries
[]*Telements are large, mutable, shared elsewhere, or identity matterstree nodes, graph nodes, ORM entities

Return value vs output parameter

StylePrefer whenExample
return valuenormal Go API designfunc Parse() Result
output parametermutation is intentional or allocation reuse mattersfunc Decode(dst *Result) error

Simple default rules

  1. Start with T.
  2. Use *T if mutation, identity, or nil matters.
  3. Start with []T for slices.
  4. Use []*T only if element pointers are genuinely useful.
  5. Avoid *[]T unless replacing the slice itself is required.
Take Quiz

Cheat Sheet

Quick rules

  • Prefer return values over output parameters.
  • Prefer struct values by default.
  • Use pointers when:
    • the function must mutate the original
    • the struct is large
    • nil is meaningful
    • identity/shared state matters
  • Prefer []T by default.
  • Use []*T when elements need shared identity or mutation.
  • Use *[]T rarely.

Structs

func Make() T
func Make() *T
func Update(t *T)
  • T: copy semantics
  • *T: shared semantics
  • Update(t *T): mutate caller's value

Slices

func List() []T
func List() []*T
func Fill(dst *[]T)
  • []T: most common
  • []*T: only when pointer elements are needed
  • *[]T: only when replacing/resizing the caller's slice header directly matters

Important slice rule

This modifies caller-visible elements:

func f(s []int) {
    s[0] = 1
}

This does not replace caller's slice:

func f(s []int) {
    s = []int{1, 2, 3}
}

Good defaults

  • Small plain struct: return T
  • Mutable object: use *T
  • List of records: return []T
  • Tree/graph nodes: maybe []*T

Watch out for

  • unnecessary pointers
  • nil pointer checks
  • unnecessary *[]T
  • using []*T when []T is simpler

FAQ

Should I return a struct or a pointer in Go?

Usually return a struct value unless you need shared mutation, nil, identity, or the struct is large enough that copying is undesirable.

Is passing a slice by value expensive in Go?

No. A slice is a small header. Passing []T copies the header, not all the elements.

When should I use a pointer to a slice in Go?

Rarely. Use *[]T only when the function must replace the caller's slice header itself. Most of the time, return the new slice instead.

Should I use []T or []*T in Go?

Use []T by default. Use []*T when elements need shared identity, mutation, or are expensive to copy.

Can a function modify a slice without using *[]T?

Yes. It can modify existing elements through []T, because the slice refers to shared underlying storage.

Are pointers always more memory efficient in Go?

No. Pointers may cause extra allocations and add indirection. They are not automatically more efficient.

Why do some APIs use output parameters like func Decode(dst *T)?

Usually to fill an existing value, reduce allocations, or match a specific API style such as decoding, unmarshalling, or scanning.

What is the most idiomatic Go default?

Prefer simple value returns and []T slices first. Move to pointers only when you have a specific reason.

Take Quiz

Mini Project

Description

Build a small Go program that demonstrates the difference between returning values, returning pointers, modifying structs through pointer parameters, and working with slices of values. This project is useful because it turns abstract rules into visible behavior you can run and inspect.

Goal

Create a program that shows which changes affect the caller and which changes stay local to the function.

Requirements

  • Define a User struct with at least Name and Score fields.
  • Write one function that returns a User value and one that returns a *User.
  • Write one function that updates a User through a pointer parameter.
  • Write one function that modifies elements of a []User slice and another that tries to replace the slice.
  • Print results in main to show what changed and what did not.
Take Quiz

Keep learning

Related questions

View more