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.
$ go run interfaces.go
helloThe 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")))from a File
from a strings.ReaderlogLines only needs Write, so it never has to know it's writing to an in-memory File in one call and to the terminal itself in the next.
func logLines(w Writer, lines []string) {
for _, line := range lines {
w.Write([]byte(line + "\n"))
}
}
f := &File{}
logLines(f, []string{"build started", "build finished"})
fmt.Print(string(f.data))
logLines(os.Stdout, []string{"also works on os.Stdout"})build started
build finished
also works on os.StdoutThis 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.