РозробкаБудь-який рівень

Go

Go — це мова, свідомо спроєктована нудною: мало можливостей, один очевидний спосіб для більшості речей і компілятор, що видає єдиний виконуваний файл, до якого нічого не треба доставляти. Винятково добре вона робить паралельність: ви пишете звичайний послідовний код, запускаєте стільки горутин, скільки в задачі частин, а решту бере на себе середовище виконання. На цій сторінці — планувальник, канали, помилки як значення, свідомі прогалини й те, де Go-команди втрапляють у халепу.

Артефакт
Один статичний бінарник
Паралельність
Горутини, по кілька КБ
Помилки
Значення, не винятки

Чому блокування тут безкоштовне

Горутина — не потік. Вона стартує зі стеком на пару кілобайтів, що росте за потреби, тож створити сто тисяч горутин — це звичайно, а не безрозсудно. Середовище виконання Go розкладає їх на невелику кількість справжніх потоків операційної системи — типово стільки, скільки у вас ядер, — і планує їх саме.

Горутина коштує кілька кілобайтів, тож програми на Go створюють її на кожен запит чи з’єднання, а не тримають пул. Середовище виконання розкладає їх на невелику кількість потоків ОС: кожен логічний процесор P має власну чергу виконання й працює на одному потоці M, а GOMAXPROCS визначає, скільки їх працює одночасно. Коли горутина робить блокуючий виклик, середовище від’єднує потік і передає чергу іншому, тож решта продовжує працювати, — і саме тому прямолінійний блокуючий код у Go швидкий, а в однопотоковому середовищі — ні.

Наслідок і є тим, що варто забрати з собою. Коли горутина робить блокуючий виклик — запит до бази, HTTP-запит, читання файлу, — середовище паркує її й передає її чергу виконання іншому потоку, тож ніщо не зупиняється. Ви отримуєте масштабованість асинхронного сервера, пишучи код, що читається згори вниз, без колбеків, без ланцюгів промісів, без «кольорових» функцій і зі стектрейсами, які означають те, що кажуть. Node розв’язав ту саму задачу, зробивши асинхронним усе; Java прийшла до відповіді Go на два десятиліття пізніше — з віртуальними потоками.

Горутини спілкуються через канали: типізовану трубу, де надсилання блокується, доки не готовий отримувач, і навпаки. Це блокування і є синхронізацією: передача значення через канал передає й відповідальність за нього — саме це на практиці означає спільнотне гасло «діліться пам’яттю, спілкуючись». select чекає на кілька каналів одразу, а в парі з context це той спосіб, яким запит скасовує розпочату роботу, коли клієнт пішов.

Канали — не відповідь на все, і новачки в Go ними зловживають. Коли кільком горутинам просто треба читати й писати одну спільну структуру, sync.Mutex зрозуміліший, швидший і легший для міркування, ніж протокол на каналах. Беріть канали, щоб передавати роботу й сигналізувати; беріть м’ютекс, щоб захищати стан. І запускайте тести з -race, бо Go спокійно дозволить двом горутинам писати в одну мапу, а скаже вам про це лише детектор гонок.

Помилки — це значення

У Go немає винятків. Функція, здатна зазнати невдачі, повертає помилку поруч із результатом, а викликач її перевіряє. Це те архітектурне рішення, про яке сперечаються найбільше, і суперечка проминає, що воно дає: кожне місце, де може статися збій, видно в коді на тому самому рядку, і немає невидимого шляху, яким керування покидає вашу функцію. Забути врахувати помилку не вийде, бо компілятор не дозволить проігнорувати оголошену для неї змінну.

Ціна — повторюваність: та сама перевірка на три рядки скрізь. Сучасний Go її пом’якшує: обгортання помилки через fmt.Errorf і %w будує ланцюг, який несе контекст угору, не втрачаючи оригіналу, а errors.Is та errors.As досліджують цей ланцюг нагорі, де й місце рішенню. Взірець, до якого варто прагнути: низькорівневий код додає контекст і повертає, а рівно один шар біля межі вирішує, що робити — залогувати, повторити чи перетворити на код статусу.

Свідомо мала

Go лишає поза бортом можливості, які інші мови вважають необхідними, і ці пропуски — задум, а не недогляд. Немає спадкування: типи вбудовують інші типи й задовольняють інтерфейси неявно, не оголошуючи цього. Немає конструкторів, перевантаження операторів, аргументів за замовчуванням, а донедавна не було й дженериків. Сенс усього цього в тому, що Go-код, написаний незнайомцем, схожий на Go-код, написаний вами, — і саме це робить робочою велику команду на великій кодовій базі.

РішенняЩо ви отримуєтеЧим жертвуєте
Неявні інтерфейсиСпоживач визначає потрібний йому інтерфейс; пакети лишаються незв’язаними.Знайти всі реалізації інтерфейсу — це справа інструментів, а не grep.
Без спадкуванняКомпозиція через вбудовування; немає крихкої ієрархії, яку треба простежувати вгору.Патерни на ієрархіях класів доводиться переспроєктовувати, а не перекладати.
Помилки як значенняКожен шлях збою видно; немає невидимого розкручування стека.Повторюваність і реальний ризик загубити контекст на кожному стрибку.
Один статичний бінарникРозгортання копіюванням файлу; образи контейнерів на кілька мегабайтів.Крос-компіляція потребує обережності, щойно в грі з’являються C-бібліотеки.

Ще дві речі визначають щоденну роботу. gofmt завершує будь-яку дискусію про форматування, роблячи його непредметом обговорення: розкладка одна, і застосовує її інструмент. А стандартна бібліотека незвично повна для бекенда: HTTP-сервер, JSON, TLS, шаблони, тестування й профілювання — усе в ній, тож робочий сервіс справді може майже не мати сторонніх залежностей. Дженерики прийшли в Go 1.18 і корисні для контейнерів та допоміжних функцій; це не запрошення відбудовувати механіку рівня типів іншої мови.

Як це виявляється в реальній поставці

Операційна історія й пояснює, чому Go забрав собі інфраструктуру. Збірка дає один статично злінкований виконуваний файл без середовища, яке треба доставляти: образ контейнера в мегабайтах, старт у мілісекундах і розгортання, що є копіюванням файлу. Docker, Kubernetes, Terraform і Prometheus написані на Go, і це не збіг: це саме ті програми, де малий, швидкий і без залежностей бінарник є вимогою.

Збирач сміття Go налаштований на затримку, а не на пропускну здатність: паузи зазвичай значно менші за мілісекунду, і досягає він цього тим, що більше працює паралельно й витрачає більше CPU, ніж витрачав би збирач, орієнтований на пропускну здатність. Для сервісу це правильна угода. Інструменти, щоб побачити, що ваша програма робить насправді, постачаються з нею: pprof для профілів CPU, пам’яті та горутин і детектор гонок під go test -race, який має бути частиною CI, а не тим, що ви пам’ятаєте запустити.

Де це вироджується

  • Горутини, запущені без способу зупинити їх, течуть весь час життя процесу й ростуть разом із трафіком.
  • Помилки, повернені без обгортання: збій десятьма шарами нижче приходить як «connection refused» без контексту.
  • panic і recover у ролі механізму винятків для звичайних, очікуваних збоїв.
  • Канали там, де простішим інструментом був м’ютекс, — виходить протокол, за яким ніхто не встежить.
  • Спільні мапи, у які пишуть кілька горутин без блокування, — це спіймав би детектор гонок.
  • Шаруваті абстракції, перенесені з Java чи C#, воюють із мовою замість того, щоб нею користуватися.
  • context, прийнятий параметром і потім проігнорований: скасування ніколи не доходить до роботи.

Коли це застосовувати

Застосовуйте, коли

  • Мережеві сервіси та API з високою паралельністю — саме під це навантаження й проєктували середовище.
  • Інфраструктура й CLI-інструменти, де єдиний бінарник без залежностей вартий більше за будь-яку можливість мови.
  • Контейнери й serverless-функції, які мають стартувати за мілісекунди й лишатися малими.
  • Команди, які цінують один читабельний стиль вище за виразність, — мова забезпечує його за вас.

Уникайте, коли

  • Домени з глибоким моделюванням через типи, де свідомо мала система типів відчуватиметься кліткою.
  • Наука про дані й машинне навчання, де бібліотек просто немає, а в Python вони є.
  • Швидка CRUD-розробка, якій першого дня потрібні адмінка, ORM і автентифікація, — Go не дає нічого з цього.
  • Жорсткі вимоги реального часу, де неприйнятна будь-яка пауза збирача сміття.

Було корисно?

Поділіться з тим, хто працює над тією ж задачею.