Creational patterns deal with how objects are created. In Go, most classic creational patterns simplify significantly because Go doesn’t have class constructors, overloading, or inheritance. The idiomatic Go equivalents are plain constructor functions — New* functions that accept dependencies and return initialized values.
That said, a few patterns have distinctly Go-shaped implementations worth knowing: Singleton uses sync.Once, Object Pool uses sync.Pool, and Builder uses functional options rather than a separate builder struct.
For other design pattern categories see Go behavioral design patterns and Go structural design patterns.
Singleton: sync.Once
A Singleton ensures only one instance of a type is created, no matter how many goroutines race to initialize it. In Go, sync.Once is the tool for this:
var (
dbInstance *sql.DB
dbOnce sync.Once
)
func DB() *sql.DB {
dbOnce.Do(func() {
db, err := sql.Open("postgres", os.Getenv("DATABASE_URL"))
if err != nil {
log.Fatalf("opening DB: %v", err)
}
db.SetMaxOpenConns(25)
db.SetMaxIdleConns(10)
dbInstance = db
})
return dbInstance
}
sync.Once.Do executes the function exactly once, even if called concurrently from thousands of goroutines. All other goroutines block until the first completes, then return immediately on subsequent calls.
The tradeoff: if the initialization fails and you want to retry, sync.Once doesn’t help — it considers any execution (even one that set an error) as “done.” For failable initialization with retries, use a plain mutex and a flag.
In practice, most Go programs don’t use singletons — they wire dependencies at startup and pass them through constructors. Singletons are most useful for expensive resources (DB connections, config, registries) that genuinely should only exist once and are accessed from many callsites.
Factory: Constructor Functions
Go’s factory pattern is a plain function that returns an initialized value. The convention is New*:
type Logger interface {
Info(msg string, args ...any)
Error(msg string, args ...any)
}
type slogLogger struct{ l *slog.Logger }
func (s *slogLogger) Info(msg string, args ...any) { s.l.Info(msg, args...) }
func (s *slogLogger) Error(msg string, args ...any) { s.l.Error(msg, args...) }
type noopLogger struct{}
func (n *noopLogger) Info(msg string, args ...any) {}
func (n *noopLogger) Error(msg string, args ...any) {}
// Factory: returns different implementations based on config
func NewLogger(env string) Logger {
switch env {
case "production":
return &slogLogger{slog.New(slog.NewJSONHandler(os.Stdout, nil))}
case "test":
return &noopLogger{}
default:
return &slogLogger{slog.Default()}
}
}
The factory encapsulates which concrete type to create and how to initialize it. Callers only see the Logger interface — they can’t accidentally depend on the concrete type.
For more complex object families (creating a whole set of related objects for a platform), the Abstract Factory pattern adds a factory interface:
type StorageFactory interface {
NewUserStore() UserStore
NewOrderStore() OrderStore
}
type PostgresFactory struct{ db *sql.DB }
func (f *PostgresFactory) NewUserStore() UserStore { return postgres.NewUserStore(f.db) }
func (f *PostgresFactory) NewOrderStore() OrderStore { return postgres.NewOrderStore(f.db) }
type InMemoryFactory struct{}
func (f *InMemoryFactory) NewUserStore() UserStore { return inmem.NewUserStore() }
func (f *InMemoryFactory) NewOrderStore() OrderStore { return inmem.NewOrderStore() }
Pass StorageFactory to your application setup — swap the entire storage layer between production (Postgres) and tests (in-memory) by passing a different factory.
Builder: Functional Options
The Builder pattern constructs complex objects step by step. In Go, the idiomatic form is functional options — a variadic list of option functions passed to the constructor:
type Server struct {
addr string
readTimeout time.Duration
writeTimeout time.Duration
maxConnections int
logger Logger
tlsConfig *tls.Config
}
type Option func(*Server)
// Each option is a function that modifies one field
func WithAddr(addr string) Option {
return func(s *Server) { s.addr = addr }
}
func WithReadTimeout(d time.Duration) Option {
return func(s *Server) { s.readTimeout = d }
}
func WithMaxConnections(n int) Option {
return func(s *Server) { s.maxConnections = n }
}
func WithLogger(l Logger) Option {
return func(s *Server) { s.logger = l }
}
func WithTLS(config *tls.Config) Option {
return func(s *Server) { s.tlsConfig = config }
}
// Constructor applies defaults then each option in order
func NewServer(opts ...Option) *Server {
s := &Server{
addr: ":8080", // defaults
readTimeout: 15 * time.Second,
writeTimeout: 15 * time.Second,
maxConnections: 1000,
logger: &noopLogger{},
}
for _, opt := range opts {
opt(s)
}
return s
}
// Usage — clean, named, order-independent, optional
srv := NewServer(
WithAddr(":9090"),
WithReadTimeout(30 * time.Second),
WithLogger(myLogger),
WithTLS(tlsConfig),
)
// Minimal usage — all defaults
srv := NewServer()
This pattern is used by grpc.NewServer, http.NewServeMux, and many other Go libraries. It’s more flexible than a config struct (options can have validation logic, ordering can matter) and more readable than a large struct literal with many zero-value fields.
Prototype: Copying with Modified Fields
The Prototype pattern creates new objects by copying an existing one and modifying some fields. In Go, this is just a copy constructor or a method that returns a modified copy:
type Config struct {
Host string
Port int
Debug bool
MaxRetries int
}
// WithHost returns a copy with a different host
func (c Config) WithHost(host string) Config {
c.Host = host // c is a copy (Config is a value type)
return c
}
func (c Config) WithDebug(debug bool) Config {
c.Debug = debug
return c
}
// Base config
base := Config{Host: "localhost", Port: 8080, MaxRetries: 3}
// "Prototypes" for different environments
devConfig := base.WithHost("dev.example.com").WithDebug(true)
prodConfig := base.WithHost("prod.example.com")
testConfig := base.WithHost("test.example.com").WithDebug(true)
For pointer-based types (structs with slice/map fields that shouldn’t be shared between copies), implement an explicit Clone method that deep-copies the fields that need it.
Object Pool: sync.Pool
Object Pool reuses expensive-to-create objects. Go’s sync.Pool is purpose-built for this: it holds temporary objects that can be reused to reduce GC pressure:
var bufPool = sync.Pool{
New: func() any {
// Called when the pool is empty — creates a new object
return bytes.NewBuffer(make([]byte, 0, 4096))
},
}
func processRequest(data []byte) string {
buf := bufPool.Get().(*bytes.Buffer)
buf.Reset() // clear contents, keep capacity
defer bufPool.Put(buf) // return to pool when done
// Use buf for intermediate work
json.NewEncoder(buf).Encode(transform(data))
return buf.String()
}
sync.Pool is the right tool for short-lived objects that are:
- Expensive to allocate (large byte slices, complex parsers)
- Identical in structure (you reset before reuse)
- Discardable (GC can collect pool contents between GC cycles)
The GC clears the pool contents at each collection cycle, so pool objects don’t accumulate. Don’t use sync.Pool for objects with state that must survive GC cycles — use a channel-based pool or sync.Mutex-protected free list instead.
The standard library uses sync.Pool extensively: fmt.Sprintf borrows a buffer from a pool for each call, encoding/json reuses encoder state, net/http reuses request/response buffers.
Summary
- Singleton with
sync.Once.Do: guaranteed single initialization even under concurrent access - Factory functions (
New*) return interface types — encapsulate which concrete type to create and hide initialization complexity - Abstract Factory groups related factories behind an interface — swap entire storage backends with one parameter change
- Builder with functional options (
WithXxx) provides clean, documented, order-independent construction of complex types - Prototype: value-type structs support “copy and modify” naturally; pointer types need explicit
Clonemethods sync.Poolfor short-lived, reusable objects — reduces allocation rate and GC pressure in hot paths
Resources
- sync.Pool documentation
- Refactoring Guru: Creational patterns in Go
- Functional options pattern (Dave Cheney)
- Go design patterns (tmrts)
Comments