Clean code in Go · 3 MIN READ
SOLID in Go: principles without class scaffolding
Translate responsibility, substitution and dependency inversion into Go contracts.
Translate responsibility, substitution and dependency inversion into Go contracts. Single responsibility concerns reasons to change. Trace the worked Go example, then compare the benefit with the cost before applying this design to your own project.
Clean Go · A MAP OF THE IDEA
- Consumer depends on a small contract
- Wrapper preserves Read semantics
- Concrete dependency is supplied explicitly
Useful when
- A design has multiple reasons to change
- Substitutable implementations need shared semantics
Watch out
- Principles do not justify interfaces everywhere
- Method sets cannot prove behavioral compatibility
Implementation & operation cost: Medium
The design decision
Single responsibility concerns reasons to change. Open/closed design isolates real variation. Substitution means preserving behavior promised to callers, not merely matching method signatures. Interface segregation and dependency inversion work well with small consumer-owned interfaces and explicit constructors.
A complete Go example
Read this program from the entry point and follow the calls shown in the diagram. The output is included at the end so you can check the behavior without a compiler.
package main
import (
"fmt"
"io"
"strings"
)
type UpperReader struct{ source io.Reader }
func (r UpperReader) Read(p []byte) (int, error) {
n, err := r.source.Read(p)
for i := 0; i < n; i++ {
if p[i] >= 'a' && p[i] <= 'z' {
p[i] -= 'a' - 'A'
}
}
return n, err
}
func main() {
b, err := io.ReadAll(UpperReader{source: strings.NewReader("Go")})
fmt.Println(string(b), err)
}
// Output:
// GO <nil>
UpperReader decorates Read while preserving its byte count and error. It changes ASCII letters only, which is an explicit scope rather than a Unicode claim. Consumers still use io.Reader, so the abstraction protects a behavior that actually varies.
Tradeoffs and the simpler alternative
Go has no class inheritance hierarchy to organize by default. Favor composition and concrete values until substitution matters. A type can satisfy an interface and still violate its semantic contract, such as discarding partial reads; test those contracts rather than counting interfaces.
Translate principles into Go decisions
Single responsibility asks which reason for change a package owns. Interface segregation favors small consumer contracts. Dependency inversion keeps business policy independent of transport and storage details. Open/closed can mean a function value or composed strategy rather than an inheritance hierarchy. Liskov substitution means an implementation preserves the promised behavior, not merely that its methods compile. These principles are review questions, not a requirement for five new abstraction layers.
Check substitution with behavioral examples
If a store promises that Get never mutates returned snapshots, an implementation returning an internal mutable slice violates the contract despite matching the interface. If one implementation ignores cancellation or changes missing-record semantics, callers cannot safely substitute it. Run the same contract suite against implementations. Use constructor validation to reject unsupported combinations rather than weakening the interface promise silently.
Balance extensibility against current complexity
A small stable function is often better than a registry, factory and configurable strategy system for one use case. Introduce variation at a demonstrated seam, then keep dependencies explicit. A new requirement that changes a business rule is not automatically a failure of open/closed design. Evaluate how many files and contracts a realistic change touches, and whether the abstraction makes tests and ownership clearer. Prefer readable composition and concrete values over copying class-oriented ceremony.
TAKE THIS WITH YOU
Requires preserving behavior as well as method signatures. Go has no class inheritance hierarchy to organize by default. Favor composition and concrete values until substitution matters. A type can satisfy an interface and still violate its semantic contract, such as discarding partial reads; test those contracts rather than counting interfaces.
Check your understanding
Which statement best describes SOLID in Go: principles without class scaffolding?
Go to the source
Progress stays in this browser. No account needed.