Почему Go отвергает тернарный оператор и почему это может быть ошибкой
Go появился в 2009 году и одним из осознанных решений при его проектировании было отсутствие тернарного оператора. Официальная позиция команды, зафиксированная в Go FAQ, с тех пор совсем не менялась.
Условное выражение в форме condition ? then : else присутствует в промышленных языках с 1972 года. За следующие полвека оно перекочевало в C++, Java, JavaScript, TypeScript, PHP, Swift, C# и др., и в каждом из них сообщество выработало устоявшиеся практики применения. Go стоит особняком, как единственный широко используемый язык, где это решение было принято явно и аргументированно защищается.
Гипотеза
Как часто разработчики сталкиваются с паттернами кода, где тернарный оператор будет являться идиоматичным однострочным решением?
Сама гипотеза состоит из 2х проверяемых утверждений:
- Отсутствие тернарного оператора создаёт значимый объём лишних строк кода в реальных проектах.
- Отсутствие приводит к фрагментации экосистемы, так как независимые команды реализуют несовместимые helper-функции для решения одной и той же задачи.
Для проверки гипотезы мне удалось провести статический анализ нескольких публичных репозиториев для получения двух основных метрик:
TRP (Ternary Replacement Potential) – это доля функций, содержащих хотя бы один паттерн, который однозначно заменяется тернарным выражением.
DLOC (Deleted Lines Of Code) – количественное значение сокращения строк при замене паттерна
Значимость исследования
Проблема документируется в публичном трекере Go начиная с 2019 года. Issue #33171, открытый в июле 2019 года, собрал ~80 комментариев и 35 участников. Но был закрыт в начале октября 2019 года с формулировкой цитирую: «Мы согласны с тем, что в некоторых случаях синтаксис ?: был бы удобен, но в целом его добавление в язык кажется нецелесообразным.», а в мае 2021 года тред уже был заблокирован.
Но создание новых предложений не останавливается... Issue #60502 открыт в мае 2023 года и закрыт в тот же день, с пометкой «not planned». Issue #67959 открытый в июне 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 условное выражение отсутствовало до версии 2.5 (2006), и споры шли годами (ничего не напоминает?)... В итоге Гвидо ван Россум выбрал синтаксис a if cond else b – намеренно неудобный для вложенности. А языки Rust и Kotlin пошли путём ALGOL, if в них является выражением и поэтому отдельный тернарный синтаксис не нужен.
PHP же демонстрирует другой вид проблем – до версии 8.0 тернарный оператор был левоассоциативным, в отличие от 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
Зафиксированная позиция core-команды в 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.
В этом ответе присутствуют 3 отдельных аргумента, которые стоит рассмотреть самостоятельно.
?:слишком часто порождает непроницаемо сложные выражения. Это наблюдение верно для вложенных тернарных конструкций видаa ? b ? c : d : e. Но из него следует, что проблема во вложенности, а не в операторе как таковом. Плоскоеcondition ? a : bв качестве правой части присваивания не создаёт сложности, которую нельзя было бы создать иначе.if-elseбесспорно яснее. Слово «бесспорно» здесь несёт лишнюю нагрузку. Ясность контекстуальна. В пятистрочном блоке, инициализирующем переменную в зависимости от условия, однострочная запись читается быстрее именно потому, что в ней меньше структурных элементов, на которые нужно обращать внимание.- Языку нужна только одна конструкция управления потоком. Этот аргумент содержит терминологическую неточность. Тернарный
?:– это условное выражение, а не конструкция управления потоком. Управление потоком изменяет порядок исполнения инструкций. Выражение возвращает значение. В Go уже естьif,for,switch,selectиgoto– пять конструкций управления потоком. Запрет шестой через ссылку на «одну конструкцию» не согласуется с реальным состоянием языка.
В разделе значимость исследования видна хронология отклонений issues. Это подтверждает, что позиция команды не эволюционировала и каждое закрытие issue ссылалось на FAQ.
Текущие альтернативы
В текущей версии Go (1.26 на время публикаци статьи), разработчики обходят «проблему» разными способами. Каждый способ решает задачу либо частично и создаёт собственные издержки, либо порождает другую проблематику.
cmp.Or
Добавленный в Go 1.22 cmp.Or, возвращает первый ненулевой аргумент из последовательности:
result := cmp.Or(userInput, defaultValue)
Работает только с zero-value семантикой и не nil значениями. Если userInput намеренно содержит ноль, пустую строку или false, то cmp.Or трактует это как «значение отсутствует». Для булевых флагов и числовых значений, где ноль легитимен это решение неприменимо.
Generics
Самый распространённый способ у разработчиков. В репозиториях встречаются разные наименования хелперов 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() независимо от условия. Если функция имеет побочные эффекты или может паниковать, код соответственно будет некорректен. Также функция добавляет уровень косвенности и скрытую логику вместо явного синтаксиса. Это конечно же полноценной заменой это не является и выглядит, как я считаю, антипаттерном. Также дополнительно вам придется таскать эту helper-функцию из проекта в проект.
map[bool]T
Такой «хак» встречается реже, но факт есть факт – он есть и появляется в кодовой базе с редкой, но всё же регулярностью.
result := map[bool]string{true: "yes", false: "no"}[conditions]
Конструкция создаёт новый map при каждом выполнении, вычисляет оба значения, и читается значительно хуже, чем любой из вариантов, которые она якобы упрощает.
Сторонние пакеты
Внешние пакеты, например github.com/samber/lo, предоставляют функции lo.Ternary[T] и lo.TernaryF. Обе полностью наследуют проблемы generic-хелпера. В защиту 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. Запрет на вложенность проверяется автоматически при каждом коммите и не остаётся рекомендацией в документе.
Google Java Style Guide упоминает ?: только в контексте форматирования, в виде расстановки пробелов и переноса строк. Никаких ограничений на использование нет и оператор считается достаточно безопасным, чтобы не требовать оговорок.
PEP 8 прямых рекомендаций по условным выражениям не содержит вообще. Python решил проблему вложенности на уровне синтаксиса, так как конструкция a if cond else b структурно неудобна для цепочек, а апрещать то, что неудобно писать само по себе не нужно.
clang-tidy использует правило readability-avoid-nested-conditional-operator. Поскольку вложенные условные операторы могут снижать читаемость кода, их следует разделять на несколько отдельных операторов. Но плоское использование это правило не затрагивает.
MISRA C – это стандарт для safety-critical систем и он не запрещает тернарный оператор, а вводит лишь требования к типам операндов. Оператор признан достаточно безопасным для встраиваемых систем при соблюдении типовой дисциплины.
Как видно – сообщества не отказались от оператора из-за риска злоупотреблений. Они ограничили конкретный антипаттерн – вложенность. Оставили оператор для простых случаев. Это именно то, чего нет в Go – не оператора или не правил к нему, а самой возможности сделать этот выбор.
Академический контекст
Структура влияет на когнитивную нагрузку разработчика. И сегодня это не интуиция, а предмет целый измеримых исследований!
Существует метрика цикломатической сложности Томаса Маккейба (1976). Она измеряет число линейно независимых путей исполнения – каждый if, for, case увеличивает счётчик. Данная метрика удобна для оценки покрытия тестами, но плохо отражает именно читаемость. Например: два блока с одинаковой цикломатической сложностью могут кардинально отличаться по воспринимаемой сложности.
В SonarSource была разработана метрика когнитивной сложности. Она добавляет штраф за каждый уровень вложенности. Исследование An Empirical Validation of Cognitive Complexity as a Measure of Source Code Understandability, проанализировавшее ~24 000 оценок понимаемости по 427 фрагментам кода подтверждает положительную корреляцию когнитивной сложности со временем понимания и субъективными оценками читаемости. Journal of Systems and Software зафиксировал, что эти показатели коррелируют с читаемостью на уровне, сопоставимом с традиционными метриками.
Исследование An Empirical Study of the Relationships between Code Readability and Software Complexity, анализировавшее связь читаемости и сложности в Java-коде, эмпирически подтвердило отрицательную корреляцию и идентифицировало вложенные if-else как один из конструктов, наиболее негативно влияющих на читаемость.
Из этого можно сделать практический вывод – вложенность стоит дороже, чем развёртывание условия в линию. Пятистрочный if-else блок инициализации переменной добавляет уровень вложенности и требует удерживать контекст объявления и значения одновременно. Однострочная запись убирает этот уровень при простой логике.
Аргумент команды Go о том, что if-else «бесспорно яснее» больше похоже на аксиому, принятую без проверки. И она не выдерживает проверку академическими исследованиями.
Методология
Что ж, данные – это то что ожидает команда Go, судя по последним issue в их трекере. Попробуем их получить, чтобы в будущих аргументациях можно было опираться на них.
Исследование использует смешанный метод, состоящий из компонентов:
- Количественная часть
статический анализ нескольких публичных 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. Парсит 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. «Раздутый» конструктор
Вызов функции с несколькими полями, значения которых зависят от условий. Каждое поле требует отдельного 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 строк кода
DLOC (Deleted Lines Of Code) – количественное значение сокращения строк при замене паттерна
DLOC delta – среднее сокращение строк при замене паттерна
Ограничения
Исследование использует эвристический подход и честная оценка его границ важна для интерпретации результатов.
Во внимание не берутся generated, mock, test и другие файлы, которые могут искажать результаты.
Eager evaluation. Часть паттернов категорий 3 и 4 содержит аргументы с побочными эффектами или дорогими вычислениями. Для таких случаев замена тернарным оператором была бы семантически некорректной. Анализатор детект побочных эффектов не производит, поэтому реальный процент корректных замен будет несколько ниже.
Смещение выборки. Отобранные репозитории не представляют всё разнообразие go-кода. А именно не охвачены монорепозитории крупных компаний, закрытый корпоративный код, инфраструктурный генерируемый код. Поэтому результаты стоит рассматривать как индикативные.
Эвристика детектора. Семантически эквивалентный код в нестандартной форме может быть пропущен. Это смещает результаты в сторону занижения и итоговые цифры являются консервативной оценкой.
Результаты анализа
golang/go
TRP – 10.58%, где каждая 9-я функция содержит хотя бы один тернарный паттерн TKLOC – 4.66, ~4.5 паттерна на 1 000 строк кода DLOC – 2%, ~35 872 строк можно сократить из 1 803 530 DLOC delta – 4.26, ~4 лишних строки на каждый паттерн
Функций всего – 64 863
Паттернов всего – 8 412
| Категория | Найдено | Доля | DLOC 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 строк кода DLOC – 1.7%, ~9 961 строк можно сократить из 593 124 DLOC delta – 5.68, ~5.5 лишних строк на каждый паттерн
Функций всего – 11 857
Паттернов всего – 1 754
| Категория | Найдено | Доля | DLOC 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 строк кода DLOC – 2%, ~49 523 строк можно сократить из 2 612 342 DLOC delta – 4,49, ~4.5 лишних строк на каждый паттерн
Функций всего – 60 971
Паттернов всего – 11 027
| Категория | Найдено | Доля | DLOC 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 строк кода DLOC – 2.15%, ~4 561 строк можно сократить из 195 332 DLOC delta – 5,02, ~5 лишних строк на каждый паттерн
Функций всего – 3 902
Паттернов всего – 826
| Категория | Найдено | Доля | DLOC 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 в среднем варьируется от 12% до 15% , где замена тернарником возможно в 7-й функции в среднем. Это конкретные места, где разработчик создал временную переменную, написал блок из четырёх-пяти строк и продолжил писать код дальше. Если умножить на размер типичной кодовой базы, то эти места будут формировать постоянный фоновый шум.
TKLOC стабильно держится в диапазоне 3–5 строк. При среднем DLOC delta ~4.5 строк, это от 1 300 до 2 200 строк кода, которые существуют потому что нет синтаксиса короче.
Категория 2 (условный return) оказалась самая частотная, которая стабильно занимает ~60%. За ней следом идет Категория 1 (условно присвоение) с ~30%. Три разные команды, четыре разных проекта – совпадение?
Разбор аргументов команды
Теперь, имея измеримые данные и больший контекст, давайте попробуем их сопоставить с аргументами команды Go.
«Создаёт непроницаемо сложные выражения»
Это верно, но только для одного конкретного случая – когда есть вложенная конструкция. a ? b ? c : d : e это действительно сложно читать и проблема в том, что из одного антипаттерна (который сообщество прекрасно определяет) сделан вывод об операторе в целом.
Данные также не поддерживают такое обобщение. Все обнаруженные паттерны в выборке – это плоские однострочные присваивания и возвраты без какой-либо вложенности. То есть это именно те случаи, где тернарник является прямой и безопасной заменой.
Операторы && и || в сложных и громоздких уловиях тоже порождают нечитаемый код. Но почему-то никто из этого не делает вывод, что их нужно убрать из языка. Да и других способов предостаточно в Go, чтобы написать нечитаемый код.
«if-else бесспорно яснее»
Как и подчеркивал ранее - слово «бесспорно» не выдерживает проверки данными. Из массы найденных паттернов условного return в репозиториях — это код вида if err != nil { return X } return Y, который мог бы быть одной строкой. Другая масса паттернов условного присваивания — это var x; if { x = a } else { x = b } вместо x := cond ? a : b.
В обоих случаях пятистрочный блок требует от читателя удержать в голове факт объявления, имя переменной, условие и оба значения одновременно. Однострочная запись сокращает когнитивную нагрузку именно потому, что информация находится в одном месте. И академические исследования это подтверждают.
Аргумент команды не опирается ни на одно из исследований – он остаётся аксиомой, не пересмотренной за 16 лет существования языка.
«Языку нужна только одна конструкция управления потоком»
Я бы указал тут на терминологическую ошибку, которую важно зафиксировать явно. Тернарный оператор – это условное выражение, а не конструкция управления потоком и разница достаточно принципиальная.
Управление потоком (control flow statement) изменяет порядок исполнения инструкций: if выполняет блок или нет, for повторяет его, goto прыгает. Выражение (expression) вычисляется и возвращает значение. a ? b : c не меняет поток – оно производит значение, которое затем используется в присваивании или передаётся как аргумент.
При этом в Go уже существует пять конструкций управления потоком: if, for, switch, select, goto. Выглядит так, что апелляция к «одной конструкции» не отражает реальность языка.
Фрагментация экосистемы
Одна из основных целей Go – читаемость кода, написанный любым разработчиком в любом проекте. «Go code is Go code» и идея о том, что незнакомый репозиторий можно читать без изучения локальных соглашений.
На практике же, отсутствие тернарного оператора создаёт ровно обратный эффект. Каждая команда, которой нужна однострочная условная запись, решает эту задачу самостоятельно. Во множестве репозиториев встречаются минимум три разно-совместимых паттерна cmp.Or, func Ternary[T], if-else. Ни один из них не является стандартным, ни один не работает во всех случаях, и каждый из них требует пояснения при первом появлении в кодовой базе.
Сторонний пакет github.com/samber/lo содержит функции lo.Ternary и lo.TernaryF, где последняя принимает функции вместо значений, чтобы обойти проблему eager evaluation. Это вполне рабочее решение, но оно означает: ради функциональности, которую большинство языков предоставляет синтаксически, Go-разработчик подключает внешнюю зависимость.
Фрагментация – это прямое следствие языкового решения. Она не исчезнет от того, что команда закроет очередной proposal...
Контрагрументы
Как и у любого другого языка, в котором присутствует тернарный оператор, он не обделен недостатками. И честность требует показать случай, где команда Go совершенно права и показать его полностью, без смягчений.
Умеренная вложенность. Это уже априори даже выглядит как усложение и ревьюер вынужден найти границы внешнего выражения, затем разобрать внутреннее, а при беглом чтении кода скобки легко пропустить.
role := isAdmin ? (isSuperAdmin ? "superadmin" : "admin") : "user"
Реальный «ад», когда уже три уровня и больше. Даже если было бы форматирование, оно не помогло бы. Что важно, сообщество прекрасно определяет что это антипаттерн и как в случае с громозким if-else, понимает, что идиоматичнее использовать уже switch case.
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'
Но проблема – не оператор
Ключевой вопрос неизменен: «является ли существование таких примеров достаточным основанием для запрета оператора?»
Вообще в 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)))
}
Индустрия давно решила проблему вложенного тернарного оператора без запрета самого оператора. 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-паттерны присутствуют в каждом из анализируемых репозиториев. Стандарта как такового нет и это прямое следствие языкового решения.
Все 3 аргумента из Go FAQ не выдерживают проверки применительно к плоским, однострочным случаям. Аргумент о нечитаемости описывает вложенность, а не оператор. Аргумент о ясности if-else противоречит академическим данным о когнитивной нагрузке. Аргумент об «одной конструкции управления потоком» содержит терминологическую ошибку. Все три остаются в FAQ без обновления – вне зависимости от того, что происходило в обсуждениях.
До этой работы дискуссия велась на уровне мнений и примеров. Именно отсутствие данных Ian Lance Taylor называл в 2019 году причиной, по которой предложение не стоит рассматривать. Данное исследование закрывает этот пробел: введены измеримые метрики с операциональными определениями и воспроизводимой методологией, выработана таксономия паттернов, результаты опубликованы в открытом доступе.
Данное исследование формирует первое количественное измерение «ternary gap» – разрыва между тем, что язык позволяет выразить компактно, и тем, что ему для этого недостаёт.
Ответ «doesn't seem worth it», данный в 2019 году без подтвержденных данных – был позицией. Теперь данные есть и вопрос в том, готова ли команда применить к собственному решению тот же стандарт доказательности, который она требовала от сообщества?
Голосование
Это голосование неотъемлемая часть исследования. Его цель не в том, чтобы получить репрезентативную статистику по всем Go-разработчикам мира, а проверить – совпадают ли представления команды Go о том, как разработчики воспринимают конкретные синтаксические паттерны с тем, что говорят сами разработчики.
Выборка заведомо смещена, так как сюда придут те, кто уже интересуется темой. Всего четыре простых вопроса, каждый из которых занимает меньше минуты.
Твой голос поможет сформировать общую панораму восприятий разработчиков 🙏
// Вариант А
var suffix string
if count != 1 {
suffix = "s"
}
fmt.Printf("found %d item%s\n", count, suffix)
// Вариант Б
suffix := count != 1 ? "s" : ""
fmt.Printf("found %d item%s\n", count, suffix)
// Вложенный тернарный оператор (right-associative)
label := isAdmin ? (isSuperAdmin ? "superadmin" : "admin") : "user"
// Способ 1
logLevel := "warn"
if isDev() {
logLevel = "debug"
}
// Способ 2
logLevel := getLogLevel(isDev())
...
func getLogLevel(isDev bool) string { ... }
// Способ 3
import "github.com/samber/lo"
...
logLevel := lo.Ternary(isDev(), "debug", "warn")