Interfaces & Implicit Satisfaction
Go's interface system is one of its most distinctive features. Unlike Java or C#, Go uses implicit interface satisfaction — there is no implements keyword. If a type has the methods an interface requires, it satisfies that interface automatically. This simple rule has profound consequences for how you design Go programs.
Implicit Satisfaction
In Go, you never declare that a type implements an interface. You simply define the methods, and the compiler figures it out.
type Speaker interface {
Speak() string
}
type Dog struct {
Name string
}
func (d Dog) Speak() string {
return d.Name + " says woof!"
}
func Greet(s Speaker) {
fmt.Println(s.Speak())
}
func main() {
d := Dog{Name: "Rex"}
Greet(d) // Dog satisfies Speaker — no declaration needed
}
Rex says woof!
This means you can implement interfaces from packages you have never imported. A type in your package can satisfy an interface defined in a completely separate module without any coupling between the two.
The Empty Interface & any
The empty interface interface{} has zero methods, so every type satisfies it. Go 1.18 introduced any as an alias for interface{}.
func PrintAnything(v any) {
fmt.Println(v)
}
func main() {
PrintAnything(42)
PrintAnything("hello")
PrintAnything([]int{1, 2, 3})
}
Use any sparingly. It discards type safety and forces you to use type assertions or reflection to do anything useful with the value.
io.Reader & io.Writer: The Interfaces That Run Go
The most important interfaces in the standard library are deceptively simple.
type Reader interface {
Read(p []byte) (n int, err error)
}
type Writer interface {
Write(p []byte) (n int, err error)
}
These two interfaces connect files, network connections, HTTP bodies, compression streams, encryption layers, and buffers. A function that accepts an io.Reader can read from any of these sources without knowing which one it has.
func CountLines(r io.Reader) (int, error) {
scanner := bufio.NewScanner(r)
count := 0
for scanner.Scan() {
count++
}
return count, scanner.Err()
}
func main() {
// From a string
r := strings.NewReader("line one\nline two\nline three\n")
n, _ := CountLines(r)
fmt.Println("Lines:", n)
// From a file — same function, different source
f, _ := os.Open("data.txt")
defer f.Close()
n, _ = CountLines(f)
fmt.Println("Lines:", n)
}
Lines: 3
Lines: 47
Small Interfaces: 1-2 Methods
Go's standard library favors small interfaces. The most useful ones have just one or two methods.
type Stringer interface {
String() string
}
type Closer interface {
Close() error
}
type ReadCloser interface {
Read(p []byte) (n int, err error)
Close() error
}
Small interfaces are easy to implement, easy to mock in tests, and easy to compose. If your interface has more than three methods, consider whether it can be split.
Accept Interfaces, Return Structs
This principle guides Go API design. Functions should accept interface parameters to be flexible about their inputs, but return concrete struct types to give callers full access to the returned value.
// Good: accepts an interface, returns a concrete type
func NewLogger(w io.Writer) *Logger {
return &Logger{output: w}
}
// The caller can pass any io.Writer: a file, a buffer, stdout
logger := NewLogger(os.Stdout)
logger := NewLogger(&bytes.Buffer{})
Returning an interface hides the concrete type and limits what the caller can do. Return the struct; let the caller decide which interface to store it as.
Interface Composition
Interfaces can embed other interfaces to build larger contracts from smaller ones.
type Reader interface {
Read(p []byte) (n int, err error)
}
type Writer interface {
Write(p []byte) (n int, err error)
}
type ReadWriter interface {
Reader
Writer
}
type ReadWriteCloser interface {
ReadWriter
Closer
}
This lets you define precise requirements. A function that only needs to read should accept io.Reader, not io.ReadWriteCloser.
func Process(rw io.ReadWriter) error {
buf := make([]byte, 1024)
n, err := rw.Read(buf)
if err != nil {
return err
}
_, err = rw.Write(buf[:n])
return err
}
The Power of Implicit Interfaces
Because interfaces are satisfied implicitly, you can define an interface in your own package that a third-party type already satisfies.
// In your package — you define the interface you need
type Store interface {
Get(key string) (string, error)
Set(key string, value string) error
}
// Some third-party cache library happens to have these methods.
// It satisfies your Store interface without knowing about it.
func NewApp(s Store) *App {
return &App{store: s}
}
This means you can decouple from third-party packages at the interface boundary. Your code depends on the interface you defined, not the concrete type from the external package.
Type Assertions & Type Switches
When you have an interface value and need the underlying concrete type, use a type assertion or type switch.
func Describe(v any) string {
switch val := v.(type) {
case string:
return "string of length " + strconv.Itoa(len(val))
case int:
return "integer " + strconv.Itoa(val)
case io.Reader:
return "something that can be read"
default:
return "unknown type"
}
}
Always use the comma-ok form for single type assertions to avoid panics.
s, ok := v.(string)
if !ok {
// v is not a string — handle gracefully
}
Common Pitfalls
- Defining interfaces too early. Write the concrete types first. Extract an interface when you have two or more implementations or need to mock in tests.
- Interfaces with too many methods. Large interfaces are hard to implement and hard to mock. Prefer small, focused interfaces.
- Pointer receiver confusion. If a method has a pointer receiver, only a pointer to the type satisfies the interface — not the value itself.
- Nil interface vs nil value. An interface holding a nil pointer is not itself nil. This causes subtle bugs in error handling.
- Using any everywhere. Reaching for
anyis often a sign that generics or a specific interface would be a better fit.
Key Takeaways
- Go interfaces are satisfied implicitly — no
implementskeyword. - The best interfaces are small: one or two methods.
io.Readerandio.Writerare the foundation of Go's composable I/O system.- Accept interfaces as function parameters; return concrete struct types.
- Compose interfaces by embedding smaller ones into larger ones.
- You can implement interfaces from packages you never import, enabling clean decoupling.
- Define interfaces where they are used, not where the implementation lives.