Skip to main content

Key Standard Library Interfaces

Go's standard library provides powerful, general-purpose interfaces. Understanding and implementing these interfaces lets you write code that integrates naturally with the entire standard library.

io.Reader and io.Writer

The two most important interfaces in Go. They abstract I/O so that files, network connections, memory buffers, and more can all be handled the same way.

package main

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

// io.Reader: interface for reading data
// type Reader interface {
// Read(p []byte) (n int, err error)
// }

// io.Writer: interface for writing data
// type Writer interface {
// Write(p []byte) (n int, err error)
// }

// Custom Reader implementation
type CountingReader struct {
r io.Reader
count int
}

func (cr *CountingReader) Read(p []byte) (int, error) {
n, err := cr.r.Read(p)
cr.count += n
return n, err
}

func (cr *CountingReader) BytesRead() int {
return cr.count
}

// Custom Writer implementation
type PrefixWriter struct {
w io.Writer
prefix string
}

func (pw *PrefixWriter) Write(p []byte) (int, error) {
// Add prefix to each line
lines := strings.Split(string(p), "\n")
for i, line := range lines {
if i < len(lines)-1 {
fmt.Fprintf(pw.w, "%s%s\n", pw.prefix, line)
}
}
return len(p), nil
}

func main() {
// io.Copy: copies from Reader to Writer
src := strings.NewReader("Hello, Go interfaces!\nSecond line\nThird line")
cr := &CountingReader{r: src}

var buf bytes.Buffer
pw := &PrefixWriter{w: &buf, prefix: ">>> "}

io.Copy(pw, cr)
fmt.Print(buf.String())
fmt.Printf("bytes read: %d\n", cr.BytesRead())

// Standard library Readers
readers := []io.Reader{
strings.NewReader("from string"),
bytes.NewReader([]byte("from bytes")),
}
multi := io.MultiReader(readers...)
data, _ := io.ReadAll(multi)
fmt.Println("MultiReader:", string(data))
}

fmt.Stringer

Implementing fmt.Stringer provides a custom string representation used by fmt.Println and others.

package main

import (
"fmt"
"strings"
)

// fmt.Stringer interface
// type Stringer interface {
// String() string
// }

type Direction int

const (
North Direction = iota
South
East
West
)

func (d Direction) String() string {
switch d {
case North:
return "North"
case South:
return "South"
case East:
return "East"
case West:
return "West"
default:
return fmt.Sprintf("Direction(%d)", int(d))
}
}

type Point struct {
X, Y float64
}

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

type Person struct {
Name string
Age int
Tags []string
}

func (p Person) String() string {
tags := ""
if len(p.Tags) > 0 {
tags = fmt.Sprintf(" [%s]", strings.Join(p.Tags, ", "))
}
return fmt.Sprintf("%s(%d)%s", p.Name, p.Age, tags)
}

// GoString: used with %#v format (fmt.GoStringer interface)
func (p Person) GoString() string {
return fmt.Sprintf("Person{Name:%q, Age:%d, Tags:%v}", p.Name, p.Age, p.Tags)
}

func main() {
dir := North
fmt.Println(dir) // North
fmt.Printf("direction: %v\n", dir) // direction: North
fmt.Printf("value: %d\n", dir) // value: 0

pt := Point{3.14, 2.72}
fmt.Println(pt) // (3.1, 2.7)

p := Person{
Name: "John Doe",
Age: 30,
Tags: []string{"Go", "Developer"},
}
fmt.Println(p) // John Doe(30) [Go, Developer]
fmt.Printf("%#v\n", p) // Person{Name:"John Doe", Age:30, Tags:[Go Developer]}

// Automatically applied in slices
directions := []Direction{North, East, South, West}
fmt.Println(directions) // [North East South West]
}

The error Interface

In Go, an error is simply an interface with a single Error() string method.

package main

import (
"errors"
"fmt"
)

// error interface
// type error interface {
// Error() string
// }

// Hierarchical error type
type AppError struct {
Code int
Message string
Err error // wrapped cause error
}

func (e *AppError) Error() string {
if e.Err != nil {
return fmt.Sprintf("[%d] %s: %v", e.Code, e.Message, e.Err)
}
return fmt.Sprintf("[%d] %s", e.Code, e.Message)
}

func (e *AppError) Unwrap() error {
return e.Err
}

// Domain-specific error
type DBError struct {
Query string
Err error
}

func (e *DBError) Error() string {
return fmt.Sprintf("DB error (query=%s): %v", e.Query, e.Err)
}

func (e *DBError) Unwrap() error { return e.Err }

var (
ErrNotFound = errors.New("resource not found")
ErrUnauthorized = errors.New("unauthorized")
)

func queryDB(id int) error {
if id == 0 {
return &DBError{
Query: "SELECT * FROM users WHERE id=0",
Err: errors.New("invalid ID"),
}
}
if id > 1000 {
return &AppError{
Code: 404,
Message: "user not found",
Err: fmt.Errorf("DB lookup failed: %w", ErrNotFound),
}
}
return nil
}

func main() {
testIDs := []int{0, 42, 9999}

for _, id := range testIDs {
err := queryDB(id)
if err == nil {
fmt.Printf("ID=%d: success\n", id)
continue
}

fmt.Printf("ID=%d: %v\n", id, err)

// Walk the error chain
var dbErr *DBError
if errors.As(err, &dbErr) {
fmt.Printf(" → DB query: %s\n", dbErr.Query)
}

var appErr *AppError
if errors.As(err, &appErr) {
fmt.Printf(" → app error code: %d\n", appErr.Code)
}

if errors.Is(err, ErrNotFound) {
fmt.Println(" → 404 handling needed")
}
}
}

sort.Interface

Used to customize sorting behavior.

package main

import (
"fmt"
"sort"
)

// sort.Interface
// type Interface interface {
// Len() int
// Less(i, j int) bool
// Swap(i, j int)
// }

type Student struct {
Name string
Grade int
Score float64
}

// Sort by score descending
type ByScore []Student

func (s ByScore) Len() int { return len(s) }
func (s ByScore) Less(i, j int) bool { return s[i].Score > s[j].Score } // descending
func (s ByScore) Swap(i, j int) { s[i], s[j] = s[j], s[i] }

// Sort by name ascending
type ByName []Student

func (s ByName) Len() int { return len(s) }
func (s ByName) Less(i, j int) bool { return s[i].Name < s[j].Name }
func (s ByName) Swap(i, j int) { s[i], s[j] = s[j], s[i] }

func printStudents(students []Student) {
for _, s := range students {
fmt.Printf(" %s (grade:%d, score:%.1f)\n", s.Name, s.Grade, s.Score)
}
}

func main() {
students := []Student{
{"Charlie", 3, 87.5},
{"Alice", 1, 95.0},
{"Bob", 2, 87.5},
{"Diana", 1, 91.0},
{"Eve", 3, 78.0},
}

// Sort by score descending
sort.Sort(ByScore(students))
fmt.Println("By score (descending):")
printStudents(students)

// Sort by name ascending
sort.Sort(ByName(students))
fmt.Println("By name (ascending):")
printStudents(students)

// sort.Slice — sort with closure, no interface needed (Go 1.8+)
sort.Slice(students, func(i, j int) bool {
if students[i].Grade != students[j].Grade {
return students[i].Grade < students[j].Grade // grade ascending
}
return students[i].Score > students[j].Score // score descending
})
fmt.Println("By grade asc, score desc:")
printStudents(students)
}

http.Handler

The core interface for web server development.

package main

import (
"fmt"
"net/http"
"strings"
)

// http.Handler interface
// type Handler interface {
// ServeHTTP(ResponseWriter, *Request)
// }

// Custom handler
type GreetHandler struct {
greeting string
}

func (h GreetHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
name := r.URL.Query().Get("name")
if name == "" {
name = "World"
}
fmt.Fprintf(w, "%s, %s!\n", h.greeting, name)
}

// Middleware pattern (handler wrapping a handler)
type LoggingHandler struct {
handler http.Handler
}

func (lh LoggingHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
fmt.Printf("[LOG] %s %s\n", r.Method, r.URL.Path)
lh.handler.ServeHTTP(w, r)
}

type AuthHandler struct {
handler http.Handler
token string
}

func (ah AuthHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
auth := r.Header.Get("Authorization")
if !strings.HasPrefix(auth, "Bearer "+ah.token) {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
ah.handler.ServeHTTP(w, r)
}

// http.HandlerFunc — converts a function to a Handler
func helloFunc(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "function-based handler")
}

func main() {
// Struct-based handler
greet := GreetHandler{greeting: "Hello"}
logged := LoggingHandler{handler: greet}

// Simulate instead of running actual server
fmt.Println("http.Handler interface implemented")
fmt.Printf("GreetHandler: %T\n", greet)
fmt.Printf("LoggingHandler: %T\n", logged)
fmt.Printf("HandlerFunc: %T\n", http.HandlerFunc(helloFunc))
// To run: http.ListenAndServe(":8080", logged)
}

io.Closer and defer Pattern

package main

import (
"fmt"
"io"
"strings"
)

// io.Closer
// type Closer interface {
// Close() error
// }

type Resource struct {
name string
closed bool
}

func (r *Resource) Read(p []byte) (int, error) {
if r.closed {
return 0, fmt.Errorf("%s: already closed", r.name)
}
data := []byte("data from " + r.name)
copy(p, data)
return len(data), io.EOF
}

func (r *Resource) Close() error {
if r.closed {
return fmt.Errorf("%s: already closed", r.name)
}
r.closed = true
fmt.Printf("[%s] resource closed\n", r.name)
return nil
}

func openResource(name string) io.ReadCloser {
return &Resource{name: name}
}

func processResource(name string) error {
rc := openResource(name)
defer rc.Close() // guaranteed to close

var sb strings.Builder
buf := make([]byte, 64)
for {
n, err := rc.Read(buf)
if n > 0 {
sb.Write(buf[:n])
}
if err == io.EOF {
break
}
if err != nil {
return err
}
}
fmt.Printf("[%s] read: %s\n", name, sb.String())
return nil
}

func main() {
processResource("DB connection")
processResource("file handle")
}

Key Summary

  • io.Reader/io.Writer: Go I/O core — abstracts all I/O sources and destinations
  • fmt.Stringer: implement String() string for custom output formatting
  • error: simple interface with just Error() string; chain with Unwrap()
  • sort.Interface: custom sort criteria (or use sort.Slice)
  • http.Handler: foundation of web server middleware chains
  • io.Closer: use defer rc.Close() to guarantee resource cleanup