Instantiate -> Initialize -> Open, Over Functional Options: 2025 Update

This is a refresh of blog post I wrote several years ago. I’ve since shared this several times with coworkers over the years and haven’t gotten much feedback until recently; feedback is a gift. I’ve reread Rob Pike and Dave Cheney’s posts and have refined my unpopular opinion.


In this article I will describe the Functional Options constructor pattern, disagree with it, and suggest my preferred methods of construction; Struct Literal and Instantiate, Initialize, Open.


Functional Options

A very popular and widely adopted pattern for constructing an object is the functional options pattern. This pattern in Go is frequently attributed to Rob Pike, Dave Cheney, and has further iterations. It’s important to note that while these patterns noted are similar in form, they’re very different in function.

I will focus this post on addressing the Dave Cheney pattern which I have most often seen implemented, discussed and referenced as the standard. At the end, I will briefly touch on the Rob Pike pattern and where the uber-go suggestion improves upon the Dave Cheney pattern.

The pattern looks like this:

func NewObject(required string, options ...Option) (*Object, error) {
	obj := &Object{
		required: required,
	}

	obj.default = "default"
	// ...and any other defaults needed

	for _, opt := range options {
		if err := opt(obj); err != nil { 
			return nil, err
		}
	}

	return obj, nil
}

type Option func(*Object) error

func WithProperty(p string) Option {
	return func(o *Object) error {
		o.property = p
		return nil
	}
}
//... and many more functions, however many you need.

// ---
// main.go

func main() {
	obj, err := NewObject("required", WithProperty("string"))
	if err != nil {
		log.Panicf(err)
	}
	obj.Open()
}

This pattern claims some advantages over other constructor patterns.

  • API’s that are beautiful and can grow over time
  • default use case to be simplest
  • more readable, meaningful parameters
  • provide direct control over the initialization of complex values

I disagree

I don’t think any of these advantages claimed by functional options are more true over other patterns. I believe the appeal of functional options is vanity. They look good, are fun and exciting to use when you understand their indirection. However, Go is beautiful because it’s boring. The excitement of this pattern holds no benefit over out of the box features in Go. More discussion can be found in this old twitter thread from Jaana Dogan:

Please stop using functional options where a struct is the solution. - Jaana Dogan

Design patterns like functional options for constructors often appear because the language in which they exist is missing a feature to fulfill what the design pattern fulfills. Many suggest the missing feature to be named parameters with defaults. I think this pattern is attempting to fill a perceived missing feature of Go.

See “Are Design Patterns Missing Language Features” and “Design Patterns are Missing Language Features” to help form your own opinion.

In this case, I disagree that Go is missing features to solve this problem.

API’s that are beautiful and can grow over time

Beauty to me is readability, ease of discovery, and a balance of simplicity and complexity.

The functional options pattern is only easily readable to those who know the pattern.

How do you discover available optional functional options? There are a couple of ways; you can type pkg.With... and let your IDE intellisense show potential completions. What if your package has two objects constructed with such a pattern? Which of the pkg.With... functions work for your constructor?

withwhat

A second way is to observe that the constructor takes a variadic, to go to the type, and find it’s references in the package. If the package is a bit disorganized, I wish you luck finding what you need. Furthermore this is a more sparse means of exposing documentation to the consumer about what your API does.

Lastly, in what way does the functional options pattern grow more elegantly than other patterns? Whichever way you look at it you add, modify, or remove another “something” and then implement it, and to do so for functional options pattern you must know the pattern.

default use case is the simplest

I used to agree that

// functional options
o := NewObject("required")
// or
o = NewObject("required", WithFoo("foo"), WithBar("bar"))

appeared simpler than

// instantiate initialize open example 
o := NewObject("required")
// or
o := NewObject("required")
o.Foo = "foo"
o.Bar = "bar"
// or for unexported
// o.SetFoo("foo")
// o.SetBar("bar")

but both of these are able to have default cases and both are simple.

more readable, meaningful parameters

The pattern of constructing an object with functions whose names follow a convention like WithXYZ() is not a more readable nor meaningful.

o := NewObject("required", WithSomeValue())
// vs
o := NewObject("required")
o.SomeValue = val
// or
o.SetSomeValue

What matters is good naming conventions, consistency, and documentation blocks; documentation blocks which are harder to find in the functional options pattern, in my opinion.

provide direct control over the initialization of complex values

Again, a functional options pattern provides no more control over initialization of complex values than say a method to configure an object.

o := NewObject("required", WithComplexConfiguration())
// vs
o := NewObject("required")
o.ComplexConfiguration()
// or even
o := NewObjectWithComplexConfiguration()

I hope I’ve demonstrated that the functional options pattern holds nothing over standard constructors and struct literals.

What I prefer

I prefer two ways of constructing objects. The first is the simplest, most common, and easiest to read. Creating a struct literal.

o := &Object{Property: ""}

I hope this looks familiar. In fact those who work with http web servers in Go use this a lot.

srv := &http.Server{} // put whatever in here
src.ListenAndServe()

As linked to at the start of the article, I have often seen functional options used in place of a simple struct literal when the use case is simple.

When we need a more complex construction with default values, we can use a Constructor function following Instantiate, Initialize and Open.

// instantiate
s := NewServer()

// initialize
s.Addr = ":6060"
s.ClientTimeout = 30 * time.Second
s.MaxConns = 5
s.MaxConcurrent = 10
s.Cert = myCert

// open
if err := s.Open(); err != nil {
	return err
}

If you have more complex configuration, need to set unexported fields, or often need a non default specially configured server, the following works.

// instantiate
s := NewServer()

// initialize
s.SetDefaults()
// or
s.SetSpecial()
s.SetUnexportedField()

// open
if err := s.Open(); err != nil {
	return err
}

But what if we have a lot of required values for construction? Doesn’t it become complex and annoying to maintain?

s := NewServer(
	"name",
	":1234",
	5, 
	...,
)
s.OptionalField = "tls"
// vs
s := NewServer(
	WithName("name"),
	WithPort(":1234"),
	WithMaxRetry(5"),
	WithTLS(),
	...,
)

No. GoDoc, autocompletion, intellisense, and Go IDE plugins all indicate what the values are in the standard constructor; which is also the reason I prefer to not use option/configuration structs. Whereas GoDoc, Autocompletion, intellisense and plugins do not play nicely with functional options because they’re not language features.

Considerations

I compared against the Dave Cheney pattern. This was perhaps unfair because this is the least useful example of the three patterns. If we read Rob Pike’s original blog post, we can see reason for why this pattern made sense for his intricate object. The package is intricate and there will probably end up being dozens of options. - Rob Pike. I suspect Rob is not implementing optional parameters by default, ie if a struct or constructor will work.

The uber-go suggestions for functional options improve upon some of the weaknesses of the Dave Cheney pattern such as in usability and testability. However, it’s again a bit more complex than the Dave Cheney example. I want the contextual overhead of having to know a pattern to implement an API to provide more benefits than this pattern provides; however if it’s Dave Cheney vs uber-go pattern, I would pick uber-go.


Summary

We should default to struct literals or constructors, one of which is a feature of the language and the other a engineering standard pattern, for creating objects. In the cases our API evolves such that this pattern is required, we should of course implement it.

I have implemented all of the above many times. I regret the number of times I implemented the functional options constructor pattern. The amount of explanation that it took to demonstrate and disagree with the pattern is a testament to its complexity. Counterpoint to its complexity are the two self evident simple examples provided.


links

https://commandcenter.blogspot.com/2014/01/self-referential-functions-and-design.html

https://dave.cheney.net/2014/10/17/functional-options-for-friendly-apis

https://sagikazarmark.hu/blog/functional-options-on-steroids/

https://github.com/uber-go/guide/blob/master/style.md#functional-options

https://wiki.c2.com/?AreDesignPatternsMissingLanguageFeatures

https://wiki.c2.com/?DesignPatternsAreMissingLanguageFeatures

https://twitter.com/rakyll/status/1000128803153170432