MidТеория4 min

Builder

Паттерн Builder в Go: пошаговое создание, functional options и method chaining

Builder (Строитель)

Проблема

Вам нужно создавать сложные объекты с множеством параметров, часть из которых опциональна. Конструктор с 10+ параметрами нечитаем и хрупок. Builder позволяет создавать объект пошагово.

В Go есть три подхода, от самого идиоматичного к менее:

  1. Functional Options -- самый Go-идиоматичный
  2. Config struct -- простой и понятный
  3. 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!")

Проверь себя

Какой подход к Builder-паттерну считается самым идиоматичным в Go?

Как Method Chaining Builder обрабатывает ошибки валидации?

Как Functional Options обрабатывают значения по умолчанию?