How to Return a Pointer from a Function in Go
You are building a configuration loader. The config struct holds a database URL, a cache size, and a list of allowed origins. You parse the file, populate the struct, and return it. If you return the struct by value, Go copies the entire list of origins into the caller's stack frame. The copy consumes memory and CPU. Worse, if the caller updates the cache size, the change only affects the copy. The original data remains untouched. You need a mechanism to hand over the original object, not a duplicate.
Pointers are addresses, not copies
A pointer is a memory address. Returning a pointer means returning the address where the data resides, not the data itself. Imagine handing someone the coordinates to a warehouse instead of carrying the inventory in your backpack. The caller receives the coordinates, navigates to the location, and reads or modifies the contents.
In Go, you declare a pointer return type with an asterisk, such as *Config, and you return the address using the ampersand operator, &. The caller receives the address and can dereference it to access the underlying value.
The minimal pattern
Here is the basic pattern. Define a struct, create an instance, and return its address.
package main
import "fmt"
// Config holds application settings.
type Config struct {
Host string
Port int
}
// NewConfig creates a Config and returns a pointer to it.
func NewConfig() *Config {
// Create a local variable.
c := Config{Host: "localhost", Port: 8080}
// Return the address of c.
// The compiler detects that c escapes to the heap.
return &c
}
func main() {
// cfg holds the address, not the struct.
cfg := NewConfig()
// Modify the struct via the pointer.
cfg.Port = 9090
fmt.Println(cfg.Host, cfg.Port)
}
When you write return &c, the compiler runs escaping analysis. It inspects c and determines that the address must remain valid after NewConfig returns. The variable cannot stay on the stack because the stack frame will be destroyed. The compiler automatically moves c to the heap. You do not need to request heap allocation manually. The garbage collector reclaims the memory when the last reference disappears.
This behavior differs from C, where returning the address of a local variable causes undefined behavior. In Go, the compiler guarantees safety. The variable lives as long as the pointer exists.
Trust the escaping analysis. Let the compiler move variables to the heap.
Composite literals and the address operator
You can combine allocation and addressing in a single expression. This is the most common idiom in Go codebases. It avoids declaring a temporary variable.
// ShortForm returns a pointer using a composite literal.
func ShortForm(name string) *Config {
// Create and address in one expression.
// The compiler allocates this on the heap automatically.
return &Config{Host: name, Port: 8080}
}
The compiler performs the same escaping analysis. The composite literal creates the struct, and the & operator takes its address. The result is cleaner and equally efficient.
Combine the literal and the address operator. The result is cleaner and equally efficient.
Slices and maps are already references
A common confusion is wrapping slices or maps in pointers. A slice header contains a pointer to the underlying array, a length, and a capacity. Passing a slice passes the header by value, but the header points to the same array. Modifying the slice contents updates the shared data. Returning a slice is cheap.
Returning a pointer to a slice, like *[]int, adds an unnecessary layer of indirection. The caller must dereference the pointer to get the slice, then dereference the slice header to get the array. This adds overhead without benefit. The same rule applies to maps and channels. They are reference types. You return the slice, map, or channel value directly.
The Go community avoids pointers to slices and maps. They add complexity without performance benefits.
Slices and maps travel by value. Pointers to them are almost always wrong.
Realistic example: lifecycle management
In production code, you often return pointers to objects with lifecycles. A server, a database connection, or a cache manager needs to be initialized and later shut down. The caller must hold the reference to perform cleanup.
package main
import (
"fmt"
"time"
)
// Server represents a running service.
type Server struct {
Address string
Started time.Time
}
// StartServer initializes and returns a pointer to a Server.
// Returning a pointer allows the caller to stop the server later.
func StartServer(addr string) *Server {
s := &Server{
Address: addr,
Started: time.Now(),
}
// Simulate initialization work.
fmt.Println("Server starting on", s.Address)
return s
}
// StopServer modifies the server state.
// It takes a pointer because it needs to update the original.
func StopServer(s *Server) {
s.Address = ""
fmt.Println("Server stopped")
}
func main() {
srv := StartServer(":8080")
fmt.Println("Running:", srv.Address)
StopServer(srv)
fmt.Println("After stop:", srv.Address)
}
The function returns a pointer so the caller can pass the same object to StopServer. If StartServer returned a value, StopServer would receive a copy, and the modification would not affect the original server.
Pointers enable lifecycle management. Hold the reference to control the resource.
Nil safety and error handling
Functions that return pointers must handle errors carefully. If initialization fails, the function should return nil for the pointer and an error. The caller must check the error before using the pointer. Accessing a field on a nil pointer triggers a panic with runtime error: invalid memory address or nil pointer dereference.
package main
import "fmt"
// User represents a database record.
type User struct {
ID int
Name string
}
// FetchUser returns a pointer to a User or an error.
func FetchUser(id int) (*User, error) {
if id <= 0 {
// Return nil pointer and an error.
return nil, fmt.Errorf("invalid id: %d", id)
}
return &User{ID: id, Name: "Alice"}, nil
}
func main() {
u, err := FetchUser(1)
if err != nil {
fmt.Println("Error:", err)
return
}
// Safe to use u because err is nil.
fmt.Println(u.Name)
}
The compiler does not enforce nil checks. You must write the check explicitly. The if err != nil pattern is verbose by design. It makes the unhappy path visible.
Nil checks are mandatory. A nil pointer dereference crashes the process.
Method receivers and return types
Returning a pointer affects which methods the caller can invoke. If you define methods with pointer receivers, the caller needs a pointer to call them. If you define methods with value receivers, the caller can call them on both values and pointers because Go auto-dereferences.
However, a value cannot call a pointer receiver method unless the value is addressable. Returning a pointer gives the caller full access to both receiver types. The caller can call methods defined on *T and methods defined on T.
The convention "accept interfaces, return structs" suggests returning concrete types. Returning a pointer to a struct is still returning a concrete type. The rule warns against returning interface values. Pointers are fine.
Pointers unlock pointer receiver methods. Values are more restrictive.
Escaping analysis in practice
You can verify where the compiler places variables. Run go build -gcflags="-m" to see escape analysis output. The compiler reports when a variable escapes to the heap. This flag is useful for debugging performance issues. You might see a message like escapes to heap next to a variable. If you are surprised by heap allocations, this flag reveals the cause.
The compiler is the expert. It moves variables to the heap only when necessary. If a variable does not escape, it stays on the stack. Stack allocation is faster and does not trigger garbage collection.
Use the escape flag to debug allocations. The compiler knows best.
Pitfalls and anti-patterns
Returning *string or *int is a common anti-pattern. Simple types are cheap to pass by value. Wrapping them in pointers forces heap allocation and requires nil checks. Use the value directly. If you need to represent absence, use an empty string or a separate boolean flag.
The Go community avoids pointers to primitives. Strings are immutable and cheap to pass by value. Integers and booleans are even smaller. Pointers to these types add overhead without benefit.
Don't pass a *string. Strings are already cheap to pass by value.
Simple types stay on the stack. Pointers to primitives add overhead.
When to use pointers versus values
Use a pointer return when the struct is large and copying it would waste CPU cycles or memory bandwidth. Use a pointer return when the caller needs to modify the returned value and see those changes reflected in the original object. Use a pointer return when the value represents a resource with a lifecycle, such as a connection or a server, and the caller must be able to close or update it. Use a pointer return when you need to distinguish between a zero value and an absent value, and the type does not support a natural empty state.
Return the value directly when the struct is small, typically under 64 bytes, and the caller only needs to read the data. Return the value directly when you want to enforce immutability and prevent the caller from changing the internal state. Return the value directly when the function creates a temporary result that should not outlive the call, though Go's escaping analysis usually handles this automatically.
Pointers are for sharing and mutating. Values are for copying and safety.