Golang - functional options pattern

Golang - functional options pattern

In the previous article we discussed the Builder pattern. There we noticed a drawback that we can't perform validation when assigning parameters. As a result, we are deprived of the ability to build a chain of methods. Let's look at another more interesting pattern - Functional options. There are different implementations with slight differences, but the basic idea is as follows:

  • You create a non-exportable structure containing the configuration: options.
  • Each option is a function that returns the same type: 

type Option func(options *options) error. 

For example, the WithEmail function, which takes a string of email address as an argument and returns an Option type, which is a function with instructions on how to update the structure.

Below I will give a specific example from a CRM system that allows you to create a new contact:

 

package main

 

import (

 "errors"

 "fmt"

 "net/mail"

 "strings"

)

 

type Contact struct {

 ID       string

 Name     string

 Phone    string

 Email    string

 Company  *CompanyInfo

 Address  string

 Notes    string

}

 

type CompanyInfo struct {

 Name     string

 Position string

}

 

type ContactOptions struct {

 Phone    string

 Email    string

 Company  *CompanyInfo

 Address  string

 Notes    string

}

 

type ContactOption func(*ContactOptions) error

 

func WithPhone(phone string) ContactOption {

 return func(options *ContactOptions) error {

  if len(phone) < 10 {

   return errors.New("Phone number must be at least 10 digits")

  }

  options.Phone = phone

  return nil

 }

}

 

func WithEmail(email string) ContactOption {

 return func(options *ContactOptions) error {

  _, err := mail.ParseAddress(email)

  if err != nil {

   return errors.New("Invalid email address")

  }

  options.Email = email

  return nil

 }

}

 

func WithCompany(name string, position string) ContactOption {

 return func(options *ContactOptions) error {

  if strings.TrimSpace(name) == "" || strings.TrimSpace(position) == "" {

   return errors.New("Both company name and position must be provided")

  }

  options.Company = &CompanyInfo{

   Name:     name,

   Position: position,

  }

  return nil

 }

}

 

func WithAddress(address string) ContactOption {

 return func(options *ContactOptions) error {

  options.Address = address

  return nil

 }

}

 

func WithNotes(notes string) ContactOption {

 return func(options *ContactOptions) error {

  options.Notes = notes

  return nil

 }

}

 

func CreateContact(id string, name string, opts ...ContactOption) (*Contact, error) {

 options := ContactOptions{}

 

 for _, opt := range opts {

  err := opt(&options)

  if err != nil {

   return nil, err

  }

 }

 

 contact := &Contact{

  ID:       id,

  Name:     name,

  Phone:    options.Phone,

  Email:    options.Email,

  Company:  options.Company,

  Address:  options.Address,

  Notes:    options.Notes,

 }

 

 return contact, nil

}

 

func main() {

 contact, err := CreateContact("1", "John Doe", WithPhone("1234567890"), WithEmail("john.doe@gmail.com"), WithCompany("ABC Corp", "Manager"), WithAddress("123 Main St"), WithNotes("Important client"))

 if err != nil {

  fmt.Println("Failed to create contact:", err)

  return

 }

 fmt.Println(contact)

}

 

Here we see many Builder-like methods that populate our structure with the necessary data. For example, WithNotes, WithAddress, WithCompany all return a closure. That is, an anonymous function that binds variables outside its scope. In our case, these are notes, company, and address. The function corresponds to the Option type and contains the data validation logic. For each field in this case we need to write our own function, which by standards starts with the With prefix. And this function must contain the same logic: data validation if necessary and structure updating.

Let's take a look at the CreateContact function. We pass options as variable arguments. Therefore, to change the structure, we must iterate over these options.

It all starts by creating an empty structure

 

options := ContactOptions{}

 

Then, we iterate over each Option argument, execute it as a function, this function in turn performs the structure change.

 

for _, opt := range opts {

  err := opt(&options)

  if err != nil {

   return nil, err

  }

 }

 

Once we have assembled the structure, we can implement the final logic for creating the contact.

 

contact := &Contact{

  ID:       id,

  Name:     name,

  Phone:    options.Phone,

  Email:    options.Email,

  Company:  options.Company,

  Address:  options.Address,

  Notes:    options.Notes,

 }

 

Because CreateContact accepts options as an argument, developers can now very conveniently use our API by passing various optional parameters, with the identifier and name being mandatory:

 

contact, err := CreateContact("1", "John Doe", WithPhone("1234567890"), WithEmail("john.doe@gmail.com"), WithCompany("ABC Corp", "Manager"), WithAddress("123 Main St"), WithNotes("Important client"))

 

We can still create a contact without any additional data, passing only the ID and name.

This pattern is functional options. It provides a convenient and API friendly way to work with options. While the builder pattern may be a viable option, it has some minor drawbacks that make the functional options pattern an idiomatic way to handle this problem in Go. Note also that this pattern is used in various Go libraries such as gRPC.

Popular Posts

My most popular posts

Maximum productivity on remote job
Business

Maximum productivity on remote job

I started my own business and intentionally did my best to work from anywhere in the world. Sometimes I sit with my office with a large 27-inch monitor in my apartment in Cheboksary. Sometimes I’m in the office or in some cafe in another city.

Hello! I am Sergey Emelyanov and I am hardworker
Business PHP

Hello! I am Sergey Emelyanov and I am hardworker

I am a programmer. I am an entrepreneur in my heart. I started making money from the age of 11, in the harsh 90s, handing over glassware to a local store and exchanging it for sweets. I earned so much that was enough for various snacks.

Hire Professional CRM developer for $25 per hour

I will make time for your project. Knowledge of Vtiger CRM, SuiteCRM, Laravel, and Vue.js. I offer cooperation options that will help you take advantage of external experience, optimize costs and reduce risks. Full transparency of all stages of work and accounting for time costs. Pay only development working hours after accepting the task. Accept PayPal and Payoneer payment systems. How to hire professional developer? Just fill in the form

Telegram
@sergeyem
Telephone
+4915211100235