惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

www.infosecurity-magazine.com
www.infosecurity-magazine.com
D
DataBreaches.Net
T
Tailwind CSS Blog
M
MIT News - Artificial intelligence
Stack Overflow Blog
Stack Overflow Blog
F
Full Disclosure
V2EX - 技术
V2EX - 技术
N
News and Events Feed by Topic
Help Net Security
Help Net Security
L
LangChain Blog
Y
Y Combinator Blog
宝玉的分享
宝玉的分享
Google Online Security Blog
Google Online Security Blog
P
Proofpoint News Feed
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
T
The Blog of Author Tim Ferriss
Google DeepMind News
Google DeepMind News
The Register - Security
The Register - Security
B
Blog RSS Feed
N
Netflix TechBlog - Medium
N
News | PayPal Newsroom
TaoSecurity Blog
TaoSecurity Blog
酷 壳 – CoolShell
酷 壳 – CoolShell
V
Vulnerabilities – Threatpost
B
Blog
C
Cyber Attacks, Cyber Crime and Cyber Security
I
Intezer
H
Hackread – Cybersecurity News, Data Breaches, AI and More
博客园_首页
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
AI
AI
aimingoo的专栏
aimingoo的专栏
大猫的无限游戏
大猫的无限游戏
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
Cyberwarzone
Cyberwarzone
P
Proofpoint News Feed
Google DeepMind News
Google DeepMind News
G
GRAHAM CLULEY
Vercel News
Vercel News
罗磊的独立博客
MyScale Blog
MyScale Blog
Last Week in AI
Last Week in AI
博客园 - 司徒正美
C
CERT Recently Published Vulnerability Notes
GbyAI
GbyAI
Scott Helme
Scott Helme
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
T
Troy Hunt's Blog
A
About on SuperTechFans
P
Privacy International News Feed

Все публикации подряд на Хабре

Ловим музу за клавиатуру: как айтишнику стать автором Что умеет Midjourney в 2026? Мой немного грустный разбор этого шикарного инструмента Никто не любит писать тесты, но ИИ может исправить это IPv8 выглядит как мечта. Поэтому почти наверняка не взлетит Производители вернули в продажу материнки с DDR3. Что происходит? Управление агентом с телефона через Telegram теперь в KodaCode От координации к лидерству: как меняется роль руководителя разработки Я сделала родителям бизнес вместо пенсии: зарабатываем 70 тысяч, мама не даёт продать В три раза быстрее приемка товара и оптимизация трудозатрат на 73%: как «РСТ-Инвент» помог Gulliver Group ИИ-шечный мир победил? О влиянии искусственного интеллекта на игропром Кремль снижает давление на Телеграмм пока Европа строит интернет по паспорту Как CEO, CTO и CIO за 8 часов собрали ИИ-директора, который умеет держать позицию под давлением Как (не) потерять домен за выходные Вместо 8 разных VPS: как я организовал практику студентам на одном сервере Почему твой Open Source проект не замечают? R&D: искусство управления неопределенностью в разработке AI-дефляция: вакансий для разработчиков больше, а рост зарплат — худший за 15 лет Мы отдали управление роботами OpenClaw. Что из этого вышло Галактический ID: система идентификации для всех форм разумной жизни Шесть основ бизнес-анализа: начинаем с вопроса «Кто в игре?» Код-ревью, в котором дело не в коде Данные переехали. Команда — нет Системной подход к сдаче OSWE в 2025 Почему комната управления реактором покрашена в цвет морской пены 4 YAML-файла вместо PySpark: как аналитикам строить пайплайны без разработчиков LLM-агент для поиска свободных доменов: автоматизируем подбор Когда, зачем и как правильно начинать новую сессию в Claude Code? Как я заставил нейросеть писать макросы для FreeCAD Анатомия ИИ‑агента для подбора персонала. От тысячи резюме к топ‑10 за минуты Опыт разработчика как экономика внимания Автономность как точка невозврата: кто будет субъектом в цифровом будущем Обучение ИИ в «диких» условиях: как рутинные действия превращаются в датасеты Как измерить LLM для задач кибербеза: обзор открытых бенчмарков Где хранить код? Сравнение GitHub, GitLab и Bitbucket Математика объясняет, почему нормальное распределение встречается повсюду Почему ваш FinOps не работает: 12 тезисов от практиков Как подписать проектную документацию УКЭП с использованием бесплатных лицензий Pilot Адаптивное администрирование Sigla Vision Я грузил уран в бочки, а потом 20 лет строил ИТ в атомной отрасли Чем позвонить с Эвереста? История и обзор спутниковой связи. Часть 2 Как языковая модель помогает контролировать качество инструктажей по охране труда в металлургии Как не передать на desktop свой IP в РКН Анатомия SAP Privileges: как устроено управление правами в macOS MoneyDev: Сказка про три главных слова Обновлённый токенизатор видео K-VAE 2.0 от Сбера Как сделать диспетчеризацию дома на 1284 квартиры почти бесплатно Как мы разогнали железную дорогу Мы дали агентам рутину. Теперь надо решить — что делать с освободившимся временем Токсичный контент, промпт-хакинг и защита ИИ — всё о Guardrails для LLM Умный город начинается с точного взгляда: как «Фалькон Тех» меняет пространство к лучшему Навайбкодил приложение для анализа графов Почему Дюну так интересно читать? Упрощаем работу с рутиной или как стать Гендальфом Белым Деконструкция Go: CPU, RAM и что там происходит. Go Assembler база. Часть 1.1 Какие профессии исчезнут из-за ИИ, а какие появятся? И что с этим делать Как мы построили IT-отдел, где хочется расти: архитектурные встречи, прозрачные метрики и книжные подарки Rufler: Делаем из Claude Code автономный рой через один YAML-конфиг Sing-box и белый список приложений Как построить надёжный обмен сообщениями в микросервисах: лучшие практики для enterprise OpenAI строит MLM-пирамиду, а McKinsey и Accenture помогают ей в этом Дом, который не построил Фишер (Часть 2) «Сверхзвуковой математик» против «Вдумчивого логиста»: битва алгоритмов 3D-упаковки Мультимодальные модели – грубый и дорогой инструмент Разговоры ничего не стоят. Код тоже Проверки физических лиц: с кого начнет ФНС Топ-10 бесплатных нейросетей для создания видео в 2026 году Первые слои кода: как наши решения сегодня определяют архитектуру ИИ на десятилетия Разработка нового статического анализатора: PVS-Studio JavaScript Поиск уязвимостей ПО: базовый минимум или роскошный максимум Почему оценка персонала не работает как инструмент управления Как мы разработали ИИ-ассистента и сократили рутину продуктовой команды на 50% Как я ушел из найма, нажарил косточек и продал на маркетплейсах на 168 млн в год Когда 1С:ERP уже внедрена, а нормального производственного плана всё ещё нет Как я сделал Claude мультимодальным, подключив к нему Qwen Omni Как приглашение на вакансию мечты превращается в атаку Infrastructure as Code: философия и лучшие практики IaC Тестируем Yandex Code Assistant на задаче, в которой нужно хранить секреты nxs-universal-chart v3.0: новое поколение универсального Helm-чарта Callback Injection: Техника, которая отправила Microsoft Defender в глухой нокаут «Все идеи на стол»: митап как способ вывести проект из тупика Сегодня я узнал нечто новое о GPU благодаря багу в своей игре Как заставить LLM ̶ ̶г̶а̶л̶л̶ю̶ ̶ эволюционировать Карта событий как фундамент аналитики: практический кейс для E-commerce Что выбрать для AI: x86, ARM или RISC-V? Дайджест железа за март Роль соматических мутаций в развитии аутоиммунных заболеваний: путь к избирательной терапии Mythos от Anthropic — тревожный сигнал для всех, а не только для банков Guardrails для LLM на Java: как приручить промпт‑инъекции и токсичные ответы Green-VLA: как мы собрали VLA-модель для реального антропоморфного робота и не потеряли обобщение Финансовая гонка вооружений: почему умные люди добровольно в ней участвуют Эра ИИ-агентов наступила: выбираем лучшего цифрового сотрудника # Практический опыт внедрения WinCC Redundancy на производственном предприятии Сделал MVP за 3 дня, а потом неделю прикручивал оплату. Оно того стоило? Физика против Маска: почему Starship V3 может оказаться ещё одной катастрофой Нефть Венесуэлы: крупнейшие запасы в мире, но не крупнейшая нефтяная держава JPA 4. Переосмысление Hibernate Почему зеркальная фотокамера Nikon D5 десятилетней давности идеально подошла для миссии «Артемида-2» Проект «Уровень-Спутник» или как мы сделали платформу для гидрологов «Замедлиться, чтобы ускориться»: почему ИИ повышает цену ошибок в требованиях и архитектуре Как с нуля поднять трафик IT-компании на 1657% при бюджете 55 тыс. и выжить Pixel-perfect Downsampling — идеальная отрисовка 50 миллионов точек без потерь
Resolvable Config Struct — отличная альтернатива Functional Options в Go
Alex Efros · 2026-05-29 · via Все публикации подряд на Хабре

Если честно, то лично я считаю, что это не просто «альтернатива», а предпочтительный выбор по умолчанию для большинства API (где конфигурация — это именно данные).

По большому счёту, паттерн Resolvable Config Struct просто наводит порядок в тех структурах Config и Options, которыми мы все пользуемся на порядок-два чаще, чем Functional Options. Чтобы его внедрить понадобится лишь минимальный рефакторинг, потому что всеми элементами этого паттерна мы и так постоянно пользуемся, просто сейчас там бардак, в который нужно добавить немного строгости чтобы стало намного лучше.

TL;DR: Код говорит лучше тысячи слов, да?

// Enum.
type TLSMode uint8

const (
    TLSDefault TLSMode = iota
    TLSEnabled
    TLSDisabled

    tlsModeCount
)

func (m TLSMode) Valid() bool { return m < tlsModeCount }

// Sentinel.
const NoTimeout time.Duration = -1

type Config struct {
    // --- Core fields (no default semantics) ---
    AppName          string
    Tags             []string
    Headers          map[string]string
    // --- Options (has defaults) ---
    // Zero value is not valid and thus means "use default".
    Host             string        // "" = default
    Port             int           // 0 = default
    // Enum (alternative to *bool which does not enforce Clone requirement).
    TLS              TLSMode       // default/enabled/disabled
    // Pointer (all values are valid including zero).
    WriteBufferBytes *int          // nil = default, new(0) = explicit zero
    // Sentinel (zero is a valid value, but not all values are valid).
    ReadTimeout      time.Duration // 0 = default, NoTimeout = disable
    // Separate flag/enum (alternative to Pointer and Sentinel).
    WriteTimeout     time.Duration // 0 = default
    NoWriteTimeout   bool          // true = disable (ignore WriteTimeout value)
}

// Required only if Config contains references.
func (c Config) Clone() Config {
    c.Tags = slices.Clone(c.Tags)
    c.Headers = maps.Clone(c.Headers)
    if c.WriteBufferBytes != nil {
        c.WriteBufferBytes = new(*c.WriteBufferBytes)
    }
    return c
}

// Required! Idempotent.
func (c Config) Resolve() (Config, error) {
    c = c.Clone()
    // Defaults.
    if c.Host == "" {
        c.Host = "localhost"
    }
    if c.Port == 0 {
        c.Port = 1234
    }
    if c.TLS == TLSDefault {
        c.TLS = TLSEnabled
    }
    if c.WriteBufferBytes == nil {
        c.WriteBufferBytes = new(4096)
    }
    // Normalize.
    c.AppName = strings.TrimSpace(c.AppName)
    // Validate.
    var err error
    if c.AppName == "" {
        err = errors.Join(err, ErrNoAppName)
    }
    if !c.TLS.Valid() {
        err = errors.Join(err, ErrInvalidTLSMode)
    }
    if c.ReadTimeout < NoTimeout {
        err = errors.Join(err, ErrInvalidReadTimeout)
    }
    if c.NoWriteTimeout && c.WriteTimeout != 0 {
        err = errors.Join(err, ErrConflictingWriteTimeout)
    }
    return c, err
}

func NewClient(cfg Config) (*Client, error) {
    cfg, err := cfg.Resolve()
    if err != nil {
        return nil, err
    }
    return &Client{cfg: cfg}, nil
}

Предыстория

В Go есть паттерн, который принято считать почти стандартным ответом на проблему «у конструктора слишком много опций». Это Functional Options.

Обычно история рассказывается так: позиционные параметры перестают масштабироваться, поэтому мы заменяем их на WithX(...), получаем аккуратный вызов, и дальше живём счастливо.

Проблема в том, что в реальном коде Functional Options почти всегда проигрывают обычной структуре конфигурации. Не иногда, не «в некоторых командах», а почти всегда. И именно поэтому в нашем коде структуры Config и Options встречаются намного чаще, чем Functional Options.

Functional Options требуют безумный объём шаблонного кода. У них плохая discoverability — сложно увидеть список всех доступных опций. Их неудобно задавать по условиям. Их неудобно выводить для отладки. Их неудобно хранить как данные. Их неудобно передавать между слоями. Их неудобно читать из файла. Их неудобно показывать пользователю как итоговую конфигурацию. Их… или я уже придираюсь? Ладно, хватит так хватит, но если что - я могу ещё.

Безусловно, когда появились Functional Options они решали вполне реальные проблемы. Но если сравнивать с Resolvable Config Struct у Functional Options останется только один реальный плюс: они позволяют нагляднее задавать сложные опции. Например, WithRetry(backoff, attempts, jitter) выглядит естественно. На структуре это потребует отдельного подтипа, что более громоздко и редко оправдано. Но это не настолько критично и не так часто нужно, чтобы оправдать все остальные издержки.

На практике в Go на порядки чаще побеждают обычные Config-структуры. Просто они обычно реализованы хаотично. Где-то есть значения по умолчанию, где-то нет… Где-то есть проверка, где-то нет… Где-то ссылочное поле копируется, где-то нет… Где-то 0 означает «используй значение по умолчанию», а где-то — «явно выключено». Плюс много других мелких отличий, но, думаю, вы все уже узнали свои проекты.

Именно из этого хаоса и вырастает паттерн, который я называю Resolvable Config Struct.

Это не «ещё одна структура с полями», а способ привести распространённый, уже доминирующий в Go стиль к единому и безопасному виду почти не усложняя реализацию.

Что такое Resolvable Config Struct

Resolvable Config Struct — это паттерн, в котором:

  • параметры с поведением «не задано → взять значение по умолчанию» существуют как поля в обычной структуре Config или Options;

  • для каждого такого поля представление выбирается так, чтобы его zero значение означало «не задано → использовать default»; если zero значение исходного типа нужно как самостоятельное значение, поле переоформляется любым сохраняющим эту семантику способом (напр. используя указатель, sentinel value, enum или вспомогательное поле-флаг);

  • если в конфигурации есть ссылочные поля, она реализует Clone() (deep copy);

  • перед использованием конфигурация проходит через идемпотентный Resolve(), который клонирует, применяет значения по умолчанию, нормализует и валидирует.

То есть это полный жизненный цикл конфигурации: сырой ConfigClone() → значения по умолчанию → нормализация → проверка → готовый Config.

Добавление Resolve() может показаться незначительной деталью, которую можно реализовать массой других способов, но на самом деле это центральная часть паттерна. Именно она превращает структуру из пассивного набора полей в соглашение, с которым комфортно жить.

Если обязательные параметры хочется оставить позиционными - паттерн этому не мешает. Можно сделать NewClient(appName string, cfg Config). Он описывает не все возможные параметры конструктора, а именно необязательную часть конфигурации.

Почему Functional Options почти всегда хуже

Обычно Functional Options защищают ссылкой на «красивый вызов конструктора». Но если смотреть не на одну строку вызова, а на весь срок жизни конфигурации, картина резко меняется.

1. Это огромный объём шаблонного кода

Для каждой опции нужно написать отдельную функцию. Часто — отдельный тип. Иногда — отдельную логику проверки.

type Option func(*Config)

func WithHost(host string) Option {
    return func(c *Config) { c.Host = host }
}

func WithPort(port int) Option {
    return func(c *Config) { c.Port = port }
}

func WithTLS(mode TLSMode) Option {
    return func(c *Config) { c.TLS = mode }
}

На три поля это выглядит терпимо. На десять — уже раздражает. Если валидацию делают сами опции и возвращают error - код раздувается намного сильнее.

Со структурой конфигурации эквивалентный API выглядит так:

type Config struct {
    Host string
    Port int
    TLS  TLSMode
}

2. У них плохая discoverability

Когда я вижу Config, я открываю определение структуры и сразу вижу весь набор параметров, их типы, взаимосвязи, и нередко комментарии про семантику.

Когда я вижу Functional Options, мне нужно искать все WithX по пакету, собирать модель API по нескольким точкам, и отдельно разбираться, какие из этих функций относятся именно к этому конструктору, а какие вообще к другому объекту.

Это особенно неприятно в больших пакетах, где WithTimeout или WithLogger могут существовать в нескольких вариантах.

3. Их не так удобно задавать условно

Очень частый реальный сценарий: часть параметров включается только при некоторых условиях.

Со структурой это выглядит естественно:

cfg := Config{AppName: "billing"}
if debug {
    cfg.TLS = TLSDisabled
}
if writeBuf > 0 {
    cfg.WriteBufferBytes = new(writeBuf)
}
client, err := NewClient(cfg)

С тернарником ещё лучше:

client, err := NewClient(Config{
    AppName:          "billing",
    TLS:              lo.Ternary(debug, TLSDisabled, TLSDefault),
    WriteBufferBytes: lo.Ternary(writeBuf > 0, new(writeBuf), nil),
})

С Functional Options это обычно превращается в накопление среза:

opts := []Option{}
if debug {
    opts = append(opts, WithTLS(TLSDisabled))
}
if writeBuf > 0 {
    opts = append(opts, WithWriteBufferBytes(writeBuf))
}
client, err := NewClient("billing", opts...)

Формально, это тоже работает нормально. Но структура здесь чаще выигрывает тем, что остаётся обычным значением, с которым можно свободно работать до вызова конструктора.

4. Их неудобно отлаживать

Если у вас есть Config, вы можете его напечатать, передать дальше, сохранить, сравнить, или вывести как «итоговую конфигурацию после применения значений по умолчанию».

С Functional Options вы в лучшем случае видите набор функций-замыканий. То есть теряете конфигурацию как данные.

Да, внутри конструктора вы почти наверняка всё равно разворачиваете эти опции в какой-то внутренний Config. Но именно это и делает паттерн сомнительным: он заставляет сначала спрятать данные в поведение, а потом обратно восстановить поведение в данные.

5. Структура лучше работает как компактная форма данных

Одна из сильных сторон структуры в Go — возможность быстро видеть соответствие «имя поля → значение».

client, err := NewClient("billing", Config{
    Host:             "localhost",
    Port:             8443,
    TLS:              TLSEnabled,
    WriteBufferBytes: new(0),
})

Такой код удобно читать, править и проверять в ревью.

С Functional Options запись обычно не короче, но при этом уже не выглядит как одно компактное значение конфигурации:

client, err := NewClient(
    "billing",
    WithHost("localhost"),
    WithPort(8443),
    WithTLS(TLSEnabled),
    WithWriteBufferBytes(0),
)

В чём проблема существующих структур конфигурации

Обычные Config-структуры в Go на пару порядков популярнее Functional Options. Потому, что они проще, прозрачнее, понятнее, и лучше соответствуют природе языка.

Проблема в том, что они везде реализованы по-разному.

Одна команда считает, что 0 значит «возьми значение по умолчанию». Другая — что это валидное явное значение. Где-то ссылочные поля копируются. Где-то — нет. Где-то проверка вызывается из конструктора. Где-то — руками заранее. Где-то есть нормализация. Где-то никто не помнит, когда она должна происходить.

Именно это паттерн Resolvable Config Struct и пытается исправить. Не придумать новый способ писать конструкторы, а стандартизировать самый естественный и распространённый способ.

Ключевая идея: конфигурация — это данные

Если принять, что конфигурация — это прежде всего данные, то из этого почти автоматически следуют остальные правила.

Она должна:

  • быть удобно представима в виде значения;

  • быть пригодной для хранения и передачи;

  • иметь явную модель необязательных полей;

  • иметь явную точку приведения к рабочему состоянию;

  • не делить внутреннее состояние со внешним кодом по ошибке.

Минимальный пример выглядит так:

type Config struct {
    Host string
    Port int
}

func (c Config) Resolve() (Config, error) {
    if c.Host == "" {
        c.Host = "localhost"
    }
    if c.Port == 0 {
        c.Port = 8080
    }
    return c, nil
}

Пока структура состоит только из значимых нулевых значений и простых типов, этого достаточно.

Но в реальном API почти сразу появляются более сложные случаи.

Как кодировать «не задано»

В Go нет встроенного Option[T], поэтому различие между «не задано» и «задано явно» приходится моделировать вручную.

Плюс паттерна в том, что он заставляет решить этот вопрос явно, а не размазывать по десятку WithX.

Обычно достаточно четырёх вариантов.

Обычное поле

Если нулевое значение не нужно как явное, то его можно объявить прямо:

type Config struct {
    Host string // "" = значение по умолчанию
    Port int    // 0 = значение по умолчанию
}

Указатель

Если все значения валидны, включая ноль, а отсутствие значения тоже важно, то подходит указатель:

type Config struct {
    WriteBufferBytes *int // nil = значение по умолчанию, new(0) = явный ноль
}

Sentinel

Если у типа есть заведомо недопустимые значения, одно из них можно занять под специальный режим:

const NoTimeout time.Duration = -1

type Config struct {
    ReadTimeout time.Duration // 0 = значение по умолчанию, NoTimeout = отключить
}

Флаг или enum

Если поле выражает режим, а не просто число, то лучше выделить это в явный тип:

type TLSMode uint8

const (
    TLSDefault TLSMode = iota
    TLSEnabled
    TLSDisabled
)

type Config struct {
    TLS TLSMode
}

Или так:

type Config struct {
    WriteTimeout   time.Duration // 0 = значение по умолчанию
    NoWriteTimeout bool          // true = отключить
}

Clone() — не boilerplate, а штатный паттерн языка…

…хотя и кривой, но тут уж ничего не поделаешь.

Как только в конфигурации появляются ссылочные поля, нужно решить вопрос владения.

Если вызвать конструктор с таким Config:

type Config struct {
    Tags             []string
    Headers          map[string]string
    WriteBufferBytes *int
}

что будет, если вызывающий код потом изменит Tags, Headers или значение по ссылке WriteBufferBytes?

Если конструктор просто сохранит ссылки, внутреннее состояние объекта станет разделяемым снаружи. Для параметров конструктора это почти всегда ошибка.

Поэтому у паттерна здесь жёсткое правило: если структура содержит ссылочные поля, то она реализует Clone() (deep copy).

В актуальном Go это можно сделать компактно:

func (c Config) Clone() Config {
    c.Tags = slices.Clone(c.Tags)
    c.Headers = maps.Clone(c.Headers)
    if c.WriteBufferBytes != nil {
        c.WriteBufferBytes = new(*c.WriteBufferBytes)
    }
    return c
}

Resolve() — сердце паттерна

Без Resolve() структура конфигурации быстро превращается в набор полей, вокруг которых все договорённости держатся на памяти команды.

С Resolve() появляется единая точка, в которой происходит всё приведение к рабочему виду:

  • копирование ссылочных значений;

  • проставление значений по умолчанию;

  • нормализация;

  • валидация.

Почему это стоит принять как стандартный подход

Самое важное достоинство Resolvable Config Struct, на мой взгляд, не в том, что он придумывает новую технику. Его сила в другом: он берёт уже естественный, самый массовый, самый дешёвый для Go стиль и добавляет к нему недостающую строгость.

Не нужно строить дополнительный слой API поверх структуры. Не нужно плодить десятки WithX. Не нужно прятать данные в замыкания. Не нужно выбирать между «простая структура» и «безопасная структура».

Нужно просто договориться, что:

  • необязательные параметры живут в Config;

  • zero-значение для них всегда означает “значение по умолчанию”;

  • ссылочные поля копируются в Clone();

  • рабочее состояние получается через Resolve();

  • весь код ниже по стеку работает уже с результатом Resolve().

Это почти ничего не стоит, но очень сильно уменьшает хаос.

Вывод

У Functional Options была реальная историческая задача.

Роб Пайк показал красивую технику самоссылочных функций для временного изменения состояния с возможностью отката. Это изящный приём, но он вообще-то не про конструкторы как таковые, а про обратимые настройки.

А вот Дейв Чейни уже говорил именно про API конструкторов, и его претензии были вполне справедливы. Длинные сигнатуры неудобны. Зоопарк NewXxxWithYyy не масштабируется. Обязательная передача Config{} или nil ради значений по умолчанию выглядит плохо. 0 в конфигурации по значению часто двусмысленен. *Config создаёт риск разделяемого состояния. Все эти проблемы реальны.

Но Functional Options решают их в основном на поверхности API. Они убирают длинную сигнатуру и позволяют растить конструктор без её изменения, ценой того, что конфигурация перестаёт быть данными и превращается в набор функций.

Resolvable Config Struct предлагает решить те же самые проблемы в более правильном месте — в самой модели конфигурации.

Если не хочется заставлять пользователя писать Config{} ради вызова по умолчанию, то это решается дизайном конструктора (например принимать nil или добавить NewClient() рядом с NewClientWithConfig(cfg Config)), а не обязательным переходом к WithX(...). Если 0 двусмысленен, то это надо чинить явным кодированием состояния «не задано» через zero-значение, используя другие техники для компенсации “съеденного” zero-значения когда оно нужно (через указатель, sentinel, enum или отдельный флаг). Если *Config опасен из-за shared state, то это чинится Clone(). Если значения по умолчанию, нормализация и проверка размазаны по коду, то это чинится Resolve().

В результате мы сохраняем главное обещание хорошего API: конструктор можно расширять без раздувания сигнатуры, а поведение по умолчанию остаётся простым. Но при этом не теряем конфигурацию как данные. Её можно вывести для отладки, сравнить в тесте, передать между слоями, сериализовать, загрузить из файла и показать пользователю как итоговое состояние после Resolve().

Мой поинт не в том, что Functional Options были ошибкой. Скорее это был разумный ответ на плохие конструкторы и наивные Config-структуры образца «просто положим всё в поля и как-нибудь разберёмся». Но сегодня есть решение получше, и его стоит внедрять.

Поэтому, если у вас уже есть Config, осталось сделать совсем немного:

  • явно кодировать «не задано» в zero-значениях;

  • копировать ссылочные поля в Clone();

  • свести Clone, значения по умолчанию, нормализацию и валидацию в Resolve().

После этого вы получаете не просто “очередную структуру с тьмой полей”, а нормальный, цельный, безопасный стандарт для конструкторов.

Именно его я и предлагаю называть Resolvable Config Struct.