Go Interfaces: The Type System Feature You Implement By Accident
Go's implicit interface satisfaction means you can implement interfaces without knowing they exist. Learn how structural typing enables accidental implementation and when it's brilliant vs problematic.
- tags
- #Go #Golang #Interfaces #Type-System #Structural-Typing #Design-Patterns #Api-Design #Testing #Composition #Polymorphism #Java-Comparison #Programming-Languages #Software-Architecture #Implicit-Interfaces #Duck-Typing #Compile-Time-Safety
- categories
- Programming Go
- published
- reading time
- 9 minutes
You’re building a custom logger. You write this:
| |
Three months later, a teammate asks: “Why did you implement io.Writer for the logger?”
You didn’t. You just wrote a Write method because loggers write data. But now your logger works everywhere io.Writer is expected - fmt.Fprintf, log.New, any function accepting io.Writer.
You implemented an interface by accident.
The Go Difference
In Java or C#, you must explicitly declare implements MyInterface. In Go, if your type has the right methods, it satisfies the interface automatically. No declaration needed.
This is called structural typing or implicit interface satisfaction, and it’s one of Go’s most distinctive features.
How Accidental Implementation Happens
The Explicit Way (Java)
In Java, interface implementation is a contract you must declare:
| |
The implements keyword creates a compile-time link between Database and Storage. If you forget to declare it, the code won’t compile, even if Database has the exact save method Storage requires.
The Implicit Way (Go)
Go eliminates the explicit declaration:
| |
The compiler checks: Does Database have a method named Save with signature (string) error? If yes, Database satisfies Storage. That’s it.
When You Discover It By Accident
The surprise comes later. You wrote Database for database operations. You never thought about the Storage interface because it didn’t exist yet, or you didn’t know about it.
Months later:
| |
You accidentally implemented an interface you didn’t know existed.
The Standard Library Trap
The most common accidental implementations involve standard library interfaces because they use obvious method names like Read, Write, Close, String, and Error.
Example: io.Writer
The interface:
| |
Things that accidentally implement io.Writer:
| |
You wrote Write because your type writes data. You didn’t think about io.Writer. But now your type composes with the entire ecosystem of io.Writer consumers.
Why This Is Good
Your Logger can now be used with:
fmt.Fprintf(logger, "message: %s", msg)- formatted outputlog.New(logger, "", 0)- standard loggingio.Copy(logger, reader)- stream data- Any function accepting io.Writer
You got this composition for free by using a common method name.
The Dangers: When Accidents Go Wrong
Method Name Collisions
Common method names like Start, Stop, Close, Run can cause semantic mismatches.
Example:
| |
The signatures match, so the code compiles. But is your game server really a generic service? The framework might assume things about Start/Stop behavior that don’t apply to games.
Invisible Breaking Changes
The scenario:
| |
Six months later, you add context support:
| |
Everything breaks:
ERROR: *Database does not implement Storage
(wrong type for Save method)
have Save(context.Context, string) error
want Save(string) error
You changed your database for legitimate reasons. You didn’t know code elsewhere depended on your exact signature through the Storage interface. The implicit coupling bit you.
Protection: Compile-Time Guards
Go developers use guard variables to make implicit implementations explicit.
The pattern:
| |
This line does nothing at runtime. It declares a variable (discarded with _) of type Storage and assigns a nil Database pointer. If Database doesn’t satisfy Storage, compilation fails immediately.
When to use guards:
| |
When NOT to Use Guards
Don’t add guards for every possible interface. Only guard interfaces that are critical to your type’s purpose. Over-guarding creates unnecessary coupling.
The Benefits Outweigh the Risks
Despite the potential for confusion, Go’s implicit interfaces enable patterns impossible in explicit languages.
Benefit 1: Define Interfaces for Code You Don’t Own
In Java, you cannot create interfaces for external types:
| |
In Go, this just works:
| |
The time package authors never declared that time.Time implements your Displayable interface because your interface didn’t exist when they wrote time.Time. Yet it works.
Benefit 2: Zero Import Dependencies
In Java, interfaces create coupling:
| |
In Go, no coupling exists:
| |
The database package has no idea Storage exists. This enables consumer-driven interface design: interfaces belong to the package that uses them, not the package that provides implementations.
Benefit 3: Testing Without Mocking Frameworks
In Go, test fakes are just structs:
| |
No Mockito, no reflection, no framework. Just plain Go code that automatically satisfies the interface.
Why Accidental Implementation Works
Go’s implicit interfaces turn potential confusion into compositional power. Yes, you’ll occasionally implement interfaces by accident. But the benefits are worth it:
- Define interfaces for any type (even stdlib types you don’t control)
- Zero coupling between interface and implementation
- Extract interfaces retroactively as patterns emerge
- Testing with simple structs instead of mocking frameworks
- Flexible composition without explicit declarations
The solution isn’t avoiding accidental implementation - it’s being intentional about which interfaces matter. Use compile-time guards for critical interfaces, keep interfaces small (1-3 methods), and embrace the flexibility.
The bottom line: Accidental implementation is Go’s way of saying “behavior matters more than declarations.” If your type has the right methods, it works. No inheritance hierarchies, no explicit contracts, just simple structural compatibility.