1. Навіщо потрібен replace: «хочу перевірити правку, але релізу немає»
Уявіть типову ситуацію з реальної розробки: ви знайшли баг у бібліотеці, яку використовуєте, і вже навіть написали виправлення. Але його ще не опубліковано: немає тега версії, немає релізу, а іноді й прав пушити в репозиторій немає. Вам хочеться просто зараз зібрати й запустити застосунок з локальною правкою, не змінюючи імпорти по всьому коду й не перетворюючи проєкт на ручний набір ZIP-архівів на кшталт «dependencies_final_final2.zip».
Ось тут і з’являється replace: це механізм, який каже інструментам Go приблизно таке: «коли бачиш модуль із таким шляхом модуля, бери його не звідти, звідки зазвичай, а звідси». Важливо: це саме правило розв’язання залежностей, а не зміна вашого вихідного коду.
Що робить replace і чого не робить
Коли ви вперше бачите replace, легко сприйняти його як «магічну кнопку: завантаж мені іншу версію». Але це не завантаження й не встановлення — це перепризначення джерела.
Точніше кажучи, replace впливає на те, звідки інструменти go візьмуть вихідні тексти модуля під час збірки вашого основного модуля. Тобто replace живе «на межі проєкту», поруч із вашими require, і впливає на збірку та тести, а не на синтаксис мови Go.
Окремо корисно пам’ятати: інструменти Go дуже орієнтовані на передбачуваність і детермінізм. Тому вони люблять фіксувати версії та контрольні суми. А replace — це як «службовий вхід»: ним можна користуватися, але обережно, бо він легко робить збірку залежною від вашої локальної машини. А локальна машина — штука творча: сьогодні одна, а завтра «ой, я перевстановив систему».
2. Синтаксис replace: дві основні форми
Зараз буде важливий момент: ми подивимося на replace не як на теорію, а як на текст у go.mod, який ви реально побачите в дифі PR.
Заміна на локальний шлях
Цю форму використовують, коли на диску є папка з іншим модулем, тобто всередині неї лежить власний go.mod, і ви хочете тимчасово підставити її замість «звичайно завантажуваної» залежності.
// go.mod (ваш основний модуль)
module example.com/todoapp
go 1.25
require example.com/todolib v0.1.0
replace example.com/todolib => ../todolib
Тут важливо одразу помітити два нюанси.
Перший нюанс: replace вказує на корінь модуля, а не на підпапку пакета. Тобто ../todolib має бути каталогом, де лежить ../todolib/go.mod.
Другий нюанс: шлях ../todolib рахують відносно папки, де лежить go.mod, а не відносно того місця, звідки ви запускаєте IDE або go test.
Заміна на інший шлях модуля і версію
Ця форма трапляється, коли ви використовуєте форк залежності або альтернативне джерело, але хочете залишити імпорти в коді незмінними.
Схематично це може виглядати так:
replace example.com/todolib => example.com/todolib-fork v0.1.1
Ідея проста: у коді ви й далі пишете import "example.com/todolib/task", але фактично Go візьме модуль з іншого місця. Так, це схоже на те, ніби ви підмінили постачальника в накладній, але коробки на складі все одно підписані старим ім’ям.
3. Практичний сценарій: спільний модуль і підключення через replace
Щоб replace не залишився абстрактною страшилкою, давайте зіграємо в просту й дуже життєву історію. У нас є навчальний застосунок todoapp. Ми хочемо винести частину коду в маленьку бібліотеку todolib, щоб повторно використовувати типи й функції в іншому проєкті, наприклад в окремій утиліті або в майбутніх експериментах. Але публікувати бібліотеку «в інтернет» ми не будемо — просто покладемо її поруч на диску й підключимо через replace.
Структура каталогів
Нам важливо уявляти структуру файлів. Уявімо таку структуру:
workspace/
todolib/
go.mod
task/
id.go
title/
normalize.go
todoapp/
go.mod
main.go
Зверніть увагу: це два різні модулі, тому що в кожній папці є свій go.mod. Саме ця незалежність і робить replace можливим та осмисленим.
Мінімальний todolib: тип ID
Зробимо крихітний пакет task у модулі todolib.
package task
// ID — ідентифікатор задачі.
// Поки що це просто int, але тип допомагає не плутати його з іншими числами.
type ID int
Тут ми не робимо нічого «розумного»: просто вводимо тип. На практиці це підвищує читабельність і зменшує шанс переплутати «id задачі» з «кількістю задач». Зовні обидва значення ніби int, але сенс у них різний.
Ще один маленький пакет title: нормалізація заголовка
Додамо утиліту для заголовка. Нехай вона поки що тільки обрізає пробіли. Ми ж не воюємо з усіма пробілами у світі.
package title
import "strings"
// Normalize приводить заголовок до охайного вигляду.
func Normalize(s string) string {
return strings.TrimSpace(s)
}
Використовуємо todolib у todoapp як зовнішню залежність
Тепер у todoapp/main.go імпортуємо пакети з todolib за шляхом модуля.
package main
import (
"fmt"
"example.com/todolib/task"
"example.com/todolib/title"
)
func main() {
var id task.ID = 1
fmt.Println("id =", id) // id = 1
fmt.Println(title.Normalize(" купити молоко ")) // купити молоко
}
Зверніть увагу на важливу річ: імпорти виглядають як імпорти справжньої зовнішньої бібліотеки. У цьому й зручність: коли ви пізніше вирішите публікувати todolib або підключати її без replace, код todoapp переписувати не доведеться.
Підключаємо локальний модуль через replace
А тепер ключовий момент: у todoapp/go.mod фіксуємо залежність і підміняємо джерело.
module example.com/todoapp
go 1.25
require example.com/todolib v0.0.0
replace example.com/todolib => ../todolib
Чому версія v0.0.0? Бо в цьому сценарії сама версія не така важлива: ми все одно беремо код із локальної папки. У «бойових» проєктах частіше все ж намагаються мати нормальні версії, але для навчання й локальної розробки цей запис допомагає зрозуміти механіку.
4. Ризики replace: як перетворити проєкт на «працює лише в мене»
Зараз буде трохи занудства від інженера зі збірок, але без цього replace легко стає пасткою. Головний ризик дуже простий: replace легко ламає відтворюваність.
Якщо ви зробили replace ... => ../todolib, то ваш проєкт тепер залежить від того, що в іншого розробника є папка ../todolib у точно такому самому місці. На CI, де репозиторій зазвичай клонують у чисту директорію, цієї папки не буде. І збірка впаде.
Ба більше, якщо ви випадково зафіксували такий replace в репозиторії, то ви ніби приклеїли до проєкту записку: «збирається лише на моєму ноутбуці, бо поруч лежить ще один репозиторій». Це не завжди зло — інколи так роблять свідомо в монорепах — але це точно вимагає дисципліни.
Є й тонший ризик: коли ви використовуєте go install some/tool@version, Go свідомо уникає неоднозначностей і накладає обмеження на те, що може бути в go.mod. Зокрема, у деяких сценаріях replace просто не можна використовувати, щоб збірка конкретної версії інструмента була однозначною.
І ще одне спостереження з практики: інструменти Go часто поводяться дуже послідовно — замість того, щоб мовчки щось лагодити, вони видають помилку і прямо в тексті пишуть, яку команду виконати. Це корисно пам’ятати, бо за неправильних залежностей ви побачите підказки рівня «no required module provides package ...; to add it: go get ...».
5. Коли replace доречний
Щоб не лишитися з відчуттям «то це заборонена магія?», давайте сформулюємо здоровий підхід. replace — нормальний інструмент. Просто він не для щоденного «так зручніше», а для конкретних робочих ситуацій.
- replace доречний, коли ви одночасно активно розробляєте бібліотеку й застосунок, але бібліотека ще не готова до публікації. Історично це був один із типових способів підключати локальні неопубліковані зміни. Після публікації важливо не забути прибрати replace, інакше проєкт ставав погано переносимим.
- replace інколи доречний, коли ви використовуєте форк залежності, наприклад коли вам потрібне термінове виправлення. Тоді ви можете тимчасово замінити модуль на форк, а згодом повернутися до офіційного джерела.
- replace доречний як «ремонтний режим», коли зовнішній світ тимчасово недоступний, наприклад якщо ви працюєте в ізольованому середовищі. Але це радше рідкісна історія, і краще, щоб вона була явно оформлена й задокументована.
Блок-схема: чи потрібен нам replace
Іноді корисніше один раз побачити алгоритм, ніж прочитати десять абзаців. Хоча ми все одно прочитаємо їх — ми ж програмісти.
flowchart TD
A[Потрібно використати залежність] --> B{Чи є потрібна версія у звичайному require?}
B -->|Так| C[Використовуємо require / go get]
B -->|Ні| D{Чи є локальні зміни модуля?}
D -->|Так| E[Тимчасово ставимо replace на локальний шлях]
D -->|Ні| F[Шукаємо іншу версію або форк і підміняємо replace на інший шлях модуля]
E --> G{Час публікувати, релізити або зливати зміни?}
G -->|Так| H[Прибираємо replace або робимо нормальний реліз бібліотеки]
G -->|Ні| I[Залишаємо replace локально, але пам’ятаємо про CI]
Тут важлива мораль: replace — майже завжди тимчасове рішення. Якщо воно стає постійним, це має бути свідоме архітектурне рішення, а не «ой, я забув прибрати рядок».
Мініправило гігієни
Якщо ви використовуєте replace, намагайтеся хоча б залишати поруч коментар, щоб за місяць ви або ваш колега не гадали, чому залежність тягнеться з ../something.
У Go це виглядає нормально:
replace example.com/todolib => ../todolib // локальна розробка todolib
Це не «краса заради краси». Це реально економить години життя.
Ремарка про альтернативи
Існує механізм workspaces (go.work), який якраз і задуманий для того, щоб зручніше працювати з кількома модулями одночасно без постійного редагування go.mod. В офіційних матеріалах його прямо протиставляють старому ручному replace для локальних неопублікованих змін.
Але в межах цієї лекції ми тримаємо фокус: сьогодні нам важливо навчитися читати й розуміти replace, бо ви обов’язково зустрінете його в чужих проєктах, навіть якщо самі користуватиметеся workspaces.
6. Типові помилки під час роботи з replace
Помилка № 1: replace вказує на підпапку пакета, а не на корінь модуля.
Дуже часта плутанина у новачків: «мені потрібен пакет task, він лежить у ../todolib/task, отже replace треба ставити туди». Але replace працює на рівні модуля, а модуль визначається файлом go.mod. Тому правильна ціль — каталог, де лежить go.mod, а пакети вже всередині нього.
Помилка № 2: випадково зафіксували локальний replace і зламали збірку всім іншим.
Це класика жанру: у вас усе працює, ви радісно робите PR, а колега або CI отримує помилку «папки немає». Причина в тому, що ../todolib — це деталь вашої файлової системи. Якщо replace потрібен лише вам локально, найчастіше йому не місце в основній гілці.
Помилка № 3: думають, що replace «завантажує» залежність або «оновлює» її.
replace нічого не завантажує й не оновлює. Він просто змінює правило «звідки брати вихідні тексти». У результаті можна потрапити в дивну ситуацію: ви думаєте, що використовуєте v1.2.3, бо так написано в require, але фактично код береться з локальної папки. Це особливо неприємно, коли ви налагоджуєте баги: дивитеся на одну версію, а збираєте іншу.
Помилка № 4: забувають прибрати replace, коли зміни вже опубліковані або більше не потрібні.
Історично це був реальний біль: зробили локальну правку, підключили через replace, потім опублікували модуль, але забули прибрати replace — і проєкт продовжує посилатися на локальні вихідні тексти, яких у інших немає. В офіційних матеріалах про workspaces цей сценарій прямо згадують як типову причину, через яку людям знадобився зручніший режим роботи з кількома модулями.
Помилка № 5: дивуються, що деякі команди поводяться суворіше через replace.
Наприклад, є сценарії, де Go навмисно обмежує неоднозначності під час встановлення інструментів за точною версією, і replace там заборонений. Якщо ви впираєтеся в несподіване «не можна», це не особиста неприязнь Go до вас, а спроба зберегти детермінізм поведінки.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ