Go Functions, Structs & Pointers: Complete Tutorial

Go Functions, Structs & Pointers: Complete Tutorial
Quick Answer: Functions, Structs, and Pointers in Go
Go functions are defined with func name(params) returnType. They uniquely support multiple return values — used idiomatically for returning a result alongside an error. Structs group related fields into custom types. Pointers (*T) allow functions to modify the original value rather than a copy, and are essential for method receivers that need to mutate struct state.
Go Functions, Structs & Pointers
In our previous modules, we established the basic building blocks of data and logic. Now, we're ready to organize our code into reusable components. Go's approach to functions and data structures is both powerful and deceptively simple.
There are no classes or inheritance hierarchies. Instead, Go uses Structs for data and Functions for behavior. This "composition over inheritance" philosophy is what makes Go code so robust and easy to maintain at scale.
Pointers Without the Pain
Reusable Logic: Functions
Functions in Go are first-class citizens. They can return multiple values, take other functions as arguments, and can even be assigned to variables.
Creating Custom Types: Structs
Since Go is not a traditional object-oriented language, it uses Structs to group related data together. Think of a Struct as a blueprint for a typed collection of fields.
Performance & References: Pointers
By default, when you pass a variable to a function, Go creates a copy of that data. If you want a function to modify the original data, or if the data is very large, you should pass a Pointer.
Working with Pointers
&myVarThe ampersand operator returns the memory address of a variable.
func change(p *int)The asterisk denotes a pointer type when used in a variable or argument declaration.
val := *myPointerThe asterisk can also reach 'inside' the pointer to read or modify the actual value stored at that address.
Functional Comparison
| Task / Feature | Pass by Value (Copy) | Pass by Pointer (Reference) |
|---|---|---|
| Memory Usage | Heavier (Creates duplicates) | Lightweight (Shared access) |
| Original Data | Safe (Cannot be modified) | Mutable (Function can change it) |
| Performance | Slightly slower for large data | Extremely fast for complex structs |
First-Class Functions and Closures
In Go, functions are first-class values — you can assign them to variables, pass them as arguments, and return them from other functions. This enables powerful patterns like middleware, callbacks, and functional programming idioms.
Closures
A closure is a function that "closes over" variables from its surrounding scope:
Closures are used extensively in Go for middleware, deferred cleanup functions, and goroutine payloads.
Named Return Values
Go allows you to name return values in the function signature. This has two effects: the named return value is automatically declared as a local variable, and a bare return statement (without arguments) returns the current values of all named returns.
Named returns are useful for short functions where they add clarity. For longer functions, explicit returns are preferred to avoid confusion.
Struct Embedding: Composition in Practice
Go achieves inheritance-like behaviour through struct embedding. An embedded type's methods and fields are promoted to the outer struct:
This is how Go implements code reuse without classical inheritance — and it is far more flexible because you can embed multiple types.
When to Use Pointers vs. Values
The rule of thumb:
- Use a value receiver for small, immutable structs (like a
Point{X, Y}). Copying is cheap and safe. - Use a pointer receiver if the method needs to modify the struct, or if the struct is large (slices, maps, or structs with many fields).
- Be consistent within a type: if any method uses a pointer receiver, all methods should use pointer receivers.
Further Reading
For the next step after functions and structs, see Go methods and interfaces which builds on these concepts to implement polymorphism. For applying structs to concurrent programs, see Go goroutines and the scheduler. To understand the broader Go module system that organises your functions and packages, see Go modules and packages.
Next Steps
Now that you can create functions, custom data types, and manage how they're passed through memory using pointers, we're ready for the "killer feature" of Go. In the next tutorial, we will explore Concurrency through Goroutines and Channels—the secret to Go's incredible performance in cloud-native applications.
Common Mistakes with Go Functions, Pointers, and Structs
1. Passing large structs by value
Go copies the entire struct when you pass it by value. For structs with many fields, pass a pointer (*MyStruct) instead. The rule of thumb: if the struct is larger than a few words, use a pointer.
2. Nil pointer dereference
Declaring var p *Person gives you a nil pointer. Accessing p.Name without checking if p != nil first causes a runtime panic. Always initialise pointers before use: p := &Person{Name: "Alice"}.
3. Forgetting pointer receivers for mutation
A method with a value receiver (func (p Person) SetName(...)) receives a copy — changes do not affect the original. Use a pointer receiver (func (p *Person) SetName(...)) when the method needs to modify the struct's fields.
4. Not exporting fields for JSON marshalling
json.Marshal and json.Unmarshal only process exported (capitalised) struct fields. A field like name string will be silently ignored. Use Name string with a JSON tag: Name string \json:"name"``.
5. Using struct literals without field names
Person{"Alice", 30} works today but breaks silently if someone adds a field to the struct. Always use named fields: Person{Name: "Alice", Age: 30}. See the Go spec on composite literals.
Frequently Asked Questions
When should I use a pointer vs a value receiver? Use pointer receivers when the method modifies the receiver, or when the struct is large. Use value receivers for small, immutable structs and when the method does not modify state. Consistency within a type matters more than the individual choice — if any method on a type uses a pointer receiver, all methods should.
What is the difference between new(T) and &T{}?
Both allocate a zeroed T and return a pointer. &T{} is idiomatic and lets you initialise fields inline. new(T) is rarely used in modern Go code but is equivalent for zero-value allocation.
Can Go structs implement interfaces?
Yes. In Go, interface implementation is implicit — a struct satisfies an interface by simply implementing all of the interface's methods. There is no implements keyword. This duck-typing approach is documented in the Go tour on interfaces.
