Почему Go отвергает тернарный оператор и почему это может быть ошибкой
Go появился в 2009 году и одним из осознанных решений при его проектировании было отсутствие тернарного оператора. Официальная позиция команды, зафиксированная в Go FAQ, с тех пор совсем не менялась.
Условное выражение в форме condition ? then : else присутствует в промышленных языках с 1972 года. За следующие полвека оно перекочевало в C++, Java, JavaScript, TypeScript, PHP, Swift, C# и др., и в каждом из них сообщество выработало устоявшиеся практики применения. Go – стоит особняком, как единственный широко используемый язык, где это решение было принято явно и аргументированно защищается.
Вопрос и гипотеза
Насколько часто разработчики Go сталкиваются с паттернами кода, для которых тернарный оператор является идиоматичным однострочным решением, а текущая конструкция
if-elseсоздаёт измеримый синтаксический шум?
Гипотеза состоит из двух проверяемых утверждений:
- Отсутствие тернарного оператора создаёт статистически значимый объём лишних строк кода в реальных Go-проектах.
- Это отсутствие приводит к фрагментации экосистемы, так как независимые команды реализуют несовместимые helper-функции для решения одной и той же задачи.
Для проверки гипотезы проводится статический анализ нескольких публичных Go-репозиториев с двумя основными метриками:
TRP (Ternary Replacement Potential) – доля функций, содержащих хотя бы один паттерн, который однозначно заменяется тернарным выражением.
TKLOC (Ternary patterns per Kilo Lines Of Code) – число паттернов на 1000 строк кода. Позволяет сравнивать репозитории разного размера.
Значимость исследования
Проблема документируется в публичном трекере Go начиная с 2019 года. Issue #33171, открытый в июле 2019 года, собрал ~80 комментариев и 35 участников. Но был закрыт в начале октября 2019 года с формулировкой «Мы согласны с тем, что в некоторых случаях синтаксис ?: был бы удобен, но в целом его добавление в язык кажется нецелесообразным.», а в мае 2021 года тред был заблокирован.
Это не остановило поток новых предложений. Issue #60502 открыт 30 мая 2023 года и закрыт в тот же день с пометкой «not planned». Issue #67959 открыт 13 июня 2024 года и закрыт как дубль в тот же день со ссылками на #60502, #31659, #36288, #23248 и #33171. Issue #71808, открытый в феврале 2025 года и сфокусированный на упрощении обработки ошибок через тернарный оператор, прожил три дня.
Но официальный трекер – это только видимая часть. Тема регулярно всплывает на r/golang и r/programming, собирая множество комментариев в каждом новом треде. Она разбирается в личных блогах на Hacker News, Medium, Habr и др. Масштаб обсуждения давно вышел за рамки официального трекера и это устойчивый фоновый шум в Go-сообществе, существующий параллельно с официальными отказами.
Паттерн очевиден! Одно и то же предложение возникает снова и снова на протяжении шести лет и каждый раз получает отказ без содержательного разбора данных. Авторы proposals – это разработчики с практическим опытом в C++, Java, TypeScript, PHP, для которых условное выражение является частью рабочего словаря. Это не запрос от людей, не понявших философию Go, а запрос от тех, кто понял её и продолжает работать с языком, считая конкретное решение ошибочным.
Данное исследование ставит задачу заменить аргумент «так сложилось» на измеримые данные.
История условных выражений
Концепция условного выражения как вычисляемой единицы появилась раньше, чем принято думать. Джон Маккарти предложил её в конце 1950-х в работе над Lisp – там cond был выражением, возвращающим значение. ALGOL 60 перенял идею, включив if-then-else как выражение в правой части присваивания. Это заложило семантический фундамент, который затем перекочевал в C.
Деннис Ритчи создал C в 1972 году в Bell Labs и именно там ?: получил свою привычную форму. C не изобрёл условное выражение, но индустриализировал его: компактный синтаксис, ленивые вычисления (только одна ветка вычисляется), совместимость с системным программированием. C++ унаследовал ?: без изменений. Java сохранила его в 1995 году. JavaScript в 1995-м, PHP в 1994-м, C# в 2000-м – все из C-традиции.
Python появился в 1991 году без условного выражения. Оно отсутствовало до версии 2.5 (2006), и споры шли годами. В итоге Гвидо ван Россум выбрал синтаксис a if cond else b – намеренно неудобный для вложенности. Rust и Kotlin пошли путём ALGOL, if в них является выражением и поэтому отдельный тернарный синтаксис не нужен.
PHP демонстрирует другой вид проблем. До версии 8 ?: в PHP был левоассоциативным – в отличие от C, Java и всех остальных языков, где он правоассоциативен. Цепочка $a ? 'x' : $b ? 'y' : 'z' вычислялась не так, как ожидал разработчик с фоном в любом другом языке. Анализ топ-1000 Composer-пакетов, проведённый Никитой Поповым в рамках RFC 2019 года, обнаружил 12 затронутых мест – 9 из них оказались реальными багами. В PHP 7.4 поведение получило deprecation-предупреждение, в PHP 8.0 стало compile-time ошибкой. Этот случай показателен, так как проблему создало конкретное решение при реализации, а не сам оператор. И оно поддалось исправлению.
К 2009 году, когда вышел Go, условное выражение в той или иной форме существовало в каждом широко используемом языке.
Позиция команды Go
Позиция команды зафиксирована в Go FAQ и с момента публикации не менялась.
The reason
?:is absent from Go is that the language's designers had seen the operation used too often to create impenetrably complex expressions. Theif-elseform, although longer, is unquestionably clearer. A language needs only one conditional control flow construct.
Этот ответ содержит три отдельных аргумента, каждый из которых стоит рассмотреть самостоятельно.
?:слишком часто порождает непроницаемо сложные выражения. Это наблюдение верно для вложенных тернарных конструкций видаa ? b ? c : d : e. Но из него следует, что проблема во вложенности, а не в операторе как таковом. Плоскоеcondition ? a : bв качестве правой части присваивания не создаёт сложности, которую нельзя было бы создать иначе.if-elseбесспорно яснее. Слово «бесспорно» здесь несёт лишнюю нагрузку. Ясность контекстуальна. В пятистрочном блоке, инициализирующем переменную в зависимости от условия, однострочная запись читается быстрее именно потому, что в ней меньше структурных элементов, на которые нужно обращать внимание.- Языку нужна только одна конструкция управления потоком. Этот аргумент содержит терминологическую неточность. Тернарный
?:– это условное выражение, а не конструкция управления потоком. Управление потоком изменяет порядок исполнения инструкций. Выражение возвращает значение. В Go уже естьif,for,switch,selectиgoto– пять конструкций управления потоком. Запрет шестой через ссылку на «одну конструкцию» не согласуется с реальным состоянием языка.
Хронология отклонений issues подтверждает, что позиция команды не эволюционировала вместе с дискуссией. Каждое закрытие ссылалось на FAQ или на предыдущие закрытые issues. Это не диалог, а процедура.
Текущие альтернативы
Go-разработчики не игнорируют проблему – они обходят её. Каждый workaround решает задачу частично и создаёт собственные издержки и порождает другую проблематику.
cmp.Or
Добавленный в Go 1.22 cmp.Or, возвращает первый ненулевой аргумент из последовательности:
result := cmp.Or(userInput, defaultValue)
Ограничение принципиальное: работает только с zero-value семантикой. Если userInput намеренно содержит ноль, пустую строку или false, то cmp.Or трактует это как «значение отсутствует». Для булевых флагов и числовых значений, где ноль легитимен – неприменима.
Generics
Самый распространённый workaround в Go-сообществе. От репозитория к репозиторию встречаются разные наименования хелперов If(), Ternary(), Or() и другие вариации:
func If[T any](cond bool, a, b T) T {
if cond {
return a
}
return b
}
Проблемы тоже есть: оба аргумента вычисляются до вызова функции – eager evaluation. If(condition, expensiveComputation(), cheapDefault()) всегда выполнит expensiveComputation() независимо от условия. Если функция имеет побочные эффекты или может паниковать, код некорректен. Также функция добавляет уровень косвенности и скрытую логику вместо явного синтаксиса, полноценной заменой это не является.
map[bool]T
Такой «хак» встречается реже, но появляется в кодовой базе с редкой, но всё же регулярностью:
result := map[bool]string{true: "yes", false: "no"}[condition]
Конструкция создаёт новый map при каждом выполнении, вычисляет оба значения, и читается значительно хуже, чем любой из вариантов, которые она якобы упрощает. Всё это выглядит как демонстрация отчаяния.
Сторонние пакеты
Внешние пакеты, например github.com/samber/lo, предоставляют функции lo.Ternary[T] и lo.TernaryF – последняя принимает функции вместо значений для обхода eager evaluation. Опять же – добавлять внешнюю зависимость ради однострочника?
Каждый из подходов покрывает разные подмножества случаев, несовместим с остальными и требует объяснения на code review. Это и есть фрагментация, то есть прямое следствие языкового решения.
Лучшие практики из ЯП
Но что в других языках и как у них получается жить с тернарным оператором? У таких языков сложился устойчивый консенсус: плоское использование – норма, вложенность – антипаттерн.
Airbnb JavaScript Style Guide:
Ternaries should not be nested and generally be single line expressions.
Правило подкреплено ESLint-правилом [no-nested-ternary](https://eslint.org/docs/latest/rules/no-nested-ternary){:target="_blank"}. Запрет на вложенность проверяется автоматически при каждом коммите, не остаётся пожеланием в документе.
Google Java Style Guide упоминает ?: только в контексте форматирования, в виде расстановки пробелов и переноса строк. Никаких ограничений на использование нет и оператор считается достаточно безопасным, чтобы не требовать оговорок.
PEP 8 прямых рекомендаций по условным выражениям не содержит. Python решил проблему вложенности на уровне синтаксиса, так как конструкция a if cond else b структурно неудобна для цепочек – запрещать то, что неудобно писать само по себе не нужно.
clang-tidy включает правило readability-avoid-nested-conditional-operator: «Вложенные условные операторы могут снижать читаемость кода. Поэтому их следует разделять на несколько отдельных операторов». Плоское использование правило не затрагивает.
MISRA C – стандарт для safety-critical систем, не запрещает тернарный оператор, вводя лишь требования к типам операндов. Оператор признан достаточно безопасным для встраиваемых систем при соблюдении типовой дисциплины.
Cообщества не отказались от оператора из-за риска злоупотреблений. Они ограничили конкретный антипаттерн – вложенность. Оставили оператор для простых случаев. Это именно то, чего нет в Go – не оператора и не правил к нему, а самой возможности сделать этот выбор.
Академический контекст
Влияние структуры кода на когнитивную нагрузку разработчика давно вышло за рамки интуиции и стало предметом измеримых исследований.
Метрика цикломатической сложности Томаса Маккейба (1976) измеряет число линейно независимых путей исполнения – каждый if, for, case увеличивает счётчик. Удобна для оценки покрытия тестами, но плохо отражает читаемость: два блока с одинаковой цикломатической сложностью могут кардинально отличаться по воспринимаемой сложности.
Метрика когнитивной сложности, разработанная SonarSource, добавляет штраф за каждый уровень вложенности. Исследование Fard et al. (2020), проанализировавшее ~24 000 оценок понимаемости по 427 фрагментам кода, подтвердило положительную корреляцию когнитивной сложности со временем понимания и субъективными оценками читаемости. Journal of Systems and Software (2023) зафиксировал, что эти показатели коррелируют с читаемостью на уровне, сопоставимом с традиционными метриками.
Исследование arxiv:1909.01760, анализировавшее связь читаемости и сложности в Java-коде, эмпирически подтвердило отрицательную корреляцию и идентифицировало вложенные if-else как один из конструктов, наиболее негативно влияющих на читаемость.
Из этого можно сделать практический вывод, что вложенность стоит дороже, чем развёртывание условия в линию. Пятистрочный if-else блок для инициализации переменной добавляет уровень вложенности и требует от читателя удерживать контекст объявления и значения одновременно. Однострочная запись убирает этот уровень именно тогда, когда логика проста.
Аргумент команды Go о том, что if-else «бесспорно яснее», не подкреплён ссылками ни на одно из этих исследований, поэтому он остаётся аксиомой принятой без проверки.
Методология
Но давайте попробуем получить данные, на которые сможем опираться в будущих аргументациях. Данное исследование использует смешанный метод, состоящий из компонентов:
- Количественная часть
статический анализ нескольких публичных Go-репозиториев, который отвечает на вопрос о частоте паттернов - Качественная часть
тематический анализ issues и данные опроса сообщества, которое даёт контекст о том, как разработчики воспринимают проблему и что делают для её обхода
Оба компонента взаимно усиливают друг друга. Статический анализ показывает масштаб, но не намерение. Опрос показывает намерение, но не масштаб. Совместно они формируют аргумент, который сложнее отклонить как субъективный.
Для анализа отобраны следующие публичные репозитории:
- github.com/golang/go
- github.com/hashicorp/terraform
- github.com/kubernetes/kubernetes
- github.com/traefik/traefik
Автоматически сгенерированные файлы (proto, generators, mock, ...) исключены, так как они искажают статистику в сторону завышения.
Основной инструмент – это кастомный анализатор на базе go/token, go/ast и go/parser. Написан на Go, без зависимостей, парсит AST стандартной библиотекой, сопоставляет с таксономией, пропускает сгенерированное, формирует таблицу метрик и находки с контекстом. Для перекрёстной валидации применяется semgrep с независимо написанными правилами.
Кастомный анализатор, не верификатор. В нем побочные эффекты не анализируются, а цифры считаются нижней границей.
Выложил в публичный репозиторий: github.com/dmitryburov/go-ternary-audit
Таксономия паттернов
Детектируются четыре основные категории паттернов. Критерий включения в таксономию: паттерн должен однозначно заменяться тернарным выражением без изменения семантики и без проблемы eager evaluation.
Категория 1. Условное присваивание
Переменная получает одно из двух значений в зависимости от условия. Охватывает как объявление через var с последующим присваиванием, так и случаи, где переменная уже объявлена выше.
var label string
if isAdmin {
label = "admin"
} else {
label = "user"
}
// или default+override
label := "user"
if isAdmin {
label = "admin"
}
// c тернарным оператором
label := isAdmin ? "admin" : "user"
Это самая массовая категория паттерна в выборке. Промежуточная переменная здесь существует исключительно как синтаксический артефакт и её не было бы, если бы if-else мог возвращать значение.
Например только в исходниках go в build-хендлере xinit() наберется порядка 12-ти подряд паттернов.
Категория 2. Условный return
Функция возвращает одно из двух значений. В текущем Go это всегда минимум три-четыре строки.
if err != nil {
return http.StatusInternalServerError
}
return http.StatusOK
// c тернарным оператором
return err != nil ? http.StatusInternalServerError : http.StatusOK
Категория паттерна особенно болезненна в функциях с несколькими точками возврата, так как каждая из них добавляет блок и функция быстро превращается в последовательность похожих if-return структур, между которыми сложно заметить семантическое различие.
Категория 3. «Раздутый» конструктор
Struct literal или вызов функции с несколькими полями, значения которых зависят от условий. Каждое поле требует отдельного if-else блока до конструктора, что разрывает визуальную связь между структурой и её значениями.
var label string
if item.Active {
label = "active"
} else {
label = "inactive"
}
var priority string
if item.Score > 100 {
priority = "high"
} else {
priority = "normal"
}
row := Row{Label: label, Priority: priority}
// С тернарным оператором – конструктор читается как таблица
row := Row{
Label: item.Active ? "active" : "inactive",
Priority: item.Score > 100 ? "high" : "normal",
}
Эффект особенно усиливается в циклах, так как при трёх-четырёх полях подготовительный код занимает в несколько раз больше места, чем сам конструктор.
Категория 4. Inline-выражение
Условное значение передаётся напрямую как аргумент функции. Чаще всего в fmt.Sprintf, fmt.Printf, log.Printf и аналогах. Промежуточная переменная объявляется, используется ровно один раз и существует только потому, что передать условное выражение как аргумент нельзя.
var suffix string
if count != 1 {
suffix = "s"
}
fmt.Printf("found %d item%s\n", count, suffix)
// С тернарным оператором
fmt.Printf("found %d item%s\n", count, count != 1 ? "s" : "")
Категория паттерна показательна тем, что здесь разрыв между намерением и кодом особенно очевиден. Разработчик думает «если не один – добавить 's'», но вынужден превратить это в процесс: объявление -> блок -> использование.
Четыре категории покрывают принципиально разные контексты применения: присваивание, возврат, инициализацию составных типов и передачу аргументов. Семантика во всех случаях одна – выбор одного из двух значений по условию, но синтаксический шум в каждом контексте проявляется по-своему.
Определяемые метрики
TRP (Ternary Replacement Potential) – доля функций, содержащих хотя бы один паттерн из таксономии, в процентах от общего числа функций
TKLOC (Ternary patterns per Kilo Lines Of Code) – число паттернов на 1000 строк кода
SLOC (Saved Lines Of Code) – количественное значение сокращения строк при замене паттерна
SLOC delta – среднее сокращение строк при замене паттерна
Ограничения
Исследование использует эвристический подход, и честная оценка его границ важна для интерпретации результатов.
Eager evaluation. Часть паттернов категорий 3 и 4 содержит аргументы с побочными эффектами или дорогими вычислениями. Для таких случаев замена тернарным оператором была бы семантически некорректной. Детектор анализ побочных эффектов не производит, поэтому реальный процент корректных замен будет несколько ниже.
Смещение выборки. Отобранные репозитории не представляют всё разнообразие go-кода. А именно не охвачены монорепозитории крупных компаний, закрытый корпоративный код, инфраструктурный генерируемый код. Результаты следует рассматривать как индикативные.
Эвристика детектора. Семантически эквивалентный код в нестандартной форме может быть пропущен. Это смещает результаты в сторону занижения – итоговые цифры являются консервативной оценкой.
Результаты анализа
golang/go
TRP – 10.58%, то есть каждая 9-я функция содержит хотя бы один тернарный паттерн TKLOC – 4.66, ~5 паттернов на 1 000 строк кода SLOC – 2%, ~35 872 строк можно сократить из 1 803 530 SLOC delta – 4.26, ~4 лишних строки на каждый паттерн
Функций всего – 64 863
Паттернов всего – 8 412
| Категория | Найдено | Доля | Строк на паттерн (SLOC delta) |
|---|---|---|---|
| Условный return | 4 913 | 58.4% | 3.4 |
| Условное присваивание | 2 649 | 31.5% | 3.8 |
| Inline-выражение | 743 | 8.8% | 13.7 |
| Раздутый конструктор | 107 | 1.3% | 7.0 |
hashicorp/terraform
TRP – 11.96%, то есть каждая 8-я функция содержит хотя бы один тернарный паттернTKLOC – 2.96, ~3 паттерна на 1 000 строк кода SLOC – 1.7%, ~9 961 строк можно сократить из 593 124 SLOC delta – 5.68, ~5.5 лишних строк на каждый паттерн
Функций всего – 11 857
Паттернов всего – 1 754
| Категория | Найдено | Доля | Строк на паттерн (SLOC delta) |
|---|---|---|---|
| Условный return | 1 013 | 57.8% | 4.5 |
| Условное присваивание | 511 | 29.1% | 4.9 |
| Inline-выражение | 195 | 11.1% | 12.7 |
| Раздутый конструктор | 35 | 2.0% | 11.9 |
kubernetes
TRP – 14,71%, каждая 7-я функция содержит хотя бы один тернарный паттерн TKLOC – 4,22, ~4 паттерна на 1 000 строк кода SLOC – 2%, ~49 523 строк можно сократить из 2 612 342 SLOC delta – 4,49, ~4.5 лишних строк на каждый паттерн
Функций всего – 60 971
Паттернов всего – 11 027
| Категория | Найдено | Доля | Строк на паттерн (SLOC delta) |
|---|---|---|---|
| Условный return | 7 324 | 66.4% | 3.9 |
| Условное присваивание | 2 640 | 23.9% | 4.2 |
| Inline-выражение | 788 | 7.1% | 9.6 |
| Раздутый конструктор | 275 | 2.5% | 8.6 |
traefik
TRP – 16,02%, каждая 6-я функция содержит хотя бы один тернарный паттерн TKLOC – 4,22, ~4.2 паттерна на 1 000 строк кода SLOC – 2.15%, ~4 561 строк можно сократить из 195 332 SLOC delta – 5,02, ~5 лишних строк на каждый паттерн
Функций всего – 3 902
Паттернов всего – 826
| Категория | Найдено | Доля | Строк на паттерн (SLOC delta) |
|---|---|---|---|
| Условный return | 505 | 61.1% | 4.7 |
| Условное присваивание | 200 | 24.2% | 3.6 |
| Раздутый конструктор | 61 | 7.4% | 6.4 |
| Inline-выражение | 60 | 7.3% | 11.2 |
Обсуждение
Статический анализ репозиториев выявил более 22 000+ паттернов в более чем 140 000+ функциях. TRP варьируется от 10.5% до 16% — от каждой 9-й до каждой 6-й функции. Это не абстрактная статистика, это конкретные места, где разработчик создал временную переменную, написал блок из четырёх-пяти строк и двинулся дальше. Умноженные на размер типичной кодовой базы, эти места формируют постоянный фоновый шум.
TKLOC стабильно держится в диапазоне 3–5. При среднем SLOC delta ~4.5 строк, это от 1 300 до 2 200 строк кода, которые существуют не потому, что они нужны, а потому что нет синтаксиса короче.
Пропорции категорий устойчивы во всех четырёх репозиториях: условный return стабильно занимает 58–66%, условное присвоение — 24–32%. Три разные команды, четыре разных проекта, один профиль. Это не совпадение — это характеристика языка.
Разбор аргументов команды
«Создаёт непроницаемо сложные выражения»
Это наблюдение верно, но только для одного конкретного случая – для вложенных тернарных конструкций. a ? b ? c : d : e действительно сложно читать и проблема в том, что из одного антипаттерна сделан вывод об операторе целиком.
Данные не поддерживают такое обобщение. Все 22 000+ паттернов, обнаруженных в выборке – это плоские однострочные присваивания и возвраты без какой-либо вложенности. Это именно те случаи, где тернарный оператор является прямой и безопасной заменой. Сложность появляется не от оператора, а от вложенности – и эта проблема решается не запретом оператора, а запретом вложенности. Именно так поступили в JavaScript-экосистеме с правилом no-nested-ternary, и именно так работает readability-avoid-nested-conditional-operator в clang-tidy.
Уместна аналогия внутри самого Go. Операторы && и || в сложных булевых выражениях тоже порождают нечитаемый код. Никто из этого не делает вывод, что их нужно убрать из языка. Логика «оператор создаёт возможность злоупотребления – значит, оператора быть не должно» в Go применяется избирательно.
«if-else бесспорно яснее»
Слово «бесспорно» не выдерживает проверки данными. 13 000+ паттернов условного return в репозиториях — это код вида if err != nil { return X } return Y, который мог бы быть одной строкой. 6 000+ паттернов условного присваивания — это var x; if { x = a } else { x = b } вместо x := cond ? a : b.
В обоих случаях пятистрочный блок требует от читателя удержать в голове факт объявления, имя переменной, условие и оба значения одновременно. Однострочная запись сокращает когнитивную нагрузку именно потому, что информация находится в одном месте.
Исследование Fard et al. (2020) подтвердило корреляцию когнитивной сложности со временем понимания кода. Исследование arxiv:1909.01760 идентифицировало вложенные if-else блоки как один из конструктов, наиболее негативно влияющих на читаемость. Аргумент команды Go не опирается ни на одно из исследований – он остаётся аксиомой, принятой без проверки и не пересмотренной за шестнадцать лет существования языка.
«Языку нужна только одна конструкция управления потоком»
Здесь содержится терминологическая ошибка, которую важно зафиксировать явно. Тернарный ?: – это условное выражение, а не конструкция управления потоком. Разница принципиальная.
Управление потоком (control flow statement) изменяет порядок исполнения инструкций: if выполняет блок или нет, for повторяет его, goto прыгает. Выражение (expression) вычисляется и возвращает значение. a ? b : c не меняет поток – оно производит значение, которое затем используется в присваивании или передаётся как аргумент.
При этом в Go уже существует пять конструкций управления потоком: if, for, switch, select, goto. Апелляция к «одной конструкции» не отражает реальность языка. Добавление условного выражения не нарушало бы никакой существующей инварианты – потому что этой инварианты фактически нет.
Фрагментация экосистемы
Одна из декларируемых целей Go – читаемость кода, написанного любым разработчиком в любом проекте. «Go code is Go code» – идея о том, что незнакомый репозиторий можно читать без изучения локальных соглашений.
На практике отсутствие тернарного оператора создаёт ровно обратный эффект. Каждая команда, которой нужна однострочная условная запись, решает эту задачу самостоятельно. Во множестве репозиториев встречаются минимум три несовместимых паттерна:
func Ternary[T any](cond bool, a, b T) Tкак самый популярныйcmp.Orтам, где подходит zero-value семантикаif-elseвезде, где ни первое, ни второе не работает
Ни один из них не является стандартным, ни один не работает во всех случаях, и каждый из них требует пояснения при первом появлении в кодовой базе.
Сторонний пакет github.com/samber/lo содержит функции lo.Ternary и lo.TernaryF, где последняя принимает функции вместо значений, чтобы обойти проблему eager evaluation. Это вполне рабочее решение, но оно означает: ради функциональности, которую большинство языков предоставляет синтаксически, Go-разработчик подключает внешнюю зависимость.
Фрагментация – это прямое следствие языкового решения. Она не исчезнет от того, что команда закроет очередной proposal...
Контрагрументы
Честность требует показать случай, где команда Go права и показать его полностью, без смягчений.
role := isAdmin ? (isSuperAdmin ? "superadmin" : "admin") : "user"
Умеренная вложенность, уже сложно. Читатель вынужден найти границы внешнего выражения, затем разобрать внутреннее. При беглом чтении кода скобки легко пропустить.
result := a > b ? a > c ? "a" : c > b ? "c" : "b" : b > c ? "b" : "c"
Реальный «ад», когда три уровня и больше.
// JavaScript, реальный паттерн из frontend-кода
const className = isLoading
? isSkeleton
? 'skeleton'
: 'spinner'
: hasError
? isRetrying
? 'error--retrying'
: 'error'
: 'content'
Такой код существует в реальных C и JavaScript кодовых базах, это pаставляет ревьюера перечитывать строку несколько раз. Форматирование помогает, но ненамного. switch или объект-маппинг здесь были бы значительно понятнее.
Но проблема – не оператор
Ключевой вопрос неизменен: «является ли существование таких примеров достаточным основанием для запрета оператора?»
В Go уже есть инструменты, которые создают сложный для чтения код при злоупотреблении и никто не предлагает их убрать:
// Цепочка методов – читается нормально в простом случае
result := strings.TrimSpace(strings.ToLower(input))
// При злоупотреблении – тот же эффект вложенного тернарника
val := reflect.TypeOf(getConfig().Handlers[getIndex()].Process(ctx).Result()).Name()
// Горутины и каналы – мощный инструмент
go func() { ch <- compute() }()
// При злоупотреблении – никто не запрещает писать так
go func() { go func() { go func() { ch <- f(g(h(x))) }() }() }()
Go не запрещает глубокие цепочки вызовов и вложенность горутин, даже несмотря на то, что при злоупотреблении каждый из них создаёт ту же нечитаемость.
// Ничего не мешает злоупотребить и дженериком
func (p Player) getScore() int {
return Ternary(p.HasWon, 100, Ternary(p.HasBonus, 80, Ternary(p.HasCombo, 70, 0)))
}
Индустрия решила проблему вложенного тернарного оператора без запрета самого оператора:
- Airbnb JS Style Guide: «Ternaries should not be nested» + ESLint
no-nested-ternary - clang-tidy:
readability-avoid-nested-conditional-operator– правило только для вложенности - в PHP8 вложенный ?: без явных скобок стал compile-time ошибкой – не запретили оператор, запретили неявную вложенность
Go мог бы запретить вложенность через go vet или компилятор и тогда главный аргумент против оператора перестал бы существовать, не лишив разработчиков плоских однострочных случаев, ради которых оператор и нужен.
Страх перед a > b ? a > c ? "a" : c > b ? "c" : "b" : b > c ? "b" : "c" обоснован. Но правильный ответ – «запретить вложенность», не «запретить оператор».
Заключение
Статический анализ четырёх репозиториев зафиксировал 22 000+ паттернов, структурно идентичных тернарному присваиванию, при среднем TRP ~13% и TKLOC 3.5. В типичном Go-проекте разработчик сталкивается с ними несколько раз на каждую тысячу строк – не как с редким исключением, а как с рутиной. Каждый паттерн занимает в среднем на 3–4 строки больше, чем требует логика.
Фрагментация экосистемы подтверждается эмпирически: несовместимые workaround-паттерны присутствуют в каждом из анализируемых репозиториев. Стандарта нет – и это прямое следствие языкового решения.
Три аргумента из Go FAQ не выдерживают проверки применительно к плоским, однострочным случаям. Аргумент о нечитаемости описывает вложенность, а не оператор. Аргумент о ясности if-else противоречит академическим данным о когнитивной нагрузке. Аргумент об «одной конструкции управления потоком» содержит терминологическую ошибку: условное выражение управлением потоком не является. Все три остаются в FAQ без обновления – вне зависимости от того, что происходило в обсуждениях.
До этой работы дискуссия велась на уровне мнений и примеров. Именно отсутствие данных Ian Lance Taylor называл в 2019 году причиной, по которой предложение не стоит рассматривать. Данное исследование закрывает этот пробел: введены измеримые метрики с операциональными определениями и воспроизводимой методологией, выработана таксономия паттернов, результаты опубликованы в открытом доступе.
Это первое количественное измерение «ternary gap» в Go – разрыва между тем, что язык позволяет выразить компактно, и тем, что ему для этого недостаёт.
Ответ «doesn't seem worth it», данный в 2019 году без данных, был позицией. Теперь данные есть. Вопрос в том, готова ли команда применить к собственному решению тот же стандарт доказательности, который она требовала от сообщества.
Голосование
Это голосование неотъемлемая часть исследования. Его цель не в том, чтобы получить репрезентативную статистику по всем Go-разработчикам мира, а проверить – совпадают ли представления команды Go о том, как разработчики воспринимают конкретные синтаксические паттерны с тем, что говорят сами разработчики.
Выборка заведомо смещена, так как сюда придут те, кто уже интересуется темой. Всего четыре простых вопроса, каждый из которых занимает меньше минуты.
Твой голос поможет сформировать общую панораму восприятий разработчиков 🙏