This page looks best with JavaScript enabled

Optional Parameters using Functional Options

 ·   ·  ☕ 5 min read

Introduction

In Python you can assign default value to the functions, like so:

1
2
def my_func(posarg, named_arg=True, another_named_arg="Okay")
    # logic goes here...

But in Go you can’t do this:

1
2
3
func myFunc(posarg, named_arg bool = true, another_named_arg string = "Okay") {
    // logic goes here...
}

This will throw a syntax error. Try that?

In this post, we’ll see how can we overcome this issue the go way, with something called Function Options. Not sure if go is the only language that uses this syntax, if there is another language you are using, please let me know.

Also the end result will not look similar to the example above. But you will get the point and know how to proceed further.

Let us go step by step.

Functions are First-Class Citizens

In a language, functions are called the first-class citizen when they are treated like any other object and can be passed around like any other value. For example, a function can be passed as an argument to a function, can be returned by other functions and can be assigned as a value to a variable.

Python and JavaScript both are first-class function languages, so is Go.

This is more like an axiom on which we’ll build our proof on 😉. We’ll bulid more upon this in next section.

Variadic Function

We can’t have default parameters in our functions, but the language allows us to pass an unlimited number of parameters.

If you follow JavaScript, you must have seen the spread operator.

1
2
3
const sum = (...nums) => nums.reduce((a, b) => a + b);

console.log(sum(...[1, 2, 3, 4]))  // 10

The same syntax is available in under the name variadic function. We can do similar thing in Go.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
func sum(nums ...int) int {
    res := 0
    for _, n := range nums {
        res += n
    }
    return res
}

func main()
    fmt.Println(Sum(1, 2, 3, 4))  // 6
}

Functional Options

We can combine these both behavior of variadic function and first-class functions to pass an unlimined number of parameters to our main function.

For a simple example, let’s create a new function which creates a cup of coffee. The idea here is to define the constructor function which will make the base coffee, make it accept function as argument (because function are first-class citizen in Go) which are variadic (meaning it accept any number of argument).

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
type Coffee struct {
    CoffeeBeans bool
    Sugar       bool
    SteamedMilk bool
}

// New returns new instance of coffee
func New(opts ...func(Coffee) Coffee) {
    coffee := Coffee{CoffeeBeans: true}
    
    for _, opt := range opts {
        coffee = opt(coffee)
    }

    return coffee
}

The idea here is to keep signature of the all function which are going to be argument to be same as defined in the main function.

16
17
18
19
20
21
22
23
24
25
26
// WithSugar adds sugar to the coffee.
func WithSugar(coffee Coffee) Coffee {
    coffee.Sugar = true
    return coffee
}

// WithSteamedMilk adds steamed milk to the coffee.
func WithSteamedMilk(coffee Coffee) Coffee {
    coffee.SteamedMilk = true
    return coffee
}

In the above over-simplified example, the signature is func(Coffee) Coffee which means the all the functional option should take Coffee as a param and also should return one for option to work.

You can also modify the functional argument to accept arguments in its own. The key is to keep the signature same.

1
2
3
4
5
6
7
func WithSugar(cubes int) func(coffee Coffee) Coffee {
    return func(coffee Coffee) Coffee {
        sugar := "sugar" + strconv.Itoa(cubes)
        coffee = append(coffee, sugar)
        return coffee
    }
}

The complete picture

Let’s finish our main package and test. I have added a Prepare method on Coffee to demonstrate how composition works in Golang. Here is the complete code:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
package main

import (
    "fmt"
    "reflect"
)

type Coffee struct {
    CoffeeBeans bool
    Sugar       bool
    SteamedMilk bool
}

func New(opts ...func(Coffee) Coffee) Coffee {
    coffee := Coffee{CoffeeBeans: true}

    for _, opt := range opts {
        coffee = opt(coffee)
    }

    return coffee
}

// WithSugar adds sugar to the coffee.
func WithSugar(coffee Coffee) Coffee {
    coffee.Sugar = true
    return coffee
}

// WithSteamedMilk adds steamed milk to the coffee.
func WithSteamedMilk(coffee Coffee) Coffee {
    coffee.SteamedMilk = true
    return coffee
}

// Prepare makes a cup of coffee with given items.
// Ignore the implementation for now.
func (c Coffee) Prepare() string {
    v := reflect.ValueOf(c)
    typeOfC := v.Type()

    var items []string

    for i := 0; i < v.NumField(); i++ {
        if v.Field(i).Interface() == true {
            items = append(items, typeOfC.Field(i).Name)
        }
    }
    return fmt.Sprintf("Preparing coffee with %q:", items)
}

func main() {
    s := New(WithSugar)
    fmt.Println(s.Prepare())
}

In a later post, I’ve demonstrated an alternative approach to functional options using Options struct. Both patterns are seen all over the Go ecosystem.

Related readings:

Share on

santosh
WRITTEN BY
santosh
Pipeline & Backend Developer