32. The standard library's interfaces
You'll define a few interfaces of your own. You'll satisfy the standard library's constantly. These four are worth knowing by heart.
fmt.Stringer — how your type prints
package main
import "fmt"
type Duration struct {
Minutes int
}
func (d Duration) String() string {
h := d.Minutes / 60
m := d.Minutes % 60
if h == 0 {
return fmt.Sprintf("%dm", m)
}
return fmt.Sprintf("%dh%02dm", h, m)
}
func main() {
d := Duration{Minutes: 145}
fmt.Println(d)
fmt.Printf("%v | %s\n", d, d)
fmt.Println([]Duration{{30}, {90}, {200}})
}
One method, and every printing path in the language uses it — inside slices, inside maps, in log output. It's the cheapest polish you can add to a type.
error — just an interface
The error type isn't special machinery. It's this:
type error interface {
Error() string
}
So any type with an Error() string method is an error:
package main
import "fmt"
type ValidationError struct {
Field string
Value string
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("invalid %s: %q", e.Field, e.Value)
}
func validate(field, value string) error {
if value == "" {
return &ValidationError{Field: field, Value: value}
}
return nil
}
func main() {
if err := validate("email", ""); err != nil {
fmt.Println("got:", err)
if v, ok := err.(*ValidationError); ok {
fmt.Println("the bad field was:", v.Field)
}
}
fmt.Println(validate("email", "ada@example.com"))
}
A custom error is just a type with a method, and a type assertion gets your
structured data back out. Note the pointer receiver and the & on the
return — that's the convention for custom error types, and the errors module
explains why it matters.
Note also that fmt.Println(err) printed the message: error and Stringer
work the same way, and fmt checks for Error() string first.
sort.Interface — three methods, any ordering
package main
import (
"fmt"
"sort"
)
type Person struct {
Name string
Age int
}
type ByAge []Person
func (a ByAge) Len() int { return len(a) }
func (a ByAge) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a ByAge) Less(i, j int) bool { return a[i].Age < a[j].Age }
func main() {
people := []Person{
{Name: "Ada", Age: 36},
{Name: "Grace", Age: 45},
{Name: "Alan", Age: 41},
}
sort.Sort(ByAge(people))
for _, p := range people {
fmt.Printf("%-6s %d\n", p.Name, p.Age)
}
}
ByAge is a named slice type — module 5's trick — carrying the three methods
sort.Sort needs. ByAge(people) is a conversion, not a copy of the
data: same backing array, so sorting through it sorts people.
In modern code you'd write sort.Slice(people, func(i, j int) bool {...})
and skip the type. sort.Interface is still worth understanding, because it
shows how an interface turns "an algorithm" into something reusable — and
because you'll read plenty of code that predates sort.Slice.
io.Writer — the most useful interface in Go
type Writer interface {
Write(p []byte) (n int, err error)
}
Files, network connections, HTTP responses, gzip compressors, hash functions
and in-memory buffers all satisfy it. Which means a function that writes to
an io.Writer works with all of them:
package main
import (
"bytes"
"fmt"
"io"
"os"
"strings"
)
func report(w io.Writer, items []string) {
fmt.Fprintf(w, "%d items\n", len(items))
for i, item := range items {
fmt.Fprintf(w, "%d. %s\n", i+1, item)
}
}
func main() {
items := []string{"slices", "maps", "interfaces"}
report(os.Stdout, items)
var buf bytes.Buffer
report(&buf, items)
fmt.Println("captured", len(buf.String()), "bytes")
fmt.Print(strings.ToUpper(buf.String()))
report(io.Discard, items)
}
The same report wrote to the terminal, into memory, and into the void —
without knowing anything about any of them. Note fmt.Fprintf: every fmt
printing function has an F variant that takes a writer first. fmt.Printf
is literally fmt.Fprintf(os.Stdout, ...).
Take an io.Writer instead of printing directly. It's the single easiest
habit that makes Go code testable — the test passes a bytes.Buffer and
asserts on the string.
Implementing io.Writer yourself
Because it's one method, you can write your own sink in a few lines:
package main
import (
"fmt"
"strings"
)
type UpperWriter struct {
sb strings.Builder
}
func (u *UpperWriter) Write(p []byte) (int, error) {
u.sb.WriteString(strings.ToUpper(string(p)))
return len(p), nil
}
func main() {
u := &UpperWriter{}
fmt.Fprintf(u, "hello %s\n", "world")
fmt.Fprintln(u, "second line")
fmt.Print(u.sb.String())
}
Write must return how many bytes it consumed and an error. Return
len(p), nil when you handled everything — returning less without an error
is a protocol violation that will confuse callers.
That's the shape of a gzip writer, a line-counting writer, a tee-to-two-places writer. Composition all the way down.
Your turn
Give Money a String() method that formats cents as dollars, so the
program prints:
$19.99
total: $24.98
package main
import "fmt"
type Money struct {
Cents int
}
// add String() string — format as $D.CC
func main() {
price := Money{Cents: 1999}
shipping := Money{Cents: 499}
fmt.Println(price)
fmt.Println("total:", Money{Cents: price.Cents + shipping.Cents})
}
package main
import "fmt"
type Money struct {
Cents int
}
func (m Money) String() string {
return fmt.Sprintf("$%d.%02d", m.Cents/100, m.Cents%100)
}
func main() {
price := Money{Cents: 1999}
shipping := Money{Cents: 499}
fmt.Println(price)
fmt.Println("total:", Money{Cents: price.Cents + shipping.Cents})
}
Interfaces done. Next module: the one thing every Go function seems to return — errors.