03open 25 min

Structs and methods: composition over class trees

See how Go replaces many common Python class patterns with simpler building blocks.

by the end of this lesson you can

  • Defines a small interface instead of mirroring a large class surface
  • Uses a struct to hold dependencies
  • Keeps the method set as small as the use case requires

Overview

Python developers often reach for classes early. Go still supports methods, but it nudges you toward small structs, interfaces, and composition instead of deep object hierarchies.

In Python, you often

bundle state and behavior into classes, then extend them with inheritance or mixins as the model grows.

In Go, the common pattern is

to define a simple struct for data, attach a few methods when they improve readability, and compose behavior through interfaces or embedded fields.

why this difference matters

This keeps the model flatter. You spend less time designing inheritance and more time making data flow obvious.

Python

class User:
    def __init__(self, name):
        self.name = name

    def greet(self):
        return f"hello {self.name}"

Go

type User struct {
    Name string
}

func (u User) Greet() string {
    return "hello " + u.Name
}

Deeper comparison

Python version

class EmailSender:
    def send(self, message):
        ...

class NotificationService:
    def __init__(self, sender):
        self.sender = sender

    def notify(self, message):
        self.sender.send(message)

Go version

type Sender interface {
    Send(message string) error
}

type NotificationService struct {
    sender Sender
}

func (s NotificationService) Notify(message string) error {
    return s.sender.Send(message)
}

Reflect

What gets easier when you stop modeling behavior through inheritance and start modeling it through small interfaces and composition?

what a strong answer notices

A strong answer points to flatter designs, simpler dependencies, and clearer data ownership instead of large class hierarchies.

Rewrite

Rewrite this class-oriented Python pattern into a Go design that uses a struct plus a narrow interface.

Rewrite this Python

class Cache:
    def get(self, key):
        ...

class UserService:
    def __init__(self, cache):
        self.cache = cache

    def load(self, user_id):
        return self.cache.get(user_id)

what good looks like

  • Defines a small interface instead of mirroring a large class surface
  • Uses a struct to hold dependencies
  • Keeps the method set as small as the use case requires

Practice

Sketch a Go service that depends on a storage implementation without recreating an object-oriented inheritance tree.

success criteria

  • Separates the data-holding struct from the behavior contract
  • Uses dependency injection through a field or constructor
  • Avoids adding methods that are not required by the use case

Common mistakes

  • Recreating Python class hierarchies one-for-one in Go.
  • Defining interfaces too early and too broadly instead of at the consumer boundary.
  • Adding methods to structs just because the equivalent Python object had them.

takeaways

  • This keeps the model flatter. You spend less time designing inheritance and more time making data flow obvious.
  • A strong answer points to flatter designs, simpler dependencies, and clearer data ownership instead of large class hierarchies.
  • Separates the data-holding struct from the behavior contract