
Golang
Stop Gluing Contracts to Code: The Go Interface Placement You’re Probably Getting Wrong
TL;DR
In Go, an interface describes what you (the caller) need, not what the provider can do. Keep it in the package that uses it. Doing so chops dependencies, avoids import loops, and lets tests swap in cheap stubs. Go.dev
1. Interfaces Express Needs, Not Capabilities
- “Go interfaces generally belong in the package that uses values of the interface type.” Go.dev
- Rob Pike: “The bigger the interface, the weaker the abstraction.” Go Proverbs
- Dave Cheney’s rule: let callers define the interface they require. dave.cheney.net
In other words, consumers own the contract; providers merely satisfy it.
2. What Goes Wrong When You Park Interfaces with Implementations
Problem | Why it Happens |
---|---|
Fat import graph | Consumers must import repo just to reach the interface. |
Cyclic packages | Implementation package needs consumer logic and vice‑versa |
Brittle tests | Unit tests pull real DB drivers instead of a mock. |
Hidden APIs | New devs dig through concrete types to find the contract. |
3. Consumer‑First Layout (in ≈15 lines)
internal/
├─ service/ <-- uses repo; defines the interface
│ ├─ repo_if.go <-- type Repository interface { ... }
│ └─ user.go <-- business logic
└─ repository/ <-- concrete suppliers
├─ postgres.go <-- PostgresRepo implements service.Repository
└─ memory.go
// service/repo_if.go
package service
type Repository interface {
Save(User) error
Find(id int) (User, error)
}
// service/user.go
package service
type Service struct {
repo Repository
}
func (s *Service) Register(u User) error {
return s.repo.Save(u)
}
The provider only needs to satisfy the interface:
// repository/postgres.go
package repository
type PostgresRepo struct {
// ...
}
func (p *PostgresRepo) Save(u service.User) error {
// SQL logic
}
func (p *PostgresRepo) Find(id int) (service.User, error) {
// SQL logic
}
No extra glue, no explicit implements clause — Go’s implicit satisfaction does the work.
4. Quick Checklist
- Open the consumer file. Interface lives in the same package? ✔︎
- Imports look light? You’re not dragging in infra just for a contract. ✔︎
- Tests swap in a stub effortlessly? No real network/database? ✔︎
If any item fails, move the interface up a package.
5. Further Reading
- Go Wiki – Code Review Comments › Interfaces: Go.dev
- Rob Pike – Go Proverbs: go-proverbs.github.io
- Dave Cheney – Practical Go: Let callers define the interface they require: dave.cheney.net
Keep your contracts with their callers, and your Go codebase will stay lean, testable, and cycle-free.
Афоризм дня:
Надо уметь переносить то, чего нельзя избежать. (506)
By den
On July 20, 2025