This page looks best with JavaScript enabled

Optional Parameters Using Option Struct

 ·  ☕ 5 min read

Introduction

In last post we learned how we can use functional options to pass parameter to our functions. In this post we’ll see an alternative way to do the same thing.

I’ll take the same code from the last post to keep things simple.

Variadic Functions for Options

 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
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())
}

Here on line 1-5 we define type Coffee (or class if you would like to say). This is the analogy our code is build upon because I like coffee whenever I can.

On line 7-15 we define our New function. func(Coffee) Coffee says New takes a function with Coffee object and spits out a Coffee Object. The ... part tells it can take many of these functions. Yes, there’s no limit. Then New function itself returns a Coffee object.

In the body of the function, we create a default instance of Coffee. Then run all the functions one by one. As all the function return same modified Coffee object, we can modify it an many time the number of function are passed. At last return that object.

Now for every option we want to create, we can create a function with the same signature we saw above, i.e., func(Coffee) Coffee. I have created two such functions between. There can be any logic you want. just make sure to return the modified coffee object.

On line 46, we define a New coffee object with WithSugar parameter. The logic of the function will modify Coffee to turn Sugar field to be true.

On line 47, Prepare() prints out whatever fields are set to be true.

Now let’s do the same thing with Options struct in next section.

Options Struct for Options

 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
type Coffee struct {
    CoffeeBeans bool
    Sugar       bool
    SteamedMilk bool
}

type Options struct {
    WithCoffeeBeans bool
    WithSugar       bool
    WithSteamedMilk bool
}

func New(opts Options) Coffee {
    c := Coffee{
        // well make coffeebeans necessary
        CoffeeBeans: true,
        Sugar:       opts.WithSugar,
        SteamedMilk: opts.WithSteamedMilk,
    }

    return c
}

// 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() {
    options := Options{WithSugar: true}

    s := New(options)
    fmt.Println(s.Prepare())
}

Changes

1. Variadic functions are gone

That seems like a save of real estate. Because every function takes a fare amount of space. Also there are less moving parts to work with.

WithSugar and WithSteamedMilk are completely gone. Along with the signature of the New function, which is changed from being New(opts ...func(Coffee) Coffee) Coffee to New(opts Options) Coffee. Pretty concise, eh?

2. A new Options struct is introduced

All the settings which could have been there in the form of functions are listed directly here. Functions were just a way to encapsulate the settings.

You must be wondering, can’t we modify all the fields in Coffee directly? Do we need a separate Options struct for this thing? That’s an extra step!

Note that all the fields in Coffee struct could have been unexported. And if you have called this API from your driver code, you won’t be able to set it directly, as it is unexported. Consider this case:

coffee/coffee.go:

1
2
3
4
5
6
7
package coffee

type Coffee struct {
    coffeeBeans bool
    sugar       bool
    steamedMilk bool
}

main.go:

1
2
3
4
5
6
package main

func main() {
    c := coffee.Coffee{coffeeBeans: true}
    fmt.Println()
}

You might get an error like this when run:

# command-line-arguments
./main.go:10:21: unknown field 'coffeeBeans' in struct literal of type coffee.Coffee

That’s because coffeeBeans is an unexported field. The idea here I’m trying to put is, encapsulated inside, while only interface to modify it being the Option struct.

The struct method also gives freedom to set default values. As you can see on line 13-22, I have set CoffeeBeans to true, no matter what you pass to it.

3. New function accepts an Option

Instead of taking functions, like before, we now create an option struct beforehand. And pass it New function.

New is also modified to generate a new Coffee object with all the options passed into it. Here we can also make some behaviors default. Like in the above example, even if the user passes CoffeeBeans value as false, our function will always create a Coffee object with CoffeeBeans.

Conclusion

I haven’t yet form any solid opinion with any of them. Both of them seem equally reasonable to use. I need to practice more and more in order to have a solid opinion on any of them.

Which one do you like any why? Let me know in the comments.

Share on

santosh
WRITTEN BY
santosh
Pipeline & Backend Developer