simon

simon

github

Functional Options Pattern Default Parameter Passing in Go Functions

Functional Options Pattern: Default Parameter Passing in Go Functions#

Created: May 4, 2021 2:32 PM
Tags: Golang

1. Problem#

During the use of Golang, it was found that it lacks some syntactic sugar compared to Python, such as function parameters having default values, which can be omitted. Although Java does not have this syntactic sugar, it has overloading to achieve similar effects.

example

def example(must_have, option_value = "world"):
    print("{} {}".format(must_have + option_value))

example("hello")
output: hello world

example("hello", "friends")
output: hello friends

Today, while writing UT, I needed to construct data and found that we have two ways to construct data:

  1. Directly use ORM to create an instance with custom parameter values.
  2. Write a factory using faker to generate some data, partially specified and partially using fake data.

Both methods have obvious drawbacks.

Method 1 requires constructing all fields every time, which is cumbersome.

Method 2 does not allow customizing the parameters of the instance, making it impossible to specify associated fields when constructing two related instances.

2. Expectation#

So, is there a way to maximize the use of faker to generate data while conveniently customizing certain parameter values for association? Let's take a look at how factory boy in Python does it.

class ModelAFactory(factory.django.DjangoModelFactory):
    class Meta:
        model = ModelA

    name = factory.Sequence(lambda n: "ModelA%s" % n)
    description = factory.fuzzy.FuzzyText()
    pic = factory.Sequence(lambda n: "image%s" % n)
    status = Status.ACTIVE.value

    @factory.post_generation
    def related_models(obj, create, extracted, **kwargs):
        RelatedModelFactory.create(model_a=obj, **kwargs)

ModelAFactory defines the Model in the meta and customizes the generation methods for four fields, while other fields are generated using default methods.

When used, one option is to completely rely on factory-generated parameters, while the other allows specifying some field values. The example I provided is for name, but a more common situation is associating fields from other tables.

instance_a = ModelAFactory()  # Completely relies on factory
instance_b = ModelAFactory(name="option")  # Customized the instance name value

3. Solution and Implementation#

3.1 Background Knowledge#

Function types#

  • Define a custom type (type) that represents a collection of similar functions (func).
  • These functions have the same input and output.
// The type named TypeName is an abstract type for a series of functions that share a common characteristic,
// receiving a string parameter and returning a string parameter.
type TypeName func(var1 string) string

// Add a method say to the TypeName type, all functions of TypeName will have this method.
func (typeName TypeName) say(n string) {
    fmt.Println(typeName(n))  // Print the return value of a TypeName function when passing in parameter n.
}

// Let's implement a TypeName type function method.
func english(name string) string {
    return "Hello, " + name
}

func french(name string) string {
    return "Bonjour, " + name
}

func main() {
    // Convert the english function to TypeName type, thus adding a method say() to english.
    g := TypeName(english)
    g.say("World")
    g = TypeName(french)
    g.say("World")
}

Closure#

  • Concept

    A closure is a record storing a function together with an environment.
    A closure is an entity formed by combining a function with its associated reference environment.

    • The function refers to both the outer function and the inner function.
      • The outer function is mainly to wrap the environment, primarily to construct a closure function with an environment.
      • The inner function is the actual function that needs to be executed, which is the closure function itself.
      • The closure function returns an executable function (the inner function).
    • Environment
      • The so-called environment refers to the parameter values passed in by the outer function or the internal parameters, which will be stored in the closure function and can be used in the calculations of the inner function.
    • The same closure function, when called multiple times, will be independent of each other.

    This concept has a lot of content in Go, here we only introduce enough to implement the current functionality.

  • Example of a closure function

    func foo(x int) func() {
        return func(y int) {
            fmt.Printf("foo val = %d\n", x + y)
        }
    }
    
    // Initialize a closure function, assigning 1 to foo's parameter x, returning a closure function (which is func(y int) with x value).
    f1 := foo(1)
    // Execute the closure function, y=2, x=1, executing the anonymous function in func.
    f1(2)
    // output: foo val = 3
    

Go Functions with Variable Number of Parameters#

  • Go functions have variadic parameters, meaning they can accept a variable number of parameters of the same type. The Go print function utilizes this feature. The function parameters can be of various types, including custom types, such as function types.
  • The representation is as follows:
// ... indicates that an unspecified number of string parameters can be passed.
func test(v1 string, ...vs string) {
    fmt.Println(v1)
    for _, s := range vs {
       fmt.Println(s)
    }
}

Usage is as follows:

test("A")
Out: "A"
test("A", "B", "C")
Out: "A"
     "B"
     "C"

3.2 Solution#

Functional Options Pattern.

With the previous knowledge, we can understand the main focus of this article—the functional options pattern, which modifies or adds functionality to a function through variations in its parameters.

3.2.1 Example#

h := NewHouse(
   WithConcrete(),
   WithoutFireplace()
)

NewHouse here is a constructor function, WithConcrete() and WithoutFireplace() are optional parameters for the constructor function, which can modify the return value of the function.

3.2.2 Defining the Constructor Function#

// Define a struct to hold the values of our optional parameters.
type User struct {
    Name     string
}

// Constructor function, default name is Zhang San.
func NewUser() User {
    options := User{}
    options.Name = "Zhang San"
    return options
}

3.2.3 Defining Function Options#

The external function WithName passes the desired custom value for the Name field, returning a function of type UserOption, which is the anonymous function returned. The function's purpose is to copy the value of the name parameter to the corresponding field Name in the pointer to the optional parameter struct, so that after assignment, the value of *User outside the function will also be modified.

// Define a custom function type as the return of the closure function.
// The parameter is a pointer to a variadic struct because it needs to modify the value outside the closure for storage.
type UserOption func(options *User)

// Create an option function.
func WithName(name string) UserOption {
	return func(options *User) {
		options.Name = name
	}
}

3.2.4 Adding Function Options to the Constructor Function#

At this point, we need to modify the previous constructor function to support passing multiple function options. "...UserOption" indicates that an unspecified number of UserOption type functions can be passed.

func NewUser(opts ...UserOption) User {
	options := User{}
	options.Name = "Zhang San"
  // Loop through the option functions to assign values to fields.
	for _, o := range opts {
		o(&options)
	}
	return options
}

Usage, note that the WithName function actually generates a UserOption type function with assignable parameter values using a closure, and then the constructor function just needs to pass in the struct that stores the values to execute it, which can replace the values inside.

func main() {
	fmt.Println("Hello, playground")
  // Use default values.
	user := NewUser()
	fmt.Println("Name", user.Name)
  // Use custom values.
	user2 := NewUser(WithName("Huang Zilong"))
	fmt.Println("Name", user2.Name)
}

4. Show me the code!#

// WeChat related.
// Define a type that can be a function, passing in our model, as we use closures to ensure assignment, so no output is needed.
type WechatOption func(options *entschema.WeChat)

// Use closures for custom assignments.
func WithAppIDWechat(s string) WechatOption {
	return func(options *entschema.WeChat) {
		options.AppID = s
	}
}

func WithUnionIDWechat(s string) WechatOption {
	return func(options *entschema.WeChat) {
		options.UnionID = &s
	}
}

func WithOpenIDWechat(s string) WechatOption {
	return func(options *entschema.WeChat) {
		options.OpenID = s
	}
}

func WithAccountIDWechat(s uint64) WechatOption {
	return func(options *entschema.WeChat) {
		options.AccountID = s
	}
}

func WithTypeWechat(s wechat.Type) WechatOption {
	return func(options *entschema.WeChat) {
		options.Type = s
	}
}

func WeChatFactory(suite TestSuite, opts ...WechatOption) entschema.WeChat {
	options := entschema.WeChat{}
	// Configure default fake data, fake parameters are within the factory.
	suite.NoError(faker.FakeData(&options))
	// If parameters are passed, use the passed parameters for assignment, achieving variable parameter passing.
	for _, o := range opts {
		o(&options)
	}
	aWechat := app.Database.WeChat.Create().
		SetAppID(options.AppID).
		SetOpenID(options.OpenID).
		SetAccountID(options.AccountID).
		SetUnionID(*options.UnionID).
		SetType(options.Type).
		SaveX(suite.Context())
	return *aWechat
}

wechatFactory(WithUnionIDWechat("id"))

5. One More Thing#

Although we have learned this knowledge and indeed know that this is quite good, it does not mean we will use it. I noticed that many of our colleagues actually use it in some places, but not everywhere it is needed.

The reason is still that it is too cumbersome, requiring a lot of code to be written, compared to Python where you only need to define a parameter name and set a default value. To achieve the same functionality in Go requires writing much more code, and clearly, we do not have that time. It also does not justify spending that time. This can be considered a performance cost. But is there a way to have both? Yes.

Generate-function-opts Command#

./generate-function-opts 
   -definitionFile gen/entschema/workwechatuser.go   
   -structTypeName WorkWeChatUser 
   -outFile pkg/testutils/workwechatuserfactory.go  
   -skipStructFields Edges

reference#

  1. Golang.org. 2021. The Go Programming Language Specification - The Go Programming Language. [online] Available at: https://golang.org/ref/spec#Function_types [Accessed 25 February 2021].
  2. talks, C., 2018. Go Functional Options Pattern. [online] Code talks. Available at: https://lingchao.xin/post/functional-options-pattern-in-go.html [Accessed 25 February 2021].
  3. Sohamkamani.com. 2019. [online] Available at: https://www.sohamkamani.com/golang/options-pattern/ [Accessed 26 February 2021].
Loading...
Ownership of this post data is guaranteed by blockchain and smart contracts to the creator alone.