Go Interfaces: A Comprehensive Guide
Interfaces are Go’s primary mechanism for abstraction and polymorphism. Unlike class-based languages where interfaces are explicitly declared, Go interfaces are satisfied implicitly — a type implements an interface simply by having the required methods. This design philosophy enables flexible, decoupled code without the ceremony of formal inheritance hierarchies.
What Is an Interface?
An interface is a type that defines a set of method signatures. Any concrete type that implements those methods satisfies the interface automatically. There is no implements keyword — if a type has the right methods, it fits the interface.
type Stringer interface {
String() string
---This interface is built into the fmt package. Any type that implements String() string can be printed with fmt.Println and friends.
Implicit Satisfaction
type Book struct {
Title string
Author string
---
func (b Book) String() string {
return b.Title + " by " + b.Author
---
func main() {
b := Book{"1984", "George Orwell"}
fmt.Println(b) // 1984 by George Orwell
---Book implements fmt.Stringer without explicitly declaring it. This implicit satisfaction means you can write interfaces for types you do not own, fitting them into your abstraction after the fact — a technique called “adapter without adaptation.”
Defining and Using Interfaces
Interfaces are typically defined at the consumer side. The package that needs the abstraction defines the interface it expects, keeping dependencies pointing inward:
package storage
// Storage defines what our application needs from a data store.
type Storage interface {
Save(key string, value []byte) error
Load(key string) ([]byte, error)
Delete(key string) error
---
type FileStorage struct {
basePath string
---
func (f *FileStorage) Save(key string, value []byte) error {
return os.WriteFile(filepath.Join(f.basePath, key), value, 0644)
---
func (f *FileStorage) Load(key string) ([]byte, error) {
return os.ReadFile(filepath.Join(f.basePath, key))
---
func (f *FileStorage) Delete(key string) error {
return os.Remove(filepath.Join(f.basePath, key))
---
func NewFileStorage(path string) *FileStorage {
return &FileStorage{basePath: path}
---Now consumers depend on the Storage interface, not on FileStorage directly. You could swap in RedisStorage, S3Storage, or an in-memory implementation without changing any consumer code.
The io.Reader and io.Writer Interfaces
The most widely used interfaces in Go are in the io package:
type Reader interface {
Read(p []byte) (n int, err error)
---
type Writer interface {
Write(p []byte) (n int, err error)
---Hundreds of types implement these interfaces: files, network connections, compressed streams, encrypted pipes, HTTP response bodies, and byte buffers. This universality enables powerful composition patterns — you can chain readers transparently:
file, _ := os.Open("data.txt")
gzipReader, _ := gzip.NewReader(file)
decrypted := decryptStream(gzipReader)
// Everything is an io.ReaderInterface Values
Under the hood, an interface value is a two-word pair: a pointer to the concrete type information (the “type table”) and a pointer to the concrete data (or the data itself if it fits in one word).
var r io.Reader
r = os.Stdin // r holds (*os.File, fd)
r = bytes.NewReader([]byte("hello")) // r holds (*bytes.Reader, pointer-to-buffer)An interface value is nil only when both words are nil. This leads to a common pitfall:
var r io.Reader = nil // r is truly nil
var buf *bytes.Buffer = nil
r = buf // r is NOT nil — the type word is *bytes.Buffer
fmt.Println(r == nil) // false!Checking r == nil after assigning a nil concrete pointer returns false because the interface holds type information, even though the data pointer is nil. Always convert typed nils through a function that returns the interface type to avoid this.
The Empty Interface
The empty interface interface{} (aliased as any in Go 1.18+) has zero methods, so every type satisfies it:
var x any
x = 42
x = "hello"
x = struct{ Name string }{"Alice"}Use empty interfaces sparingly. They opt out of type safety and force runtime type assertions. Their primary legitimate uses are:
- Containers that hold heterogeneous data (rare in idiomatic Go)
- JSON decoding where structure is unknown
- Formatting functions like
fmt.Println
Before reaching for any, consider whether a generic type parameter or a specific interface would be more appropriate.
Type Assertions and Type Switches
To recover the concrete type from an interface value, use a type assertion:
var v any = "hello"
s := v.(string) // panics if v is not a string
s, ok := v.(string) // ok is false if assertion failsType switches handle multiple possible types:
func format(v any) string {
switch val := v.(type) {
case int:
return fmt.Sprintf("integer: %d", val)
case string:
return fmt.Sprintf("string: %s", val)
case bool:
return fmt.Sprintf("bool: %v", val)
default:
return fmt.Sprintf("unknown: %v", val)
}
---Type switches are cleaner than chained assertions and are evaluated in order. The default case catches anything not explicitly listed.
Interface Composition
Interfaces can be composed by embedding other interfaces:
type ReadWriter interface {
Reader
Writer
---
type ReadWriteCloser interface {
Reader
Writer
Closer
---This is composition over inheritance in practice. The io.ReadWriter interface is the union of io.Reader and io.Writer. A type that implements both Read and Write methods automatically satisfies io.ReadWriter without any declaration.
You can also embed concrete types in structs to satisfy interfaces through promotion:
type LoggedReader struct {
io.Reader
log *log.Logger
---
func (r *LoggedReader) Read(p []byte) (int, error) {
n, err := r.Reader.Read(p)
r.log.Printf("read %d bytes, err=%v", n, err)
return n, err
---Best Practices
Keep Interfaces Small
The Go proverb “the bigger the interface, the weaker the abstraction” holds true. A single-method interface is the most powerful abstraction Go offers. The standard library is full of them: io.Reader, io.Writer, fmt.Stringer, http.Handler, net.Listener.
Accept Interfaces, Return Structs
Returning concrete types keeps your API flexible for the caller. Accepting interfaces makes your code testable and decoupled.
// Good
func Process(r io.Reader) (*Result, error) { ... }
// Avoid — unnecessary abstraction
func Process(r io.Reader) (io.Writer, error) { ... }Define Interfaces Where They Are Used
Put the interface definition next to the consumer, not the implementer. The io package defines its interfaces because many things consume them, not because files or buffers need to declare “I am a Reader.”
Prefer Small Interfaces for Testing
Interfaces make it easy to provide mock implementations:
type Storage interface {
Save(key string, value []byte) error
---
type mockStorage struct {
saved map[string][]byte
---
func (m *mockStorage) Save(key string, value []byte) error {
m.saved[key] = value
return nil
---Use Interface Guard Variables
If you want to document that a type satisfies an interface at compile time:
var _ io.Reader = (*MyType)(nil)This line compiles only if *MyType satisfies io.Reader, and it produces no runtime cost.
Generics and Interfaces
Since Go 1.18, interfaces can be used as type constraints in generic functions:
func Min[T constraints.Ordered](a, b T) T {
if a < b {
return a
}
return b
---The constraints.Ordered interface bundles types that support <, <=, >, >=. You can define your own constraint interfaces:
type Number interface {
~int | ~float64 | ~int64
---
func Sum[T Number](values []T) T {
var total T
for _, v := range values {
total += v
}
return total
---When Not to Use Interfaces
Interfaces add indirection and can obscure the control flow. Avoid them when:
- You have a single concrete implementation and no test motivation to abstract
- The abstraction would leak implementation details
- Performance is critical (interface calls go through dynamic dispatch)
- The types cannot be known at compile time (use generics instead)
FAQ
Q: Can a type satisfy multiple interfaces?
A: Yes, a single type can implement any number of interfaces. A *os.File satisfies io.Reader, io.Writer, io.Closer, io.Seeker, and more simultaneously.
Q: How do I check if an interface value implements another interface at runtime?
A: Use a type assertion to the interface type: if w, ok := r.(io.Writer); ok { w.Write(data) }.
Q: Are interfaces in Go the same as in Java or C#?
A: No. Go interfaces are implicitly satisfied (duck typing), while Java/C# require explicit implements declarations. Go interfaces also cannot hold state or define constructors.
Q: What is the relationship between interfaces and nil?
A: An interface is nil only when its type and value words are both nil. Assigning a nil *T to an interface makes the interface non-nil.
Q: Should I use interface{} or any?
A: Use any — it is the same thing but shorter and clearer. any was introduced in Go 1.18 as an alias for interface{}.
Q: How do interfaces affect performance? A: Interface method calls involve dynamic dispatch (looking up the method in the type table), which is slightly slower than direct calls. In most applications the difference is negligible.
Q: Can I add methods to an interface? A: No. Adding a method to an interface breaks all existing implementations. Instead, define a new interface that composes the old one.
Q: How do I define an interface with type parameters?
A: Use generic interfaces: type Storage[T any] interface { Save(key string, value T) error }. These can only be used as constraints, not as value types.
For a comprehensive overview, read our article on Getting Started With Go.
For a comprehensive overview, read our article on Go Concurrency Guide.