Skip to main content

Go Interfaces: Definition and Implementation

Published: December 17, 2025 Updated: August 29, 2026 Larry Qu 6 min read

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() string on every type that gets logged or printed
  • The error interface is just Error() string — define domain error types that satisfy it
  • Don’t create interfaces speculatively — let them emerge from real need

Resources

Comments

👍 Was this article helpful?