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