Code of the Day
IntermediateInterfaces & errors

Interfaces

Go interfaces are satisfied implicitly — any type with the right methods qualifies, no declaration needed.

GoIntermediate12 min read
Recommended first
By the end of this lesson you will be able to:
  • Define an interface type with one or more method signatures
  • Explain implicit interface satisfaction and why there is no implements keyword
  • Use io.Reader and io.Writer as real-world interface examples
  • Describe what an interface value holds (type + value pair)
  • Explain the nil interface and its common pitfall

In the beginner track you built structs and attached behaviour with methods. Interfaces take that one step further: they let you write code that works with any type that has the right behaviour, without requiring those types to declare anything up front. This is the foundation of Go's approach to polymorphism, and it's simpler and more powerful than inheritance.

Defining an interface

A is a named set of method signatures:

type Stringer interface {
    String() string
}

Any type that has a String() string method satisfies Stringer automatically. There is no implements keyword and no declaration of intent required — if the methods match, the type satisfies the interface. This is called or duck typing in a statically-verified form.

type Point struct {
    X, Y float64
}

func (p Point) String() string {
    return fmt.Sprintf("(%g, %g)", p.X, p.Y)
}

// Point now satisfies Stringer — no annotation needed.
var s Stringer = Point{1, 2}
fmt.Println(s.String())   // (1, 2)

The compiler checks satisfaction at the assignment site. If Point is missing the String method, the assignment fails with a clear error.

io.Reader and io.Writer — the canonical examples

The standard library's io package defines two interfaces that appear everywhere in Go:

type Reader interface {
    Read(p []byte) (n int, err error)
}

type Writer interface {
    Write(p []byte) (n int, err error)
}

Read fills a byte slice and returns how many bytes were read. Write writes bytes from a slice and returns how many were written. That's it. The beauty is that files, HTTP request bodies, in-memory buffers, gzip streams, and network connections all satisfy Reader or Writer. Any function that accepts an io.Reader works with all of them without knowing the concrete type.

func countBytes(r io.Reader) (int, error) {
    buf := make([]byte, 1024)
    total := 0
    for {
        n, err := r.Read(buf)
        total += n
        if err == io.EOF {
            return total, nil
        }
        if err != nil {
            return total, err
        }
    }
}

Pass an os.File, a strings.Reader, or a network connection — the function doesn't change.

Go's standard library is built on small interfaces. If you design a function to accept io.Reader rather than *os.File, callers can pass anything that reads — making your code reusable without you writing any extra abstraction layer.

Interface values: the type-value pair

An interface value internally holds two things: a concrete type and a concrete value. When you assign a Point to a Stringer, the interface value remembers that it holds a Point.

var s Stringer = Point{1, 2}
// s holds: (type=Point, value={1,2})

This is why Go can dispatch the right method at runtime even though the variable is typed as Stringer.

The nil interface and its pitfall

A nil interface holds no type and no value. It is the zero value of any interface variable:

var s Stringer   // nil interface — both type and value are nil
fmt.Println(s == nil)   // true

There is a subtle trap: a non-nil interface can hold a nil concrete value, and that interface is not nil:

var p *Point = nil
var s Stringer = p   // s holds (type=*Point, value=nil)
fmt.Println(s == nil)   // false!

The interface has a type set (*Point), so it is not nil even though the value it holds is nil. Calling s.String() will panic because it tries to dereference a nil pointer.

Never assign a typed nil pointer to an interface and then test for nil. Return a bare nil when returning an interface — not a typed nil variable — to avoid this confusion. We will revisit this pattern in the error-interface lesson.

Composing interfaces

Interfaces can embed other interfaces:

type ReadWriter interface {
    Reader
    Writer
}

A type that satisfies both Reader and Writer automatically satisfies ReadWriter. This is composition over inheritance in action — small, focused interfaces combine into larger capabilities without a class hierarchy.

Check your understanding

Knowledge check

  1. 1.
    A struct type has a method Describe() string. Which statement is true?
  2. 2.
    A variable of interface type is nil when it holds a typed nil pointer (e.g., var p *Point = nil; var s Stringer = p).
  3. 3.
    Why is io.Reader described as a 'small interface'?

Do it yourself

Define a Shape interface with an Area() float64 method. Implement it for at least two struct types (e.g., Circle and Rectangle). Write a function totalArea(shapes []Shape) float64 and call it with a mixed slice.

go run main.go

Where to go next

Now that you understand interface satisfaction, the next lesson covers type assertions — how to recover the concrete type from an interface value when you need it.

Finished reading? Mark it complete to track your progress.

On this page