Dwarves
Memo
Type ESC to close search bar

Go extension interface pattern

The extension interface pattern is when an interface embeds another one. The extension pattern helps to add new features to an existing object without changing its original code.

Whether you are working with the standard library (io, http, sql), third-party packages, or your own codebase, this pattern provides a way to add functionality in a flexible, non-intrusive manner.

1. Extending io.Reader and io.Writer

The io.Reader and io.Writer interfaces are simple but versatile interfaces that are widely used in Go. You can extend them to add features like compression, encryption, logging, or even buffering.

Example: adding logging to an io.Writer

Let’s say you want to add logging functionality to an io.Writer. You can use the extension interface pattern to wrap an existing io.Writer and log any data written to it.

type LoggingWriter struct {
    io.Writer  // Embed the original io.Writer
}

func (lw LoggingWriter) Write(p []byte) (n int, err error) {
    fmt.Printf("Writing %d bytes: %s\n", len(p), string(p))  // Log the write
    return lw.Writer.Write(p)  // Call the original Write method
}

Usage:

func main() {
    var writer io.Writer = LoggingWriter{Writer: os.Stdout}
    
    writer.Write([]byte("Hello, World!"))  
    // Output:
    // Writing 13 bytes: Hello, World!
    // Hello, World!
}

This allows you to add logging to any writer without modifying the original io.Writer type.

2. Extending HTTP middleware in http.Handler

In web development with Go, the http.Handler interface is central to building web servers. It’s common to use the extension interface pattern to create middleware that extends the behavior of http.Handler.

Example: Adding a request logger middleware

You can create middleware that wraps an http.Handler to log HTTP requests.

type LoggingMiddleware struct {
    handler http.Handler
}

func (lm LoggingMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    fmt.Printf("Received request: %s %s\n", r.Method, r.URL.Path)  // Log request
    lm.handler.ServeHTTP(w, r)  // Call the original handler
}

Usage:

func main() {
    originalHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        w.Write([]byte("Hello, World!"))
    })

    loggingHandler := LoggingMiddleware{handler: originalHandler}
    http.ListenAndServe(":8080", loggingHandler)
}

This example extends http.Handler to log incoming requests, wrapping the original handler without modifying it.

3. Extending sql.DB for database connections

You can extend the sql.DB type from Go’s database/sql package to add functionalities like logging, connection retries, or metrics tracking.

Example: Adding query logging to sql.DB

type LoggingDB struct {
    *sql.DB  // Embed the original sql.DB
}

func (ldb LoggingDB) Query(query string, args ...interface{}) (*sql.Rows, error) {
    fmt.Printf("Executing query: %s\n", query)  // Log the query
    return ldb.DB.Query(query, args...)
}

Usage:

func main() {
    db, _ := sql.Open("mysql", "user:password@tcp(127.0.0.1:3306)/dbname")
    
    loggingDB := LoggingDB{DB: db}
    loggingDB.Query("SELECT * FROM users")
}

This extension allows you to log SQL queries without altering the behavior of sql.DB.

4. Adding caching to HTTP clients

Go’s http.Client is a widely used type for making HTTP requests. You can extend http.Client to add caching, retries, or additional logging.

Example: adding caching to an http.Client

You can wrap an http.Client to cache responses based on URLs.

type CachingClient struct {
    client   *http.Client
    cache    map[string]*http.Response
}

func (cc *CachingClient) Do(req *http.Request) (*http.Response, error) {
    if cachedResp, ok := cc.cache[req.URL.String()]; ok {
        fmt.Println("Returning cached response")
        return cachedResp, nil
    }
    
    resp, err := cc.client.Do(req)
    if err == nil {
        cc.cache[req.URL.String()] = resp
    }
    return resp, err
}

Usage:

func main() {
    httpClient := &http.Client{}
    cachingClient := &CachingClient{
        client: httpClient,
        cache:  make(map[string]*http.Response),
    }

    req, _ := http.NewRequest("GET", "http://example.com", nil)
    cachingClient.Do(req)  // Fetches from the internet and caches the result
    cachingClient.Do(req)  // Uses the cached result
}

This allows you to extend the functionality of the http.Client without altering the original type, adding caching behavior.

5. Adding context or timeouts to http.Request

Go’s http.Request does not have built-in timeout functionality, but you can extend the http.Request type to add it.

Example: timeout extension for http.Request

type TimeoutRequest struct {
    req *http.Request
    timeout time.Duration
}

func (tr *TimeoutRequest) Do(client *http.Client) (*http.Response, error) {
    ctx, cancel := context.WithTimeout(tr.req.Context(), tr.timeout)
    defer cancel()

    reqWithTimeout := tr.req.WithContext(ctx)
    return client.Do(reqWithTimeout)
}

Usage:

func main() {
    req, _ := http.NewRequest("GET", "http://example.com", nil)
    timeoutReq := &TimeoutRequest{
        req:     req,
        timeout: 2 * time.Second,
    }

    client := &http.Client{}
    timeoutReq.Do(client)  // The request will timeout after 2 seconds
}

This allows you to extend http.Request with timeout functionality without modifying the original type.

6. Decorators for fmt.Stringer

Go’s fmt.Stringer is a simple but powerful interface used for customizing string representations of types. You can use the extension interface pattern to add additional behaviors when printing.

Example: Add a prefix to fmt.Stringer

You can wrap a fmt.Stringer type to add a prefix to its string representation.

type PrefixedStringer struct {
    prefix string
    fmt.Stringer
}

func (ps PrefixedStringer) String() string {
    return ps.prefix + ps.Stringer.String()
}

Usage:

type User struct {
    Name string
}

func (u User) String() string {
    return u.Name
}

func main() {
    user := User{Name: "John"}
    prefixedUser := PrefixedStringer{prefix: "User: ", Stringer: user}

    fmt.Println(prefixedUser.String())  // Output: User: John
}

This wraps the original fmt.Stringer and adds a prefix to the output.


That’s good as far as it goes, but I think the key part of extension interfaces is why they’re useful and how they’re used – they can add optional functionality to an API which (according to the statically-checked type signature) only takes the “base” interface.

For example, io.WriteString is one of the simplest examples of an extension interface: it takes a plain io.Writer, but if that writer has been extended to support the io.StringWriter interface (i.e., has the WriteString method), it will use that for efficiency, otherwise fall back to the regular Write method which all io.Writer implementations have.

Sticking with your File / ReadDirFile example, the fs.Open method returns a plain File, but if the “file” is actually a directory, you can convert it to a ReadDirFile and use the ReadDir extension method. Something like:

f, _ := fs.Open("dir_or_file") // in real life, handle errors
st, _ := f.Stat()
if st.IsDir() {
    // f is a directory
    d := f.(ReadDirFile)
    d.ReadDir(10)
} else {
    // f is a normal file
}

As an alternative to calling st.IsDir() (and what would probably be more typical for extension interfaces), you could just check whether the file implements the interface directly, like so:

f, _ := fs.Open("dir_or_file")
if d, ok := f.(ReadDirFile); ok {
    // f is a directory
    d.ReadDir(10)
} else {
    // f is a normal file
}

Source