SoFunction
Updated on 2025-03-05

Detailed explanation of Golang functional options (Functional Options) mode

Overview

Recently, when I read the source code, I saw a good piece of code, but I didn’t understand why I wrote it like this at that time.
Let's take a look at the source code first:

type User struct {
	ID     string
	Name   string
	Age    int
	Email  string
	Phone  string
	Gender string
}

type Option func(*User)

func WithAge(age int) Option {
	return func(u *User) {
		 = age
	}
}

func WithEmail(email string) Option {
	return func(u *User) {
		 = email
	}
}

func WithPhone(phone string) Option {
	return func(u *User) {
		 = phone
	}
}

func WithGender(gender string) Option {
	return func(u *User) {
		 = gender
	}
}

func NewUser(id string, name string, options ...func(*User)) (*User, error) {
	user := User{
		ID:     id,
		Name:   name,
		Age:    0,
		Email:  "",
		Phone:  "",
		Gender: "female",
	}
	for _, option := range options {
		option(&user)
	}
	//...
	return &user, nil
}

func main() {
	user, err := NewUser("1", "Ada", WithAge(18), WithPhone("123456"))
	if err != nil {
		("NewUser: err:%v", err)
	}
	("NewUser Success")
}

At that time, I didn't understand very wellNewUserWhy did this constructor be written like this? Later, after checking the information, I realized that this is a design pattern –Functional options mode

What is the functional option mode, why do you need to write this way? What problems does this programming mode solve?

In fact, it is to solve the problem of dynamic and flexible configuration of different parameters.

If we have a requirement:
There are many options to register an account on the website, which can be filled in or not. Will initialize your information based on the information you fill inUserObject.

Overload functions

If you haveC++orJavaThe programming basis, your first reaction should be function overloading.

But becauseGolangThe language is not the sameC++Overloaded functions are also supported, so you have to use different function names to deal with different configuration options.

Like this:

func NewUserDefault(id string, name string) (*User, error) {
  return &User{ID: id, Name: name}, nil
}

func NewUserWithPhone(id string, name string, phone string) (*User, error) {
  return &User{ID: id, Name: name, Phone: phone}, nil
}

func NewUserWithEmail(id string, name string, email string) (*User, error) {
  return &User{ID: id, Name: name, Email: email}, nil
}

If there are only two or three parameters in total, it is simpler, but the combination of more options makes the code look messy.

Configurable

At this time, we may think of configuration.
Put all optional parameters into a Config struct.

type Config struct {
    Age    int
    Email  string
    Phone  string
    Gender string
}

Then put Config into the User struct.

type User struct {
    ID   string
    Name string
    Conf *Config
}

Therefore, we only need a NewUser() function, and we need to construct a Config object before use.

func NewUser(id string, name string, conf *Config) (*User, error) {
    //...
}
//Using the default configuratrion
user, _ := NewUser("1", "Ada", nil)

conf := Config{Age:18, Phone: "123456"}
user2, _ := NewUser("2", "Bob", &conf)

This code looks pretty good, and many times I use some open source libraries that are written in this way. The problem of combining multiple parameters is solved by introducing a Config object.

Builder mode

Some students who often use Java will think of the Builder mode.

So it can be used in the following way:

user := new(UserBuilder).Create("1", "Ada").
  age(18).
  phone("123456").
  gender("female").
  Build()

An abstract UserBuilder object needs to be introduced to wrap the User object, and finally build a User object to return.

Functional Options Mode

Functional Options ModeFirst, you need to define a function type:

type Option func(*User)

Then, we define a set of functions that return functions:

func WithAge(age int) Option {
	return func(u *User) {
		 = age
	}
}

func WithEmail(email string) Option {
	return func(u *User) {
		 = email
	}
}

func WithPhone(phone string) Option {
	return func(u *User) {
		 = phone
	}
}

func WithGender(gender string) Option {
	return func(u *User) {
		 = gender
	}
}

The difference between this mode and Builder mode is that the Builder mode returns* UserObject,Functional OptionsReturns the function typefunc(* User)

The above set of codes pass in a parameter and then returns a function. The returned function will set its ownUserparameter.

This way we can be convenientNewUserInitialize it uniformly. Loop performs call operations on our function type.option(&user)

func NewUser(id string, name string, options ...func(*User)) (*User, error) {
	user := User{
		ID:     id,
		Name:   name,
		Age:    0,
		Email:  "",
		Phone:  "",
		Gender: "female",
	}
	for _, option := range options {
		option(&user)
	}
	//...
	return &user, nil
}

The call method is as follows:

user1, err := NewUser("1", "Ada")
user2, err := NewUser("2", "Bob", WithPhone("123456"), WithGender("male"))

This looks neat and elegant, with only one NewUser on the external interface.

Compared to the Builder mode, there is no need to introduce a Builder object.

Compared with the configuration mode, there is no need to introduce a new Config.

Summarize

GolangDue to the characteristics of the language itself, function overloading is not supported.Functional OptionsThe programming mode solves the problem that other languages ​​need to solve through function overloading to a certain extent.
Functional OptionsProgramming has the following advantages:

advantage:

Passing parameters in any order supports default values ​​backward compatibility is easy to maintain and extend

After reading so much, do you want to start refactoring your previous code immediately?

AlthoughFunctional OptionsProgramming mode has many advantages, but the existence of design mode is a means to make up for the shortcomings of language characteristics. It is to solve the problem of code scalability, often by adding abstraction to sacrifice simplicity and never overuse it. Some simple configurations do not need to be designed so versatile.

What are the usage scenarios of the functional option mode:
We generally use some basic service configurations, such as MySQL, Redis, and Kafka configurations. There are many optional parameters, which can facilitate dynamic and flexible configuration of the parameters you want to configure.

This is the article about the Golang functional options mode. For more information about Golang functional options, please search for my previous articles or continue browsing the related articles below. I hope you support me in the future!