HowtoGo
Home / Functions / Interfaces

Interfaces define behavior instead of data. A type satisfies an interface just by having the right methods, and the same type can satisfy several interfaces at once.

An interface is a list of method signatures. Any type with a Read(p []byte) (int, error) method satisfies Reader, no matter what the type is otherwise.

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

Writer is the mirror image: any type with a matching Write method satisfies it. Two separate interfaces, each demanding exactly one method.

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

File is a stand-in for a real file: a byte slice and a read position. Giving it a Write method makes it satisfy Writer.

type File struct {
    data []byte
    pos  int
}

func (f *File) Write(p []byte) (int, error) {
    f.data = append(f.data, p...)
    return len(p), nil
}

Adding Read makes the same *File also satisfy Reader, alongside Writer it already satisfies. Nothing was declared to make that happen, it's a consequence of the method set *File now has. io.EOF is the sentinel error every Read implementation returns once there's nothing left.

func (f *File) Read(p []byte) (int, error) {
    if f.pos >= len(f.data) {
        return 0, io.EOF
    }
    n := copy(p, f.data[f.pos:])
    f.pos += n
    return n, nil
}

An interface can embed other interfaces. ReadWriter requires both Read and Write, and *File satisfies it too, automatically, since it already has both methods.

type ReadWriter interface {
    Reader
    Writer
}

One value, three interfaces. rw is declared as a ReadWriter, but the same *File could just as well be assigned to a Reader or a Writer variable instead.

package main

import (
    "fmt"
    "io"
)

// Reader, Writer, ReadWriter, File, and its Read/Write methods as above

func main() {
    var rw ReadWriter = &File{}
    rw.Write([]byte("hello, gopher"))

    buf := make([]byte, 5)
    n, _ := rw.Read(buf)
    fmt.Println(string(buf[:n]))
}

Write appends 13 bytes. Read fills a 5-byte buffer with the first 5 of them.

Terminal
$ go run interfaces.go
hello

The real power: code that doesn't care which type it gets

*File never declares which interfaces it satisfies. It satisfies Reader, Writer, and ReadWriter just by having the right methods, and any other type with the same methods, *os.File, *strings.Reader, a network connection, satisfies them too. A function written against the interface works on all of them without changes.

Examples

drain only asks for a Reader, so it works identically on a *File and on a strings.Reader, two completely unrelated types from two different packages.

func drain(r Reader) string {
    buf, _ := io.ReadAll(r)
    return string(buf)
}

f := &File{}
f.Write([]byte("from a File"))
fmt.Println(drain(f))

fmt.Println(drain(strings.NewReader("from a strings.Reader")))
Output
from a File
from a strings.Reader

This is why idiomatic Go functions take the narrowest interface that does the job, io.Writer instead of *os.File, rather than the broadest concrete type available. The caller's type stops being the function's concern at all.