Builder (Строитель)
Проблема
Вам нужно создавать сложные объекты с множеством параметров, часть из которых опциональна. Конструктор с 10+ параметрами нечитаем и хрупок. Builder позволяет создавать объект пошагово.
В Go есть три подхода, от самого идиоматичного к менее:
- Functional Options -- самый Go-идиоматичный
- Config struct -- простой и понятный
- Method Chaining Builder -- классический GoF
Диаграмма
Functional Options (Go-идиоматичный):
NewServer(":8080",
WithTimeout(30*time.Second), --+
WithMaxConns(100), |--> Option functions
WithTLS(cert, key), --+
) --> *Server
Method Chaining Builder:
NewServerBuilder().
Port(":8080"). --+
Timeout(30*time.Second). |--> Builder methods
MaxConns(100). --+
Build() --> (*Server, error)
Подход 1: Functional Options (рекомендуемый)
Это самый идиоматичный способ в Go. Используется в стандартной библиотеке и большинстве популярных пакетов.
package server
import (
"crypto/tls"
"log/slog"
"time"
)
// Server represents an HTTP server with configurable options.
type Server struct {
addr string
readTimeout time.Duration
writeTimeout time.Duration
maxConns int
tlsConfig *tls.Config
logger *slog.Logger
}
// Option configures a Server.
type Option func(*Server)
// WithReadTimeout sets the read timeout.
func WithReadTimeout(d time.Duration) Option {
return func(s *Server) {
s.readTimeout = d
}
}
// WithWriteTimeout sets the write timeout.
func WithWriteTimeout(d time.Duration) Option {
return func(s *Server) {
s.writeTimeout = d
}
}
// WithMaxConns sets the maximum number of connections.
func WithMaxConns(n int) Option {
return func(s *Server) {
s.maxConns = n
}
}
// WithTLS configures TLS.
func WithTLS(cfg *tls.Config) Option {
return func(s *Server) {
s.tlsConfig = cfg
}
}
// WithLogger sets the logger.
func WithLogger(l *slog.Logger) Option {
return func(s *Server) {
s.logger = l
}
}
// New creates a Server with the given address and options.
func New(addr string, opts ...Option) *Server {
// Set defaults
s := &Server{
addr: addr,
readTimeout: 5 * time.Second,
writeTimeout: 10 * time.Second,
maxConns: 1000,
logger: slog.Default(),
}
// Apply options
for _, opt := range opts {
opt(s)
}
return s
}
Использование:
srv := server.New(":8080",
server.WithReadTimeout(30*time.Second),
server.WithMaxConns(5000),
server.WithLogger(myLogger),
)
Подход 2: Config struct
Простой и явный подход для случаев, когда параметров немного.
package server
import "time"
// Config holds server configuration.
type Config struct {
Addr string
ReadTimeout time.Duration
WriteTimeout time.Duration
MaxConns int
}
// DefaultConfig returns a Config with sensible defaults.
func DefaultConfig() Config {
return Config{
Addr: ":8080",
ReadTimeout: 5 * time.Second,
WriteTimeout: 10 * time.Second,
MaxConns: 1000,
}
}
// NewWithConfig creates a Server from the given configuration.
func NewWithConfig(cfg Config) *Server {
return &Server{
addr: cfg.Addr,
readTimeout: cfg.ReadTimeout,
writeTimeout: cfg.WriteTimeout,
maxConns: cfg.MaxConns,
}
}
Использование:
cfg := server.DefaultConfig()
cfg.MaxConns = 5000
cfg.ReadTimeout = 30 * time.Second
srv := server.NewWithConfig(cfg)
Подход 3: Method Chaining Builder
Классический GoF Builder с цепочкой вызовов. Полезен когда построение требует валидации.
package query
import (
"fmt"
"strings"
)
// Query represents a SQL-like query.
type Query struct {
table string
conditions []string
orderBy string
limit int
offset int
fields []string
}
// Builder constructs a Query step by step.
type Builder struct {
query Query
err error
}
// NewBuilder starts building a query for the given table.
func NewBuilder(table string) *Builder {
if table == "" {
return &Builder{err: fmt.Errorf("table name is required")}
}
return &Builder{
query: Query{
table: table,
fields: []string{"*"},
},
}
}
// Select specifies which fields to retrieve.
func (b *Builder) Select(fields ...string) *Builder {
if b.err != nil {
return b
}
if len(fields) == 0 {
b.err = fmt.Errorf("at least one field is required")
return b
}
b.query.fields = fields
return b
}
// Where adds a condition.
func (b *Builder) Where(condition string) *Builder {
if b.err != nil {
return b
}
b.query.conditions = append(b.query.conditions, condition)
return b
}
// OrderBy sets the ordering.
func (b *Builder) OrderBy(field string) *Builder {
if b.err != nil {
return b
}
b.query.orderBy = field
return b
}
// Limit sets the maximum number of results.
func (b *Builder) Limit(n int) *Builder {
if b.err != nil {
return b
}
if n <= 0 {
b.err = fmt.Errorf("limit must be positive, got %d", n)
return b
}
b.query.limit = n
return b
}
// Offset sets the number of results to skip.
func (b *Builder) Offset(n int) *Builder {
if b.err != nil {
return b
}
b.query.offset = n
return b
}
// Build validates and returns the final Query.
func (b *Builder) Build() (Query, error) {
if b.err != nil {
return Query{}, b.err
}
return b.query, nil
}
// String returns the SQL representation of the query.
func (q Query) String() string {
var sb strings.Builder
sb.WriteString("SELECT ")
sb.WriteString(strings.Join(q.fields, ", "))
sb.WriteString(" FROM ")
sb.WriteString(q.table)
if len(q.conditions) > 0 {
sb.WriteString(" WHERE ")
sb.WriteString(strings.Join(q.conditions, " AND "))
}
if q.orderBy != "" {
sb.WriteString(" ORDER BY ")
sb.WriteString(q.orderBy)
}
if q.limit > 0 {
fmt.Fprintf(&sb, " LIMIT %d", q.limit)
}
if q.offset > 0 {
fmt.Fprintf(&sb, " OFFSET %d", q.offset)
}
return sb.String()
}
Использование:
q, err := query.NewBuilder("users").
Select("id", "name", "email").
Where("age > 18").
Where("active = true").
OrderBy("name").
Limit(10).
Build()
if err != nil {
log.Fatal(err)
}
fmt.Println(q) // SELECT id, name, email FROM users WHERE age > 18 AND active = true ORDER BY name LIMIT 10
Сравнение подходов
| Критерий | Functional Options | Config Struct | Method Chaining |
|---|---|---|---|
| Идиоматичность | Высокая | Высокая | Средняя |
| Расширяемость | Отличная | Хорошая | Хорошая |
| Валидация | При создании | Отдельно | В Build() |
| Значения по умолчанию | Встроены | DefaultConfig() | В Build() |
| Сложность | Средняя | Низкая | Средняя |
| Лучший для | Библиотечных API | Простых конфигов | Сложных объектов |
Когда использовать
Используйте, когда:
- Объект имеет много опциональных параметров
- Нужны разумные значения по умолчанию
- Построение требует пошаговой валидации
- API должен быть расширяемым без breaking changes
Не используйте, когда:
- Объект имеет 1-3 параметра (просто передайте аргументы)
- Все параметры обязательны
- Нет значений по умолчанию
Реальные примеры в экосистеме Go
// google.golang.org/grpc -- functional options
server := grpc.NewServer(
grpc.MaxRecvMsgSize(10<<20),
grpc.UnaryInterceptor(myInterceptor),
)
// net/http -- config struct
srv := &http.Server{
Addr: ":8080",
ReadTimeout: 5 * time.Second,
WriteTimeout: 10 * time.Second,
}
// strings.Builder -- method chaining
var sb strings.Builder
sb.WriteString("Hello, ")
sb.WriteString("World!")