· Kareem Hamed Ibrahim · Go · 4 min read
If It Acts Like a Duck, Then It's a Duck: Go's Implicit Interfaces
Understanding Go's structural typing, the hidden pitfalls, and the one-line trick that saves you from silent implementation bugs.
The Duck That Isn’t Quite a Duck
I was listening to a podcast with Yahya ElAraby interviewing Ehab Terra, and Ehab mentioned something that perfectly captures one of Go’s most confusing concepts for beginners:
“If it acts like a duck, then it’s a duck.”
This is Go’s philosophy on interfaces. And it’s deceptively powerful—until it bites you.

The Problem: Implicit Implementation
In languages like Java or PHP, you’re explicit about what you implement:
// Java - you MUST declare this
public class ProductRepository implements IProductRepository {
// Must implement every method
}// PHP - explicit declaration
class ProductRepository implements ProductRepositoryInterface {
// Must implement every method
}But in Go? There’s no implements keyword.
// Go - no explicit declaration needed
type ProductRepository struct {
db *sql.DB
}
// If it has all the methods, it automatically implements the interface
func (r *ProductRepository) GetProduct(id string) (*Product, error) {
// implementation
}
func (r *ProductRepository) SaveProduct(p *Product) error {
// implementation
}If your ProductRepository has all the methods that ProductRepository interface requires, it automatically implements it. No declaration. No compiler check. No proof.
Why This Is Confusing
You might think you’ve implemented an interface. Tests might pass. Your code might run.
But you might have:
- Forgotten a method
- Changed a method signature by mistake
- Named something slightly differently
- Added the method to the wrong receiver type
And you won’t know until runtime—or worse, until production.
The Solution: One Line You Must Know
Here’s the trick that most Go developers don’t know until they’ve burned by it:
var _ domain.ProductRepository = (*ProductRepository)(nil)What does this do?
It tells the compiler: “Verify that ProductRepository actually implements ProductRepository interface.”
If you forgot a method? Compilation fails. If you changed a signature? Compilation fails. If the struct and interface don’t match? The code won’t compile.
This one line catches implementation mistakes at compile time, not runtime.
Why Does Go Work This Way?
Go’s designers made a conscious choice: implicit interface implementation.
Why?
1. Encourages Composition Over Inheritance
When interfaces don’t require explicit declarations, you stop thinking about rigid class hierarchies. You start thinking about what a thing does, not what it is.
A FileWriter and a NetworkWriter both do the same thing: write bytes. In Go, you define the Writer interface once, and any type that has a Write([]byte) error method automatically satisfies it.
No inheritance chains. No artificial hierarchies.
2. Makes Interfaces Small
Because you can implement an interface anywhere—even in a different package, on a type you didn’t define—interfaces tend to be focused and small.
The io.Reader interface has one method:
type Reader interface {
Read(p []byte) (n int, err error)
}That’s it. Small. Composable. Reusable.
3. Decouples Code
You can define an interface in one package and have types in completely different packages implement it without knowing about each other.
This is powerful. But it requires discipline.
The Downside: Freedom Requires Responsibility
Here’s the trade-off:
Great freedom. Greater responsibility.
With implicit interfaces, you can:
- Implement multiple interfaces
- Create interfaces on the fly
- Compose types easily
But you must:
- Verify your implementations are correct
- Test edge cases rigorously
- Use the compiler trick to catch mistakes early
- Review code carefully
In large projects, this difference matters. A missing method in Java fails at compile time immediately. In Go, if you don’t actively verify, it silently fails.
How to Use This Safely
If you’re learning Go, adopt this pattern from day one:
1. Define Your Interface
// domain/repository.go
package domain
type ProductRepository interface {
GetProduct(id string) (*Product, error)
SaveProduct(p *Product) error
DeleteProduct(id string) error
}2. Implement It
// infrastructure/postgres_repository.go
package infrastructure
type PostgresProductRepository struct {
db *sql.DB
}
func (r *PostgresProductRepository) GetProduct(id string) (*Product, error) {
// implementation
}
func (r *PostgresProductRepository) SaveProduct(p *Product) error {
// implementation
}
func (r *PostgresProductRepository) DeleteProduct(id string) error {
// implementation
}3. Verify the Implementation (The Critical Step)
// infrastructure/postgres_repository.go - add this near the type definition
var _ domain.ProductRepository = (*PostgresProductRepository)(nil)This tells the compiler: “If PostgresProductRepository doesn’t fully implement ProductRepository, this code won’t compile.”
You’ll catch mistakes immediately. At compile time. Before they reach production.
The Bigger Picture
This is one of Go’s defining characteristics:
Simple syntax. Powerful semantics. High responsibility.
It’s not Go saying “you can ignore interfaces.” It’s Go saying “here’s the freedom to use interfaces intelligently. And here’s the responsibility to verify you did it correctly.”
For Go Learners
When you start writing Go:
- Don’t just rely on implicit implementation
- Use the
var _ Interface = (*Type)(nil)pattern consistently - Test that your types actually implement their interfaces
- Keep interfaces small (typically 1-3 methods)
- Use composition, not inheritance
The payoff? Code that’s flexible, testable, and maintainable at scale.
A Note of Thanks
Thanks to Yahya ElAraby and Ehab Terra for bringing this concept into focus. These subtle design decisions are what separate good Go from great Go.
Tags: #Golang #Interfaces #StructuralTyping #SoftwareDesign #BackendDevelopment #ProgrammingTips #SoftwareEngineering