Skip to main content

Structs & Methods

Introduction​

Go does not have classes. Instead it has structs — custom types that group related fields together. You can then attach methods to a struct, which gives you behaviour similar to class methods in Python.

This is a significant difference from Python's object-oriented model. Go has no inheritance — there are no parent and child classes. Instead, Go uses composition: you embed one struct inside another to reuse its fields and methods. This is sometimes described as "composition over inheritance", a principle Go takes to an extreme.

Go does support interfaces, which define a set of methods a type must implement (covered in a later exercise). This is how Go achieves polymorphism without inheritance.


Concepts​

Defining a struct​

type Person struct {
FirstName string
LastName string
Age int
}

Creating struct values​

// With field names (recommended)
alice := Person{FirstName: "Alice", LastName: "Smith", Age: 30}

// Positional (order matters — avoid this style)
bob := Person{"Bob", "Jones", 25}

// Zero value then fill
var charlie Person
charlie.FirstName = "Charlie"
charlie.Age = 35

Methods​

A method is a function with a receiver — the struct it is attached to.

// Value receiver — gets a copy
func (p Person) FullName() string {
return p.FirstName + " " + p.LastName
}

// Pointer receiver — can modify the original
func (p *Person) HaveBirthday() {
p.Age++
}

Embedding (composition)​

type Employee struct {
Person // Embedded struct — inherits fields and methods
Company string
Salary float64
}

Examples​

package main

import "fmt"

type Rectangle struct {
Width float64
Height float64
}

func (r Rectangle) Area() float64 {
return r.Width * r.Height
}

func (r Rectangle) Perimeter() float64 {
return 2 * (r.Width + r.Height)
}

func (r *Rectangle) Scale(factor float64) {
r.Width *= factor
r.Height *= factor
}

func main() {
rect := Rectangle{Width: 10, Height: 5}
fmt.Printf("Area: %.1f\n", rect.Area())
fmt.Printf("Perimeter: %.1f\n", rect.Perimeter())

rect.Scale(2)
fmt.Printf("After scaling: %.1f x %.1f\n", rect.Width, rect.Height)
fmt.Printf("New area: %.1f\n", rect.Area())
}

Challenges​

1. Define a struct​

  • Define a Book struct with fields: Title (string), Author (string), Year (int), and Pages (int).
  • Create three instances and print each one.
💡 Show Answer
package main

import "fmt"

type Book struct {
Title string
Author string
Year int
Pages int
}

func main() {
b1 := Book{Title: "The Go Programming Language", Author: "Donovan & Kernighan", Year: 2015, Pages: 380}
b2 := Book{Title: "Clean Code", Author: "Robert Martin", Year: 2008, Pages: 431}
b3 := Book{Title: "Designing Data-Intensive Applications", Author: "Martin Kleppmann", Year: 2017, Pages: 616}

fmt.Printf("%+v\n", b1)
fmt.Printf("%+v\n", b2)
fmt.Printf("%+v\n", b3)
}

%+v prints a struct with field names included, e.g. {Title:Clean Code Author:Robert Martin Year:2008 Pages:431}.


2. Add a method​

  • Add a method Summary() string to your Book struct that returns a string like "Clean Code by Robert Martin (2008), 431 pages".
💡 Show Answer
func (b Book) Summary() string {
return fmt.Sprintf("%s by %s (%d), %d pages", b.Title, b.Author, b.Year, b.Pages)
}

// In main:
fmt.Println(b1.Summary())

3. Pointer receiver method​

  • Add a method UpdateYear(newYear int) to Book that updates the year in place.
  • Demonstrate that the change persists after the method call.

Ask Copilot: When should I use a pointer receiver vs a value receiver in Go?

💡 Show Answer
func (b *Book) UpdateYear(newYear int) {
b.Year = newYear
}

// In main:
fmt.Println(b1.Year) // 2015
b1.UpdateYear(2024)
fmt.Println(b1.Year) // 2024

Use a pointer receiver when the method needs to modify the struct. Use a value receiver when the method only reads the struct. If in doubt, use a pointer receiver — it's more efficient for large structs and consistent behaviour is easier to reason about.


4. Slice of structs​

  • Create a []Book slice with at least four books.
  • Write a loop that prints only books published after 2010.
💡 Show Answer
package main

import "fmt"

type Book struct {
Title string
Author string
Year int
Pages int
}

func main() {
books := []Book{
{Title: "Clean Code", Author: "Robert Martin", Year: 2008, Pages: 431},
{Title: "The Go Programming Language", Author: "Donovan & Kernighan", Year: 2015, Pages: 380},
{Title: "Designing Data-Intensive Applications", Author: "Kleppmann", Year: 2017, Pages: 616},
{Title: "The Pragmatic Programmer", Author: "Hunt & Thomas", Year: 1999, Pages: 352},
}

fmt.Println("Books published after 2010:")
for _, b := range books {
if b.Year > 2010 {
fmt.Printf(" %s (%d)\n", b.Title, b.Year)
}
}
}

5. Struct embedding​

  • Define an Animal struct with Name and Sound fields.
  • Define a Dog struct that embeds Animal and adds a Breed field.
  • Create a Dog and access its Name and Sound directly (without .Animal.Name).
💡 Show Answer
package main

import "fmt"

type Animal struct {
Name string
Sound string
}

func (a Animal) Speak() string {
return fmt.Sprintf("%s says %s", a.Name, a.Sound)
}

type Dog struct {
Animal
Breed string
}

func main() {
d := Dog{
Animal: Animal{Name: "Rex", Sound: "Woof"},
Breed: "Labrador",
}
fmt.Println(d.Name) // Rex — promoted field
fmt.Println(d.Speak()) // Rex says Woof — promoted method
fmt.Println(d.Breed) // Labrador
}

6. Compare to Python class​

  • Here is a Python class:
class Circle:
def __init__(self, radius):
self.radius = radius

def area(self):
import math
return math.Pi * self.radius ** 2

def scale(self, factor):
self.radius *= factor
  • Ask Copilot to convert it to a Go struct with methods.
  • Note the key differences in how state (fields) and behaviour (methods) are defined.
💡 Show Answer
package main

import (
"fmt"
"math"
)

type Circle struct {
Radius float64
}

func (c Circle) Area() float64 {
return math.Pi * c.Radius * c.Radius
}

func (c *Circle) Scale(factor float64) {
c.Radius *= factor
}

func main() {
c := Circle{Radius: 5}
fmt.Printf("Area: %.4f\n", c.Area())
c.Scale(2)
fmt.Printf("After scaling, radius: %.1f\n", c.Radius)
fmt.Printf("New area: %.4f\n", c.Area())
}

Key differences: no __init__ — structs are initialised with field names; no self — the receiver is named explicitly and by convention is one or two letters; no import inside the method.


7. Ask Copilot about inheritance​

  • Ask Copilot: Go has no inheritance. How do Go programmers reuse code and model "is-a" relationships?
  • Read the explanation of composition and interfaces. Write a brief summary in your own words.
💡 Show Answer

Go uses two mechanisms instead of inheritance:

  1. Embedding (composition): Embed one struct inside another. The embedded struct's fields and methods are "promoted" to the outer struct, giving a similar effect to inheritance without the tight coupling.

  2. Interfaces: Define a set of methods. Any type that implements those methods satisfies the interface automatically — no explicit implements keyword needed. This is called "structural typing" or "duck typing at compile time".

These two together allow flexible code reuse without the pitfalls of deep inheritance hierarchies.