# Structs, methods & interfaces

> Model data with structs, attach behaviour with methods, decouple code with interfaces and write reusable generic functions.

Backend with Go roadmap · Foundations · 17 min read · https://levelcode.org/roadmaps/go-backend/go-types-and-interfaces

Go has no classes and no inheritance. Instead it gives you three small tools that combine well: **structs** to group data, **methods** to attach behaviour to types, and **interfaces** to describe behaviour without naming a concrete type. Add **generics** for code that works across many types, and you have everything you need to model a backend's domain. This lesson covers each tool, how they fit together, and the traps that catch newcomers.

## Structs: grouping data

A struct is a typed collection of named fields:

```go
type User struct {
	ID    int
	Email string
	Name  string
	Admin bool
	Tags  []string
}
```

Create values with a **composite literal**. Always name the fields; positional literals break as soon as someone adds a field.

```go
u := User{ID: 1, Email: "ana@example.com", Name: "Ana"}
u.Admin = true
fmt.Println(u.Name, u.Admin, len(u.Tags)) // Ana true 0

// The zero value: every field is its own zero value.
var empty User
fmt.Printf("%+v\n", empty) // {ID:0 Email: Name: Admin:false Tags:[]}
```

Structs are **values**. Assigning one or passing it to a function copies every field. To share a single struct, use a pointer, written `*User`. The `&` operator takes the address of a value:

```go
func promote(u *User) { u.Admin = true } // changes the caller's struct

bo := User{Name: "Bo"}
promote(&bo)
fmt.Println(bo.Admin) // true

p := &User{Name: "Cy"} // pointer to a new struct
p.Name = "Cyrus"       // Go dereferences automatically: no (*p).Name needed
```

Field names follow the export rule: `Email` is visible to other packages, a field named `email` is not. This matters later, because packages like `encoding/json` can only see exported fields.

## Methods and receivers

A method is a function with a **receiver**, the value it is called on, declared before the name:

```go
type Rect struct{ W, H float64 }

func (r Rect) Area() float64 { return r.W * r.H } // value receiver

func (r *Rect) Scale(f float64) { // pointer receiver
	r.W *= f
	r.H *= f
}

func main() {
	r := Rect{W: 2, H: 3}
	r.Scale(2)            // Go takes &r for you
	fmt.Println(r.Area()) // 24
}
```

A **value receiver** gets a copy, so it cannot change the original. A **pointer receiver** gets the address, so it can. Use a pointer receiver when the method must modify the receiver, when the struct is large, or when the struct contains something that must not be copied, such as a `sync.Mutex`. If any method needs a pointer receiver, give all of the type's methods pointer receivers so the type behaves consistently.

You can define methods on any named type declared in your package, not just structs:

```go
type Celsius float64

func (c Celsius) Fahrenheit() float64 { return float64(c)*9/5 + 32 }
```

Go has no constructors. By convention, a function named `NewX` builds a valid `X`, filling in defaults and returning a pointer when the value will be shared or mutated:

```go
type Counter struct {
	name  string
	count int
}

func NewCounter(name string) *Counter { return &Counter{name: name} }

func (c *Counter) Inc()       { c.count++ }
func (c *Counter) Value() int { return c.count }
```

## Embedding: composition, not inheritance

A struct can **embed** another type by listing it without a field name. The embedded type's fields and methods are *promoted*, so you can use them as if they belonged to the outer struct:

```go
type Timestamps struct {
	CreatedAt time.Time
	UpdatedAt time.Time
}

func (t *Timestamps) Touch() { t.UpdatedAt = time.Now() }

type Post struct {
	Timestamps // embedded
	ID         int
	Title      string
}

func main() {
	var p Post
	p.Touch()                           // promoted method
	fmt.Println(!p.UpdatedAt.IsZero())  // promoted field: true
	fmt.Println(p.Timestamps.UpdatedAt) // still reachable by its type name
}
```

This is *composition*. A `Post` is not a `Timestamps`; you cannot pass a `Post` where a `Timestamps` is expected. The compiler simply forwards `p.Touch()` to `p.Timestamps.Touch()`. If the outer struct defines a field or method with the same name, the outer one wins.

## Interfaces: behaviour as a contract

An interface type lists method signatures. Any type that has those methods **automatically** satisfies the interface; there is no `implements` keyword.

```go
type Notifier interface {
	Notify(to, msg string) error
}

type EmailNotifier struct{ From string }

func (e EmailNotifier) Notify(to, msg string) error {
	fmt.Printf("email from %s to %s: %s\n", e.From, to, msg)
	return nil
}

type LogNotifier struct{}

func (LogNotifier) Notify(to, msg string) error {
	fmt.Println("log:", to, msg)
	return nil
}

func Welcome(n Notifier, email string) error {
	return n.Notify(email, "Welcome aboard!")
}

func main() {
	Welcome(EmailNotifier{From: "team@example.com"}, "ana@example.com")
	Welcome(LogNotifier{}, "bo@example.com") // e.g. in tests or local dev
}
```

`Welcome` neither knows nor cares which notifier it gets. That is the point: the code that *uses* behaviour declares a small interface for exactly what it needs, and any type can plug in. The standard library is built this way. `io.Reader` has one method, `Read(p []byte) (n int, err error)`, and files, network connections, HTTP request bodies and in-memory buffers all satisfy it. `fmt.Stringer` has one method, `String() string`, which `fmt` uses when printing your types.

Guidelines that keep interfaces useful:

- **Keep them small.** One to three methods is typical. Big interfaces are hard to implement and to fake in tests.
- **Define them where they are used**, not next to the implementation. A package that sends welcome emails declares `Notifier`; the email package just exports a concrete type.
- **Accept interfaces, return concrete types.** `NewCounter` returns `*Counter`, not an interface, so callers get the full type.

### Method sets: the pointer trap

If a method has a pointer receiver, only the *pointer* type has that method. A plain value does not satisfy the interface:

```go
type Shape interface{ Area() float64 }

type Square struct{ S float64 }

func (s *Square) Area() float64 { return s.S * s.S }

var _ Shape = &Square{S: 2} // ok
// var _ Shape = Square{S: 2} // compile error: Area method has pointer receiver
```

The line `var _ Shape = &Square{}` is also a handy idiom: a compile-time check that a type satisfies an interface.

## Type assertions, type switches and nil interfaces

An interface value holds two things: a concrete type and a value of that type. A **type assertion** extracts the concrete value; use the two-result form so a mismatch returns `false` instead of panicking:

```go
var n Notifier = EmailNotifier{From: "team@example.com"}

if e, ok := n.(EmailNotifier); ok {
	fmt.Println("sender is", e.From)
}
```

A **type switch** branches on the dynamic type. Inside each case the variable has that type:

```go
func describe(v any) string { // any is an alias for interface{}
	switch x := v.(type) {
	case int:
		return fmt.Sprintf("int %d", x)
	case string:
		return fmt.Sprintf("string of length %d", len(x))
	case fmt.Stringer:
		return "stringer: " + x.String()
	default:
		return fmt.Sprintf("something else: %T", x)
	}
}
```

> [!WARNING]
> An interface is `nil` only when both its type and value are unset. Assign a nil `*EmailNotifier` to a `Notifier` and the interface is **not** nil: it has a type (`*EmailNotifier`) holding a nil pointer. Code that returns a typed nil pointer through an interface makes `== nil` checks lie. Return a literal `nil` instead.

## Generics: types as parameters

Interfaces abstract over *behaviour*. Sometimes you want to abstract over *types* while keeping full type safety, for example a function that works on a slice of anything. That is what type parameters are for:

```go
func Filter[T any](items []T, keep func(T) bool) []T {
	var out []T
	for _, it := range items {
		if keep(it) {
			out = append(out, it)
		}
	}
	return out
}

evens := Filter([]int{1, 2, 3, 4}, func(n int) bool { return n%2 == 0 }) // [2 4]
```

`T` is inferred from the arguments. The **constraint** after the parameter says what `T` must support. `any` allows every type but lets you do little with it. `comparable` allows `==`, which map keys need. `cmp.Ordered` allows `<` and `>`:

```go
func MaxOf[T cmp.Ordered](first T, rest ...T) T {
	m := first
	for _, v := range rest {
		if v > m {
			m = v
		}
	}
	return m
}

fmt.Println(MaxOf(3, 9, 4), MaxOf("pear", "apple")) // 9 pear
```

Types can be generic too:

```go
type Stack[T any] struct{ items []T }

func (s *Stack[T]) Push(v T) { s.items = append(s.items, v) }

func (s *Stack[T]) Pop() (T, bool) {
	var zero T
	if len(s.items) == 0 {
		return zero, false
	}
	v := s.items[len(s.items)-1]
	s.items = s.items[:len(s.items)-1]
	return v, true
}
```

> [!TIP]
> Reach for generics when you would otherwise copy the same code for several types (containers, slice helpers). Reach for an interface when different types have different *behaviour* behind the same method. Most domain code needs neither and is clearer with plain concrete types.

## Practice

Build a tiny inventory model:

1. Define `Product` with `SKU string`, `Name string`, `Qty int`, and a `String()` method that prints like `KB-01 Keyboard (x3)`.
2. Define `Inventory` holding a `map[string]*Product`, with `NewInventory()`, `Add(p *Product)` and `Remove(sku string, n int) bool` (returns `false` if the SKU is unknown or stock is insufficient).
3. Write a generic `Filter` and use it to print every product with `Qty < 5`, in SKU order.

**Solution**

```go
package main

import (
	"fmt"
	"maps"
	"slices"
)

type Product struct {
	SKU  string
	Name string
	Qty  int
}

func (p *Product) String() string { return fmt.Sprintf("%s %s (x%d)", p.SKU, p.Name, p.Qty) }

type Inventory struct{ items map[string]*Product }

func NewInventory() *Inventory { return &Inventory{items: make(map[string]*Product)} }

func (inv *Inventory) Add(p *Product) { inv.items[p.SKU] = p }

func (inv *Inventory) Remove(sku string, n int) bool {
	p, ok := inv.items[sku]
	if !ok || p.Qty < n {
		return false
	}
	p.Qty -= n
	return true
}

func Filter[T any](items []T, keep func(T) bool) []T {
	var out []T
	for _, it := range items {
		if keep(it) {
			out = append(out, it)
		}
	}
	return out
}

func main() {
	inv := NewInventory()
	inv.Add(&Product{SKU: "KB-01", Name: "Keyboard", Qty: 8})
	inv.Add(&Product{SKU: "MS-02", Name: "Mouse", Qty: 2})
	inv.Add(&Product{SKU: "MN-03", Name: "Monitor", Qty: 4})

	fmt.Println(inv.Remove("KB-01", 5), inv.Remove("MS-02", 9)) // true false

	var all []*Product
	for _, sku := range slices.Sorted(maps.Keys(inv.items)) {
		all = append(all, inv.items[sku])
	}
	for _, p := range Filter(all, func(p *Product) bool { return p.Qty < 5 }) {
		fmt.Println(p) // fmt calls String() because *Product is a fmt.Stringer
	}
}
```

```text
true false
KB-01 Keyboard (x3)
MN-03 Monitor (x4)
MS-02 Mouse (x2)
```

The map stores pointers, so `Remove` changes the product in place. `String` has a pointer receiver, which is why the slice holds `*Product`: a plain `Product` value would not be a `fmt.Stringer`.

## Key takeaways

- Structs group named fields and are copied on assignment; use pointers (`*T`, `&v`) to share and mutate one value.
- Use pointer receivers when a method mutates state or the struct is large, and keep receivers consistent per type.
- Embedding promotes fields and methods for composition; it does not create an "is-a" relationship.
- Interfaces are satisfied implicitly: keep them small, define them where they are used, and return concrete types.
- An interface holding a nil pointer is not nil; return a literal `nil` instead.
- Generics with constraints (`any`, `comparable`, `cmp.Ordered`) remove duplication across types without losing type safety.

## Quiz

1. Square has func (s *Square) Area() float64. Why does var sh Shape = Square{S: 2} fail to compile, where Shape requires Area() float64?

   A. Interfaces can only hold pointers
   B. Square must declare that it implements Shape
   C. Area returns the wrong type
   D. Area has a pointer receiver, so only *Square has that method and the value Square does not satisfy Shape

   **Answer: D.** The method set of the value type Square excludes pointer-receiver methods. &Square{S: 2} would compile. Go interfaces are satisfied implicitly, so there is no implements declaration to add.

2. Post embeds Timestamps, which has a Touch() method. What does that give you?

   A. You can call p.Touch() directly, but a Post still cannot be passed where a Timestamps is required
   B. Post becomes a subtype of Timestamps and can be used anywhere a Timestamps is expected
   C. Timestamps' fields are copied into Post and the embedded value is discarded
   D. Nothing unless Post re-declares Touch()

   **Answer: A.** Embedding promotes fields and methods so p.Touch() is forwarded to p.Timestamps.Touch(). It is composition, not inheritance: the types remain distinct, and p.Timestamps is still accessible.

3. What does this print? var p *EmailNotifier; var n Notifier = p; fmt.Println(n == nil)

   A. true
   B. It panics
   C. false
   D. It does not compile

   **Answer: C.** n holds a type (*EmailNotifier) and a nil pointer value. An interface is only nil when both type and value are unset, so n == nil is false. It compiles because *EmailNotifier's method set includes the value-receiver Notify method, and comparing with nil never panics.

4. You want a generic function Max[T ...](a, b T) T that uses the > operator. Which constraint fits?

   A. any
   B. cmp.Ordered
   C. comparable
   D. fmt.Stringer

   **Answer: B.** cmp.Ordered permits the ordering operators < <= > >= on integers, floats and strings. comparable only permits == and !=, and any permits neither.
