In Go, variadic options provide a flexible way to structure your API, similar to the functional options pattern, allowing your API to evolve over time. I assume you're already familiar with functional options—if not, I highly recommend reading Dave Cheney’s blog on the topic.
A nil interface variadic options API functions similarly to the functional options pattern, except that instead of accepting functions, it takes values of type (any
or interface{}
). This approach is particularly useful in tests, as it enables reusable, configurable code with less boilerplate than the functional options pattern. However, handling a variadic any
parameter often requires additional code to process different option types.
Let’s see this in action. The following example defines runServer
, a test function that initializes a *TestServer
and invokes the provided test function (fn
). The function also accepts variadic any
parameters to configure the *TestServer
based on the given options.
import (
"net/http"
"net/http/httptest"
"time"
)
type Addr string // Address to listen on. Default is ":0".
type TestServer struct {
*httptest.Server
// Wait after *httptest.Server starts
// and before func(*TestServer) runs.
delay time.Duration
// Default handler to use
// in httptest.NewServer.
h http.Handler
addr Addr
}
// runServer configures and starts a new *TestServer,
// then executes the provided test function.
func runServer(fn func(*TestServer), opts ...any) {
ts := &TestServer{}
var h http.Handler
for _, opt := range opts {
switch v := opt.(type) {
case time.Duration:
ts.delay = v
case Addr:
if v == "" {
v = ":0"
}
ts.addr = v
case http.Handler:
h = v
case http.HandlerFunc:
h = v
}
}
ts.Server = httptest.NewServer(h)
defer ts.Close()
time.AfterFunc(ts.delay, func() {
fn(ts)
})
}
Looking at the for _, opt := range opts
loop and switch v := opt.(type)
statement in runServer
, you’ll notice that handling options requires additional code compared to the functional options pattern. However, the key difference lies in how options are passed when calling runServer. Let's write a example test using runServer and configure it with a few options.
func TestHTTP_StatusOk(t *testing.T) {
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte("Ok"))
})
runServer(func(ts *TestServer) {
resp, err := http.Get(ts.URL)
if err != nil {
t.Fatalf("failed to send request: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
t.Errorf("want status 200, got %d", resp.StatusCode)
}
}, time.Millisecond*50, Addr(":8080"), handler)
}
Notice how runServer is called with options as values:
time.Millisecond*50, Addr(":8080"), handler
If we were using a functional options API, the call would look like this:
WithDelay(time.Millisecond*50), WithAddr(":8081"), WithHandler(handler)
While the difference in usage is minimal, the key difference is in how the API itself is structured. Defining the functional options API requires writing additional functions, as shown below:
// Option defines a function that configures TestServer
type Option func(*TestServer)
// WithDelay sets a delay before running the test function
func WithDelay(d time.Duration) Option {
return func(ts *TestServer) {
ts.delay = d
}
}
// WithAddr sets the address for the server
func WithAddr(addr Addr) Option {
return func(ts *TestServer) {
if addr == "" {
addr = ":0"
}
ts.addr = addr
}
}
// WithHandler sets the HTTP handler
func WithHandler(h http.Handler) Option {
return func(ts *TestServer) {
ts.h = h
}
}
# Key Takeaways
The nil interface options API is not a replacement for functional options but rather an alternative pattern that can be useful in tests where common test utilities require configurable options.
# Summary:
- Nil interface options can be useful when designing test utilities.
- More flexible but prone to incorrect argument order due to lack of structure.
- No compile-time type enforcement, unlike the functional options pattern.
Both patterns have their place—choosing between them depends on the level of flexibility and type safety you need.