Students who are familiar with Python development know that Python has default parameters, so that when we instantiate an object, we can selectively override some default parameters according to our needs, so as to decide how to instantiate the object. This feature is very useful when an object has multiple default parameters and can simplify the code elegantly.
The Go language does not support default parameters grammatically, so in order to create objects with default parameters and pass custom parameters, we need to use some programming skills to achieve it. For these common problems in program development, the pioneers of the software industry have summarized many best practices for solving common scenario coding problems, which later became what we call design patterns. The option mode is often used in Go language development.
Usually we have the following three ways to create objects with default parameters, and create objects by passing custom parameters:
- Use multiple constructors
- Default parameter options
- option mode
Implemented with multiple constructors
The first way is through multiple constructors, the following 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: Return the Server object directly without passing parameters
- NewServerWithOptions: Need to pass addr and port two parameters to construct the Server object
If the object created by the default parameters can meet the requirements, we can use NewServer to generate the object (s1) when we do not need to customize the Server. And if we need to customize the Server, we can use NewServerWithOptions to generate the object (s2).
Implemented with default parameter options
Another solution for implementing default parameters is to define an option structure for the structure object to be generated, which is used 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 specifically implemented a ServerOptions for the Server structure to generate default parameters. Call the NewServerOptions function to get the default parameter configuration. The constructor NewServerWithOptions receives a *ServerOptions type as a parameter. So we can complete the function in the following two ways:
- Directly pass the return value of calling the NewServerOptions function to NewServerWithOptions to generate an object with default parameters (s1)
- Generate custom objects by manually constructing the ServerOptions configuration (s2)
Implemented via option mode
Although the above two methods can complete the function, they have the following disadvantages:
- The solution implemented by multiple constructors requires us to call different constructors respectively when instantiating objects, and the code encapsulation is not strong, which will increase the burden on the caller.
- The solution implemented by default parameter options requires us to construct an options structure in advance, and the code looks redundant when generating objects with default parameters.
The option mode allows us to solve this problem more elegantly. The code is implemented 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 a lot more complicated, but in fact, the code complexity of calling the constructor to generate the object has not changed, but the definition is complicated.
We define the ServerOptions structure to configure default parameters. Because Addr and Port have default parameters, the definition of ServerOptions is the same as that of Server. However, there may be some parameters in the structure with certain complexity that do not have default parameters and must be configured by the user. At this time, there will be fewer fields in ServerOptions, and you can define them as needed.
At the same time, we also define a ServerOption interface and a FuncServerOption structure that implements this interface. Their function is to allow us to configure a parameter for the ServerOptions structure individually 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 that need to be overridden by calling the WithXXX function.
At this time, the default parameters are defined in the constructor NewServer. The constructor receives a variable length parameter, the type is ServerOption. Inside the constructor, the apply method of each incoming ServerOption object is called through a for loop, and the parameters configured by the user are sequentially Assign it to the default parameter object options inside the constructor to replace the default parameter. After the for loop is executed, the obtained options object will be the final configuration. Assign its properties to Server in turn to generate a new object.
Summarize
From the print results of s2 and s3, it can be found that the constructor implemented using the option mode is more flexible. Compared with the first two implementations, in the option mode, we can freely change any one or more of the default configurations.
While option mode does require a bit more code, it's worth it in most cases. For example, in the implementation of Google's gRPC framework Go language, the constructor NewServer to create gRPC server uses the option mode. Interested students can see that the implementation idea of its source code is exactly the same as the sample program here.
The above is a little bit of my experience about Golang option mode, I hope today's sharing can bring you some help.
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。