Go

How does the "optional interface" (interface upgrade) pattern work? Give a stdlib example and its pitfall.

Question 103MediumGo 1.22 to 1.25

A function accepts a small interface, then uses a type assertion to check at runtime whether the value also implements a richer interface. If it does, the function takes a faster or more capable path, and the public API stays minimal. io.Copy is the classic example:

// simplified io.Copy
func Copy(dst Writer, src Reader) (int64, error) {
    if wt, ok := src.(WriterTo); ok {
        return wt.WriteTo(dst)       // e.g. *os.File -> sendfile/splice
    }
    if rf, ok := dst.(ReaderFrom); ok {
        return rf.ReadFrom(src)      // e.g. *net.TCPConn
    }
    return copyBuffer(dst, src, nil) // generic 32 KiB buffer loop
}

// http: optional capabilities of a ResponseWriter
if f, ok := w.(http.Flusher); ok {
    f.Flush()
}

Other examples:

  • io.StringWriter, used by io.WriteString.
  • fmt.Formatter, fmt.Stringer and error, checked by fmt.
  • json.Marshaler and encoding.TextMarshaler.
  • http.Hijacker and http.Pusher.

The pitfall is wrapper types. Middleware that embeds http.ResponseWriter, or a bufio.Writer wrapped around a file, hides the optional methods of the inner value, so the fast path or the feature silently disappears. Server-Sent Events that never flush are a typical symptom. Remedies:

  • Implement the optional methods on the wrapper and forward them.
  • Expose Unwrap() for http.ResponseController (Go 1.20).
  • Use libraries that generate every combination of optional interfaces (httpsnoop).

More on Interfaces, Methods & Embedding

All 35 Interfaces, Methods & Embedding questions