SyntaxStudy
Sign Up
Go Interface Composition and nil Interfaces
Go Beginner 1 min read

Interface Composition and nil Interfaces

Interfaces in Go can be composed from smaller interfaces using embedding. The `io.ReadWriter` interface in the standard library is defined simply as embedding `io.Reader` and `io.Writer`. This compositional approach encourages small, focused interfaces that are easier to implement and test, following the Interface Segregation Principle. A common source of bugs in Go is the nil interface pitfall. An interface value is nil only when both its type and value components are nil. If you assign a typed nil pointer to an interface variable, the interface is not nil — it holds a non-nil type component. This can cause unexpected behaviour when comparing interface values to nil. To avoid the nil interface pitfall, return a bare `nil` of the interface type rather than a typed nil. When a function returns an error, the return statement should be `return nil` rather than returning a typed nil pointer variable. Understanding this distinction is important for writing correct Go code, especially in error-handling paths.
Example
package main

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

// Composed interface
type ReadWriteCloser interface {
    io.Reader
    io.Writer
    io.Closer
}

// nopCloser wraps a ReadWriter and adds a no-op Close method
type nopCloser struct{ io.ReadWriter }

func (nopCloser) Close() error { return nil }

func NewReadWriteCloser(rw io.ReadWriter) ReadWriteCloser {
    return nopCloser{rw}
}

// Nil interface pitfall demonstration
type MyError struct{ msg string }

func (e *MyError) Error() string { return e.msg }

func riskyFunc(fail bool) error {
    var err *MyError // typed nil pointer
    if fail {
        err = &MyError{"something went wrong"}
    }
    // BUG: returning a typed nil pointer as error is NOT nil!
    // return err  ← do NOT do this
    if err != nil {
        return err
    }
    return nil // correct: return untyped nil
}

func main() {
    // Interface composition
    buf := strings.NewReader("data to process")
    var sb strings.Builder
    rw  := struct {
        io.Reader
        io.Writer
    }{buf, &sb}
    rwc := NewReadWriteCloser(rw)
    io.Copy(rwc, rwc.Reader.(io.Reader))
    defer rwc.Close()
    fmt.Println("buffer:", sb.String())

    // Nil interface
    err := riskyFunc(false)
    fmt.Println("err is nil:", err == nil) // true

    err = riskyFunc(true)
    fmt.Println("err is nil:", err == nil) // false
    fmt.Println("error:", err)
}