GoBackend

Go Functions, Structs & Pointers: Complete Tutorial

TT
TopicTrick Team
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.

    go

    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.

    go

    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

    Address-of (&)&myVar

    The ampersand operator returns the memory address of a variable.

    Pointer Type (*)func change(p *int)

    The asterisk denotes a pointer type when used in a variable or argument declaration.

    Dereferencing (*)val := *myPointer

    The asterisk can also reach 'inside' the pointer to read or modify the actual value stored at that address.

    Functional Comparison

    Task / FeaturePass by Value (Copy)Pass by Pointer (Reference)
    Memory UsageHeavier (Creates duplicates)Lightweight (Shared access)
    Original DataSafe (Cannot be modified)Mutable (Function can change it)
    PerformanceSlightly slower for large dataExtremely 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.

    go

    Closures

    A closure is a function that "closes over" variables from its surrounding scope:

    go

    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.

    go

    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:

    go

    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.
    go

    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.