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