Go

Go Interfaces👨‍💻

Go interfaces are fundamentally different from interfaces in Java, C#, or TypeScript. There is no implements keyword. A type satisfies an interface by having the right methods -- nothing more. This implicit satisfaction is one of Go's most powerful design decisions: it means you can define an interface in package A and have a type in package B satisfy it without package B ever importing package A. The decoupling this creates is what makes Go codebases composable at scale.

The standard library is built on small, focused interfaces. io.Reader has one method. io.Writer has one method. fmt.Stringer has one method. These tiny contracts compose into larger ones through embedding, and they show up everywhere: HTTP handlers, database drivers, serialization, logging. Understanding how to design and consume interfaces this way is the difference between writing Go code and writing idiomatic Go.

Since Go 1.18, the any type alias replaced interface{} as the way to express "any type." Type assertions and type switches give you escape hatches when you need to recover concrete types from interface values. Interface compliance checks at compile time catch missing methods before your code ships. Together, these mechanisms form a type system that is both strict and remarkably flexible.

Key Takeaways

  • 1Go interfaces are satisfied implicitly -- a type implements an interface by having the required methods, with no explicit declaration needed
  • 2Small interfaces (1-2 methods) are the idiomatic building block; compose them via embedding to build larger contracts
  • 3The `any` type (alias for `interface{}`) holds any value but requires type assertions or type switches to use the underlying type
  • 4Accept interfaces, return structs: function parameters should be interfaces for flexibility, return values should be concrete types for clarity
  • 5Compile-time interface compliance checks (`var _ Interface = (*Type)(nil)`) catch missing methods at build time, not at runtime
  • 6The io.Reader/io.Writer pattern enables composable I/O pipelines -- this is the foundation of Go's standard library design

Master go interfaces

Take the Go Programming course with hands-on lessons and challenges.

Examples

Implicit interface satisfaction -- no implements keyword

go

Neither EmailNotifier nor SlackNotifier mentions Notifier in its definition. They satisfy the interface simply by having a Notify method with the correct signature. The Alert function depends on the interface, not on concrete types, so adding a new notification channel requires zero changes to existing code.

Interface composition via embedding

go

Large interfaces are built by embedding small ones. ReadWriteCloser combines three single-method interfaces. This mirrors the standard library: io.ReadWriter, io.ReadCloser, and io.ReadWriteCloser are all composed this way. Functions accept the narrowest interface they need.

Type assertions and the comma-ok idiom

go

Type assertions extract concrete types from interface values. The comma-ok form (value, ok := iface.(Type)) is safe and returns false if the type does not match. For error handling, errors.As is preferred over direct type assertions because it unwraps wrapped errors. Direct assertions without the ok check will panic on type mismatch.

Type switches for polymorphic dispatch

go

Type switches dispatch on the dynamic type of an interface value. Inside each case, the variable v has the concrete type, giving you access to type-specific fields. The default case handles unknown implementations. This pattern is common in parsers, serializers, and any code that needs type-specific behavior.

Accept interfaces, return structs -- dependency injection

go

NewUserService accepts interfaces (UserStore, Clock) and returns a concrete *UserService. The interfaces are defined where they are consumed, not where they are implemented. This makes testing trivial -- fakeClock and fakeStore satisfy the interfaces without importing the production implementations. This is the standard dependency injection pattern in Go.

Compile-time interface compliance check

go

The blank identifier assignment var _ io.WriteCloser = (*FileWriter)(nil) creates a zero-cost compile-time assertion that *FileWriter implements io.WriteCloser. If you later remove or change the Write or Close method, the build fails immediately. This is standard practice in Go libraries and catches interface drift before any test runs.

Common Mistakes

Mistake:

Defining large interfaces with many methods upfront, mirroring Java-style interface design

Fix:

Go favors small interfaces -- often just one or two methods. Define interfaces where they are consumed, not where they are implemented. A function that only calls Read() should accept an io.Reader, not an io.ReadWriteCloser. Large interfaces reduce the number of types that can satisfy them and make testing harder.

Mistake:

Returning interfaces from functions instead of concrete types, hiding what the caller actually receives

Fix:

Follow 'accept interfaces, return structs.' Returning an interface removes information: the caller cannot access type-specific methods or fields. Return the concrete *MyStruct and let the caller decide whether to store it in an interface variable. The exception is when the function is a factory that genuinely returns different types.

Mistake:

Using type assertions without the comma-ok form, causing panics at runtime on type mismatch

Fix:

Always use the two-value form: value, ok := iface.(ConcreteType). The single-value form panics if the type does not match. For error types specifically, use errors.As() which handles wrapped errors correctly.

Mistake:

Defining interfaces in the same package as the implementation, creating tight coupling

Fix:

Interfaces should be defined by the consumer, not the producer. If package 'storage' defines both the Store interface and PostgresStore, every consumer imports the storage package. Instead, each consumer defines the interface it needs with only the methods it calls. This is how Go achieves dependency inversion without a framework.

Best Practices

  • Define interfaces at the point of use (consumer side), not alongside the implementation. This keeps packages decoupled and interfaces minimal.
  • Name single-method interfaces with the method name plus -er suffix: Reader, Writer, Closer, Stringer, Handler. This is a strong Go convention that makes code instantly readable.
  • Use compile-time compliance checks (var _ Interface = (*Type)(nil)) in library code to catch interface drift early. Place them near the type definition.
  • Prefer io.Reader and io.Writer as function parameters over []byte or string when the data source or destination could vary. This enables streaming and composition.
  • Avoid the empty interface (any) unless you genuinely need to accept arbitrary types. Generics (Go 1.18+) are usually a better choice for type-safe polymorphism.

Summary

Go interfaces are satisfied implicitly: any type with the right methods matches, no declaration required. This enables powerful decoupling -- define small interfaces where they are consumed, compose them via embedding, and use type assertions or type switches when you need the concrete type back. The standard library's io.Reader and io.Writer demonstrate the pattern at its best: tiny contracts that compose into a rich ecosystem of interchangeable components.

Practice Go with hands-on challenges

Learn go interfaces hands-on in your IDE

Interactive lessons and challenges on Stanza, practice in VS Code, Cursor, or the web.

Related Concepts

Related Cheatsheets

Master Go with Stanza

Interactive lessons and challenges, right in your code editor.

Check the free courses. No credit card.