Go’s secret weapon: the standard library interfaces

2025-12-28

Go is peculiar in the sense that there’s a very tight social contract between the community and the standard library. Seemingly universally, the community agrees to use these specific, small interfaces, as glue between unrelated libraries. In fact, I think there’s even an expectation that implementations should, whenever possible, snap nicely together with standard library interfaces, like LEGO bricks.

I think that at least one enabler to this community culture stems from Go’s implicit interface design and the distinct level of composability that comes out of it. In languages with explicit implementation (like Java or C#), you have to plan to implement an interface. In Go, you can implement io.Writer by accident just by having a Write([]byte) (int, error) method.

Implicit interfaces

Because you don’t have to import “io” to implement io.Reader, libraries don’t become coupled. That specific lack of coupling is what makes the standard library interfaces so powerful in Go compared to similar interfaces in other languages. This behavior is technically referred to as structural typing and can be observed in other languages, like TypeScript. In contrast, Java, C#, Rust and Swift use (explicit) nominal typing and for Python, Ruby and JavaScript you have duck typing.

Go feels unique here because it is one of the few mainstream, compiled languages where structural typing is the default and primary way to model abstraction.

What I find especially appealing by all this is that by leveraging the standard library interfaces you don’t just solve your specific problem; you might actually create tools that fit into the wider Go ecosystem, allowing others to use your code in ways you might not have anticipated.

However, this implies you know about said interfaces. When I was completely new to Go, I came across just a handful of such interfaces naturally. In this post I’ve gathered some interfaces from the Go standard library that I think are nice to know about, especially for newcomers to the language. I’m also focusing on examples around accepting said interfaces, as this is where I think composability shines. Where helpful, implementation examples are also included to show the other side of the coin.

Example on “accepting interfaces”

Consider a function that parses data. If you write it to accept a string filepath and open the file internally, users can e.g. only use it with files on disk. But if you write it to accept an io.Reader, your function can work with:

  • Files on disk
  • HTTP response bodies
  • In-memory string buffers
  • Network connections
  • Standard input (stdin)
  • Anything else that implements io.Reader

Interfaces

Core Built-ins

Accepting Error, Any, Comparable and Implementing Custom Errors

Code Examples

package examples

import (
	"encoding/json"
	"fmt"
)

// ============================================================================
// ACCEPTING INTERFACES
// ============================================================================

// Annotate wraps any error with additional context using error composition.
func Annotate(err error, op string) error {
	if err == nil {
		return nil
	}
	return fmt.Errorf("%s: %w", op, err)
}

// ToJSON marshals any value to JSON using the empty interface.
func ToJSON(v any) string {
	b, _ := json.Marshal(v)
	return string(b)
}

// Find searches for a target in a slice using generics.
func Find[T comparable](slice []T, target T) int {
	for i, v := range slice {
		if v == target {
			return i
		}
	}
	return -1
}

// ============================================================================
// IMPLEMENTING INTERFACES
// ============================================================================

// ValidationError is a custom error type that implements the error interface.
type ValidationError struct {
	Field   string
	Message string
}

func (e *ValidationError) Error() string {
	return fmt.Sprintf("%s: %s", e.Field, e.Message)
}

// QueryError is an error type that chains errors using the Unwrap method.
type QueryError struct {
	Query string
	Err   error
}

func (e *QueryError) Error() string {
	return e.Query + ": " + e.Err.Error()
}

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

Usage Examples

package examples

import (
	"errors"
	"fmt"
)

// ExampleAnnotate demonstrates wrapping an error with additional context.
func ExampleAnnotate() {
	err := fmt.Errorf("file not found")
	err = Annotate(err, "loading config")
	fmt.Println(err)
	// Output: loading config: file not found
}

// ExampleToJSON demonstrates marshaling any value to JSON.
func ExampleToJSON() {
	fmt.Println(ToJSON(42))
	fmt.Println(ToJSON(map[string]int{"a": 1}))
	// Output:
	// 42
	// {"a":1}
}

// ExampleFind demonstrates searching for a target in different types.
func ExampleFind() {
	fmt.Println(Find([]int{1, 2, 3}, 2))
	fmt.Println(Find([]string{"a", "b"}, "b"))
	fmt.Println(Find([]int{1, 2, 3}, 99))
	// Output:
	// 1
	// 1
	// -1
}

// ExampleValidationError demonstrates a custom error type.
func ExampleValidationError() {
	err := &ValidationError{Field: "email", Message: "invalid format"}
	fmt.Println(err)
	// Output: email: invalid format
}

// ExampleQueryError demonstrates error chaining.
func ExampleQueryError() {
	inner := errors.New("no rows")
	err := &QueryError{Query: "SELECT *", Err: inner}
	fmt.Println(err)
	fmt.Println(errors.Unwrap(err))
	// Output:
	// SELECT *: no rows
	// no rows
}

Formatting

Accepting and Implementing fmt.Stringer and fmt.GoStringer

Code Examples

package examples

import "fmt"

// ============================================================================
// ACCEPTING INTERFACES
// ============================================================================

// FormatLabel accepts fmt.Stringer to work with any type that has a string representation.
func FormatLabel(s fmt.Stringer) string {
	return fmt.Sprintf("[%s]", s.String())
}

// ============================================================================
// IMPLEMENTING INTERFACES
// ============================================================================

// UserID implements fmt.Stringer to customize how the type is printed.
type UserID int

func (u UserID) String() string {
	return fmt.Sprintf("user-%d", u)
}

// Config implements fmt.GoStringer to control debug formatting with %#v.
type Config struct {
	Host     string
	Password string
}

func (c Config) GoString() string {
	return fmt.Sprintf("Config{Host:%q, Password:\"***\"}", c.Host)
}

Usage Examples

package examples

import "fmt"

// ExampleFormatLabel demonstrates accepting fmt.Stringer.
func ExampleFormatLabel() {
	id := UserID(42)
	result := FormatLabel(id)
	fmt.Println(result)
	// Output: [user-42]
}

// ExampleUserID demonstrates implementing fmt.Stringer.
func ExampleUserID() {
	id := UserID(42)
	fmt.Println(id)
	// Output: user-42
}

// ExampleConfig demonstrates implementing fmt.GoStringer for redaction.
func ExampleConfig() {
	cfg := Config{Host: "localhost", Password: "secret"}
	fmt.Printf("%#v\n", cfg)
	// Output: Config{Host:"localhost", Password:"***"}
}

Testing

Accepting testing.TB for Reusable Test Helpers

Code Examples

package examples

import "testing"

// ============================================================================
// ACCEPTING INTERFACES
// ============================================================================

// assertEqual is a reusable test helper that accepts testing.TB.
func assertEqual(tb testing.TB, expected, actual any) {
	// Helper() ensures error messages point to the caller's line, not here.
	tb.Helper()
	if expected != actual {
		tb.Fatalf("expected %v, got %v", expected, actual)
	}
}

Usage Examples

package examples

import "testing"

func TestAssertEqual(t *testing.T) {
	assertEqual(t, 1, 1)
}

func BenchmarkAssertEqual(b *testing.B) {
	for b.Loop() {
		assertEqual(b, 1, 1)
	}
}

Context

Accepting context.Context for Cancellation and Timeout Control

Code Examples

package examples

import (
	"context"
	"time"
)

// ============================================================================
// ACCEPTING INTERFACES
// ============================================================================

// GetUser demonstrates accepting context.Context for cancellation and deadline support.
func GetUser(ctx context.Context, id int) (*User, error) {
	// Simulate work that respects context cancellation
	select {
	case <-ctx.Done():
		return nil, ctx.Err()
	case <-time.After(100 * time.Millisecond):
		return &User{Name: "Alice"}, nil
	}
}

// ============================================================================
// TYPES
// ============================================================================

// User represents a user from database.
type User struct {
	Name string
}

Usage Examples

package examples

import (
	"context"
	"fmt"
	"time"
)

// ExampleGetUser demonstrates accepting context.Context for cancellation.
func ExampleGetUser() {
	ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
	defer cancel()

	user, err := GetUser(ctx, 1)
	if err != nil {
		fmt.Println("Error:", err)
	} else {
		fmt.Println("User:", user.Name)
	}
	// Output: User: Alice
}

// ExampleGetUser_timeout demonstrates context cancellation via timeout.
func ExampleGetUser_timeout() {
	// Set a timeout shorter than the simulated 100ms work in GetUser
	ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
	defer cancel()

	_, err := GetUser(ctx, 1)
	if err != nil {
		fmt.Println("Error:", err)
	}
	// Output: Error: context deadline exceeded
}

Logging

Accepting and Implementing slog Interfaces

Code Examples

package examples

import (
	"context"
	"fmt"
	"log/slog"
)

// ============================================================================
// ACCEPTING INTERFACES
// ============================================================================

// WithRequestID wraps a slog.Handler to add a request ID attribute.
// This demonstrates how accepting slog.Handler allows you to decorate any logging backend.
func WithRequestID(h slog.Handler, requestID string) slog.Handler {
	return h.WithAttrs([]slog.Attr{slog.String("request_id", requestID)})
}

// ============================================================================
// IMPLEMENTING INTERFACES
// ============================================================================

// CustomHandler implements slog.Handler to demonstrate a simple text logger.
type CustomHandler struct {
	attrs []slog.Attr
}

func (h *CustomHandler) Enabled(ctx context.Context, level slog.Level) bool {
	return true
}

func (h *CustomHandler) Handle(ctx context.Context, r slog.Record) error {
	// Print the level and message
	fmt.Printf("[%s] %s", r.Level, r.Message)

	// Print handler attributes (e.g. from WithAttrs)
	for _, a := range h.attrs {
		fmt.Printf(" %s=%v", a.Key, a.Value.Any())
	}

	// Print record attributes (e.g. from logger.Info args)
	r.Attrs(func(a slog.Attr) bool {
		fmt.Printf(" %s=%v", a.Key, a.Value.Any())
		return true
	})

	fmt.Println()
	return nil
}

// WithAttrs returns a new handler with the added attributes.
func (h *CustomHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
	// Copy existing attributes to the new handler
	newAttrs := make([]slog.Attr, len(h.attrs)+len(attrs))
	copy(newAttrs, h.attrs)
	copy(newAttrs[len(h.attrs):], attrs)
	return &CustomHandler{attrs: newAttrs}
}

// WithGroup is a no-op in this simplified example.
func (h *CustomHandler) WithGroup(name string) slog.Handler { return h }

// Token implements slog.LogValuer for redacting sensitive data.
type Token string

func (t Token) LogValue() slog.Value {
	return slog.StringValue("REDACTED")
}

Usage Examples

package examples

import (
	"fmt"
	"log/slog"
)

// ExampleCustomHandler demonstrates implementing slog.Handler.
func ExampleCustomHandler() {
	h := &CustomHandler{}
	logger := slog.New(h)

	// Add context with WithAttrs
	logger = logger.With("env", "prod")

	logger.Info("Application started", "version", "1.0")
	// Output: [INFO] Application started env=prod version=1.0
}

// ExampleToken demonstrates implementing slog.LogValuer for redaction.
func ExampleToken() {
	token := Token("secret-abc123")
	val := token.LogValue()
	fmt.Println("Token value:", val.String())
	// Output: Token value: REDACTED
}

// ExampleWithRequestID demonstrates wrapping a slog.Handler.
func ExampleWithRequestID() {
	h := &CustomHandler{}
	wrapped := WithRequestID(h, "req-123")
	logger := slog.New(wrapped)

	logger.Info("Request processed")
	// Output: [INFO] Request processed request_id=req-123
}

Generics

Accepting cmp.Ordered Constraint in Generic Functions

Code Examples

package examples

import "cmp"

// ============================================================================
// ACCEPTING INTERFACES
// ============================================================================

// Max returns the maximum of two ordered values using cmp.Ordered constraint.
func Max[T cmp.Ordered](a, b T) T {
	if a > b {
		return a
	}
	return b
}

Usage Examples

package examples

import (
	"fmt"
)

// ExampleMax demonstrates cmp.Ordered constraint in generic functions
func ExampleMax() {
	fmt.Println(Max(10, 20))
	fmt.Println(Max(3.14, 2.71))
	fmt.Println(Max("apple", "banana"))
	// Output:
	// 20
	// 3.14
	// banana
}

Iteration

The iter package introduces standard signatures for iterators. By accepting these types, your functions can work with any iterable source: slices, maps, channels, custom data structures, or generated sequences.

Accepting and Implementing iter.Seq Iterators

Code Examples

package examples

import "iter"

// ============================================================================
// ACCEPTING INTERFACES
// ============================================================================

// Collect gathers any sequence into a slice, accepting iter.Seq.
// Works with slices.Values(), maps.Keys(), custom iterators, etc.
func Collect[T any](seq iter.Seq[T]) []T {
	var result []T
	for v := range seq {
		result = append(result, v)
	}
	return result
}

// Filter accepts iter.Seq and returns a filtered sequence with a predicate.
func Filter[T any](seq iter.Seq[T], keep func(T) bool) iter.Seq[T] {
	return func(yield func(T) bool) {
		for v := range seq {
			if keep(v) {
				if !yield(v) {
					return
				}
			}
		}
	}
}

// ============================================================================
// IMPLEMENTING INTERFACES
// ============================================================================

// Stack is a LIFO data structure that implements iter.Seq for iteration.
type Stack[T any] struct {
	items []T
}

// Push adds an item to the stack.
func (s *Stack[T]) Push(item T) {
	s.items = append(s.items, item)
}

// Pop removes and returns the top item.
func (s *Stack[T]) Pop() (T, bool) {
	var zero T
	if len(s.items) == 0 {
		return zero, false
	}
	item := s.items[len(s.items)-1]
	s.items = s.items[:len(s.items)-1]
	return item, true
}

// All returns an iterator over the stack in LIFO order, implementing iter.Seq.
// This allows the stack to work with for-range and all iter functions.
func (s *Stack[T]) All() iter.Seq[T] {
	return func(yield func(T) bool) {
		for i := len(s.items) - 1; i >= 0; i-- {
			if !yield(s.items[i]) {
				return
			}
		}
	}
}

Usage Examples

package examples

import (
	"fmt"
	"slices"
)

// ExampleCollect demonstrates collecting iter.Seq into a slice.
func ExampleCollect() {
	nums := Collect(slices.Values([]int{1, 2, 3}))
	fmt.Println(nums)
	// Output:
	// [1 2 3]
}

// ExampleFilter demonstrates filtering a sequence with a predicate.
func ExampleFilter() {
	nums := Collect(Filter(slices.Values([]int{1, 2, 3, 4}), func(n int) bool {
		return n%2 == 0
	}))
	fmt.Println(nums)
	// Output:
	// [2 4]
}

// ExampleStack demonstrates that custom types can implement iter.Seq.
func ExampleStack() {
	stack := &Stack[int]{}
	stack.Push(1)
	stack.Push(2)
	stack.Push(3)

	nums := Collect(stack.All())
	fmt.Println(nums)
	// Output:
	// [3 2 1]
}

Command Line Flags

The flag package accepts any type implementing this interface, allowing custom flag parsing.

Implementing flag.Value for Custom Flag Parsing

Code Examples

package examples

import "strings"

// ============================================================================
// IMPLEMENTING INTERFACES
// ============================================================================

// CSVFlag implements flag.Value for custom command-line flag parsing.
type CSVFlag []string

func (c *CSVFlag) String() string { return strings.Join(*c, ",") }
func (c *CSVFlag) Set(v string) error {
	*c = append(*c, strings.Split(v, ",")...)
	return nil
}

Usage Examples

package examples

import (
	"fmt"
)

// ExampleCSVFlag demonstrates implementing flag.Value
func ExampleCSVFlag() {
	var tags CSVFlag
	tags.Set("production,api")
	fmt.Println(tags.String())
	// Output: production,api
}

Concurrency

Accepting sync.Locker for Flexible Locking Strategies

Code Examples

package examples

import "sync"

// ============================================================================
// ACCEPTING INTERFACES
// ============================================================================

// WithLock safely executes a function with a lock using the sync.Locker interface.
func WithLock(l sync.Locker, fn func()) {
	l.Lock()
	defer l.Unlock()
	fn()
}

Usage Examples

package examples

import (
	"fmt"
	"sync"
)

// ExampleWithLock demonstrates sync.Locker usage
func ExampleWithLock() {
	var mu sync.Mutex
	var counter int

	WithLock(&mu, func() {
		counter++
	})

	fmt.Println("Counter:", counter)
	// Output: Counter: 1
}

Sorting & Containers

Accepting and Implementing sort.Interface and heap.Interface

Code Examples

package examples

import "sort"

// ============================================================================
// ACCEPTING INTERFACES
// ============================================================================

// IsSorted checks if any sort.Interface is sorted.
func IsSorted(data sort.Interface) bool {
	n := data.Len()
	for i := 1; i < n; i++ {
		if data.Less(i, i-1) {
			return false
		}
	}
	return true
}

// ============================================================================
// IMPLEMENTING INTERFACES
// ============================================================================

// Person is a struct for sorting examples.
type Person struct {
	Name string
	Age  int
}

// ByAge sorts people by age and implements sort.Interface.
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 }

// IntHeap implements heap.Interface for use with container/heap.
type IntHeap []int

func (h IntHeap) Len() int           { return len(h) }
func (h IntHeap) Less(i, j int) bool { return h[i] < h[j] }
func (h IntHeap) Swap(i, j int)      { h[i], h[j] = h[j], h[i] }

func (h *IntHeap) Push(x any) { *h = append(*h, x.(int)) }
func (h *IntHeap) Pop() any {
	old := *h
	n := len(old)
	x := old[n-1]
	*h = old[0 : n-1]
	return x
}

Usage Examples

package examples

import (
	"container/heap"
	"fmt"
)

// ExampleIsSorted demonstrates accepting sort.Interface
func ExampleIsSorted() {
	people := ByAge{{Name: "Alice", Age: 20}, {Name: "Bob", Age: 25}}
	fmt.Println("Is sorted:", IsSorted(people))
	// Output: Is sorted: true
}

// ExampleIntHeap demonstrates implementing heap.Interface
func ExampleIntHeap() {
	h := &IntHeap{2, 1, 5}
	heap.Init(h)
	min := heap.Pop(h)
	fmt.Println("Min:", min)
	// Output: Min: 1
}

Encoding & Serialization

“Marshalling” (or serialization) is the process of converting a Go value (like a struct) into a format suitable for storage or transmission (like JSON text or binary data). Conversely, “unmarshalling” is parsing that data back into a Go value.

These interfaces are primarily designed to be implemented by your types to customize this process. The standard library functions (like json.Marshal) check if your type implements these interfaces before falling back to default behaviors.

Accepting and Implementing Encoding Interfaces

Code Examples

package examples

import (
	"encoding"
	"encoding/json"
	"time"
)

// ============================================================================
// ACCEPTING INTERFACES
// ============================================================================

// ToText serializes any TextMarshaler to string using the encoding.TextMarshaler interface.
func ToText(m encoding.TextMarshaler) (string, error) {
	b, err := m.MarshalText()
	return string(b), err
}

// ============================================================================
// IMPLEMENTING INTERFACES
// ============================================================================

// Status is a custom type with text marshaling that implements encoding.TextMarshaler.
type Status int

const (
	Running Status = iota
	Stopped
)

func (s Status) MarshalText() ([]byte, error) {
	if s == Running {
		return []byte("running"), nil
	}
	return []byte("stopped"), nil
}

// Timestamp implements json.Marshaler to customize JSON serialization.
type Timestamp int64

func (t Timestamp) MarshalJSON() ([]byte, error) {
	s := time.Unix(int64(t), 0).Format(time.RFC3339)
	// Use json.Marshal to ensure the string is correctly quoted and escaped.
	return json.Marshal(s)
}

// LogEntry uses Timestamp for JSON serialization.
type LogEntry struct {
	Msg  string
	Time Timestamp
}

Usage Examples

package examples

import (
	"encoding/json"
	"fmt"
	"strings"
)

// ExampleToText demonstrates accepting encoding.TextMarshaler
func ExampleToText() {
	status := Running
	text, err := ToText(status)
	if err != nil {
		fmt.Println("Error:", err)
	} else {
		fmt.Println("Status:", text)
	}
	// Output: Status: running
}

// ExampleStatus demonstrates implementing encoding.TextMarshaler
func ExampleStatus() {
	status := Running
	b, _ := json.Marshal(status)
	fmt.Println(string(b))
	// Output: "running"
}

// ExampleTimestamp demonstrates implementing json.Marshaler
func ExampleTimestamp() {
	ts := Timestamp(1735689600)
	b, _ := json.Marshal(ts)
	// Output includes timezone offset which varies by system
	fmt.Printf("Timestamp contains 2025: %v\n", strings.Contains(string(b), "2025"))
	// Output: Timestamp contains 2025: true
}

Input / Output

The io package is about moving bytes. It abstracts the concept of a stream of data, allowing you to process inputs and outputs without caring about the source (file, network, memory buffer). Because these interfaces are so universal, the standard library provides powerful combinators like io.TeeReader (read and write simultaneously, e.g., hash a file while uploading it) and io.MultiWriter (write to multiple destinations at once).

Accepting and Implementing io Interfaces

Code Examples

package examples

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

// ============================================================================
// ACCEPTING INTERFACES
// ============================================================================

// ProcessLogs processes logs from reader to writer using io.Reader and io.Writer.
func ProcessLogs(w io.Writer, r io.Reader) error {
	scanner := bufio.NewScanner(r)
	for scanner.Scan() {
		line := scanner.Text()
		if strings.Contains(line, "ERROR") {
			if _, err := fmt.Fprintln(w, line); err != nil {
				return err
			}
		}
	}
	return scanner.Err()
}

// WriteLabel writes a label using io.StringWriter for efficient string writing.
func WriteLabel(w io.StringWriter, label string) error {
	_, err := w.WriteString("[" + label + "]")
	return err
}

// StreamSize gets the size of a seekable stream using io.Seeker.
func StreamSize(s io.Seeker) (int64, error) {
	current, err := s.Seek(0, io.SeekCurrent)
	if err != nil {
		return 0, err
	}

	size, err := s.Seek(0, io.SeekEnd)
	if err != nil {
		return 0, err
	}

	_, err = s.Seek(current, io.SeekStart)
	return size, err
}

// ReadChunk reads a specific chunk from ReaderAt for random access reads.
func ReadChunk(r io.ReaderAt, offset int64, size int) ([]byte, error) {
	buf := make([]byte, size)
	_, err := r.ReadAt(buf, offset)
	return buf, err
}

// Transfer uses io.WriterTo for efficient copying when source is WriterTo.
func Transfer(dst io.Writer, src io.WriterTo) (int64, error) {
	return src.WriteTo(dst)
}

// ============================================================================
// IMPLEMENTING INTERFACES
// ============================================================================

// AReader is an infinite stream of 'A's that implements io.Reader.
type AReader struct{}

func (AReader) Read(p []byte) (int, error) {
	for i := range p {
		p[i] = 'A'
	}
	return len(p), nil
}

Usage Examples

package examples

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

// ExampleProcessLogs demonstrates accepting io.Reader and io.Writer
func ExampleProcessLogs() {
	input := strings.NewReader("INFO: ok\nERROR: fail\n")
	var output bytes.Buffer
	ProcessLogs(&output, input)
	fmt.Print(output.String())
	// Output: ERROR: fail
}

// ExampleWriteLabel demonstrates accepting io.StringWriter
func ExampleWriteLabel() {
	var b strings.Builder
	WriteLabel(&b, "INFO")
	fmt.Println(b.String())
	// Output: [INFO]
}

// ExampleStreamSize demonstrates accepting io.Seeker
func ExampleStreamSize() {
	data := bytes.NewReader([]byte("hello world"))
	size, _ := StreamSize(data)
	fmt.Println("Stream size:", size)
	// Output: Stream size: 11
}

// ExampleReadChunk demonstrates accepting io.ReaderAt
func ExampleReadChunk() {
	data := bytes.NewReader([]byte("hello world"))
	chunk, _ := ReadChunk(data, 0, 5)
	fmt.Println(string(chunk))
	// Output: hello
}

// ExampleTransfer demonstrates accepting io.WriterTo
func ExampleTransfer() {
	var src bytes.Buffer
	src.WriteString("payload")
	var dst bytes.Buffer
	Transfer(&dst, &src)
	fmt.Println(dst.String())
	// Output: payload
}

// ExampleAReader demonstrates implementing io.Reader
func ExampleAReader() {
	data, _ := io.ReadAll(io.LimitReader(AReader{}, 5))
	fmt.Println(string(data))
	// Output: AAAAA
}

File Systems

While io deals with open streams, io/fs deals with the structure of files (names, directories, hierarchy).

Crucially, io/fs interfaces are read-only. This constraint is intentional: it allows the same code to safely navigate files stored on a local disk, embedded inside the binary, wrapped in a ZIP archive, or hosted in the cloud, without needing to handle complex (and perhaps impossible) write operations for those different backends.

Accepting fs.FS for File System Abstraction

Code Examples

package examples

import (
	"fmt"
	"io/fs"
)

// ============================================================================
// ACCEPTING INTERFACES
// ============================================================================

// PrintFiles recursively lists files from any filesystem using the fs.FS interface.
func PrintFiles(fsys fs.FS) error {
	return fs.WalkDir(fsys, ".", func(path string, d fs.DirEntry, err error) error {
		if err != nil {
			return err
		}
		fmt.Println(path)
		return nil
	})
}

// LoadConfig reads a file using fs.ReadFile with any filesystem implementation.
func LoadConfig(fsys fs.FS, name string) ([]byte, error) {
	return fs.ReadFile(fsys, name)
}

// GetFileInfo gets file info from any filesystem implementation.
func GetFileInfo(fsys fs.FS, name string) (fs.FileInfo, error) {
	f, err := fsys.Open(name)
	if err != nil {
		return nil, err
	}
	defer f.Close()

	return f.Stat()
}

// ListDetails lists directory entries with information from any filesystem.
func ListDetails(fsys fs.FS) error {
	entries, err := fs.ReadDir(fsys, ".")
	if err != nil {
		return err
	}

	for _, d := range entries {
		info, _ := d.Info()
		fmt.Printf("%s: %d bytes (is dir: %v)\n", d.Name(), info.Size(), d.IsDir())
	}
	return nil
}

Usage Examples

package examples

import (
	"fmt"
	"testing/fstest"
)

// ExampleLoadConfig demonstrates accepting fs.FS
func ExampleLoadConfig() {
	fsys := fstest.MapFS{
		"config.json": &fstest.MapFile{Data: []byte(`{"env": "prod"}`)},
	}
	data, _ := LoadConfig(fsys, "config.json")
	fmt.Println(string(data))
	// Output: {"env": "prod"}
}

// ExampleGetFileInfo demonstrates accepting fs.FS with file info
func ExampleGetFileInfo() {
	fsys := fstest.MapFS{
		"config.json": &fstest.MapFile{Data: []byte(`{"env": "prod"}`)},
	}
	info, _ := GetFileInfo(fsys, "config.json")
	fmt.Println("File size:", info.Size())
	// Output: File size: 15
}

HTTP & Networking

Accepting and Implementing HTTP and Networking Interfaces

Code Examples

package examples

import (
	"fmt"
	"log/slog"
	"net"
	"net/http"
	"time"
)

// ============================================================================
// ACCEPTING INTERFACES
// ============================================================================

// EnforceJSON is middleware that requires JSON content type using http.Handler.
func EnforceJSON(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if r.Header.Get("Content-Type") != "application/json" {
			http.Error(w, "Content-Type must be application/json", http.StatusUnsupportedMediaType)
			return
		}
		next.ServeHTTP(w, r)
	})
}

// LogStatus is middleware that logs the response status using statusRecorder.
func LogStatus(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		rec := &statusRecorder{ResponseWriter: w, status: 200}
		next.ServeHTTP(rec, r)
		slog.Info("request", "path", r.URL.Path, "status", rec.status)
	})
}

// StreamEvents sends server-sent events using http.Flusher interface.
func StreamEvents(w http.ResponseWriter, r *http.Request) {
	flusher, ok := w.(http.Flusher)
	if !ok {
		http.Error(w, "Streaming not supported", http.StatusInternalServerError)
		return
	}

	w.Header().Set("Content-Type", "text/event-stream")
	for i := 0; i < 5; i++ {
		fmt.Fprintf(w, "data: Event %d\n\n", i)
		flusher.Flush()
		time.Sleep(time.Second)
	}
}

// UpgradeConnection handles protocol upgrades using http.Hijacker.
func UpgradeConnection(w http.ResponseWriter, r *http.Request) {
	hijacker, ok := w.(http.Hijacker)
	if !ok {
		http.Error(w, "Hijacking not supported", http.StatusInternalServerError)
		return
	}

	conn, _, err := hijacker.Hijack()
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
	defer conn.Close()

	conn.Write([]byte("HTTP/1.1 101 Switching Protocols\r\n\r\n"))
}

// HandleConn handles any network connection using net.Conn interface.
func HandleConn(conn net.Conn) {
	defer conn.Close()
	fmt.Fprintf(conn, "Hello from %v\n", conn.LocalAddr())
}

// RunServer accepts any listener implementing net.Listener interface.
func RunServer(l net.Listener) error {
	for {
		conn, err := l.Accept()
		if err != nil {
			return err
		}
		go HandleConn(conn)
	}
}

// ============================================================================
// IMPLEMENTING INTERFACES
// ============================================================================

// statusRecorder wraps http.ResponseWriter to capture status code.
type statusRecorder struct {
	http.ResponseWriter
	status int
}

func (rec *statusRecorder) WriteHeader(code int) {
	rec.status = code
	rec.ResponseWriter.WriteHeader(code)
}

// LoggingTransport implements http.RoundTripper to log outbound HTTP requests.
type LoggingTransport struct {
	Next http.RoundTripper
}

func (l *LoggingTransport) RoundTrip(req *http.Request) (*http.Response, error) {
	fmt.Println("Sending request to:", req.URL)
	return l.Next.RoundTrip(req)
}

Usage Examples

package examples

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

// ExampleEnforceJSON demonstrates http.Handler middleware
func ExampleEnforceJSON() {
	handler := EnforceJSON(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintln(w, "Success")
	}))

	req, _ := http.NewRequest("POST", "/", nil)
	req.Header.Set("Content-Type", "application/json")

	recorder := &mockResponseWriter{header: make(http.Header)}
	handler.ServeHTTP(recorder, req)

	fmt.Println("Status Code:", recorder.statusCode)
	// Output: Status Code: 200
}

// ExampleLoggingTransport demonstrates http.RoundTripper implementation
func ExampleLoggingTransport() {
	transport := &LoggingTransport{Next: http.DefaultTransport}
	req, _ := http.NewRequest("GET", "http://example.com", nil)
	transport.RoundTrip(req)
	// Output: Sending request to: http://example.com
}

// mockResponseWriter is a helper for testing http.ResponseWriter interactions.
type mockResponseWriter struct {
	statusCode int
	header     http.Header
	body       bytes.Buffer
}

func (m *mockResponseWriter) Header() http.Header { return m.header }
func (m *mockResponseWriter) Write(b []byte) (int, error) {
	if m.statusCode == 0 {
		m.statusCode = http.StatusOK
	}
	return m.body.Write(b)
}
func (m *mockResponseWriter) WriteHeader(code int) { m.statusCode = code }

Database

Like the encoding interfaces, these are designed to be implemented by your types. The database/sql package accepts any type and checks if it implements these interfaces when reading from or writing to the database.

Accepting and Implementing Database Interfaces

Code Examples

package examples

import (
	"database/sql/driver"
	"encoding/json"
	"fmt"
)

// ============================================================================
// ACCEPTING INTERFACES
// ============================================================================

// DebugSQLValue inspects what a custom type sends to the database using driver.Valuer.
func DebugSQLValue(v driver.Valuer) string {
	val, err := v.Value()
	if err != nil {
		return fmt.Sprintf("error: %v", err)
	}
	if b, ok := val.([]byte); ok {
		return fmt.Sprintf("SQL Value: %s", string(b))
	}
	return fmt.Sprintf("SQL Value: %v", val)
}

// ============================================================================
// IMPLEMENTING INTERFACES
// ============================================================================

// JSONB is a custom JSON type for databases that implements both sql.Scanner and driver.Valuer.
type JSONB map[string]any

func (j *JSONB) Scan(v any) error {
	var bytes []byte
	switch x := v.(type) {
	case []byte:
		bytes = x
	case string:
		bytes = []byte(x)
	case nil:
		return nil
	default:
		return fmt.Errorf("cannot scan type %T into JSONB", v)
	}
	return json.Unmarshal(bytes, j)
}

func (j JSONB) Value() (driver.Value, error) {
	return json.Marshal(j)
}

Usage Examples

package examples

import (
	"fmt"
)

// ExampleDebugSQLValue demonstrates accepting driver.Valuer
func ExampleDebugSQLValue() {
	data := JSONB{"test": true}
	result := DebugSQLValue(data)
	fmt.Println(result)
	// Output: SQL Value: {"test":true}
}

// ExampleJSONB demonstrates implementing sql.Scanner and driver.Valuer
func ExampleJSONB() {
	data := JSONB{"theme": "dark"}
	b, _ := data.Value()
	var data2 JSONB
	data2.Scan(b)
	fmt.Println("Theme:", data2["theme"])
	// Output: Theme: dark
}

Cryptography & Hashing

Accepting hash.Hash and crypto.Signer

Code Examples

package examples

import (
	"crypto"
	"crypto/rand"
	"crypto/sha256"
	"hash"
)

// ============================================================================
// ACCEPTING INTERFACES
// ============================================================================

// GetDigest computes a digest with any hash algorithm using the hash.Hash interface.
func GetDigest(h hash.Hash, data []byte) []byte {
	h.Write(data)
	return h.Sum(nil)
}

// SignData signs data with any crypto.Signer using RSA, ECDSA, or custom implementations.
func SignData(signer crypto.Signer, data []byte) ([]byte, error) {
	hash := sha256.Sum256(data)
	return signer.Sign(rand.Reader, hash[:], crypto.SHA256)
}

Usage Examples

package examples

import (
	"crypto/ecdsa"
	"crypto/elliptic"
	"crypto/rand"
	"crypto/sha256"
	"fmt"
)

// ExampleGetDigest demonstrates accepting hash.Hash
func ExampleGetDigest() {
	data := []byte("hello world")
	digest := GetDigest(sha256.New(), data)
	fmt.Printf("SHA256 (%d bytes): %x\n", len(digest), digest)
	// Output: SHA256 (32 bytes): b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9
}

// ExampleSignData demonstrates accepting crypto.Signer
func ExampleSignData() {
	key, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
	data := []byte("message to sign")
	sig, _ := SignData(key, data)
	// ECDSA signatures vary in length (68-72 bytes) due to DER encoding
	if len(sig) > 60 && len(sig) < 80 {
		fmt.Println("Signature generated successfully")
	}
	// Output: Signature generated successfully
}

Workflow

This post only scratches the surface. The standard library is full of interfaces waiting to be discovered, and the best way to find them is to actively explore the standard library documentation. Don’t stop there though; third-party libraries often define useful interfaces too. When you’re about to define a new type or write a function signature, ask yourself: is there an existing interface that fits here?

You can quickly look up interfaces from your terminal using go doc (e.g., go doc io.Reader). Leverage stdsym to search third-party packages. If you’re a Neovim user, the godoc.nvim plugin lets you search, preview and inspect documentation directly in your editor.

A really great Neovim plugin is goplements.nvim, which makes “implemented by” and “implements” information pop up inline, as you satisfy an interface or implement one. Super helpful!

Conclusion

The pattern is simple: accept interfaces, return concrete types. Returning concrete types (like structs or custom types) allows you to add new methods to them later without breaking backward compatibility, making it easier to extend functionality. When your functions accept interfaces like io.Reader, context.Context, or hash.Hash, they become building blocks that others can combine in ways you never anticipated. When your types implement interfaces like error, fmt.Stringer, or json.Marshaler, they plug seamlessly into the Go ecosystem.

Note

This post is about leveraging existing standard library interfaces. You generally shouldn’t create your own interfaces preemptively. Go’s implicit interfaces mean the caller can define an interface when they actually need one.

This is Go’s secret weapon for composability. The standard library provides the shared vocabulary; you just need to speak it.

Further Reading

If you want to dive deeper into Go’s interface philosophy and design: