Option mode of Golang common design mode

Posted by markyoung1984 on Tue, 11 Jan 2022 04:23:13 +0100

Students familiar with Python development know that Python has default parameters, so that when instantiating an object, we can selectively override some default parameters as needed to decide how to instantiate the object. When an object has multiple default parameters, this feature is very easy to use and can gracefully simplify the code.

The Go language does not support default parameters grammatically, so in order to create objects not only through default parameters, but also by passing custom parameters, we need to use some programming skills. For these common problems in program development, pioneers in the software industry summarized many best practices to solve common scene coding problems, which later became what we call design patterns. Option mode is often used in Go language development.

Generally, we have the following three methods to create objects by default parameters and by passing custom parameters:

  • Use multiple constructors

  • Default parameter options

  • Option mode

Implemented by multiple constructors

The first method is implemented through multiple constructors. Here is a simple example:

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 implement two constructors for the Server structure:

  • NewServer: you can directly return the Server object without passing parameters

  • NewServerWithOptions: you need to pass addr and port parameters to construct the Server object

If the objects created by default parameters can meet the requirements, and there is no need to customize the Server, we can use NewServer to generate objects (s1). If we need to customize the Server, we can use NewServerWithOptions to generate objects (s2).

By default parameter options

Another scheme to realize the default parameters is to define an option structure for the structure object to be generated to generate the default parameters of the object to be created. The code implementation is as follows:

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 have specially implemented a ServerOptions for the Server structure to generate default parameters. The default parameter configuration can be obtained by calling the NewServerOptions function. The constructor NewServerWithOptions receives a * ServerOptions type as a parameter. Therefore, we can complete the function in the following two ways:

  • Directly pass the return value of calling the NewServerOptions function to NewServerWithOptions to generate the object through the default parameters (s1)

  • Generate custom objects by manually constructing ServerOptions configuration (s2)

Implemented through option mode

Although the above two methods can complete the functions, they have the following disadvantages:

  • The scheme implemented through multiple constructors requires us to call different constructors when instantiating the object. The code encapsulation is not strong, which will increase the burden on the caller.

  • The scheme implemented through the default parameter option requires us to construct an option structure in advance. When using the default parameter to generate objects, the code looks redundant.

The option mode allows us to solve this problem more gracefully. The code implementation is as follows:

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 is much more complex, but in fact, the code complexity of calling the constructor to generate objects has not changed, just the complexity of definition.

We defined the ServerOptions structure to configure the default parameters. Because both Addr and Port have default parameters, the definition of ServerOptions is the same as that of Server. However, some parameters in structures with certain complexity may not have default parameters and must be configured by the user. At this time, there will be fewer fields in ServerOptions, which can be defined as needed.

At the same time, we also define a ServerOption interface and a FuncServerOption structure that implements this interface. Their function is to enable us to configure a parameter separately for the ServerOptions structure through the apply method.

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

At this time, the default parameters are defined in the constructor NewServer. The constructor receives an indefinite length parameter of type ServerOption. Within the constructor, call the apply method of each passed ServerOption object through a for loop, and assign the user configured parameters to the default parameter object options within the constructor in turn, This is used to replace the default parameters. After the execution of the for loop, the options object will be the final configuration. Assign its properties to the Server in turn to generate a new object.

summary

Through the print results of s2 and s3, we can find that the constructor implemented with option mode is more flexible. Compared with the first two implementations, we can freely change any one or more default configurations in option mode.

Although the option mode does write more code, it is worth it in most cases. For example, in the Go language implementation of Google's gRPC framework, NewServer uses the option mode to create the constructor of gRPC server. Interested students can see the implementation idea of its source code, which is actually the same as the example program here.

The above is my experience about Golang option mode. I hope today's sharing can bring you some help.

Recommended reading

Server side rendering Foundation

Cloud native grayscale update practice

Topics: Python Go Design Pattern grpc