Config Struct -- самый простой подход
Config Struct -- это идиоматичный способ конфигурации в Go, особенно для внутреннего кода. Его суть: создать экспортируемую структуру с полями конфигурации и передавать её в конструктор.
Базовый паттерн
// Config holds database connection configuration.
type Config struct {
Host string
Port int
Database string
User string
Password string
MaxOpenConns int
MaxIdleConns int
ConnMaxLifetime time.Duration
SSLMode string
}
// DefaultConfig returns a Config with sensible defaults.
func DefaultConfig() Config {
return Config{
Host: "localhost",
Port: 5432,
MaxOpenConns: 25,
MaxIdleConns: 5,
ConnMaxLifetime: 5 * time.Minute,
SSLMode: "disable",
}
}
// NewDatabase creates a database connection from config.
func NewDatabase(cfg Config) (*Database, error) {
if cfg.Host == "" {
return nil, fmt.Errorf("database host is required")
}
if cfg.Database == "" {
return nil, fmt.Errorf("database name is required")
}
dsn := fmt.Sprintf("host=%s port=%d dbname=%s user=%s password=%s sslmode=%s",
cfg.Host, cfg.Port, cfg.Database, cfg.User, cfg.Password, cfg.SSLMode,
)
db, err := sql.Open("pgx", dsn)
if err != nil {
return nil, fmt.Errorf("opening database: %w", err)
}
db.SetMaxOpenConns(cfg.MaxOpenConns)
db.SetMaxIdleConns(cfg.MaxIdleConns)
db.SetConnMaxLifetime(cfg.ConnMaxLifetime)
return &Database{db: db}, nil
}
Использование
// Start with defaults, override what you need
cfg := DefaultConfig()
cfg.Database = "myapp"
cfg.User = "admin"
cfg.Password = os.Getenv("DB_PASSWORD")
db, err := NewDatabase(cfg)
if err != nil {
log.Fatal(err)
}
Нулевое значение как дефолт
В Go нулевые значения типов ("", 0, false, nil) имеют определённый смысл. Это можно использовать:
// HTTPClientConfig configures an HTTP client.
// Zero value provides sensible defaults.
type HTTPClientConfig struct {
// Timeout is the request timeout. Zero means 30 seconds.
Timeout time.Duration
// MaxRetries is the number of retries. Zero means no retries.
MaxRetries int
// UserAgent is the User-Agent header. Empty means "Go-HTTP-Client/1.0".
UserAgent string
}
// NewHTTPClient creates a client applying zero-value defaults.
func NewHTTPClient(cfg HTTPClientConfig) *HTTPClient {
if cfg.Timeout == 0 {
cfg.Timeout = 30 * time.Second
}
if cfg.UserAgent == "" {
cfg.UserAgent = "Go-HTTP-Client/1.0"
}
return &HTTPClient{
client: &http.Client{Timeout: cfg.Timeout},
agent: cfg.UserAgent,
retries: cfg.MaxRetries,
}
}
// Usage: zero value works as-is
client := NewHTTPClient(HTTPClientConfig{})
// Override only what you need
client := NewHTTPClient(HTTPClientConfig{
Timeout: 10 * time.Second,
MaxRetries: 3,
})
Проблема: как отличить "не задано" от "задано нулевое значение"?
Если Timeout: 0 означает "использовать дефолт", то как задать "без таймаута"?
// Solution 1: use a pointer (nil = not set)
type Config struct {
Timeout *time.Duration // nil = use default, 0 = no timeout
}
// Solution 2: use a sentinel value
const NoTimeout = -1 * time.Second
// Solution 3: use a separate "set" field
type Config struct {
Timeout time.Duration
TimeoutSet bool
}
В практике чаще всего используют указатели или документируют поведение нулевого значения.
Builder Pattern
Builder полезен, когда конфигурация сложная, шаги имеют зависимости между собой, или нужна валидация на каждом этапе.
Builder с method chaining
// QueryBuilder builds SQL queries safely.
type QueryBuilder struct {
table string
columns []string
conditions []string
args []any
orderBy string
limit int
offset int
err error // accumulate first error
}
// NewQuery starts building a query for the given table.
func NewQuery(table string) *QueryBuilder {
return &QueryBuilder{
table: table,
columns: []string{"*"},
}
}
// Select sets the columns to retrieve.
func (q *QueryBuilder) Select(cols ...string) *QueryBuilder {
if len(cols) > 0 {
q.columns = cols
}
return q
}
// Where adds a WHERE condition with a placeholder argument.
func (q *QueryBuilder) Where(condition string, arg any) *QueryBuilder {
q.conditions = append(q.conditions, condition)
q.args = append(q.args, arg)
return q
}
// OrderBy sets the ORDER BY clause.
func (q *QueryBuilder) OrderBy(col string) *QueryBuilder {
q.orderBy = col
return q
}
// Limit sets the LIMIT clause.
func (q *QueryBuilder) Limit(n int) *QueryBuilder {
if n < 0 {
q.err = fmt.Errorf("limit must be non-negative, got %d", n)
return q
}
q.limit = n
return q
}
// Offset sets the OFFSET clause.
func (q *QueryBuilder) Offset(n int) *QueryBuilder {
q.offset = n
return q
}
// Build generates the SQL query and arguments.
func (q *QueryBuilder) Build() (string, []any, error) {
if q.err != nil {
return "", nil, q.err
}
if q.table == "" {
return "", nil, fmt.Errorf("table name is required")
}
var buf strings.Builder
buf.WriteString("SELECT ")
buf.WriteString(strings.Join(q.columns, ", "))
buf.WriteString(" FROM ")
buf.WriteString(q.table)
if len(q.conditions) > 0 {
buf.WriteString(" WHERE ")
for i, cond := range q.conditions {
if i > 0 {
buf.WriteString(" AND ")
}
buf.WriteString(cond)
}
}
if q.orderBy != "" {
buf.WriteString(" ORDER BY ")
buf.WriteString(q.orderBy)
}
if q.limit > 0 {
buf.WriteString(fmt.Sprintf(" LIMIT %d", q.limit))
}
if q.offset > 0 {
buf.WriteString(fmt.Sprintf(" OFFSET %d", q.offset))
}
return buf.String(), q.args, nil
}
Использование:
query, args, err := NewQuery("users").
Select("id", "name", "email").
Where("age > $1", 18).
Where("active = $2", true).
OrderBy("name ASC").
Limit(20).
Build()
if err != nil {
return fmt.Errorf("building query: %w", err)
}
// query: SELECT id, name, email FROM users WHERE age > $1 AND active = $2 ORDER BY name ASC LIMIT 20
// args: [18, true]
Builder с валидацией и Build()
// ServerBuilder constructs a validated Server.
type ServerBuilder struct {
addr string
port int
tls *tls.Config
handler http.Handler
errors []error
}
// NewServerBuilder creates a new builder with required address.
func NewServerBuilder(addr string) *ServerBuilder {
return &ServerBuilder{
addr: addr,
port: 8080,
}
}
// Port sets the server port.
func (b *ServerBuilder) Port(port int) *ServerBuilder {
if port < 1 || port > 65535 {
b.errors = append(b.errors, fmt.Errorf("invalid port: %d", port))
return b
}
b.port = port
return b
}
// TLS configures TLS for the server.
func (b *ServerBuilder) TLS(cert, key string) *ServerBuilder {
c, err := tls.LoadX509KeyPair(cert, key)
if err != nil {
b.errors = append(b.errors, fmt.Errorf("loading TLS cert: %w", err))
return b
}
b.tls = &tls.Config{
Certificates: []tls.Certificate{c},
MinVersion: tls.VersionTLS12,
}
return b
}
// Handler sets the HTTP handler.
func (b *ServerBuilder) Handler(h http.Handler) *ServerBuilder {
b.handler = h
return b
}
// Build creates the Server, returning all accumulated errors.
func (b *ServerBuilder) Build() (*http.Server, error) {
if b.handler == nil {
b.errors = append(b.errors, fmt.Errorf("handler is required"))
}
if len(b.errors) > 0 {
return nil, fmt.Errorf("server build failed: %w", errors.Join(b.errors...))
}
s := &http.Server{
Addr: fmt.Sprintf("%s:%d", b.addr, b.port),
Handler: b.handler,
TLSConfig: b.tls,
}
return s, nil
}
Использование:
srv, err := NewServerBuilder("0.0.0.0").
Port(443).
TLS("cert.pem", "key.pem").
Handler(mux).
Build()
if err != nil {
log.Fatal(err)
}
Пример: конфигурация HTTP-клиента тремя способами
Способ 1: Config Struct
type ClientConfig struct {
BaseURL string
Timeout time.Duration
Retries int
AuthToken string
Debug bool
}
func NewClient(cfg ClientConfig) *Client {
if cfg.Timeout == 0 {
cfg.Timeout = 30 * time.Second
}
return &Client{cfg: cfg}
}
// Usage
c := NewClient(ClientConfig{
BaseURL: "https://api.example.com",
Timeout: 10 * time.Second,
Retries: 3,
AuthToken: token,
})
Способ 2: Functional Options
type Option func(*Client)
func WithTimeout(d time.Duration) Option { return func(c *Client) { c.timeout = d } }
func WithRetries(n int) Option { return func(c *Client) { c.retries = n } }
func WithAuth(token string) Option { return func(c *Client) { c.token = token } }
func WithDebug() Option { return func(c *Client) { c.debug = true } }
func NewClient(baseURL string, opts ...Option) *Client {
c := &Client{baseURL: baseURL, timeout: 30 * time.Second}
for _, opt := range opts {
opt(c)
}
return c
}
// Usage
c := NewClient("https://api.example.com",
WithTimeout(10*time.Second),
WithRetries(3),
WithAuth(token),
)
Способ 3: Builder
func NewClientBuilder(baseURL string) *ClientBuilder {
return &ClientBuilder{baseURL: baseURL, timeout: 30 * time.Second}
}
func (b *ClientBuilder) Timeout(d time.Duration) *ClientBuilder {
b.timeout = d
return b
}
func (b *ClientBuilder) Retries(n int) *ClientBuilder {
b.retries = n
return b
}
func (b *ClientBuilder) Auth(token string) *ClientBuilder {
b.token = token
return b
}
func (b *ClientBuilder) Build() (*Client, error) {
if b.baseURL == "" {
return nil, fmt.Errorf("base URL is required")
}
return &Client{/* ... */}, nil
}
// Usage
c, err := NewClientBuilder("https://api.example.com").
Timeout(10*time.Second).
Retries(3).
Auth(token).
Build()
Таблица сравнения
| Критерий | Config Struct | Functional Options | Builder |
|---|---|---|---|
| Простота | Самый простой | Средняя | Сложнее всего |
| Код для написания | Минимум | Функция на каждую опцию | Метод на каждую опцию + Build |
| Дефолты | Отдельная функция | В конструкторе | В конструкторе Builder |
| Валидация | В конструкторе | В каждой опции | В Build() |
| API stability | Новое поле = возможный breaking change | Новая опция = safe | Новый метод = safe |
| Читаемость при вызове | Хорошая (named fields) | Отличная (self-documenting) | Отличная (method chain) |
| Подходит для | Внутренний код, простые случаи | Библиотеки, публичные API | Сложная пошаговая конфигурация |
| Пример в экосистеме | http.Transport{} |
grpc.Dial(opts...) |
strings.Builder |
Рекомендации
- Config Struct -- используйте по умолчанию для внутреннего кода
- Functional Options -- для библиотек и публичных API, где важна обратная совместимость
- Builder -- когда конфигурация имеет шаги, зависимости или сложную валидацию
Часто подходы комбинируются:
// Config struct with functional options for overrides
type Config struct {
Host string
Port int
}
type Option func(*Config)
func WithPort(p int) Option {
return func(c *Config) { c.port = p }
}
func NewService(cfg Config, opts ...Option) *Service {
for _, opt := range opts {
opt(&cfg)
}
return &Service{cfg: cfg}
}