Go: Reduce function parameters

Dylan Meeus
5 min readJan 18, 2020

--

Typically you don’t want functions that take a lot of parameters, and though there’s no magic number for how many is “too many”as it depends a bit on what the function is doing. But when you have a function that takes many parameters, there’s a good chance that the function is not exactly “Single Responsibility” and is doing too much.

Such functions are typically harder to refactor, harder to test and frankly just annoying to use. Fair enough, most IDEs nowadays are at least smart enough to tell you which parameter goes where, assuming the parameters have been given decent names at least.

In the best-case scenario, you end up with this:

sensible function parameter names

The worst-case scenario however, leaves you with this beauty:

a nightmare

Let’s assume you don’t need further convincing on why you don’t want a function taking too many parameters, and let’s look at how we can solve this. Often functions that take a lot of parameters are constructor functions, so we’ll take a look at how to do a constructor without passing a lot of variables directly.

Function Currying to the rescue

Here we can actually leverage some concepts of functional programming, namely currying and partial application.(If you need a refresher on currying, I’ve written about Function Currying in Go before)

(As usual, all the code can be found on github)

Our objective is to make a Server constructor where we can pass the variables in any order, and in addition provide default values for those that we don’t pass.

First, let’s define some options that we can configure. For the sake of argument and to keep it clear, I’m just using three variables for now. Not that many, but it’s to show the concept.

type options struct {
maxCon int
transportType transport
timeout int
}

Those are the options that our server can handle. Our Server is a simple struct that uses these options:

type Server struct {
opts options
}

To set the options to the values we want we will use a constructor function taking a variadic parameter of “Option Functions”. The function definition and variadic constructor would look like this:

type ServerOption func(o options) options
func NewServer(os ...ServerOption) Server{
// TODO: Keep reading :D
}

This ServerOption type defines how our functions need to look like to be accepted by the constructor. But the functions that will actually modify our options struct will have a different signature — as we have different types of parameters.

The currying part is that these ‘outer functions’ will return the inner ‘ServerOption’ function that does satisfy the signature. Sounds a bit complicated? That’s me doing a bad job at explaining it, but let’s hope the code makes it a bit clearer. 😅

For instance, our function to modify the maxCon parameter:

func MaxCon(n int) ServerOption {
return func(o options) options {
o.maxCon = n
return o
}
}

As you can tell, the outer signature is MaxCon(n int) ServerOption which does not satisfy our constructor parameters. But MaxCon returns a functions which does satisfy that signature. When MaxCon is evaluates, it has returned a function but not yet evaluated the inner function. That will happen later.

Our other two functions woud look something like this:

func Timeout(n int) ServerOption {
return func(o options) options {
o.timeout = n
return o
}
}

func Transport(t transport) ServerOption {
return func(o options) options {
o.transportType = t
return o
}
}

Now to instantiate our server, we can iterate over all of the parameters in the constructor and modify an options struct.

func NewServer(os ...ServerOption) Server {
opts := options{}
for _, o := range os {
opts = o(opts)
}
return Server{
opts: opts,
}
}

We can call our server constructor now:

s := NewServer(MaxCon(8), Timeout(-1), Transport(TCP))
s.PrintOptions()
// this prints:maxCon: 8
transport type: TCP
timeout: -1

But what happens if we miss a variable in the constructor? We’ll end up with the default-zero values for our types:

s := NewServer(Transport(TCP))
s.PrintOptions()
// this prints:maxCon: 0
transport type: TCP
timeout: 0

Setting a default configuration

We can easily create an instance of our options that contains the default values.

var defaultOptions = options{
maxCon: 4,
transportType: UDP,
timeout: 3000,
}

And in our constructor we can instantiate the defaultOptions before we start the iteration instead of starting from a clean options struct.

func NewServer(os ...ServerOption) Server {
opts := defaultOptions
for _, o := range os {
opts = o(opts)
}
return Server{
opts: opts,
}
}

Now if we call the constructor and only provide an option for our TransportType, we get the default option values for Timeout and maxCon:

s2 := NewServer(Transport(TCP))
s2.PrintOptions()
maxCon: 4
transport type: TCP
timeout: 3000

Thus we have leveraged some Functional Programming concepts in order to chain a bunch of small functions together. Where each function does one thing, and takes one parameter, in order to call a function that actually requires many (3 in this case) parameters.

Alternative Currying Implementation

Our function definition assumed that our curried functions would be pure, and returned a copy of the options with one variable changed.

type ServerOption func(o options) options

This is why in our constructor loop we constantly re-assign our ServerOptions variable:

for _, o := range os {
opts = o(opts)
}

An alternative would be to use pointers and just modify the incoming struct. Personally, I’m less a fan of this approach as I prefer my objects to be immutable, and my functions side-effect free. But, this might be more in-line with how you’d typically see this in Go:

type ServerOption func(o *options) func MaxCon(n int) ServerOption {
return func(o *options) {
o.maxCon = n
}
}
// other functionsfunc NewServer(os ...ServerOption) Server {
opts := &defaultOptions
for _, o := range os {
o(opts)
}
return Server{
opts: *opts,
}
}

If you liked this post and 💙 Go as well, consider:

  • Following me here, on Medium
  • Or twitter Twitter
  • Or check out workwithgo.com to find cool Go jobs around the world.

--

--