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.

 https://github.com/uptrace/uptrace-go/blob/40328f2437fe21511eb2f163a1ce63c8fe473f99/uptrace/config.go#L96-L96 

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.

 https://github.com/uptrace/uptrace-go/blob/40328f2437fe21511eb2f163a1ce63c8fe473f99/uptrace/config.go#L98-L100 

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.

 https://github.com/uptrace/uptrace-go/blob/40328f2437fe21511eb2f163a1ce63c8fe473f99/uptrace/config.go#L92-L94 

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.

 https://github.com/uptrace/uptrace-go/blob/40328f2437fe21511eb2f163a1ce63c8fe473f99/uptrace/config.go#L106-L110 

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.

 https://github.com/uptrace/uptrace-go/blob/40328f2437fe21511eb2f163a1ce63c8fe473f99/uptrace/config.go#L46-L49 

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

 https://github.com/uptrace/uptrace-go/blob/40328f2437fe21511eb2f163a1ce63c8fe473f99/uptrace/config.go#L55-L57 

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

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