Useful Standard Interfaces

A handful of small interfaces from the standard library connect your types to the rest of Go. Give a type a String method and fmt prints it nicely; an Error method makes it an error; Read or Write methods let it plug into files, network connections, compression and hashing. This lesson shows the interfaces you'll implement most often and what you get for free when you do.

fmt.Stringer

GO
type Stringer interface {
	String() string
}

When a value has a String() string method, fmt's %v and %s (and Println) use it. It's the standard way to give enum-like constants readable names:

GO
package main

import "fmt"

type Weekday int

const (
	Sunday Weekday = iota
	Monday
	Tuesday
)

var dayNames = [...]string{"Sunday", "Monday", "Tuesday"}

func (d Weekday) String() string {
	if d < 0 || int(d) >= len(dayNames) {
		return fmt.Sprintf("Weekday(%d)", int(d))
	}
	return dayNames[d]
}

type Money int64 // cents

func (m Money) String() string { return fmt.Sprintf("€%d.%02d", m/100, m%100) }

func main() {
	fmt.Println(Monday, Tuesday)
	fmt.Printf("%v %d %s\n", Sunday, Sunday, Weekday(9))

	price := Money(1999)
	fmt.Println("price:", price)
	fmt.Printf("%v\n", []Money{250, 1000})
}
Output
Monday Tuesday
Sunday 0 Weekday(9)
price: €19.99
[€2.50 €10.00]

%d still prints the number. Inside a String method, convert to the underlying type before formatting the value itself — fmt.Sprintf("%v", d) inside d.String() would call String again forever. (For larger enums, the stringer tool from golang.org/x/tools generates these methods.)

error

GO
type error interface {
	Error() string
}

Any type with an Error() string method is an error. Custom error types can carry extra details for the caller to inspect (lesson 45):

GO
package main

import "fmt"

type ValidationError struct {
	Field  string
	Reason string
}

func (e *ValidationError) Error() string {
	return e.Field + ": " + e.Reason
}

func validateAge(age int) error {
	if age < 0 || age > 150 {
		return &ValidationError{Field: "age", Reason: fmt.Sprintf("%d is out of range", age)}
	}
	return nil
}

func main() {
	fmt.Println(validateAge(30))
	err := validateAge(-4)
	fmt.Println(err)
	if ve, ok := err.(*ValidationError); ok {
		fmt.Println("bad field:", ve.Field)
	}
}
Output
<nil>
age: -4 is out of range
bad field: age

io.Reader and io.Writer

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

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

These two are the backbone of Go's input and output. Files, network connections, HTTP bodies, buffers, compressors, hashers and standard output all implement one or both, so code written against io.Reader and io.Writer works with all of them. Here one function writes a report to three very different destinations:

GO
package main

import (
	"crypto/sha256"
	"fmt"
	"io"
	"os"
	"strings"
)

func writeReport(w io.Writer, items []string) error {
	for i, it := range items {
		if _, err := fmt.Fprintf(w, "%d. %s\n", i+1, it); err != nil {
			return err
		}
	}
	return nil
}

func main() {
	items := []string{"apples", "bread"}

	writeReport(os.Stdout, items) // the terminal

	var sb strings.Builder
	writeReport(&sb, items) // a string
	fmt.Printf("%q\n", sb.String())

	h := sha256.New()
	writeReport(h, items) // a hash
	fmt.Printf("%x\n", h.Sum(nil)[:8])
}
Output
1. apples
2. bread
"1. apples\n2. bread\n"
4cd35472f67f33c8

Writing your own Writer

Implementing Write takes a few lines. This writer counts words passing through it and forwards the bytes to another writer — a pattern called a wrapper or decorator:

GO
package main

import (
	"bytes"
	"fmt"
	"io"
	"os"
	"strings"
)

type WordCounter struct {
	W     io.Writer
	Words int
}

func (wc *WordCounter) Write(p []byte) (int, error) {
	wc.Words += len(bytes.Fields(p))
	return wc.W.Write(p)
}

func main() {
	wc := &WordCounter{W: os.Stdout}
	src := strings.NewReader("the quick brown fox\njumps over the lazy dog\n")
	if _, err := io.Copy(wc, src); err != nil {
		fmt.Println(err)
	}
	fmt.Println("words:", wc.Words)
}
Output
the quick brown fox
jumps over the lazy dog
words: 9

io.Copy moves everything from any reader to any writer. (Counting words chunk by chunk like this would miscount a word split across two chunks; with this short input it all arrives in one Write.) Lesson 58 covers readers and writers in depth.

io.Closer

Close() error releases a resource. Files, network connections and HTTP response bodies implement it, usually combined with reading or writing as io.ReadCloser or io.WriteCloser. Whenever you get something that implements Closer, make sure it's closed — typically with defer x.Close().

Other interfaces worth knowing

InterfaceMethodWhat it's for
fmt.StringerString() stringHow a value prints
errorError() stringError values
io.Reader / io.WriterRead / WriteStreams of bytes
io.CloserClose() errorReleasing resources
json.Marshaler / UnmarshalerMarshalJSON / UnmarshalJSONCustom JSON encoding (lesson 59)
encoding.TextMarshalerMarshalTextA value's text form, used by JSON keys, flags, logs
http.HandlerServeHTTP(w, r)Answering web requests (lesson 61)
sort.InterfaceLen, Less, SwapOlder sorting API; prefer slices.SortFunc

Try it yourself

Give the Temperature type a String method that prints one decimal and the unit, then print a slice of them.

Try it yourself · main.go
Edit the code, copy it into main.go in a new folder and run it with go run main.go (Go 1.27 or later). How to install Go
Output
[21.5°C 19.0°C 23.2°C]
max today: 23.2°C
Next: organising code into packages.