Common Interfaces — Specification¶
Official Reference Source: Go Standard Library Documentation Topical sources:
builtin,fmt,io,sort,encoding,encoding/json,net/http,context,io/fs,iter,database/sql,database/sql/driver.The Go specification only defines interfaces as a structural type (https://go.dev/ref/spec#Interface_types). The "common interfaces" we describe here are not language-level; they are conventions established by the standard library and respected throughout the ecosystem.
Table of Contents¶
- What "Common Interfaces" Means
- The
errorInterface fmt.Stringerandfmt.Formatter- The
ioInterface Family sort.Interface- Marshalers:
json,encoding.TextMarshaler,encoding.BinaryMarshaler net/httpInterfacescontext.Contextio/fs.FS,iter.Seq, anddatabase/sqlInterfaces- Contract Rules and Defined Behavior
- Version History and Compliance Checklist
1. What "Common Interfaces" Means¶
The Go language specification defines interface as the type whose method set is its identity. The common interfaces are a small, conventional set of interface types declared in the standard library that:
- Have stable, documented contracts.
- Are accepted by many functions across the ecosystem.
- Compose readily (for example
io.ReadCloser = io.Reader + io.Closer). - Are detected by libraries via type assertions to enable optimizations (for example
io.Copychecks forio.WriterToandio.ReaderFrom).
Implementing a common interface is the primary way a user-defined type "plugs in" to standard library facilities — fmt.Println, json.Marshal, http.Handler, sort.Sort, bufio.NewReader, and so on. Because these interfaces are tiny (one or two methods) and orthogonal, they support the "accept interfaces, return structs" idiom.
Where the contracts live¶
Each interface declaration is annotated with a doc comment that defines the contract. The contract is prose, not code, so static checking cannot verify it; testing is the only enforcement. The remainder of this document distills the contracts directly from those doc comments.
2. The error Interface¶
Source: https://pkg.go.dev/builtin#error
Declaration¶
error is one of only a few predeclared interface types in the language; it lives in the universe block, so no import is needed.
Contract¶
- The returned string SHOULD describe what failed, not where.
Error()SHOULD NOT capitalize the first letter and SHOULD NOT end with punctuation. Source: https://go.dev/wiki/CodeReviewComments#error-strings.- A nil
errorvalue indicates success. A non-nil value indicates failure; the concrete type may carry additional context.
Implementing¶
type ParseError struct {
Line int
Msg string
}
func (e *ParseError) Error() string {
return fmt.Sprintf("parse error at line %d: %s", e.Line, e.Msg)
}
Wrapping (Go 1.13+)¶
A package-defined error type that wishes to participate in errors.Is and errors.As may implement two optional helpers:
type WrappedErr struct {
Op string
Err error
}
func (w *WrappedErr) Error() string { return w.Op + ": " + w.Err.Error() }
func (w *WrappedErr) Unwrap() error { return w.Err }
Source: https://pkg.go.dev/errors#Unwrap. errors.Is walks the chain via Unwrap(); errors.As performs type-assertion at each level. Go 1.20 added multi-error wrapping with errors.Join and Unwrap() []error.
3. fmt.Stringer and fmt.Formatter¶
Source: https://pkg.go.dev/fmt#Stringer, https://pkg.go.dev/fmt#Formatter
Stringer¶
fmt package functions (Print, Printf, Println, Sprintf) call String() whenever they need a textual representation of a value with the %s or %v verb (and the value is not already a string).
Implementing Stringer¶
type Status int
const (
StatusPending Status = iota
StatusActive
StatusClosed
)
func (s Status) String() string {
switch s {
case StatusPending: return "pending"
case StatusActive: return "active"
case StatusClosed: return "closed"
}
return fmt.Sprintf("Status(%d)", int(s))
}
Pitfall — Infinite Recursion¶
If String() calls fmt.Sprintf("%v", s) on the same value, the runtime re-enters String() and the program panics with a stack overflow. Always convert to the underlying type:
Formatter¶
Formatter overrides fmt's default verb processing. It is implemented when you need %x, %+v, width, precision, or plus/minus flags to behave differently from the default. State exposes the writer, flag tests, and width/precision queries.
type Money struct {
Cents int64
Currency string
}
func (m Money) Format(s fmt.State, verb rune) {
switch verb {
case 'v', 's':
fmt.Fprintf(s, "%d.%02d %s", m.Cents/100, m.Cents%100, m.Currency)
case 'd':
fmt.Fprintf(s, "%d", m.Cents)
default:
fmt.Fprintf(s, "%%!%c(Money=%d %s)", verb, m.Cents, m.Currency)
}
}
When both Stringer and Formatter are implemented, Formatter wins for the verbs it handles; Stringer is only consulted as a fallback.
4. The io Interface Family¶
Source: https://pkg.go.dev/io
The io package declares a set of single-method interfaces that compose into larger contracts. They are the most-imitated interfaces in Go.
Core single-method interfaces¶
type Reader interface {
Read(p []byte) (n int, err error)
}
type Writer interface {
Write(p []byte) (n int, err error)
}
type Closer interface {
Close() error
}
type Seeker interface {
Seek(offset int64, whence int) (int64, error)
}
type ReaderAt interface {
ReadAt(p []byte, off int64) (n int, err error)
}
type WriterAt interface {
WriteAt(p []byte, off int64) (n int, err error)
}
Reader contract — verbatim¶
Read reads up to len(p) bytes into p. It returns the number of bytes read (0 <= n <= len(p)) and any error encountered. Even if Read returns n < len(p), it may use all of p as scratch space during the call. If some data is available but not len(p) bytes, Read conventionally returns what is available instead of waiting for more.
When Read encounters an error or end-of-file condition after successfully reading n > 0 bytes, it returns the number of bytes read. It may return the (non-nil) error from the same call or return the error (and n == 0) from a subsequent call. An instance of this general case is that a Reader returning a non-zero number of bytes at the end of the input stream may return either err == EOF or err == nil. The next Read should return 0, EOF.
Source: https://pkg.go.dev/io#Reader
Two notes:
n > 0witherr != nilis valid. Callers must process the bytes before checking the error.- Returning
(0, nil)is "discouraged" and should be treated as a no-op, not as EOF.
Writer contract¶
Write writes len(p) bytes from p to the underlying data stream. It returns the number of bytes written from p (0 <= n <= len(p)) and any error encountered that caused the write to stop early. Write must return a non-nil error if it returns n < len(p). Write must not modify the slice data, even temporarily.
Source: https://pkg.go.dev/io#Writer
Optimization interfaces — WriterTo and ReaderFrom¶
type WriterTo interface {
WriteTo(w Writer) (n int64, err error)
}
type ReaderFrom interface {
ReadFrom(r Reader) (n int64, err error)
}
io.Copy(dst, src) checks both ends for these:
// io.Copy implementation outline (real source: src/io/io.go)
func Copy(dst Writer, src Reader) (int64, error) {
if wt, ok := src.(WriterTo); ok { return wt.WriteTo(dst) }
if rf, ok := dst.(ReaderFrom); ok { return rf.ReadFrom(src) }
// fall back to a generic 32 KiB buffer loop
}
This is why *os.File, *bytes.Buffer, and *net.TCPConn can use sendfile(2) or splice(2) under the hood — they implement ReadFrom / WriteTo.
Composed interfaces¶
type ReadWriter interface { Reader; Writer }
type ReadCloser interface { Reader; Closer }
type WriteCloser interface { Writer; Closer }
type ReadWriteCloser interface { Reader; Writer; Closer }
type ReadSeeker interface { Reader; Seeker }
These enable callers to declare a parameter type that requires multiple behaviors without inventing a new interface.
Implementing a Reader¶
A typical "rate-limited" wrapper:
type LimitReader struct {
R io.Reader
N int64 // remaining bytes
}
func (l *LimitReader) Read(p []byte) (int, error) {
if l.N <= 0 { return 0, io.EOF }
if int64(len(p)) > l.N { p = p[0:l.N] }
n, err := l.R.Read(p)
l.N -= int64(n)
return n, err
}
io.LimitReader itself is structured the same way (slightly different field names).
5. sort.Interface¶
Source: https://pkg.go.dev/sort#Interface
Declaration¶
Contract¶
Less(i, j)MUST define a strict weak order: irreflexive, asymmetric, transitive on equality.SwapMUST exchange elements at positionsiandj.LenMUST return a value that does not change betweenLess/Swapcalls during a single sort run.
sort.Sort is not stable; sort.Stable is. Both are O(n log n).
Idiomatic implementation¶
type ByAge []Person
func (a ByAge) Len() int { return len(a) }
func (a ByAge) Less(i, j int) bool { return a[i].Age < a[j].Age }
func (a ByAge) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
sort.Sort(ByAge(people))
Generic alternatives (Go 1.21+)¶
slices.Sort, slices.SortFunc, and slices.SortStableFunc largely replace hand-written sort.Interface for slices of comparable types. The interface is still used for non-slice containers and for sorts driven by external indices.
6. Marshalers: json, encoding.TextMarshaler, encoding.BinaryMarshaler¶
json.Marshaler / json.Unmarshaler¶
Source: https://pkg.go.dev/encoding/json#Marshaler
type Marshaler interface {
MarshalJSON() ([]byte, error)
}
type Unmarshaler interface {
UnmarshalJSON([]byte) error
}
Contract:
MarshalJSONMUST return valid JSON.UnmarshalJSONMUST be able to decode the JSON form produced byMarshalJSON.- The encoder calls
MarshalJSONon a value before applying struct-tag rules, so a custom marshaler bypasses field tags.
Implementing with omit logic¶
type Optional[T any] struct {
Set bool
Value T
}
func (o Optional[T]) MarshalJSON() ([]byte, error) {
if !o.Set { return []byte("null"), nil }
return json.Marshal(o.Value)
}
func (o *Optional[T]) UnmarshalJSON(data []byte) error {
if string(data) == "null" {
o.Set = false
var zero T
o.Value = zero
return nil
}
if err := json.Unmarshal(data, &o.Value); err != nil { return err }
o.Set = true
return nil
}
encoding.TextMarshaler¶
Source: https://pkg.go.dev/encoding#TextMarshaler
type TextMarshaler interface {
MarshalText() (text []byte, err error)
}
type TextUnmarshaler interface {
UnmarshalText(text []byte) error
}
encoding/json falls back to TextMarshaler when there is no MarshalJSON, wrapping the result in a JSON string. encoding/xml, flag.TextVar, and database/sql (sql.Scanner for textual columns) also look for TextMarshaler / TextUnmarshaler.
encoding.BinaryMarshaler¶
type BinaryMarshaler interface {
MarshalBinary() (data []byte, err error)
}
type BinaryUnmarshaler interface {
UnmarshalBinary(data []byte) error
}
Used by encoding/gob and by some encoding/xml paths. Reach for it when you have a tightly packed binary format (e.g. a UUID).
Detection order in encoding/json¶
For a value of type T:
- If
Timplementsjson.Marshaler— call it. - Else if
Timplementsencoding.TextMarshaler— call it, wrap in a JSON string. - Else fall back to reflection-based encoding.
The same precedence applies on decode.
7. net/http Interfaces¶
Source: https://pkg.go.dev/net/http
http.Handler¶
The single entry point for HTTP request handling. *ServeMux, middleware, and custom routers all implement Handler. http.HandlerFunc is an adapter that turns a function into a Handler.
type Healthz struct{ db *sql.DB }
func (h *Healthz) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if err := h.db.PingContext(r.Context()); err != nil {
http.Error(w, err.Error(), http.StatusServiceUnavailable)
return
}
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte("ok"))
}
http.Flusher¶
Source: https://pkg.go.dev/net/http#Flusher. The default ResponseWriter implementation also implements Flusher. Type-assert before using:
func sse(w http.ResponseWriter, r *http.Request) {
flusher, ok := w.(http.Flusher)
if !ok {
http.Error(w, "streaming unsupported", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "text/event-stream")
for i := 0; i < 5; i++ {
fmt.Fprintf(w, "data: tick %d\n\n", i)
flusher.Flush()
time.Sleep(time.Second)
}
}
http.Hijacker¶
Allows a handler to take over the underlying TCP connection (for example, a WebSocket upgrade). After Hijack returns, the HTTP server no longer owns the connection — the handler is responsible for closing it.
http.CloseNotifier (deprecated)¶
Replaced by Request.Context().Done() in Go 1.7+.
8. context.Context¶
Source: https://pkg.go.dev/context#Context
type Context interface {
Deadline() (deadline time.Time, ok bool)
Done() <-chan struct{}
Err() error
Value(key any) any
}
Contract highlights¶
Done()returns a channel that is closed when work on behalf of the context should be canceled.Err()returns nil ifDone()is not yet closed; otherwise it returnscontext.Canceledorcontext.DeadlineExceeded.Value(key)is for request-scoped data only — never for optional function parameters.
Implementing a custom Context¶
Most custom contexts wrap an existing one:
type tenantCtx struct {
context.Context
tenant string
}
type tenantKey struct{}
func WithTenant(parent context.Context, t string) context.Context {
return context.WithValue(parent, tenantKey{}, t)
}
func TenantFrom(ctx context.Context) (string, bool) {
t, ok := ctx.Value(tenantKey{}).(string)
return t, ok
}
Source: https://go.dev/blog/context. Note the tenantKey{} empty struct used to avoid collisions with other packages' keys.
Cancellation propagation¶
func fetch(ctx context.Context, url string) error {
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil { return err }
resp, err := http.DefaultClient.Do(req)
if err != nil { return err } // includes ctx.Err() when canceled
defer resp.Body.Close()
_, err = io.Copy(io.Discard, resp.Body)
return err
}
Calling cancel := context.WithCancel(parent) and then cancel() closes the channel; any goroutine waiting on ctx.Done() unblocks.
9. io/fs.FS, iter.Seq, and database/sql Interfaces¶
io/fs.FS (Go 1.16+)¶
Source: https://pkg.go.dev/io/fs#FS
A read-only filesystem abstraction. embed.FS, os.DirFS, and zip.Reader implement it. Optional richer interfaces include fs.ReadDirFS, fs.StatFS, fs.SubFS, and fs.GlobFS. Functions in io/fs (e.g. fs.WalkDir, fs.ReadFile) prefer the optional interface when present and fall back to the minimal FS.
iter.Seq and iter.Seq2 (Go 1.23+)¶
Source: https://pkg.go.dev/iter
These are not interface types but function types that participate in for ... range (the range-over-func feature). A producer drives yield until yield returns false, indicating the consumer wants to stop.
func Counter(n int) iter.Seq[int] {
return func(yield func(int) bool) {
for i := 0; i < n; i++ {
if !yield(i) { return }
}
}
}
for v := range Counter(5) { fmt.Println(v) } // 0 1 2 3 4
iter.Pull and iter.Pull2 convert a push-style Seq into pull-style iterators with next and stop functions. Many slices and maps helpers added in Go 1.23 return iter.Seq[*].
driver.Valuer and sql.Scanner¶
Source: https://pkg.go.dev/database/sql/driver#Valuer, https://pkg.go.dev/database/sql#Scanner
// in database/sql/driver
type Valuer interface {
Value() (Value, error)
}
// in database/sql
type Scanner interface {
Scan(src any) error
}
Valuer produces a value the driver knows how to send; Scanner consumes a column value into a destination. Together they let user-defined types participate in sql.DB round-trips.
type Tags []string
func (t Tags) Value() (driver.Value, error) {
return strings.Join(t, ","), nil
}
func (t *Tags) Scan(src any) error {
var s string
switch v := src.(type) {
case string: s = v
case []byte: s = string(v)
case nil: *t = nil; return nil
default: return fmt.Errorf("Tags.Scan: unsupported type %T", src)
}
if s == "" { *t = nil; return nil }
*t = strings.Split(s, ",")
return nil
}
driver.Value is a closed type set: int64, float64, bool, []byte, string, time.Time, or nil. Anything outside must be coerced.
10. Contract Rules and Defined Behavior¶
Common implementation rules¶
- Receiver consistency. If even one method on the type has a pointer receiver, all methods that mutate state SHOULD have pointer receivers. For interfaces with optional features (e.g.
io.WriterTo), receiver choice should match the primary interface (io.Reader). - Nil receivers. A method may legitimately accept a nil receiver if the doc comment says so (e.g.
(*Logger).Logmay discard). - Goroutine safety. Unless the doc comment promises it, an interface method is NOT safe for concurrent use.
io.Readerandio.Writerare famously NOT goroutine-safe;context.ContextIS. - Returning n > 0 with err != nil. Allowed for
Reader; required to handle forWriteronly when n < len(p). - Idempotent Close.
io.Closer.Closeshould tolerate being called multiple times; the second call may return an error, but it must not panic.
Optional interface detection¶
Standard library functions probe for richer interfaces:
| Function | Probes |
|---|---|
io.Copy(dst, src) | WriterTo, ReaderFrom |
bufio.NewReader | Reader (no probe; uses as-is) |
http.ServeContent | io.Seeker, io.ReaderAt |
encoding/json.Marshal | Marshaler, TextMarshaler |
fmt.Sprint | Formatter, Stringer, error |
database/sql driver | driver.Valuer, sql.Scanner |
Implementing the optional interface is the canonical way to opt into the fast path.
Type assertion patterns¶
// Comma-ok form — safe.
if rf, ok := dst.(io.ReaderFrom); ok { return rf.ReadFrom(src) }
// Compile-time interface assertion — useful in package init.
var _ http.Handler = (*Healthz)(nil)
The blank-identifier assignment is a common idiom: it forces a compile error if *Healthz ever stops satisfying http.Handler.
11. Version History and Compliance Checklist¶
Version history¶
| Go Version | Change |
|---|---|
| Go 1.0 | error, fmt.Stringer, all io.* interfaces, sort.Interface, http.Handler defined. |
| Go 1.0 | encoding.TextMarshaler / BinaryMarshaler defined. |
| Go 1.7 | context.Context moved into the standard library; Request.Context() added. |
| Go 1.13 | errors.Is, errors.As, errors.Unwrap — formal wrapping protocol. |
| Go 1.16 | io/fs.FS and family added; embed integrates with it. |
| Go 1.18 | Generics; existing interfaces unchanged but new generic helpers (slices, maps) reduce hand-written sort.Interface. |
| Go 1.20 | errors.Join and multi-error Unwrap() []error. |
| Go 1.21 | slices.Sort, cmp.Ordered. |
| Go 1.22 | http.ServeMux patterns; no interface change. |
| Go 1.23 | iter.Seq and iter.Seq2; range-over-func; slices.All, maps.Keys etc. return iter.Seq*. |
Compliance checklist for an interface implementer¶
- Read the full doc comment of the interface, not just the signature.
- Match the contract on error returns, partial reads/writes, and zero results.
- Decide receiver kind once and apply it consistently.
- Add a compile-time
var _ I = (*T)(nil)assertion. - If implementing
ReaderorWriter, also evaluateReadFrom/WriteTofor performance hooks. - Document goroutine safety in your own doc comment.
- When wrapping, expose the inner type via an
Unwrap-style method or via embedding so that callers can downgrade through the wrapper. - For marshalers, ensure round-trip equality on the values you care about and add tests.
Quick spec-anchor index¶
| Interface | Anchor |
|---|---|
error | https://pkg.go.dev/builtin#error |
fmt.Stringer | https://pkg.go.dev/fmt#Stringer |
fmt.Formatter | https://pkg.go.dev/fmt#Formatter |
io.Reader | https://pkg.go.dev/io#Reader |
io.Writer | https://pkg.go.dev/io#Writer |
io.Closer | https://pkg.go.dev/io#Closer |
io.Seeker | https://pkg.go.dev/io#Seeker |
io.ReaderAt | https://pkg.go.dev/io#ReaderAt |
io.WriterTo | https://pkg.go.dev/io#WriterTo |
io.ReaderFrom | https://pkg.go.dev/io#ReaderFrom |
sort.Interface | https://pkg.go.dev/sort#Interface |
json.Marshaler | https://pkg.go.dev/encoding/json#Marshaler |
encoding.TextMarshaler | https://pkg.go.dev/encoding#TextMarshaler |
encoding.BinaryMarshaler | https://pkg.go.dev/encoding#BinaryMarshaler |
http.Handler | https://pkg.go.dev/net/http#Handler |
http.Flusher | https://pkg.go.dev/net/http#Flusher |
http.Hijacker | https://pkg.go.dev/net/http#Hijacker |
context.Context | https://pkg.go.dev/context#Context |
io/fs.FS | https://pkg.go.dev/io/fs#FS |
iter.Seq | https://pkg.go.dev/iter#Seq |
driver.Valuer | https://pkg.go.dev/database/sql/driver#Valuer |
sql.Scanner | https://pkg.go.dev/database/sql#Scanner |
A type that satisfies all the rules above plays well with every standard library facility that consumes that interface — and, by extension, with the broader ecosystem that follows the same conventions.