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 interfacesBecause 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
stringfilepath and open the file internally, users can e.g. only use it with files on disk. But if you write it to accept anio.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
error: The most fundamental interface. Functions that accepterrorcan work with any error type.any: Alias for the empty interfaceinterface{}. Represents a value of any type.comparable: A built-in constraint for all types that can be compared with==and!=. Essential when writing generic functions. Note: this is a constraint, not a traditional interface, and can only be used with generics.
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
fmt.Stringer: Implement this to customize how your type is printed. Functions in the fmt package (likePrintlnorPrintf) accepts any type that implementsStringerwhen using%sor%v.fmt.GoStringer: Implement this to customize the%#voutput.
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
testing.TB: The common interface for*testing.Tand*testing.B. By acceptingtesting.TB, your helper functions work in both tests and benchmarks.
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
context.Context: Defines the lifecycle of requests, handling cancellation, deadlines, and request-scoped values.
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
slog.Handler: The core interface for structured logging. By accepting aslog.Handler, you can wrap or decorate any logging backend, such as the built-inslog.TextHandlerorslog.JSONHandler.slog.LogValuer: Implement this to control how your type appears in structured logs. Theslogpackage accepts any type that implements this interface.
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
cmp.Ordered: A constraint that accepts any orderable type (int,float64,string, etc.). Use this in generic function signatures to enable comparison operators.
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.
iter.Seq[V]: The standard iterator for a sequence of values.iter.Seq2[K, V]: The standard iterator for key-value pairs.
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.
flag.Value: Implement this to create custom flag types.
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
sync.Locker: Represents an object that can be locked and unlocked. By acceptingsync.Locker, you allow callers to provide any lock implementation (sync.Mutex,sync.RWMutex, or a no-op for testing).
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
sort.Interface: By acceptingsort.Interface, functions likesort.Sortwork with any sortable collection.- Note that you should probably use
slices.Sortorslices.SortFuncfor slices, as they don’t require implementing an interface.
- Note that you should probably use
heap.Interface: Extendssort.Interface. Theheappackage accepts this to provide heap operations.
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.
encoding.TextMarshaler/encoding.TextUnmarshaler: Text serialization. Used byjson,xml,yaml, and other encoders.encoding.BinaryMarshaler/encoding.BinaryUnmarshaler: Binary serialization. Used bygoband other binary encoders.json.Marshaler/json.Unmarshaler: JSON-specific serialization.xml.Marshaler/xml.Unmarshaler: XML-specific serialization.
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).
io.Reader: Reads a stream of bytes. Used for files, network connections, and more.io.Writer: Writes a stream of bytes.io.StringWriter: Writes strings efficiently. Used byio.WriteString().io.Closer: Closes a resource (file, connection). Often used withdefer.io.Seeker: Moves the current offset in a stream.io.ReadWriter: CombinesReaderandWriter.io.ReadCloser: CombinesReaderandCloser(e.g.,http.Response.Body).io.WriteCloser: CombinesWriterandCloser(e.g.,gzip.Writer).io.ReadWriteCloser: Combines all three (e.g.,net.Conn).io.ReaderAt: Reads from a specific offset without changing the underlying state.io.WriterAt: Writes to a specific offset.io.ReaderFrom: Reads data from a generic reader (optimizing copy operations).io.WriterTo: Writes data to a generic writer (optimizing copy operations).
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.
fs.FS: The base interface for a file system. Helper functions likefs.ReadFile,fs.Glob, andfs.WalkDiraccept this interface.fs.File: Represents an open file.fs.DirEntry: An item in a directory (faster thanFileInfofor listings).fs.FileInfo: Metadata about a file.
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
http.Handler: The core interface for responding to HTTP requests (ServeHTTP).http.ResponseWriter: Used to construct an HTTP response.http.RoundTripper: Represents a single HTTP transaction. Used for client middleware.http.Flusher: Flushes buffered data to the client. Essential for streaming responses (SSE, chunked transfer).http.Hijacker: Takes over the underlying connection. Used for WebSockets and protocol upgrades.net.Conn: A generic network connection.net.Listener: Listens for incoming connections.net.Addr: A network address.
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.
sql.Scanner: Implement this to control how database values are read into your type.driver.Valuer: Implement this to control how your type is written 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
hash.Hash: By acceptinghash.Hash, your functions work with any hash algorithm (MD5, SHA256, SHA512, etc.).crypto.Signer: Accept this to sign with any private key type (RSA, ECDSA, Ed25519).
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.
NoteThis 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:
- Go Proverbs - Rob Pike’s wisdom, including “The bigger the interface, the weaker the abstraction”
- Effective Go: Interfaces - The official guide on interface conventions
- Interface Segregation Principle - The SOLID principle that Go’s standard library exemplifies
- Robustness Principle - Also known as Postel’s Law: “Be conservative in what you send, and liberal in what you accept”
- Errors are values by Rob Pike -
Creative patterns with the
errorinterface - The Laws of Reflection - Understanding how interfaces work at runtime