Functional Options

The functional options pattern is a mechanism for providing flexible configuration to a Go library and in this article I will be exploring how it is implemented in uptrace-go. There are other less flexible techniques for achieving the same goal like config structs and positional arguments which I won’t cover but are good to know.

As you can tell from the title, functions are central to the implementation of this pattern and in uptrace-go it all starts with the option function type. Any function that matches the signature func(config *config) is of type option.

type option func(conf *config)

And because in Go it is possible to add a method to any user defined type, we can add a method to option. This capability makes it possible for functions to implement interfaces. In the snippet below apply is added to option.

func (fn option) apply(conf *config) {
fn(conf)
}

When option is instantiated the receiver fn takes the value of whatever function passed to option and when apply is called, it calls the instatiated function. We’ll see this in action later in the WithDSN function.

The next part is defining an interface which all functional options passed to the constructor must implement. The option type already implements this interface because it has the apply method.

type Option interface {
apply(conf *config)
}

Next we will look at an example of a functional option that updates a field in the config object. The function returns an Option which is any type that implements the apply method.

func WithDSN(dsn string) Option {
return option(func(conf *config) {
conf.dsn = dsn
})
}

The snippet below extracted from WithDSN creates an instance of the option type whose value is the function func(conf *config) { ... }. Which when called updates the dsn property of the conf object.

  option(func(conf *config) {
		conf.dsn = dsn
	})

The conf object is created with a few basic properties.

conf := &config{
tracingEnabled: true,
metricsEnabled: true,
}

And more properties are updated using the option’s apply method.

for _, opt := range opts {
opt.apply(conf)
}

Finally, the functional options are passed to the constructor as shown below.

  uptrace.ConfigureOpenTelemetry(
    ...
    uptrace.WithDSN("http://dsn.com")
    ...
  )