An interface in Go is a set of method signatures. Any type that has those methods satisfies the interface automatically — no implements keyword, no registration, no inheritance. This implicit satisfaction is Go’s most distinctive design choice, and it has far-reaching consequences for how you structure code.
The result: you can define an interface in package A that a type in package B satisfies, even if B was written before A existed and never heard of it. This decouples consumers from producers in a way that explicit declaration cannot.
For the advanced patterns that build on this foundation see Go implicit interfaces and duck typing and Go dependency injection.
Defining an Interface
An interface lists the methods a type must have. The syntax is a type declaration with interface{...}:
// A Writer must have exactly this method
type Writer interface {
Write(p []byte) (n int, err error)
}
// A Reader must have exactly this method
type Reader interface {
Read(p []byte) (n int, err error)
}
// Closeable things must have Close
type Closer interface {
Close() error
}
Any type with a matching Write([]byte) (int, error) method satisfies Writer — without declaring it. This includes types from third-party packages that were written before your interface existed.
Implementing an Interface
Implementation is implicit: write the methods, and the type satisfies any interface that requires them.
type FileWriter struct {
path string
f *os.File
}
// FileWriter satisfies io.Writer because it has this exact method signature
func (fw *FileWriter) Write(p []byte) (int, error) {
return fw.f.Write(p)
}
// FileWriter also satisfies io.Closer
func (fw *FileWriter) Close() error {
return fw.f.Close()
}
// Therefore FileWriter satisfies io.WriteCloser too
// (which is just Reader + Writer composed)
var _ io.WriteCloser = (*FileWriter)(nil) // compile-time check
The blank identifier assignment at the end is an idiom for asserting interface satisfaction at compile time without runtime cost. If *FileWriter doesn’t implement io.WriteCloser, the code won’t compile.
Interface Composition
Interfaces compose by embedding other interfaces. Go’s standard library uses this heavily:
// From package io — these are the actual definitions
type Reader interface {
Read(p []byte) (n int, err error)
}
type Writer interface {
Write(p []byte) (n int, err error)
}
type Closer interface {
Close() error
}
// Composed interfaces
type ReadWriter interface { Reader; Writer }
type ReadCloser interface { Reader; Closer }
type WriteCloser interface { Writer; Closer }
type ReadWriteCloser interface { Reader; Writer; Closer }
A type that implements Read, Write, and Close automatically satisfies all four composed interfaces. You don’t need to declare which interfaces you satisfy — the compiler figures it out.
Using Interfaces in Function Parameters
The power of interfaces comes from using them in function signatures. A function that accepts io.Reader works with any source of bytes — files, network connections, in-memory buffers, test fixtures, compressed streams:
func processLines(r io.Reader) error {
scanner := bufio.NewScanner(r)
for scanner.Scan() {
if err := handleLine(scanner.Text()); err != nil {
return err
}
}
return scanner.Err()
}
// Works with all of these:
processLines(os.Stdin)
processLines(bytes.NewReader([]byte("line1\nline2")))
processLines(resp.Body) // HTTP response
processLines(gzipReader) // compressed data
The conventional Go guidance: accept interfaces, return concrete types. Functions should accept the narrowest interface that covers what they actually need. This makes them more composable and easier to test.
The fmt.Stringer Interface
fmt.Stringer is one method — String() string — and it controls how a type prints:
type OrderStatus int
const (
StatusPending OrderStatus = iota
StatusPaid
StatusShipped
StatusDelivered
)
func (s OrderStatus) String() string {
switch s {
case StatusPending: return "pending"
case StatusPaid: return "paid"
case StatusShipped: return "shipped"
case StatusDelivered: return "delivered"
default: return fmt.Sprintf("OrderStatus(%d)", int(s))
}
}
status := StatusShipped
fmt.Println(status) // shipped — fmt calls String() automatically
fmt.Printf("%v\n", status) // shipped
slog.Info("order", slog.Any("status", status)) // status=shipped
Without String(), fmt.Println(StatusShipped) would print 2. With it, every log line, error message, and debug output shows a human-readable string without any extra formatting at the call site.
The error Interface
error is Go’s most important interface:
type error interface {
Error() string
}
Anything with Error() string is an error. This is why you can define domain-specific error types that carry structured data while still working everywhere a plain error is expected:
type NotFoundError struct {
Resource string
ID string
}
func (e *NotFoundError) Error() string {
return fmt.Sprintf("%s %q not found", e.Resource, e.ID)
}
// Returns *NotFoundError, but the caller sees it as error
func getOrder(id string) (*Order, error) {
if !exists(id) {
return nil, &NotFoundError{Resource: "order", ID: id}
}
// ...
}
// Caller can use it as error for general handling...
order, err := getOrder("ord-123")
if err != nil {
log.Println(err) // order "ord-123" not found
}
// ...or extract details with errors.As
var nfe *NotFoundError
if errors.As(err, &nfe) {
http.Error(w, nfe.Resource+" not found", http.StatusNotFound)
}
Common Standard Library Interfaces
Knowing these interfaces unlocks the standard library:
| Interface | Package | Methods |
|---|---|---|
io.Reader |
io | Read([]byte) (int, error) |
io.Writer |
io | Write([]byte) (int, error) |
io.Closer |
io | Close() error |
fmt.Stringer |
fmt | String() string |
error |
builtin | Error() string |
sort.Interface |
sort | Len() int, Less(i,j int) bool, Swap(i,j int) |
http.Handler |
net/http | ServeHTTP(ResponseWriter, *Request) |
http.ResponseWriter |
net/http | Header(), Write(), WriteHeader() |
json.Marshaler |
encoding/json | MarshalJSON() ([]byte, error) |
Implement sort.Interface on your type and sort.Sort works on it. Implement http.Handler and you can use your type anywhere an HTTP handler is expected — with any framework, with any middleware.
Interface Satisfaction and Testing
The testing advantage of interfaces is immediate: replace any dependency with a test double that satisfies the same interface.
// Service depends only on the interface
type EmailSender interface {
Send(to, subject, body string) error
}
type NotificationService struct {
email EmailSender
}
func (s *NotificationService) Notify(user *User, message string) error {
return s.email.Send(user.Email, "Notification", message)
}
// In tests: a fake that satisfies EmailSender
type fakeEmailSender struct {
sent []string
}
func (f *fakeEmailSender) Send(to, subject, body string) error {
f.sent = append(f.sent, to)
return nil
}
func TestNotify(t *testing.T) {
fake := &fakeEmailSender{}
svc := &NotificationService{email: fake}
err := svc.Notify(&User{Email: "[email protected]"}, "hello")
if err != nil {
t.Fatal(err)
}
if len(fake.sent) != 1 || fake.sent[0] != "[email protected]" {
t.Errorf("unexpected sent: %v", fake.sent)
}
}
No mocking framework needed. The fake is 5 lines of code. The test verifies behavior without sending any real email.
When Not to Use Interfaces
Interfaces add a layer of indirection. Don’t add them preemptively “in case we need flexibility later”:
- If there’s only one implementation and no test double needed, use the concrete type directly
- If a function is private and called from one place, the interface overhead isn’t worth it
- If you’re writing a library and the interface would be exported, document it carefully — exported interfaces are harder to change
The Go proverb: “Don’t design with interfaces, discover them.” Write concrete code first. Extract interfaces when you have two or more types that should be interchangeable, or when testing requires a swap.
Summary
- Any type with matching methods satisfies an interface — no declaration needed
- Use
var _ Interface = (*Type)(nil)to get a compile-time satisfaction check - Compose interfaces from smaller ones:
io.ReadWriter=io.Reader+io.Writer - Accept interfaces in function parameters; return concrete types
- Implement
String() stringon every type that gets logged or printed - The
errorinterface is justError() string— define domain error types that satisfy it - Don’t create interfaces speculatively — let them emerge from real need
Resources
- Effective Go: Interfaces
- Go Tour: Interfaces
- Go by Example: Interfaces
- Go specification: Interface types
Comments