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.
Master go interfaces
Take the Go Programming course with hands-on lessons and challenges.
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.
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 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 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.
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.
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.
Defining large interfaces with many methods upfront, mirroring Java-style interface design
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.
Returning interfaces from functions instead of concrete types, hiding what the caller actually receives
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.
Using type assertions without the comma-ok form, causing panics at runtime on type mismatch
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.
Defining interfaces in the same package as the implementation, creating tight coupling
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.
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.
Interactive lessons and challenges on Stanza, practice in VS Code, Cursor, or the web.
Interactive lessons and challenges, right in your code editor.
Check the free courses. No credit card.