1. Навіщо потрібна політика залежностей
Коли проєкт маленький, залежності часто схожі на безневинну шоколадку біля каси: «Ну, візьмемо бібліотеку для X — так буде швидше». Але зі зростанням проєкту кожна зовнішня бібліотека починає поводитися як новий учасник команди: їй треба довіряти, її треба оновлювати, а інколи ще й розбиратися, чому вона раптом зламала збірку в пʼятницю ввечері.
Політика залежностей — це заздалегідь узгоджені правила, за якими команда або ви, якщо працюєте самі, ухвалюєте рішення: чи можна додавати зовнішні пакети, на яких умовах, як оновлювати версії, як перевіряти зміни та як зберігати збірку відтворюваною. Це не бюрократія заради бюрократії, а спосіб зробити так, щоб проєкт однаково збирався на вашій машині, у колеги та в CI.
Що таке відтворюваність на побутовому рівні
Відтворюваність — це коли той самий вихідний код і зафіксовані залежності дають однаковий результат незалежно від машини й настрою Всесвіту. У Go цьому дуже допомагає модульна система: версії залежностей фіксуються, а go.sum виступає як список контрольних відбитків завантажених модулів.
Важливо розуміти: відтворюваність — це не лише про бінарник і go build. Це ще й про тести, виведення утиліт і поведінку ваших команд. Якщо ваш CLI сьогодні друкує завдання в одному порядку, а завтра — в іншому, бо ви випадково обійшли map, то збірка ніби «відтворювана», але результат роботи програми — ні. А користувачеві байдуже: йому боляче тут і зараз.
У Go сама ідея детермінізму та відтворюваності — частина філософії інструментів і екосистеми: керування версіями та збірками давно вважається одним із ключових завдань інструментарію.
2. Як тримати залежності під контролем
Політика залежностей — це не один «магічний пункт», а набір звичок: спочатку перевіряємо стандартну бібліотеку, уважно ставимося до go.mod/go.sum, регулярно «прибираємося» через tidy, окремо фіксуємо версії інструментів і обережно використовуємо replace.
Спочатку стандартна бібліотека
Коли ви шукаєте рішення, перше запитання має бути простим і прагматичним: «Чи є це в стандартній бібліотеці?». І так, це звучить як порада з серії «пийте воду та висипайтеся», але вона працює.
Стандартна бібліотека Go велика, стабільна й сумісна. Вона оновлюється разом із Go, живе за тими самими правилами сумісності й, що важливо, не тягне за собою транзитивний зоопарк із ще двадцяти залежностей.
Уявімо, що ми розвиваємо наш навчальний застосунок — невеликий менеджер завдань, умовний todo/tasks, який уже вміє зберігати завдання в памʼяті та друкувати їх у консоль. На цьому етапі нам часто потрібні речі на кшталт сортування, форматування, роботи з часом, JSON/CSV і файловою системою. Усе це є у стандартній бібліотеці.
Інколи хочеться взяти зовнішню бібліотеку «для гарного виводу таблиць», «для конфігів», «для логів», «для всього». Але на практиці часто виявляється, що стандартна бібліотека закриває 80 % потреб, а решту 20 % можна реалізувати невеликим власним кодом, який ви розумієте та контролюєте.
Невелика «таблиця тверезості» для рішення «беремо залежність чи ні»
| Ситуація | Що зазвичай розумніше | Чому |
|---|---|---|
| Потрібні сортування, рядки, JSON, файли, HTTP | Стандартна бібліотека | Менше ризиків, менше оновлень, менше сюрпризів |
| Потрібен вузький алгоритм або формат, який справді складний | Можлива зовнішня залежність | Інколи дешевше «купити» підтримуване рішення |
| «Хочу бібліотеку, бо так роблять у статтях» | Зазвичай не треба | Стаття пише про «красиво», а ви потім живете з цією залежністю |
go.mod і go.sum — частина контракту
Коли ви бачите go.mod, думайте не «файл для Go», а «документ: які залежності потрібні проєкту, щоб зібратися». Коли ви бачите go.sum, думайте: «Які саме версії були завантажені та перевірені?». І це напряму повʼязано з відтворюваністю.
З певного моменту Go став помітно суворішим щодо стану модулів: замість того щоб «тихо виправляти» проблему в go.mod, інструмент частіше повідомляє про помилку й пропонує команду, якою її виправити. Це зроблено заради передбачуваності та прозорості: краще ви побачите проблему й усвідомлено її виправите, ніж інструмент почне «лікувати» ваш проєкт за вашою спиною.
go mod tidy як щоденна гігієна
Дуже легко довести go.mod і go.sum до стану «там щось є, але ніхто не знає чому». Наприклад, ви додали імпорт, потім видалили, потім перейменували пакет, а go.sum розрісся. Або навпаки: ви використовуєте новий пакет, але його немає в go.mod, і збірка «раптово» не працює на іншій машині.
go mod tidy якраз про те, щоб привести залежності у відповідність до реального коду: додати те, що справді використовується, і прибрати те, що більше не потрібно. Це не магія, а прибирання.
«Мʼякий» робочий сценарій у голові
Уявіть, що ви додали в наш проєкт todo новий пакет, навіть внутрішній, не зовнішній. Правильна послідовність думок така: «Я змінив імпорти → я маю привести модульні файли до ладу → я маю перевірити, що зміни зрозумілі й очікувані».
У новіших версіях Go зʼявилися й додаткові зручності навколо цієї дисципліни: наприклад, режими, які дозволяють спочатку подивитися передбачувані зміни, а вже потім прийняти їх і закомітити.
Залежності інструментів: фіксуємо версії так само, як і бібліотек
Є тонка, але важлива межа: залежності бувають «рантайм» — потрібні вашому коду, — і «дев-залежності» — потрібні вам як розробнику. Приклад дев-залежності: лінтер, генератор, форматер, утиліта для розробки.
Якщо інструмент встановлюється абияк, то одна машина може перевіряти код одним golangci-lint, а інша — іншим. Підсумок буде приблизно таким самим, як у двох учителів української мови, які сперечаються, де ставити кому: в одного — підручник 1998 року, а в іншого — 2024-го.
Go свого часу чітко окреслив правильну практику: встановлювати бінарні інструменти із зазначенням версії через go install pkg@version і при цьому не чіпати go.mod вашого проєкту. Це зроблено саме для того, щоб встановлення інструментів не перетворювалося на випадкове редагування залежностей застосунку.
replace: потужно, але небезпечно для відтворюваності
replace — корисна штука, але вона схожа на суперклей: тримає міцно, а потім віддирати боляче. Головна проблема replace, особливо коли вказано локальні шляхи, у тому, що проєкт перестає бути самодостатнім: у вас він збирається, а в колеги — ні, бо в нього немає вашої локальної папки ../some-lib.
Якщо ви використовуєте replace, у вас має бути політика: чому це зроблено, як довго це припустимо і що є сигналом до видалення replace. У навчальних проєктах це часто допускають як тимчасовий костиль, але в реальному проєкті краще ставитися до цього як до винятку, а не як до норми.
3. Відтворюваність результату: детермінізм і процес
Відтворюваність збірки часто ламається не через «версію пакета», а через випадковість поведінки. І тут ми переходимо до дуже практичних речей, які раптом виявляються частиною тієї самої політики якості: однакові входи мають давати однаковий вихід.
Порядок обходу map і стабільний вивід
У Go порядок ітерації по map не гарантується. Якщо ви виводите завдання «як вийде», користувачі й тести страждатимуть.
У нашому застосунку, припустімо, є статистика за тегами завдань — лічильник map[string]int. Ми хочемо друкувати її стабільно.
package report
import (
"sort"
"strconv"
"strings"
)
func StablePairs(m map[string]int) string {
keys := make([]string, 0, len(m))
for k := range m {
keys = append(keys, k)
}
sort.Strings(keys)
var b strings.Builder
for _, k := range keys {
b.WriteString(k)
b.WriteString("=")
b.WriteString(strconv.Itoa(m[k]))
b.WriteString("\n")
}
return b.String()
}
Цей код маленький і нудний, а отже — надійний. Ви «платите» кількома рядками, зате отримуєте стабільність виводу, а разом із нею — нормальні тести, нормальні diff-и та менше сюрпризів.
Час (time.Now()) як джерело невідтворюваних тестів і логіки
Друга класична причина — поточний час. Якщо ваш код усередині бізнес-логіки викликає time.Now(), тести можуть бути або складними, або крихкими.
Простий патерн — ввести інтерфейс Clock і реальну реалізацію.
package audit
import "time"
type Clock interface {
Now() time.Time
}
type RealClock struct{}
func (RealClock) Now() time.Time { return time.Now() }
func AgeInDays(c Clock, created time.Time) int {
d := c.Now().Sub(created)
return int(d.Hours() / 24)
}
Так ви відокремлюєте «чисту логіку» від джерела часу. Це напряму стосується якості: ви контролюєте середовище виконання, і результати стають передбачуваними.
Мініпротокол зміни залежностей
Коли ви додаєте залежність або прибираєте її, дуже важливо не діяти в стилі «ну воно ж збирається в мене». Краще тримати в голові короткий, повторюваний протокол — такий самий, як у етапі контролю якості, тільки про залежності.
Нормальна послідовність виглядає так: ви додаєте імпорт або пакет, потім приводите модульні файли у відповідність до реального коду через tidy. Далі дивитеся на зміни як на смисловий diff: що саме додалося і чому. А потім проганяєте тести. Якщо проєкт друкує дані, ви перевіряєте, що вивід детермінований і не залежить від порядку map або часу.
Тут важлива звичка: залежність — це зміна архітектури проєкту, навіть якщо вона виглядає як один рядок import.
Де в проєкті живуть ризики залежностей
Щоб закріпити картину, корисно уявити залежності як шари, де кожен шар додає свої ризики:
flowchart TD
A[Ваш код] --> B[Стандартна бібліотека]
A --> C[Зовнішні бібліотеки]
C --> D[Транзитивні залежності]
A --> E[Інструменти розробки]
E --> F[Версії інструментів на машинах і в CI]
B --> G[Мінімальний ризик і стабільність]
D --> H[Зростання площі ризику]
F --> I[Дрейф якості перевірок]
Сенс схеми простий: що далі ви відходите від стандартної бібліотеки, то більша «поверхня ризику». Це не означає, що зовнішні бібліотеки — зло. Це означає, що кожна бібліотека має окупатися.
4. Типові помилки
Помилка № 1: «додамо бібліотеку, а потім розберемося».
Так зазвичай усе й починається: одна залежність для логів, одна для конфігів, одна для JSON, хоча він є у стандартній бібліотеці, а потім раптом go.mod виглядає як список гостей на весіллі, яких ви особисто не запрошували. Правильний хід — спочатку запитати себе, чого саме не вміє стандартна бібліотека, і тільки потім ухвалювати рішення.
Помилка № 2: не запускати go mod tidy регулярно й усвідомлено.
Проєкт може «випадково» збиратися на вашій машині, бо в кеші вже є потрібні модулі або IDE підтягнула щось автоматично. Але на чистій машині все розвалюється. tidy — це спосіб синхронізувати реальність коду й реальність модулів, і саме тому це частина етапу контролю якості.
Помилка № 3: використовувати replace як постійний стан проєкту.
Локальні replace зручні, але вони майже завжди ламають відтворюваність для інших людей і CI. Якщо без replace не можна, це має бути тимчасово, задокументовано і з чітким планом виходу. Інакше ви «підписуєтеся» на вічну нестабільність збірки.
Помилка № 4: не фіксувати версії інструментів розробки.
Одна людина запускає лінтер версії X, інша — версії Y, і вони сперечаються, хто «правильно» написав код. Зазвичай правий ніхто: просто інструменти різні. Якщо ви домовилися, що інструмент важливий для якості, то його версія має бути керованою, інакше етап контролю якості перетворюється на лотерею.
Помилка № 5: плутати «збірка відтворювана» і «результат детермінований».
Можна ідеально зафіксувати залежності й усе одно отримувати різні результати, якщо ви друкуєте map без сортування або використовуєте time.Now() усередині логіки. Відтворюваність — це не лише про версії модулів, а й про передбачуваність поведінки програми.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ