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
Bookstruct with fields:Title(string),Author(string),Year(int), andPages(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() stringto yourBookstruct 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)toBookthat 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
[]Bookslice 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
Animalstruct withNameandSoundfields. - Define a
Dogstruct that embedsAnimaland adds aBreedfield. - Create a
Dogand access itsNameandSounddirectly (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:
-
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.
-
Interfaces: Define a set of methods. Any type that implements those methods satisfies the interface automatically — no explicit
implementskeyword 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.