Factory pattern is a creation pattern, is used to create new objects of a design pattern, it can be a complex object construction process abstracted out (abstract class), so that the abstract process of different implementation methods can be constructed to different representations of the object (attribute).
The 23 design patterns include the Abstract Factory pattern, the Factory Method pattern, and others have been summarised as the Simple Factory pattern. This factory relies heavily on interfaces, abstract classes and concrete class implementations. In Go, there is no such complex factory creation pattern, the most common factory pattern in Go is similar to the Simple Factory pattern, and it is usually implemented through New
or NewXXX
.
Let’s say we want to implement a storage data structure, it may be a memory-based storage, a disk-based storage, or a temporary file-based storage, whatever the case may be, let’s define a Store
interface.
1
2
3
4
5
|
package data
import "io"
type Store interface {
Open(string) (io.ReadWriteCloser, error)
}
|
Then define different Store
implementations.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
package data
type StorageType int
const (
DiskStorage StorageType = 1 << iota
TempStorage
MemoryStorage
)
func NewStore(t StorageType) Store {
switch t {
case MemoryStorage:
return newMemoryStorage( /*...*/ )
case DiskStorage:
return newDiskStorage( /*...*/ )
default:
return newTempStorage( /*...*/ )
}
}
|
Usage is as follows:
1
2
3
4
|
s, _ := data.NewStore(data.MemoryStorage)
f, _ := s.Open("file")
n, _ := f.Write([]byte("data"))
defer f.Close()
|
(The above examples are taken from https://github.com/tmrts/go-patterns)
Going further, we won’t even create an interface, such as the Go standard library’s net/http.NewRequestWithContext
,to create a *http.Request
object.
According to the different types of body it will create different request.GetBody
, there is no use of interfaces, a struct is enough, because GetBody
is a function pointer, you can according to the parameters of the different Request. here make full use of Go’s type switch, func pointer and other features of Go, do not need to generate complex interfaces and concrete classes.
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
|
func NewRequestWithContext(ctx context.Context, method, url string, body io.Reader) (*Request, error) {
...
u.Host = removeEmptyPort(u.Host)
req := &Request{
ctx: ctx,
Method: method,
URL: u,
Proto: "HTTP/1.1",
ProtoMajor: 1,
ProtoMinor: 1,
Header: make(Header),
Body: rc,
Host: u.Host,
}
if body != nil {
switch v := body.(type) {
case *bytes.Buffer:
req.ContentLength = int64(v.Len())
buf := v.Bytes()
req.GetBody = func() (io.ReadCloser, error) {
r := bytes.NewReader(buf)
return io.NopCloser(r), nil
}
case *bytes.Reader:
req.ContentLength = int64(v.Len())
snapshot := *v
req.GetBody = func() (io.ReadCloser, error) {
r := snapshot
return io.NopCloser(&r), nil
}
case *strings.Reader:
req.ContentLength = int64(v.Len())
snapshot := *v
req.GetBody = func() (io.ReadCloser, error) {
r := snapshot
return io.NopCloser(&r), nil
}
default:
}
if req.GetBody != nil && req.ContentLength == 0 {
req.Body = NoBody
req.GetBody = func() (io.ReadCloser, error) { return NoBody, nil }
}
}
return req, nil
}
|
A better example is the Open
method under database/sql
:
1
|
func Open(driverName, dataSourceName string) (*DB, error)
|
You need to provide a different database type name and dcn to generate a corresponding *DB
object, note that DB is struct and does not define a DB
type interface.
It is implemented as follows.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
func Open(driverName, dataSourceName string) (*DB, error) {
driversMu.RLock()
driveri, ok := drivers[driverName]
driversMu.RUnlock()
if !ok {
return nil, fmt.Errorf("sql: unknown driver %q (forgotten import?)", driverName)
}
if driverCtx, ok := driveri.(driver.DriverContext); ok {
connector, err := driverCtx.OpenConnector(dataSourceName)
if err != nil {
return nil, err
}
return OpenDB(connector), nil
}
return OpenDB(dsnConnector{dsn: dataSourceName, driver: driveri}), nil
}
|
It will find the corresponding factory (driver) from a table (drivers
), then call the factory’s OpenConnector
to get a connector (or just generate a dsnConnector), and finally call OpenDB
to create the DB object.
Different database types can register specific database driver by Register(name string, driver driver.Driver)
, such as mysql driver:
1
2
3
|
func init() {
sql.Register("mysql", &MySQLDriver{})
}
|
clickhouse drive:
1
2
3
4
|
func init() {
var debugf = func(format string, v ...any) {}
sql.Register("clickhouse", &stdDriver{debugf: debugf})
}
|
For such scenarios where there are specific different implementations, Go’s way of doing things is often to register the different implementations with a table and lookup to find the appropriate implementation when creating it. For example, the rpcx microservices framework supports different connection protocols by looking up the corresponding creation method to create the connection.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
func init() {
makeListeners["tcp"] = tcpMakeListener("tcp")
makeListeners["tcp4"] = tcpMakeListener("tcp4")
makeListeners["tcp6"] = tcpMakeListener("tcp6")
makeListeners["http"] = tcpMakeListener("tcp")
makeListeners["ws"] = tcpMakeListener("tcp")
makeListeners["wss"] = tcpMakeListener("tcp")
}
func (s *Server) makeListener(network, address string) (ln net.Listener, err error) {
ml := makeListeners[network]
if ml == nil {
return nil, fmt.Errorf("can not make listener for %s", network)
}
if network == "wss" && s.tlsConfig == nil {
return nil, errors.New("must set tlsconfig for wss")
}
return ml(s, address)
}
|
Ref: https://colobu.com/2023/07/24/go-design-patterns-factory/