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
MyStructgives back a copy of the struct. - Returning
*MyStructgives back a pointer to a struct created inside the function. - Accepting
*MyStructas 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
nilis 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
[]Tis usually fine. - Returning
[]Tis usually fine. - Passing
*[]Tis 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:
- Start with value semantics.
- Switch to pointers when you have a clear reason.
- 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:
Tmeans “here is your own item”*Tmeans “here is access to the shared item”[]Tmeans “here is a lightweight view of a list”[]*Tmeans “here is a list of references to shared items”
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
nilis 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
nilis 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
a := Item{Value: 1}creates a struct value.b := Item{Value: 1}creates another struct value.changeValue(a)passes a copy ofainto the function.- Inside
changeValue,i.Value = 99changes only the local copy. - Back in
main,a.Valueis still1. changePointer(&b)passes the address ofb.- Inside
changePointer,i.Value = 99changes the original struct through the pointer. - Back in
main,b.Valueis now99.
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
ais a slice header pointing to an underlying array.updateFirst(a)gets a copy of the slice header.- That copied header still points to the same underlying array.
nums[0] = 100changes the shared array, so the caller sees the change.replaceSlice(a)also gets a copy of the slice header.- Inside the function,
nums = []int{7, 8, 9}makesnumspoint somewhere else. - The caller's slice header is unchanged.
- So
aremains 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
nilmeans "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.
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
nilelement 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
| Choice | What it means | Usually good when | Downsides |
|---|---|---|---|
T | pass or return a copy | small structs, clear ownership, no shared mutation | copying may cost more for large structs |
*T | pass or return shared access | mutation, large structs, nil needed, identity matters | nil handling, shared state, possible extra allocations |
[]T | slice header copied, elements shared through underlying array | most collections in Go | element copies happen when appending values |
[]*T | slice of references to structs | shared mutable elements, large or identity-based objects | more allocations, nil elements, worse locality |
*[]T | pointer to the slice header itself | rare cases where callee must replace caller's slice | less idiomatic, harder to read |
[]T vs []*T
| Option | Prefer when | Example |
|---|---|---|
[]T | elements are small to medium, mostly independent, no shared identity needed | records, DTOs, config entries |
[]*T | elements are large, mutable, shared elsewhere, or identity matters | tree nodes, graph nodes, ORM entities |
Return value vs output parameter
| Style | Prefer when | Example |
|---|---|---|
| return value | normal Go API design | func Parse() Result |
| output parameter | mutation is intentional or allocation reuse matters | func Decode(dst *Result) error |
Simple default rules
- Start with
T. - Use
*Tif mutation, identity, ornilmatters. - Start with
[]Tfor slices. - Use
[]*Tonly if element pointers are genuinely useful. - Avoid
*[]Tunless replacing the slice itself is required.
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
nilis meaningful- identity/shared state matters
- Prefer
[]Tby default. - Use
[]*Twhen elements need shared identity or mutation. - Use
*[]Trarely.
Structs
func Make() T
func Make() *T
func Update(t *T)
T: copy semantics*T: shared semanticsUpdate(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
[]*Twhen[]Tis 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.
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
Userstruct with at leastNameandScorefields. - Write one function that returns a
Uservalue and one that returns a*User. - Write one function that updates a
Userthrough a pointer parameter. - Write one function that modifies elements of a
[]Userslice and another that tries to replace the slice. - Print results in
mainto show what changed and what did not.
Keep learning
Related questions
Are Go Maps Passed by Value or Reference?
Learn Go map parameter and return semantics: maps are passed by value, while copies share underlying data. Know when map pointers are useful.
Authentication for Go REST APIs: Sessions, Tokens, and Middleware
Learn practical authentication patterns for Go REST APIs, including password hashing, server sessions, bearer tokens, middleware, and security rules.
Automatic Build Versioning in Go: Embed Incrementing Build Numbers
Learn how to add automatic build versioning in Go using linker flags, build metadata, CI counters, and Git-based version values.