As any developer familiar with Python knows, Python has default parameters that allow us to optionally override certain default parameters as needed when instantiating an object to determine how to instantiate the object. This feature is great for elegantly simplifying code when an object has multiple default arguments.

The Go language does not syntactically support default parameters, so in order to create objects both by default parameters and by passing custom parameters, we need to use some programming tricks to achieve this. For these common problems in program development, pioneers in the software industry have summarized many best practices for solving common coding scenarios, which have since become what we call design patterns. One of them is the option pattern, which is often used in Go language development.

Typically we have the following three approaches to creating objects by default parameters and by passing custom parameters.

  • using multiple constructors
  • default parameter options
  • The option pattern

Implementation via multiple constructors

The first way is through multiple constructors, and the following is a simple example.

 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
package main

import "fmt"

const (
    defaultAddr = "127.0.0.1"
    defaultPort = 8000
)

type Server struct {
    Addr string
    Port int
}

func NewServer() *Server {
    return &Server{
        Addr: defaultAddr,
        Port: defaultPort,
    }
}

func NewServerWithOptions(addr string, port int) *Server {
    return &Server{
        Addr: addr,
        Port: port,
    }
}

func main() {
    s1 := NewServer()
    s2 := NewServerWithOptions("localhost", 8001)
    fmt.Println(s1)  // &{127.0.0.1 8000}
    fmt.Println(s2)  // &{localhost 8001}
}

Here we have implemented two constructors for the Server structure.

  • NewServer: returns the Server object directly without passing parameters.
  • NewServerWithOptions: you need to pass the addr and port parameters to construct the Server object.

If the object created by default parameters can meet the requirements, we can use NewServer to generate the object (s1) when no customization of the Server is needed. If we need to customize the Server, we can use NewServerWithOptions to generate the object (s2).

Implementing with default parameter options

Another option to implement default parameters is to define an option structure for the structure object to be generated, which is used to generate default parameters for the object to be created, the code implementation is as follows.

 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
package main

import "fmt"

const (
    defaultAddr = "127.0.0.1"
    defaultPort = 8000
)

type Server struct {
    Addr string
    Port int
}

type ServerOptions struct {
    Addr string
    Port int
}

func NewServerOptions() *ServerOptions {
    return &ServerOptions{
        Addr: defaultAddr,
        Port: defaultPort,
    }
}

func NewServerWithOptions(opts *ServerOptions) *Server {
    return &Server{
        Addr: opts.Addr,
        Port: opts.Port,
    }
}

func main() {
    s1 := NewServerWithOptions(NewServerOptions())
    s2 := NewServerWithOptions(&ServerOptions{
        Addr: "localhost",
        Port: 8001,
    })
    fmt.Println(s1)  // &{127.0.0.1 8000}
    fmt.Println(s2)  // &{localhost 8001}
}

We implement a ServerOptions specifically for the Server structure to generate default parameters, and call the NewServerOptions function to get the default parameter configuration. The constructor NewServerWithOptions receives a *ServerOptions type as a parameter. So we can accomplish the function in two ways.

  • directly passing the return value of the call to the NewServerOptions function to NewServerWithOptions to achieve object generation by default parameters (s1)
  • Generate custom objects by manually constructing ServerOptions configuration (s2)

Implemented by option mode

Although both of the above ways can accomplish the function, they have the following disadvantages.

  • The scheme implemented through multiple constructors requires us to call different constructors separately when instantiating the object, which is not well encapsulated in the code and will add a usage burden to the caller.
  • The option scheme with default parameters requires us to pre-construct an option structure, which makes the code look redundant when the object is generated with default parameters.

The option pattern allows us to solve this problem more elegantly. The code implementation is as follows.

 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
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
package main

import "fmt"

const (
    defaultAddr = "127.0.0.1"
    defaultPort = 8000
)

type Server struct {
    Addr string
    Port int
}

type ServerOptions struct {
    Addr string
    Port int
}

type ServerOption interface {
    apply(*ServerOptions)
}

type FuncServerOption struct {
    f func(*ServerOptions)
}

func (fo FuncServerOption) apply(option *ServerOptions) {
    fo.f(option)
}

func WithAddr(addr string) ServerOption {
    return FuncServerOption{
        f: func(options *ServerOptions) {
            options.Addr = addr
        },
    }
}

func WithPort(port int) ServerOption {
    return FuncServerOption{
        f: func(options *ServerOptions) {
            options.Port = port
        },
    }
}

func NewServer(opts ...ServerOption) *Server {
    options := ServerOptions{
        Addr: defaultAddr,
        Port: defaultPort,
    }

    for _, opt := range opts {
        opt.apply(&options)
    }

    return &Server{
        Addr: options.Addr,
        Port: options.Port,
    }
}

func main() {
    s1 := NewServer()
    s2 := NewServer(WithAddr("localhost"), WithPort(8001))
    s3 := NewServer(WithPort(8001))
    fmt.Println(s1)  // &{127.0.0.1 8000}
    fmt.Println(s2)  // &{localhost 8001}
    fmt.Println(s3)  // &{127.0.0.1 8001}
}

At first glance our code looks much more complex, but in fact the complexity of the code that calls the constructor to generate the object is unchanged, only the definition is complex.

We have defined the ServerOptions structure to configure the default parameters. Since both Addr and Port have default parameters, the definition of ServerOptions is the same as the Server definition. However, structures with some complexity may have some parameters that do not have default parameters and must be configured by the user, in which case ServerOptions will have fewer fields and you can define them as needed.

Also, we define a ServerOption interface and a FuncServerOption structure that implements this interface. Their role is to allow us to individually configure a parameter for the ServerOptions structure via the apply method.

We can define a separate WithXXX function for each default parameter to configure the parameter, such as WithAddr and WithPort defined here, so that the user can customize the default parameter to be overridden by calling the WithXXX function.

In this case, the default parameters are defined in the constructor NewServer, which receives an indefinite parameter of type ServerOption, and a for loop inside the constructor calls the apply method of each ServerOption object passed in, assigning the user-configured parameters to the default parameter object inside the constructor in turn After the for loop is executed, the options object will be the final configuration, and the new object will be generated by assigning its properties to Server in turn.

Summary

As you can see from the results of s2 and s3, the constructor implemented in option mode is more flexible than the previous two implementations, and we are free to change any one or more of the default configurations in option mode.

While option mode does require a bit more code, in most cases it is worth it. For example, Google’s Go implementation of the gRPC framework uses the option mode in the constructor NewServer to create a gRPC server, and for those interested, the source code is actually the same as the sample program here.

The above is my experience about Golang option pattern, I hope today’s sharing can bring you some help.