Week 3: Structs, Methods & Interfaces

Go has no classes and no inheritance — and deliberately so. This week covers what it has instead: plain data structs, methods attached to them explicitly, and interfaces satisfied implicitly just by having the right methods. It's a different way of thinking about types than most languages teach, and it's worth sitting with.

Module 3 of 16 Week 3 of 16 ~3 Hours Hands-on Exercise Included

By the end of this week, you'll be able to

  • Define a struct and attach methods to it with both value and pointer receivers
  • Explain when a pointer receiver is required, and Go's rule of thumb for choosing one
  • Define an interface and understand Go's implicit, structural satisfaction — no implements keyword
  • Use struct embedding to compose behavior instead of reaching for inheritance

1. Defining Structs

A struct is a named collection of fields — Go's only real way to group related data together, with none of the class machinery (constructors, access modifiers, inheritance) that comes bundled with it in other languages.

a basic struct
type User struct {
    ID    int
    Name  string
    Email string
}

func main() {
    u := User{ID: 1, Name: "Asha", Email: "asha@example.com"}
    fmt.Println(u.Name) // Asha

    // a "zero value" struct — every field defaults to its type's zero value
    var empty User
    fmt.Println(empty) // {0  }
}

There's no separate constructor syntax — the idiomatic pattern for validated or defaulted construction is a plain function, conventionally named NewUser, that returns a fully-formed struct (or an error).

2. Methods: Value vs. Pointer Receivers

A method is a function with a receiver — the type it's attached to, written before the function name. The receiver can be a value (a copy) or a pointer (a reference to the original).

value receiver vs. pointer receiver
func (u User) Greeting() string { // value receiver: gets a copy of u
    return "Hi, " + u.Name
}

func (u *User) SetEmail(email string) { // pointer receiver: mutates the original
    u.Email = email
}

func main() {
    u := User{Name: "Asha"}
    fmt.Println(u.Greeting()) // "Hi, Asha" — reading, a copy is fine

    u.SetEmail("asha@new.com") // Go automatically takes &u here
    fmt.Println(u.Email)       // "asha@new.com" — the original was mutated
}

The rule of thumb: use a pointer receiver whenever the method needs to mutate the receiver, or the struct is large enough that copying it on every call is wasteful. Mixing receiver types on the same struct's methods is legal but considered poor style — pick one and stay consistent.

3. Interfaces & Structural Typing

An interface is a set of method signatures. A type satisfies it automatically the moment it implements those methods — there's no implements keyword, and a type can satisfy an interface it was never written with in mind.

an interface, satisfied implicitly
type Greeter interface {
    Greeting() string
}

func Welcome(g Greeter) {
    fmt.Println(g.Greeting())
}

func main() {
    u := User{Name: "Asha"}
    Welcome(u) // User satisfies Greeter just by having a Greeting() method
}

This is Go's version of duck typing, checked at compile time — Welcome accepts anything with a Greeting() string method, whether or not that type's author ever heard of Greeter. It's what makes small, focused interfaces (often just one or two methods) so common in idiomatic Go: a caller only needs to define the interface it depends on, not coordinate with the implementer.

4. Composition over Inheritance

Instead of subclassing, Go lets a struct embed another — the outer struct gains the inner struct's fields and methods directly, without an explicit delegation step.

embedding a struct
type Base struct {
    ID int
}

func (b Base) Describe() string {
    return fmt.Sprintf("entity #%d", b.ID)
}

type Product struct {
    Base   // embedded, no field name — "Base" is implied
    Name   string
    Price  float64
}

func main() {
    p := Product{Base: Base{ID: 7}, Name: "Widget", Price: 9.99}
    fmt.Println(p.ID)         // 7 — promoted field, accessed directly
    fmt.Println(p.Describe()) // "entity #7" — promoted method
}

This is composition, not inheritance — Product doesn't "become a" Base, it just has one, with its exported fields and methods promoted up for convenience. Combined with small interfaces, this is how Go achieves what inheritance-heavy languages use class hierarchies for, without the fragility of a deep inheritance tree.

5. Hands-on Exercise

Hands-on

Model a small inventory system

Structs, methods, an interface, and embedding, all in one small program.

Requirements:

  1. A Item struct with Name string, Quantity int, and Price float64 fields.
  2. A pointer-receiver method Restock(n int) that adds to Quantity, and a value-receiver method Total() float64 that returns Price * float64(Quantity).
  3. A Describer interface with a Describe() string method, satisfied by Item without ever mentioning the interface in Item's own definition.
  4. A PerishableItem struct that embeds Item and adds an ExpiresInDays int field, confirming its promoted Total() still works correctly.
  5. A function PrintDescription(d Describer) called with both an Item and a PerishableItem.
Hint

If Restock doesn't seem to actually change the quantity when called, check whether you're calling it on a value or a pointer, and whether the method itself has a pointer receiver — a value-receiver method mutates only its own copy, so Restock needs *Item as its receiver to have any lasting effect.

6. Knowledge Check

Four quick questions. Expand each to check your answer.

Q1

Why does SetEmail(email string) need a pointer receiver to actually mutate a User, where Greeting() does not?

A value receiver method operates on a copy of the struct — any change it makes is local to that copy and disappears when the method returns. A pointer receiver operates on the original via a reference, so a mutation there is visible after the call. Greeting() only reads, so a copy is harmless and even simpler; SetEmail needs to persist a change, so it needs the pointer.

Q2

What does it mean for Go's interfaces to use "structural typing," and how is that different from an implements-keyword language?

A type satisfies a Go interface automatically the moment its method set matches the interface's — there's no explicit declaration linking the two, and a type's author doesn't even need to know the interface exists. In an implements-keyword language, satisfying an interface requires the type to explicitly declare that relationship at definition time.

Q3

In the embedding example, why does p.Describe() work even though Product never defines a Describe method itself?

Embedding Base inside Product promotes Base's exported fields and methods up to Product automatically — Product effectively gains Describe() for free by containing a Base, without Product having to redeclare or delegate to it manually.

Q4

Why might mixing value and pointer receivers on the same struct's methods cause subtle bugs, even though Go allows it?

A caller working with a value (not a pointer) can call a pointer-receiver method directly only if the value is addressable — but a value stored in certain contexts (like a map value, or a non-addressable temporary) isn't addressable and won't compile at all for a pointer-receiver method. Inconsistent receiver types also make it unpredictable, at a glance, whether a given method mutates the original or a copy — which is why idiomatic Go picks one receiver kind per type and sticks with it.