Structs and Interfaces

The API Design That Finally Clicked

Month three with Go. I was building a notification system that needed to send messages via email, SMS, and Slack. Coming from object-oriented languages, I initially tried to build a class hierarchy in my head:

Notifier (base class)
β”œβ”€β”€ EmailNotifier
β”œβ”€β”€ SMSNotifier
└── SlackNotifier

But Go has no inheritance. No classes. No extends. I spent two days fighting the language before discovering interfaces:

type Notifier interface {
    Send(message string) error
}

type EmailNotifier struct { /* ... */ }
func (e *EmailNotifier) Send(message string) error { /* ... */ }

type SMSNotifier struct { /* ... */ }
func (s *SMSNotifier) Send(message string) error { /* ... */ }

type SlackNotifier struct { /* ... */ }
func (s *SlackNotifier) Send(message string) error { /* ... */ }

// This just works!
func notify(n Notifier, msg string) {
    n.Send(msg)
}

No inheritance declarations. No implements keyword. If it has the method, it satisfies the interface. This implicit satisfaction changed how I design systems.

This article covers Go's approach: composition over inheritance, interfaces for behavior.


Structs: Organizing Data

Structs group related data together.

Basic Struct

Creating Struct Instances

Accessing Fields

Pointers to Structs

This is equivalent but more common:


Anonymous Structs

Useful for temporary data:


Struct Embedding (Composition)

Instead of inheritance, Go uses composition:

Anonymous Embedding (Promoted Fields)

Real example from my user service:


Methods on Structs

We covered this briefly before, but let's go deeper:

When to Use Pointer Receivers

  1. Method modifies the receiver

  2. Receiver is large (avoid copying)

  3. Consistency (if one method needs pointer, use pointers for all)

From my code review checklist:


Interfaces: Defining Behavior

Interfaces specify what types can do, not how they do it.

Interface Declaration

Implementing Interfaces

No explicit declaration needed! If a type has the methods, it satisfies the interface.

This is duck typing at compile time. If it walks like a duck and quacks like a duck, it's a duck.


Interface Satisfaction Example

My notification system:

All three types satisfy Notifier without explicitly declaring it!


Empty Interface

The empty interface interface{} has zero methods, so all types satisfy it:

Use case: When you truly don't know the type (like json.Unmarshal).

Modern Go (1.18+) uses any as an alias:


Type Assertions

Extract concrete value from interface:

Type Switches

Check multiple types:

Real example from my API router:


Standard Library Interfaces

io.Reader and io.Writer

These are Go's most important interfaces:

Why they matter: The entire standard library uses them.

Real example - copy from any reader to any writer:

Stringer Interface

If a type implements String(), fmt.Printf uses it:


Interface Embedding

Interfaces can embed other interfaces:


Practical Example: Plugin System

From my application framework:

Add new plugins without modifying application code!


Struct Tags

Metadata for struct fields, used by reflection:

JSON marshaling:


Your Challenge

Build a shape calculator:


Key Takeaways

  1. Structs group data, methods add behavior

  2. Composition over inheritance - embed structs

  3. Interfaces define behavior - not data

  4. Implicit satisfaction - no implements keyword

  5. Empty interface accepts any type

  6. Type assertions extract concrete values

  7. io.Reader/Writer are foundational interfaces

  8. Struct tags add metadata for libraries


What I Learned

The notification system taught me that interfaces are about behavior, not hierarchy:

  • No inheritance forced better design

  • Implicit satisfaction made code flexible

  • Small interfaces (1-3 methods) are best

  • Composition beat inheritance every time

Coming from Java/C++, no classes felt limiting. But interfaces + composition proved more powerful. I could swap implementations without touching calling code. Tests became trivial with mock implementations.

Go's "composition over inheritance" isn't a restriction - it's freedom.


Next: Error Handling in Go

In the next article, we'll dive into Go's explicit error handling: the error interface, wrapping errors, when to panic, and the production incident that taught me to handle errors properly.

Last updated