Structs & Interfaces

Structs, methods, pointer receivers, and how Go's implicit interfaces enable composition over inheritance.

Structs

Go has no classes — a struct groups related fields together, and methods are attached to it separately:

Go
type User struct {
    Name string
    Age  int
}

func main() {
    u := User{Name: "Ali", Age: 22}
    fmt.Println(u.Name, u.Age) // Ali 22
}

Methods

A method is a function with a receiver — the type it's attached to:

Go
type Rectangle struct {
    Width, Height float64
}

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

func main() {
    rect := Rectangle{Width: 10, Height: 5}
    fmt.Println(rect.Area()) // 50
}

Pointer receivers

A value receiver (func (r Rectangle) ...) gets a copy — changes don't affect the original. A pointer receiver (func (r *Rectangle) ...) operates on the original struct:

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

func main() {
    rect := Rectangle{Width: 10, Height: 5}
    rect.Scale(2) // Go automatically takes &rect here
    fmt.Println(rect.Width, rect.Height) // 20 10
}

Rule of thumb: if a method needs to modify the struct, or the struct is large, use a pointer receiver. Be consistent — don't mix value and pointer receivers on the same type without a good reason.

Interfaces

An interface defines a set of method signatures. Crucially, in Go, a type satisfies an interface implicitly — there's no implements keyword:

Go
type Shape interface {
    Area() float64
}

type Circle struct {
    Radius float64
}

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

// Rectangle from above already has an Area() float64 method,
// so both Circle and Rectangle automatically satisfy Shape.

func printArea(s Shape) {
    fmt.Println("Area:", s.Area())
}

func main() {
    printArea(Circle{Radius: 4})
    printArea(Rectangle{Width: 10, Height: 5})
}

This is called structural typing — "if it has the right methods, it satisfies the interface," with zero explicit declaration required. It's a major reason Go code tends to be loosely coupled by default.

Composition over inheritance

Go has no class inheritance. Instead, structs embed other structs to reuse fields and methods:

Go
type Animal struct {
    Name string
}

func (a Animal) Describe() string {
    return a.Name + " is an animal"
}

type Dog struct {
    Animal   // embedded — Dog "has" all of Animal's fields and methods
    Breed string
}

func main() {
    d := Dog{Animal: Animal{Name: "Rex"}, Breed: "Labrador"}
    fmt.Println(d.Describe()) // Rex is an animal — promoted from Animal
    fmt.Println(d.Name)       // Rex — field access is also promoted
}

The empty interface and any

interface{} (aliased as any since Go 1.18) can hold a value of any type — useful for generic-ish code before Go had real generics, and still common in APIs like encoding/json:

Go
func describe(v any) {
    fmt.Printf("value=%v type=%T\n", v, v)
}

describe(42)        // value=42 type=int
describe("hello")   // value=hello type=string

Common mistakes

  • Mixing value and pointer receivers inconsistently on the same type, which can cause confusing bugs about whether a mutation "stuck."
  • Forgetting that Go interfaces are satisfied implicitly — there's no compile error reminding you to "implement" one; a typo in a method name just silently fails to satisfy the interface.
  • Overusing any/interface{} where a concrete type or Go generics ([T any]) would be safer and clearer.

Interview questions

Q: How does Go achieve polymorphism without class inheritance? Through interfaces (any type with the right method set implicitly satisfies an interface) and struct embedding (composition), rather than a class hierarchy.

Q: When should a method use a pointer receiver instead of a value receiver? When the method needs to mutate the receiver, or when the struct is large enough that copying it on every call would be wasteful. For small, immutable-in-practice structs, value receivers are fine and simpler.

Q: What does it mean that Go interfaces are satisfied "implicitly"? A type doesn't declare which interfaces it implements — the compiler checks structurally, at the point of use, whether the type's method set matches what the interface requires. This decouples interface definitions from concrete types entirely.