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.
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 {
u := NewUser()
u.Age =
fmt.Println(*u)
}
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 still .
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:
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:
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.
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 |
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
[]T
[]*T
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.
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
Check if a Value Exists in a Slice in Go
Learn how to check whether a value exists in a slice in Go, and why Go has no Python-style `in` operator for arrays or slices.
Concatenating Slices in Go with append
Learn how to concatenate two slices in Go using append and the ... operator, with examples, pitfalls, and practical usage.
Convert String to Integer in Go: Idiomatic Parsing with strconv.Atoi
Learn the idiomatic way to convert a string to an int in Go using strconv.Atoi, with examples, errors, and common mistakes.