Understanding closures in Go through the functional options pattern
I came across this article lately explaining 3 different and common ways to configure a server, with functional options pattern being the main idea or talking point (you can skim through the article and come back). So I thought I'd build on it and explore one of the concepts commonly used to implement that pattern: closures.
In this post, we'll use the Functional Options Pattern to understand what closures are, how they work, and why they make this pattern possible.
Let's start with the name
Let's be honest, the name sounds like one of those Go concepts you only get to understand after becoming a senior engineer, right?
But... it turns out that it's quite simpler than you thought. So, let's break it down.
Functional. Easy peasy right?! Functional simply means...functions are involved. So like in the second example from the article, rather than pass configuration values to your server, we pass functions that do the configurations.
Options. Also simple. Options mean...options. You'll want to be able to pass different things into what you're creating; like host, port, maximum connections etc. Options means you're not forced to pass in some exact set of things, you can pass in anything you want to pass in. So nothing really mysterious here.
And finally...pattern. Well, a pattern is just a reusable way of solving problems.
Bringing it all together, it's safe to define functional options pattern as a reusable patters where functions are used as optional configurable options. In other words, functions are the options you pass in.
But there's a piece of 'magic' hiding inside those functions.
Closures
Let's take a look at the Functional Options Pattern from the article. This is one of the configuration options.
func WithHost(host string) func(*Server) {
return func(s *Server) {
s.host = host
}
}
At first glance, this looks pretty straightforward.
WithHost receives a host, and instead of using it immediately, it returns a function.
Notice something interesting here: What happens to host after WithHost finishes executing?
You might expect it to disappear since it was a parameter belonging to WithHost. But, when the returned function is called later, it can still access host (the returned functions from WithPort, WithTimeout, WithMaxConn behave the same way too).
How are they able to do this?! Because they are Closures.
A closure is simply the combination of a function with references from the its surrounding scope where it was created.
In our example, we say the returned function closes over the host variable.
So when we do:
// option now holds the returned function
option := WithHost("localhost")
// The returned function still has access to `host`,
// and `host` is "localhost".
option := func(s *Server) {
s.host = host
}
option(server)
So, option is not just holding a 'regular' function. The function it holds still has access to and makes reference to the variables from the scope where it was created, even though that enclosing function had already finished executing.
And that's the closure.
Back to the pattern
Now that you understand closures, the Functional Options Pattern should look a lot less mysterious.
Do you remember the original example from the post?
server.New(
server.WithHost("localhost"),
server.WithPort(8080),
server.WithTimeout(time.Minute),
server.WithMaxConn(120),
)
To be more verbose, here's what's happening:
server.New(
func(s *Server) {
s.host = host // "localhost"
},
func(s *Server) {
s.port = port // "8080"
},
func(s *Server) {
s.timeout = timeout // "time.Minute"
},
func(s *Server) {
s.maxConn = maxConn // "120"
},
)
Each With... function creates a closure that remembers the value it was given. New(options ...func(s *Server)) then receives those functions and invokes them to configure the Server.
Now that you know what's happening inside those With... functions, you might find that the Functional Options Pattern makes a lot more sense.
So if you skimmed through the reference article at the beginning, this might be a good time to go back and read it again.
See you in the next one!