«А где мои опциональные параметры?» — крик души Go-разработчика
Вы только что перешли на Go с Python или JavaScript и пишете функцию для отправки email. В Python это выглядело бы так:
def send_email(to, subject, cc=None, bcc=None, attachments=None):
pass
Всё просто: cc, bcc и attachments — опциональные параметры. Их можно не передавать, и функция сработает со значениями по умолчанию.
Но вот незадача: в Go нет перегрузки методов и нет именованных аргументов с умолчаниями. Сообщество Stack Overflow подтверждает: вопрос «Optional Parameters in Go?» набирает тысячи просмотров. Это реальная боль новичков.
Хорошая новость: в Go есть как минимум 5 элегантных способов решить эту задачу. В этой статье мы разберём их все — от простых до промышленных.
Почему Go такой «строгий»?
Команда Go сознательно отказалась от опциональных параметров, чтобы:
- Упростить чтение кода — вы всегда видите, сколько аргументов передаётся в функцию.
- Избежать магического поведения — никаких неожиданных дефолтов.
- Ускорить компиляцию — меньше синтаксического сахара.
Но это не значит, что вы не можете сделать код гибким. Давайте посмотрим, как обойти ограничения.
Способ 1: Вариативные функции (Variadic Functions) — «дешёво и сердито»
Самый простой способ — использовать троеточие ... для передачи переменного числа однотипных аргументов.
package main
import "fmt"
// Sum принимает любое количество целых чисел
func Sum(nums ...int) int {
total := 0
for _, n := range nums {
total += n
}
return total
}
func main() {
fmt.Println(Sum(1, 2, 3)) // 6
fmt.Println(Sum(10, 20)) // 30
fmt.Println(Sum()) // 0 — тоже работает
}
Плюсы: простота, встроенная поддержка языка.
Минусы: все аргументы должны быть одного типа. Нельзя сказать «передай строку, а потом опциональный int».
Вариативные функции в Go отлично подходят для логгеров, сумматоров и любых операций, где количество данных заранее неизвестно.
Способ 2: Структура конфигурации (Configuration Struct) — «когда параметров много»
Если у вас больше 2-3 опциональных параметров разных типов — используйте структуру.
package main
import "fmt"
type EmailConfig struct {
CC []string
BCC []string
Attachments []string
IsHTML bool
}
func SendEmail(to, subject, body string, cfg EmailConfig) {
fmt.Printf("To: %s, Subject: %s\n", to, subject)
if cfg.IsHTML {
fmt.Println("HTML mode enabled")
}
if len(cfg.CC) > 0 {
fmt.Printf("CC: %v\n", cfg.CC)
}
}
func main() {
// Используем значения по умолчанию (zero values)
SendEmail("user@example.com", "Hello", "Body", EmailConfig{})
// Переопределяем некоторые поля
cfg := EmailConfig{
CC: []string{"boss@example.com"},
IsHTML: true,
}
SendEmail("user@example.com", "Hello", "Body", cfg)
}
Обратите внимание: zero values в Go (пустая строка, 0, nil для слайсов) становятся значениями по умолчанию. Это и есть наша реализация опциональных параметров в Go через структуры.
Плюсы: читаемо, типизировано, легко расширять.
Минусы: нужно создавать структуру каждый раз, даже если ничего не меняешь.
Способ 3: Functional Options Pattern — «золотой стандарт Go»
Этот паттерн придумал Дэйв Чейни, один из ключевых разработчиков Go. Он используется в таких библиотеках, как gRPC, AWS SDK, и собственной стандартной библиотеке Go (например, http.Server).
package main
import (
"fmt"
"time"
)
// Option — тип функции, которая меняет конфигурацию
type Option func(*Server)
// Server — наша структура с параметрами по умолчанию
type Server struct {
Port int
Timeout time.Duration
MaxConn int
}
// NewServer — конструктор с опциональными параметрами
func NewServer(opts ...Option) *Server {
// Значения по умолчанию
srv := &Server{
Port: 8080,
Timeout: 30 * time.Second,
MaxConn: 100,
}
// Применяем все переданные опции
for _, opt := range opts {
opt(srv)
}
return srv
}
// WithPort — опция для установки порта
func WithPort(port int) Option {
return func(s *Server) {
s.Port = port
}
}
// WithTimeout — опция для таймаута
func WithTimeout(t time.Duration) Option {
return func(s *Server) {
s.Timeout = t
}
}
func main() {
// Сервер с настройками по умолчанию
srv1 := NewServer()
fmt.Printf("Default: %+v\n", srv1)
// Сервер с кастомным портом и таймаутом
srv2 := NewServer(WithPort(9090), WithTimeout(60*time.Second))
fmt.Printf("Custom: %+v\n", srv2)
}
Плюсы: очень гибко, легко добавлять новые опции без изменения сигнатуры, читаемый код.
Минусы: немного шаблонного кода, может быть сложно для новичков.
Этот паттерн Functional Options — де-факто стандарт для Go-проектов, когда нужны опциональные аргументы в Go.
Способ 4: Builder Pattern — «понятно и цепочечно»
Если вам нужно собирать конфигурацию пошагово, используйте Builder.
package main
import "fmt"
type QueryBuilder struct {
table string
where string
orderBy string
limit int
}
func NewQueryBuilder(table string) *QueryBuilder {
return &QueryBuilder{table: table}
}
func (qb *QueryBuilder) Where(condition string) *QueryBuilder {
qb.where = condition
return qb
}
func (qb *QueryBuilder) OrderBy(field string) *QueryBuilder {
qb.orderBy = field
return qb
}
func (qb *QueryBuilder) Limit(n int) *QueryBuilder {
qb.limit = n
return qb
}
func (qb *QueryBuilder) Build() string {
query := "SELECT * FROM " + qb.table
if qb.where != "" {
query += " WHERE " + qb.where
}
if qb.orderBy != "" {
query += " ORDER BY " + qb.orderBy
}
if qb.limit > 0 {
query += fmt.Sprintf(" LIMIT %d", qb.limit)
}
return query
}
func main() {
q := NewQueryBuilder("users").
Where("age