rss twitter gitlab github linkedin linkedin instagram
Learning Go: Functional Options / Default Configuration Values Pattern
Nov 05, 2021

Disclaimer: This post includes Amazon affiliate links. If you click on one of them and you make a purchase I’ll earn a commission. Please notice your final price is not affected at all by using those links.

Because the way Go was designed there’s no way to define default values, other than the zero values, to our types; so typically we use those zero values as defaults and try to implement our types in a way those make sense, but in some cases this could lead to misuse or unexpected results.

In this post I share with you a well known pattern called Functional Options and a concrete use case when working with types that require non zero values as default configuration.



The code used for this post is available on Github.

What is the Functional Options Pattern?

This is a pattern discussed by Rob Pike in 2014, it consists of implementing a variadic function that receives function types built in such a way that it allows us to add new functions implementing the type without breaking the API.

A Variadic Function is a function that could receive N arguments of the same type, something like:

func Print(vals ...string)
	for _, val := range vals {
		fmt.Println(val)
	}
}

In can be used like:

Print("hello", "world")
Print("one", "two", "three")

And a Function Type is a function defined as a type, for example a well known type in the standard library is net/http.HandlerFunc:

type HandlerFunc func(ResponseWriter, *Request)

All of this is better explained with the following piece of code:

 1type Option func(*http.Server) error
 2
 3func WithAddr(addr string) func(*http.Server) {
 4	return func(s *http.Server) {
 5		s.Addr = addr
 6	}
 7}
 8
 9func WithReadTimeout(t time.Duration) func(*http.Server) {
10	return func(s *http.Server) {
11		s.ReadTimeout = t
12	}
13}
14
15func NewServer(opts ...Option) http.Server {
16	var s http.Server
17
18	for _, opt := range opts {
19		opt(&s)
20	}
21
22	return s
23}

Which can be used like:

s := NewServer()
s := NewServer(WithReadTimeout(1*time.Second))
s := NewServer(WithAddr(":80"))
s := NewServer(WithAddr(":8080"), WithReadTimeout(100*time.Millisecond))

To instantiate http.Server in such a way we are able to add Options for configuration purposes, sure this code may seem over-engineered because literally we could do something like this instead:

func NewServer(addr string, t time.Duration) {
	return http.Server{
		Addr:        addr,
		ReadTimeOut: t,
	}
}

Or even simpler, we could get rid of the function:

s := http.Server{
	Addr:        addr,
	ReadTimeOut: t,
}

But the use case of this pattern works better when defining Default Configuration Values.

Implementing Default Configuration Values

If we modify the Initializer we could add default values like:

 1const (
 2	ServerDefaultReadTimeout  time.Duration = 100 * time.Millisecond
 3	ServerDefaultWriteTimeout time.Duration = 100 * time.Millisecond
 4	ServerDefaultAddress      string        = ":8080"
 5)
 6
 7func NewServer(opts ...Option) http.Server {
 8	s := http.Server{
 9		Addr:         ServerDefaultAddress,
10		ReadTimeout:  ServerDefaultReadTimeout,
11		WriteTimeout: ServerDefaultWriteTimeout,
12	}
13
14	for _, opt := range opts {
15		opt(&s)
16	}
17
18	return s
19}

And if we got a bit further we can add validation as well:

 1type Option func(*http.Server) error
 2
 3func WithAddr(addr string) func(*http.Server) error {
 4	return func(s *http.Server) error {
 5		s.Addr = addr
 6		return nil
 7	}
 8}
 9
10func WithReadTimeout(t time.Duration) func(*http.Server) error {
11	return func(s *http.Server) error {
12		if t > time.Second {
13			return errors.New("timeout value not allowed")
14		}
15
16		s.ReadTimeout = t
17		return nil
18	}
19}
20
21func WithWriteTimeout(t time.Duration) func(*http.Server) error {
22	return func(s *http.Server) error {
23		if t > time.Second {
24			return errors.New("timeout value not allowed")
25		}
26
27		s.WriteTimeout = t
28
29		return nil
30	}
31}
32
33func NewServer(opts ...Option) (http.Server, error) {
34	s := http.Server{
35		Addr:         ServerDefaultAddress,
36		ReadTimeout:  ServerDefaultReadTimeout,
37		WriteTimeout: ServerDefaultWriteTimeout,
38	}
39
40	for _, opt := range opts {
41		if err := opt(&s); err != nil {
42			return http.Server{}, fmt.Errorf("option failed %w", err)
43		}
44	}
45
46	return s, nil
47}

Which allows us to define a new API that supports default options, validations and an API that is backwards compatible and allows us to extend if needed.

If you’re looking to sink your teeth into more Go-related topics I recommend the following:


Back to posts