Продолжаем разбирать
[T Constraint[T]] в объявлениях обобщённых типов. Это чинит старую проблему флюентных билдеров, где цепочку вызовов ломало встраивание.Какую боль это решает
У билдера в Go давнее противоречие. Чтобы работала цепочка
.WithX().WithY().Build(), каждый метод возвращает *Builder. Пока билдер один, всё в порядке. Но как только вы расширяете его через встраивание, цепочка рвётся. Метод встроенного типа возвращает встроенный тип, а не внешний, поэтому после первого же вызова вы теряете доступ к методам расширения. Приходится либо дублировать методы, либо приводить типы вручную.Как это работает
Ограничение задаёт набор методов, а параметр типа
B несёт конкретный тип билдера через всю цепочку:type Builder[B any] interface {
WithTimeout(d time.Duration) B
WithRetry(attempts int, backoff time.Duration) B
Build() (Config, error)
}
// ApplyDefaults возвращает B, а не Builder[B].
func ApplyDefaults[B Builder[B]](b B, env Environment) B {
b = b.WithTimeout(env.DefaultTimeout())
b = b.WithRetry(env.MaxRetries(), env.RetryBackoff())
return b
}На вызове конкретный тип остаётся живым, поэтому специфичный метод вроде
WithHeader доступен прямо в цепочке:client, err := ApplyDefaults(NewHTTPClientBuilder("https://api.internal"), prodEnv).
WithHeader("X-Service-ID", serviceID). // всё ещё доступно
Build()Здесь важно разделение двух задач. Интерфейс задаёт ограничение, какие методы обязаны быть. Параметр типа несёт идентичность, какой именно конкретный билдер вернётся. Передача обеих задач одной переменной типа как раз и заставляет паттерн работать.
Самоссылка снимает старый компромисс между цепочкой вызовов и расширением билдера. Общие настройки выносятся в одну обобщённую функцию, а конкретный тип переживает всю цепочку, поэтому доменные методы остаются под рукой без приведений. Для конфигураторов, клиентов и любых флюентных API это делает код короче и безопаснее на этапе компиляции.
📍 Навигация: Вакансии • Задачи • Собесы
🐸 Библиотека Go-разработчика
#GoToProduction