Go
Go — компільована мова, чиє середовище виконання запускає тисячі дешевих горутин на кількох потоках ОС, тож код, що блокується, лишається швидким. Через цей задум Go-сервіс читається згори вниз без жодного колбека — і через нього ж його типовий збій не повільний запит, а горутина, що чекає вічно. Сторінка починається з усіх місць, де горутина чекає, і того, що її відпускає; далі — планувальник у контейнері, життя й витоки горутин, спільна пам’ять і детектор гонок, помилки, релізи до Go 1.27, пам’ять і збирач сміття, п’ять задокументованих кейсів і те, де все це вироджується.
- Поточна версія
- Go 1.27 · 19 серп. 2026
- Модель
- Горутини на кількох потоках ОС
- Підтримка
- Дві останні версії; нова — щопівроку
Де чекає горутина — і що її відпускає
Node-сервер мусить питати, який потік платить за кожну роботу. Go-сервер — ні: коли горутина чекає — на канал, на блокування, на мережу, — середовище паркує її й запускає на тому самому потоці іншу горутину, тож очікування коштує кілька кілобайтів пам’яті й більше нічого. Через це ризик переїжджає в інше місце. Корисне питання до рядка на Go — не «чи він швидкий?», а «якщо тут доведеться чекати, що мене розбудить — і що буде, якщо ніщо так і не розбудить?». Горутина, яку ніщо не розбудить, — не помилка: ні рядка в лозі, ні падіння. Вона лежить у пам’яті разом зі стеком і всім, на що посилається, доки процес не перезапустять. Ось чотирнадцять місць, де чекає горутина. Наступні розділи посилаються на рядки за номерами.
| # | Де чекає | Що її відпускає | Якщо ніщо не відпустить | Захід |
|---|---|---|---|---|
| 1 | ch <- v у небуферизований канал | Отримувач забирає значення | Витік — зазвичай воркер, чий викликач уже повернувся | Буфер на кількість відправників або select з ctx.Done() |
| 2 | v := <-ch | Надсилання або close(ch) | Витік | Сторона, що надсилає, закриває канал, коли закінчила |
| 3 | for v := range ch | Кожне надсилання; цикл завершує лише close(ch) | Витік після останнього значення | Канал закриває рівно один власник |
| 4 | Надсилання чи отримання з nil-каналу | Ніщо й ніколи | Чекає вічно — так задумано | Створюйте канали через make; nil-канал — спосіб вимкнути гілку select |
| 5 | Надсилання в закритий канал | Не чекає — панікує | Процес зупиняється | Закриває лише сторона, що надсилає, і лише раз |
| 6 | mu.Lock() | Unlock() в іншій горутині | Стає кожна горутина, якій потрібне це блокування | defer mu.Unlock() наступним же рядком |
| 7 | wg.Wait() | Лічильник доходить до нуля | Витік; лічильник нижче нуля — паніка | wg.Go(f) (Go 1.25) замість ручних Add і Done |
| 8 | select без default | Будь-яка з його гілок | Витік, якщо жодна гілка ніколи не спрацює | Завжди додавайте case <-ctx.Done() |
| 9 | Читання з мережевого з’єднання | Дані або дедлайн | Клієнт, що нічого не надсилає, тримає горутину й сокет | conn.SetReadDeadline; http.Server{ReadHeaderTimeout: …} |
| 10 | Вихідний виклик через http.DefaultClient | Відповідь — тайм-ауту в типового клієнта немає | Чекає стільки, скільки віддалений сервер | http.Client з Timeout або запит із контекстом |
| 11 | Вільне з’єднання з пулу database/sql | З’єднання, повернене в пул | Кожен запит чекає за результатами, які ніхто не закрив | defer rows.Close(); пул під розмір бази |
| 12 | <-ctx.Done() | cancel() або дедлайн | На context.Background() не спрацює ніколи | Похідний context.WithTimeout і виклик cancel — go vet перевіряє |
| 13 | time.After чи time.Ticker у циклі | Спрацювання таймера | До Go 1.23 кожен незупинений таймер лежав у пам’яті до спрацювання | go 1.23 чи новіше в go.mod; Stop() для кожного тікера |
| 14 | Блокуючий системний виклик чи виклик у C через cgo | Повернення з виклику | Середовище запускає новий потік; понад 10 000 потоків програма падає | Обмежте, скільки таких викликів іде одночасно |
Планувальник: чому чекати дешево і що з цим робить контейнер
Горутина — не потік. Вона стартує зі стеком на кілька кілобайтів, що росте за потреби, тож сервер запускає одну горутину на запит чи на з’єднання й не тримає їх у пулі. Середовище виконує горутини на справжніх потоках операційної системи, а його планувальник має три частини, названі в коді літерами. G — горутина. M — machine, потік ОС. P — processor: дозвіл виконувати Go-код із власною чергою готових до роботи горутин. Таких P рівно GOMAXPROCS, тож саме це число визначає, скільки горутин працює в одну мить; решта чекає в чергах.
Коли горутина чекає на канал, блокування чи таймер, планувальник паркує її, а P бере з черги наступну; потік не зупиняється ніколи. Очікування мережі теж не коштує потоку. Середовище передає кожен сокет інтерфейсу готовності операційної системи — epoll у Linux, kqueue у macOS, той самий механізм, що й у Node, — і будить горутину, коли надходять її дані. P із порожньою чергою краде половину черги зайнятого, тож робота розходиться ядрами без того, щоб хтось її розподіляв. А з Go 1.14 планувальник уміє перервати горутину посеред щільного циклу; до того цикл без викликів функцій міг тримати свій потік і зупиняти збирач сміття для всього процесу.
Рядок 14 — єдиний вид очікування, який планувальник не може сховати. Блокуючий системний виклик, наприклад читання файлу, чи виклик у C через cgo тримає справжній потік, доки не повернеться. Середовище від’єднує P від цього потоку й віддає іншому — запускаючи новий, якщо вільного немає, — тож інші горутини працюють далі. Ціна — один потік ОС на кожен виклик у процесі. Тисяча одночасних повільних читань файлів — це тисяча потоків, а понад 10 000, типову межу з debug.SetMaxThreads, програма падає. Обмежуйте такі виклики семафором — досить буферизованого каналу на N місць, — а не дозволяйте кількості запитів вирішувати за вас.
GOMAXPROCS у контейнері
До Go 1.25 GOMAXPROCS за замовчуванням дорівнював кількості ядер машини — а всередині контейнера це хибне число. Ліміт CPU у Kubernetes, що дорівнює 2, не дає контейнеру двох ядер. Він дає квоту: 200 мс процесорного часу в кожному вікні по 100 мс, і стежить за нею планувальник Linux. Go-процес на 64-ядерному вузлі однаково запускав 64 P. Коли робота приходила сплеском — або коли збирач сміття, що забирає чверть P під власних воркерів, починав цикл, — 64 потоки витрачали всю квоту приблизно за 3 мс. Після цього ядро ОС зупиняло кожен потік контейнера на решту 97 мс. Власний опис команди Go: тротлінг «повністю зупиняє виконання застосунку до кінця періоду тротлінгу».
Go 1.25 читає ліміт CPU контейнера, ставить GOMAXPROCS рівним йому з округленням угору й періодично перевіряє знову, бо оркестратор може змінити ліміт, поки процес працює. Враховується саме ліміт, а не запит CPU (request). Автоматичне значення вимикається, щойно GOMAXPROCS задано вручну, тож деплой, який задав його багато років тому, лишається з тим, що задав. До 1.25 те саме давав пакет Uber automaxprocs, який імпортували заради побічного ефекту; на 1.25 і новіших його можна прибрати. Симптом старого значення варто впізнавати, бо він досі живе в старих образах: p99час відгуку для найповільнішихЧас відгуку, в який вкладаються 99% запитів; p95 і p50 — та сама величина для 95% і для медіани. Середнє приховує саме ті запити, через які люди йдуть, — тому дивляться на ці числа, а не на нього.Усі терміни латентності сходинками приблизно по 100 мс, графік CPU далеко під лімітом і лічильник тротлінгових періодів cgroup, що росте.
Кожній горутині потрібен власник: витоки, context і errgroup
Горутина не має батька. Функція, що її запустила, може повернутися, а горутина працює далі; ніщо в мові не пов’язує ці два життя. Тому рядки 1–3 і 8 — найчастіший витік у Go. Примітки до релізу Go 1.26 показують хрестоматійний випадок. Функція запускає по горутині на кожен елемент роботи, кожна надсилає результат у небуферизований канал, а функція збирає результати в циклі. Коли один результат виявляється помилкою, функція повертається раніше — і кожна горутина, що ще тримає результат, назавжди блокується на надсиланні, бо ніхто більше не отримує.
Витік росте з трафіком, а не з часом, тож переживає всі тести й з’являється за тиждень після запуску як графік пам’яті, що лише зростає. Виправлень три, і кожне відповідає своїй формі коду. Якщо викликач може перестати читати, дайте каналу буфер на кількість відправників, щоб кожне надсилання завершувалося, слухає хтось чи ні. Якщо робота має зупинятися, коли викликач здався, передайте context і зробіть кожен блокуючий крок select, що чекає ще й на ctx.Done(). А для схеми «запусти N задач, зупинись на першій помилці» беріть errgroup.WithContext з golang.org/x/sync: він скасовує спільний контекст на першому збої, а Wait повертає цю помилку, коли завершилася кожна горутина.
Як знайти витік у продакшені
Перший сигнал — число: runtime.NumGoroutine(), експортоване як метрика. У здорового сервісу воно росте й падає разом із трафіком; у сервісу з витоком — повзе вгору храповиком і ніколи не повертається. Другий — профіль горутин, /debug/pprof/goroutine з net/http/pprof: він групує тисячі горутин за рядком, на якому вони застрягли, тож витік видно як один рядок, що тримає сорок тисяч горутин. Go 1.27 додає третій, створений саме для цього. Профіль goroutineleak, який зробив Влад Сайок з Uber, за допомогою збирача сміття знаходить горутини, заблоковані на каналі чи блокуванні, до яких не може дотягнутися жодна робоча горутина, — тобто такі, яких ніщо й ніколи не відпустить. У 1.26 це був експеримент, у 1.27 — стабільна можливість на /debug/pprof/goroutineleak. Витоку через глобальну змінну він не бачить, і документація так і каже. У тестах пакет Uber goleak валить кожен тест, що лишає по собі горутини.
| Що ви бачите | Що це зазвичай означає | Куди дивитися |
|---|---|---|
| Пам’ять і кількість горутин ростуть із трафіком і не падають | Витік — рядки 1–3, 7 чи 8 | Профіль горутин, згрупований за рядками; goroutineleak на 1.27 |
| p99 латентності сходинками близько 100 мс, CPU далеко під лімітом | Тротлінг CPU — GOMAXPROCS більший за ліміт контейнера | Лічильники тротлінгу cgroup; версія Go; GOMAXPROCS |
fatal error: all goroutines are asleep - deadlock! | Кожна горутина чекає на іншу | Дамп стеків, надрукований разом із нею |
fatal error: concurrent map writes | Дві горутини писали в одну мапу без блокування | Обидва стеки в повідомленні; потім запуск із -race |
| Вихідні виклики висять хвилинами | Клієнт без тайм-ауту (рядок 10) | Як збудовано http.Client |
| Контейнер отримує OOMKilled, хоча купа виглядала нормально | GOGC дозволив купі вирости понад ліміт | GOMEMLIMIT — див. розділ про пам’ять |
Мова: помилки як значення, малий задум і релізи
У Go немає винятків. Функція, що може зазнати невдачі, повертає помилку поруч із результатом, а викликач перевіряє її наступним рядком. Про це сперечаються більше, ніж про будь-що інше в Go, і суперечка зазвичай проминає, що це дає: кожне місце, де може статися збій, видно на тому самому рядку, і жоден прихований шлях не виносить керування з функції. Ціна — повторюваність: та сама перевірка if err != nil на три рядки скрізь.
Сучасний Go змушує цю повторюваність нести інформацію. Обгортання через fmt.Errorf("load user %d: %w", id, err) додає контекст і зберігає початкову помилку всередині, тож збій десятьма шарами нижче приходить одним зрозумілим реченням, а не «connection refused». errors.Is питає, чи є певна помилка десь у цьому ланцюгу; errors.As — і його узагальнена форма errors.AsType, додана в Go 1.26, — дістає помилку заданого типу; errors.Join (Go 1.20) поєднує кілька. Взірець, до якого варто прагнути: низькорівневий код додає контекст і повертає, а рівно один шар біля краю програми вирішує — залогувати, повторити чи перетворити на код статусу.
Решта мови мала навмисно, і пропуски — це задум, а не недогляд. Спадкування немає: типи вбудовують інші типи й задовольняють інтерфейс просто тим, що мають його методи, нічого не оголошуючи. Немає конструкторів, перевантаження операторів, аргументів за замовчуванням, а перші дванадцять років не було й дженериків. Сенс у тому, що Go-код незнайомця схожий на ваш власний, і саме це тримає велику кодову базу читабельною для людей, які її не писали.
| Рішення | Що ви отримуєте | Чим жертвуєте |
|---|---|---|
| Неявні інтерфейси | Споживач визначає потрібний йому інтерфейс; пакети лишаються незв’язаними | Знайти всі реалізації — справа інструментів, а не grep |
| Без спадкування | Композиція через вбудовування; немає крихкої ієрархії, яку треба простежувати вгору | Патерни на ієрархіях класів доводиться переспроєктовувати, а не перекладати |
| Помилки як значення | Кожен шлях збою видно; немає невидимого розкручування стека | Повторюваність і реальний ризик загубити контекст на кожному кроці |
| Дженерики, стримані (1.18 → 1.27) | Типобезпечні контейнери й помічники; з 1.27 — узагальнені методи типу | Жодних параметрів типу в методах інтерфейсу; конструкції рівня типів і далі воюють із мовою |
gofmt | Одна розкладка для всього Go-коду; форматування ніколи не обговорюють | Вибору немає — у цьому й суть |
| Один статичний бінарник | Розгортання копіюванням файлу; образи на кілька мегабайтів | Крос-компіляція потребує обережності, щойно через cgo заходять C-бібліотеки |
Стандартна бібліотека незвично повна для бекенда: HTTPHyperText Transfer Protocol — протокол вебуПротокол, яким спілкуються браузер і сервер: клієнт надсилає метод і адресу, сервер відповідає кодом стану й тілом. Між запитами він нічого не памʼятає — кожен запит стоїть сам по собі, тому все, що треба памʼятати, передається явно.Усі терміни-сервер і клієнт, JSONJavaScript Object Notation — текстовий формат данихТекстовий формат для передавання даних: пари «ключ — значення», списки, числа й рядки. Він народився в JavaScript, але сьогодні його читає будь-яка мова — і саме тому ним відповідає більшість API, а не чимось потужнішим.Усі терміни, TLS, інтерфейси SQL, шаблони, тестування, фазинг і профілювання постачаються разом із компілятором, тож робочий сервіс справді може мати короткий go.mod. Go 1.27 замінив рушій під encoding/json на новий encoding/json/v2 — суворіший за замовчуванням у новому пакеті й значно швидший у декодуванні в обох, — зберігши поведінку старого пакета.
Релізи, обіцянка сумісності і який Go перед вами
Go випускає мажорний реліз кожні шість місяців, у лютому й серпні, і підтримує кожен, доки не вийдуть два новіші, — приблизно рік. Патч-релізи (1.27.1, 1.27.2…) несуть виправлення безпеки лише для двох підтримуваних ліній. Від Go 1, з 2012 року, команда обіцяє, що код, написаний під давніший реліз, і далі компілюється й працює. Коли виправлення мусить змінити поведінку, воно виходить за налаштуванням GODEBUG, яке зберігає стару поведінку для модулів, що оголошують старішу версію Go. Таблиця перелічує релізи, які ви найімовірніше зустрінете в коді.
| Версія | Випущено | Що ви зустрінете в коді | Підтримка |
|---|---|---|---|
| 1.18 | бер. 2022 | Дженерики, фазинг у go test, воркспейси | Ні |
| 1.19 | серп. 2022 | GOMEMLIMIT — м’який ліміт пам’яті для збирача сміття | Ні |
| 1.20 | лют. 2023 | errors.Join; покриття для цілих програм | Ні |
| 1.21 | серп. 2023 | min, max, clear; log/slog; рядок go стає мінімумом, а тулчейн завантажує себе сам | Ні |
| 1.22 | лют. 2024 | Нова змінна на кожну ітерацію; for i := range 10; шаблони з методами й підстановками в http.ServeMux | Ні |
| 1.23 | серп. 2024 | Ітератори — range по функції; незупинені таймери збираються | Ні |
| 1.24 | лют. 2025 | Узагальнені псевдоніми типів; рядки tool у go.mod; os.Root; testing.B.Loop; мапи на Swiss table | Ні |
| 1.25 | серп. 2025 | GOMAXPROCS з урахуванням контейнера; sync.WaitGroup.Go; testing/synctest | Завершено в серп. 2026 |
| 1.26 | лют. 2026 | new(expr); errors.AsType; збирач сміття Green Tea за замовчуванням | До Go 1.28, очікується в лют. 2027 |
| 1.27 | серп. 2026 | Узагальнені методи; профіль goroutineleak; encoding/json/v2 | До Go 1.29, очікується в серп. 2027 |
Пам’ять: GOGC, GOMEMLIMIT і ліміт контейнера
Збирач сміття в Go налаштований на короткі паузи, а не на максимальну пропускну здатність. Він позначає живі об’єкти, поки програма працює далі, і зупиняє всі горутини лише на короткі фази, зазвичай значно коротші за мілісекунду. Платить він за це процесором. Поки триває цикл, збирач забирає приблизно чверть P під власних воркерів, а горутину, що швидко виділяє пам’ять, змушують допомагати з позначанням — це «асист», який видно як латентність того запиту, що вона обслуговувала. Go 1.26 зробив новий збирач Green Tea типовим, і примітки до релізу очікують, що він зменшить витрати на збирання сміття на 10–40% у програмах, які сильно на нього спираються.
Як часто запускається цикл, вирішує одне число. GOGC, за замовчуванням 100, означає, що купа може вирости на 100% від живих даних, перш ніж почнеться наступний цикл: сервіс, що тримає 1 ГБ живих об’єктів, працює з купою близько 2 ГБ. Подвоєння GOGC приблизно вдвічі зменшує витрати збирача на CPU й додає ще стільки ж пам’яті. На машині з вільною пам’яттю це прийнятна угода, а в контейнері — небезпечна, бо GOGC нічого не знає про ліміт контейнера. Сервіс із 1,5 ГБ живих даних у контейнері на 2 ГБ дозволяє купі йти до 3 ГБ, перш ніж збирач задумається про запуск, — і ядро вбиває його дорогою.
Go 1.19 додав другу половину: GOMEMLIMIT, м’який ліміт на всю пам’ять, якою керує середовище. Що ближче до нього загальний обсяг, то частіше працює збирач, хоч би що казав GOGC. Ставте його рівним ліміту контейнера мінус запас — посібник зі збирача сміття Go радить 5–10% на пам’ять, якої середовище не бачить, як-от C-бібліотеки, — тож GOMEMLIMIT=1800MiB у контейнері на 2 ГіБ. Ліміт м’який навмисно. Якщо самі живі дані переростуть його, середовище обмежить збирач приблизно половиною CPU замість безперервного збирання, тож справжній витік однаково закінчиться вбивством через брак пам’яті — лише пізніше й після відрізка високого CPU, на який варто мати окремий алерт.
Постачання: один бінарник і коректна зупинка
Операційна історія й пояснює, чому Go забрав собі інфраструктуру. go build видає один виконуваний файл без середовища, яке треба встановлювати, а з CGO_ENABLED=0 він злінкований статично, тож образ контейнера може складатися з самого бінарника — кілька мегабайтів на основі scratch чи distroless. Він стартує за мілісекунди, а GOOS і GOARCH крос-компілюють його під іншу платформу з будь-якої машини. Docker, Kubernetes, Terraform і Prometheus написані на Go саме з цих причин. В образі scratch немає ні оболонки, ні сертифікатів центрів сертифікації, тож скопіюйте туди їхній набір, інакше перший вихідний HTTPS-виклик упаде.
Коректна зупинка займає шість рядків. signal.NotifyContext перетворює SIGTERM на скасований контекст; коли той спрацьовує, srv.Shutdown(ctx) перестає приймати з’єднання, закриває простійні й чекає на запити в польоті до дедлайну, який ви йому дали. Зробіть цей дедлайн коротшим за пільговий період оркестратора — у Kubernetes типово 30 секунд — і передайте той самий контекст далі, щоб і довга робота це помітила. І заразом задайте тайм-аути сервера: http.Server, збудований як гола структура, їх не має, а це рядок 9, що чекає на повільного клієнта.
П’ять кейсів і те, чого в кожному бракувало
Кожен кейс задокументували ті, з ким він стався: інженерні блоги, звіт про інцидент центру сертифікації, наукова стаття. Разом вони покривають час, двічі — збирач сміття, змінну циклу й гонки даних. Twitch і Uber розказано вище, там, де живе їхній механізм; решта три — далі.
| Кейс | Що сталося | Чого бракувало |
|---|---|---|
| Cloudflare, 1 січ. 2017 | Високосна секунда зробила виміряну тривалість від’ємною; DNS-компонент на ній запанікував | Монотонний годинник — його додав Go 1.9 |
| Twitch, квіт. 2019 | 8–10 циклів GC на секунду при малій купі; баласт на 10 ГіБ скоротив їх на 99% | Ліміт пам’яті, зрозумілий збирачу, — GOMEMLIMIT, 2022 |
| Discord, лют. 2020 | Сплески латентності кожні дві хвилини, бо GC сканував величезний кеш; сервіс переїхав на Rust | Розкладка купи, яку збирачу не треба було обходити |
| Let’s Encrypt, лют. 2020 | Вказівник на змінну циклу пропускав перевірки CAA; зачеплено близько 3 мільйонів сертифікатів | Змінна на кожну ітерацію — типово з Go 1.22 |
| Uber, черв. 2022 | Близько 2000 гонок даних за пів року у 2100 сервісах | Детектор гонок у CI від самого початку |
Перший кейс — годинник. Наприкінці 2016 року до останньої хвилини року додали високосну секунду, і настінний годинник на серверах Cloudflare відступив назад. RRDNS, DNS-сервер Cloudflare, написаний на Go, вимірював, як швидко відповідають вищі резолвери, віднімаючи одне показання time.Now() від іншого, — і через високосну секунду одна з цих тривалостей вийшла від’ємною. Пізніше код, що обирав резолвер із вагами за цими вимірами, отримав від’ємне число й запанікував. Паніку перехопив recover, тож процес вижив, але пошук CNAME не вдавався: у пік помилки повертали близько 0,2% DNS-запитів до Cloudflare і менш як 1% HTTP-запитів, а виправлення розгорнули по всьому світу за сім годин. Тодішній time.Now() у Go читав лише настінний годинник, який може стрибати. Go 1.9 у серпні 2017 року змусив кожен time.Time нести ще й показання монотонного годинника, тож time.Since і Sub більше не йдуть назад. Урок переживає виправлення: тривалість вимірюють, мітку часу зчитують, і беруться вони з різних годинників.
Четвертий кейс — змінна циклу в продакшені. 29 лютого 2020 року Let’s Encrypt виявив, що Boulder, його програмне забезпечення центру сертифікації, написане на Go, перевіряв записи CAA — DNS-запис, у якому власник домену вказує, які центри можуть видавати для нього сертифікати, — лише для одного імені в сертифікаті з кількома іменами. Коли запит містив N імен, які треба було перевірити повторно, Boulder перевіряв одне з них N разів. Блог Go згодом процитував код: цикл копіював ключ, kCopy := k, але передавав &v у функцію, що зберігала вказівники всередину нього, тож кожен результат вказував на ту саму змінну. Зачепило близько 3 мільйонів сертифікатів. Let’s Encrypt замінив і відкликав близько 1,7 мільйона до дедлайну 5 березня, а решті дав спливти, бо вони жили 90 днів. Go 1.22 зробив змінну циклу окремою для кожної ітерації, що прибирає весь цей клас помилок, — для модулів, чий go.mod це оголошує.
Де він вироджується
Go щедрий в один бік і мовчазний в інший. Паралельність настільки дешева, що нею користуються не замислюючись, тож просто написаний сервіс масштабується далеко. Але ніщо в мові не каже, коли горутина закінчується, стандартна бібліотека не ставить тайм-аутів, а детектор гонок повідомляє лише про те, що сталося під час тестів. Майже кожен збій — одне з цього:
- Горутини без власника, що течуть щоразу, коли їхній викликач перестає слухати (рядки 1–3, 8).
context, прийнятий параметром і проігнорований, тож скасування ніколи не доходить до роботи (рядок 12).http.Serverчиhttp.Client, збудовані без тайм-аутів (рядки 9–10).- Незакриті результати запитів, через які пул з’єднань висихає й кожен запит чекає (рядок 11).
- Спільні мапи й зрізи, у які пишуть кілька горутин, а
-raceу CI ніколи не запускають. - Образ контейнера, зібраний до Go 1.25, що працює з
GOMAXPROCS, рівним кількості ядер хоста. - Жодного
GOMEMLIMITу контейнері, чий ліміт менший за подвоєну живу купу. panicіrecoverу ролі винятків для звичайних, очікуваних збоїв.- Помилки без контексту, через які збій приходить як «connection refused» без жодного натяку, звідки.
- Рядок
goуgo.mod, старший за виправлення, на які команда розраховує, — змінну циклу, таймери.
Чесна межа — збирач сміття й свідомо мала система типів. Сервіс, чия купа — десятки мільйонів довгоживучих вказівників (величезний кеш у пам’яті, індекс), платить за кожен цикл процесором і латентністю, які налаштування може зменшити, але не прибрати, — саме звідси пішов Discord. А домен, який хоче закодувати свої правила в типах, з вичерпним зіставленням і без nil, відчує типи Go кліткою; це територія Rust — ціною значно крутішої мови. Для мережевих сервісів, шлюзів та інструментів, які пише більшість команд, угода працює навпаки, і тому так багато інфраструктури під ними написано на Go.
Практична перевірка займає десять хвилин. Запустіть go version для продакшн-бінарника й знайдіть його рядок у таблиці релізів. Прочитайте рядок go у go.mod. Подивіться на кількість горутин на дашборді за тиждень. Порівняйте GOMAXPROCS і GOMEMLIMIT з лімітами контейнера. Знайдіть у коді http.Client{ і http.Server{ і прочитайте їхні тайм-аути. І переконайтеся, що CI запускає тести з -race.
Коли це застосовувати
Застосовуйте, коли
- Мережеві сервіси та API з високою паралельністю — саме під це навантаження проєктували середовище.
- Інфраструктура й CLICommand-Line Interface — керування командамиКерування програмою через набрані команди замість натискань. Справжня перевага не у швидкості набору: будь-яку команду можна записати у скрипт і повторити точно, без людини, — а саме це й робить автоматизацію можливою.Усі терміни-інструменти, де один бінарник без залежностей вартий більше за будь-яку можливість мови.
- Контейнери й serverless-функції, які мають стартувати за мілісекунди й лишатися малими.
- Команди, що цінують один читабельний стиль вище за виразність, — мова забезпечує його за вас.
Уникайте, коли
- Величезні довгоживучі купи з безліччю вказівників, де проблемою є будь-яка пауза збирача, — Rust звільняє пам’ять у відому мить.
- Домени з глибоким моделюванням через типи, де свідомо мала система типів відчувається кліткою.
- Наука про дані й машинне навчання, де бібліотеки є в Python, а тут — ні.
- Швидка CRUD-розробка, якій першого ж дня потрібні адмінка, ORMObject-Relational Mapping — обʼєкти замість SQLБібліотека, що підставляє обʼєкти мови там, де в базі лежать рядки таблиці, — тож SQL ви майже не пишете. Зручність коштує контролю: згенерований запит буває значно важчим за той, який ви написали б самі, і зʼясовується це вже під навантаженням.Докладніше в статті →Усі терміни і автентифікація, — Go не дає нічого з цього.
Джерела й метод
На чому стоїть ця стаття
- 19 джерел
- 2 задокументовані збої
- Перевірено 13 жовтня 2026 р.
Практики й походження термінів
усі місця, де чекає горутина, планувальник, GC і версії- СтандартThe Go Programming Language Specification (відкривається в новій вкладці)go.dev
- СтандартThe Go Memory Model (відкривається в новій вкладці)go.dev
- ГайдEffective Go (відкривається в новій вкладці)go.dev
- ГайдA Guide to the Go Garbage Collector (відкривається в новій вкладці)go.dev
- ДокументаціяData Race Detector (відкривається в новій вкладці)go.dev
- ДокументаціяGo 1.14 Release Notes (відкривається в новій вкладці)go.dev
- ДокументаціяGo 1.19 Release Notes (відкривається в новій вкладці)go.dev
- ДокументаціяGo 1.22 Release Notes (відкривається в новій вкладці)go.dev
- ДокументаціяGo 1.25 Release Notes (відкривається в новій вкладці)go.dev
- ДокументаціяGo 1.26 Release Notes (відкривається в новій вкладці)go.dev
- ДокументаціяGo 1.27 Release Notes (відкривається в новій вкладці)go.dev
- ДокументаціяRelease History (відкривається в новій вкладці)go.dev
- СтаттяMichael Pratt, Carlos Amedee — Container-aware GOMAXPROCS (відкривається в новій вкладці)The Go Blog · 20.08.2025
- СтаттяFixing For Loops in Go 1.22 (відкривається в новій вкладці)The Go Blog · 09.2023
Збої, на яких вчиться стаття
Cloudflare і Let’s Encrypt у «П’ять кейсів»Дослідження
Twitch, Discord і Uber: GC і гонки даних у масштабі- СтаттяRoss Engers — Go memory ballast: How I learnt to stop worrying and love the heap (відкривається в новій вкладці)Twitch Blog · 10.04.2019
- СтаттяJesse Howarth — Why Discord is switching from Go to Rust (відкривається в новій вкладці)Discord · 04.02.2020
- ДослідженняMilind Chabbi, Murali Krishna Ramanathan — Data Race Patterns in Go (відкривається в новій вкладці)Uber Blog · PLDI 2022 · 02.06.2022
Дослідження з Claude. Пошук і звірку першоджерел зроблено з Claude (Anthropic). Відбір, текст і перевірку фактів — редакція Anatomy of IT Product.
Було корисно?
Поділіться з тим, хто працює над тією ж задачею.