Functional Options in Go: With Generic

In this story, we will know the Functional Options pattern and implement it simply, and at the end, we will learn how to apply Generic (Go +1.18) to this solution.

Introduction

Functional options are a method of implementing clean APIs in Go. You've probably seen this pattern if you have done some integrations with GCP SDK or gRPC.

As you can see above, we can configure the clients by using optional functions, and we don't have to use a separate function for different purposes. E.g.

Implementation

This pattern allows us to design a flexible set of APIs to help arbitrary configurations and initialization of a struct.

Let's assume we have a client struct and it has two configurable fields:

Typically, we would need to construct the struct first and then modify its values if we wanted a different variant — first of all, we need to make these fields public. But with this pattern, we can reduce the complexity and give a list of options to the constructor itself. Since the user won't modify these fields directly, having a field/option deprecation plan would be much easier.

Let's define a function type that accepts a pointer to this struct.

Why pointer? Because we want to modify the existing struct.

The user sends option(s), and now we need setter functions to apply those inputs to the struct:

It's time to create our constructor function:

Time to test our constructor function with some spices (options):

The output must be the following values:

So far, we've understood how this pattern works. Now let's think about some improvements to this implementation.

🌸 FYI: No sweat! This is easy to deprecate an option because we can simply let an existing option function not effecting anymore or behave differently.

When we talk about an improvement strategy, the first answer usually is It depends! . What if StorageClient contains many other internal fields? Do you think putting our option fields between them is a good idea? My preference for improving the StorageClientstructure is to put all options under a dedicated field.

By taking this approach, we have an organized structure. In addition, we can use this as a shared option between other clients (StorageClient, MailClient , etc.) as well.

Issue!

This solution is not very good and, like many other things in the engineering world, has some disadvantages (We always deal with trade-offs, Right?). In addition, it scales hideous; at some point, if we need to use these options in two or more different structs, we have to have many options and multiple types in one package. Maybe you think we can use a shared options struct like above, but It won't apply to most scenarios — We can take this approach only if we have a different client which needs the same options — but it does not sound scalable and customizable!

Let me explain this through an example. Let's say MailClient needs another option which is secret . If we add this to clientOptions struct, basically we've added an option to a shared options struct that StorageClient doesn't need it!

Another Option Set

To solve this problem, we have to create a dedicated option set and another type which can lead us to something very ugly. 😥

The next problem with this solution is now we have to create all With* for mailClientOptions. I think you can imagine the output 😶.

Using `Interface{}`

A quick solution to deal with this problem is to use interface{} as a function type. E.g.

As you can see, It solves the problem. Since both clients use the same option interface, we can use WithSecret for NewStorageClient 🙊 . But this behavior is not what we expect because Storage doesn't do anything with secret .

Generic

From the Go 1.18, we have a nice feature which is Generic. Considering this feature, we can simplify the previously mentioned solution and also make the solution safer and more user-friendly.

In the example below, We created a shared struct SharedClientOptions and added it to Mail and Storage. but Mail struct has another field for itself secret.

As you've realized, WithTimeout and WithUserAgent accept any type. It makes sense because they are shared options, but WithSecret only accepts MailClientOptions type.

👾 Better to specify the types instead of usingany

Let's modify our clients:

Here, we specified the option type, so New* functions won't accept other option types.

func NewStorageClient(opts ...ClientOption[StorageClientOptions])...
func NewMailClient(opts ...ClientOption[MailClientOptions])...

Line:10 we must receive an error because WithSecret doesn't accept StorageClientOptionstype.

Conclusion

We've tried different solutions, from basic to generic, which is something new in Golang. But as always, It depends. So it would be best if you considered all aspects beforehand. Anyway, It was one pattern that I explained; there are other ways you can also implement this feature.

💁‍♂ There’s always room for improvement

--

--

--

I’m a senior software engineer (Payment, Game, Live Streaming, SaaS industries), Solution architect, Google Honorable Mention | AWS & GCP Certified

Love podcasts or audiobooks? Learn on the go with our new app.

Recommended from Medium

Which data storage solution do you need?

SecurityTube Linux Assembly Expert (SLAE) Assignment Writeups — \x05 Analysing Metasploit…

How to create custom plugin in Mautic

Putting the Dev in DevOps

Tutorial ESP32 Door Lock 3in1 Fingerprint Keypad 4x3 and RFID PN532

Tutorial ESP32 Door Lock 3in1 Fingerprint Keypad 4x3 and RFID PN532

Introducing LakePy: Accessing Lake Water Level Data Through a Python API

You can now test the Feren OS Major Updater (ALL the update paths)!

Time Complexities By Insertion Sort , Merge Sort And Quick Sorts

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
Amir Soleimani

Amir Soleimani

I’m a senior software engineer (Payment, Game, Live Streaming, SaaS industries), Solution architect, Google Honorable Mention | AWS & GCP Certified

More from Medium

What is a Goroutine ? Find the right way to implement goroutines

Go and MySQL: Setting up Connection Pooling

What are the limits of Go channels?

Go Interface 101