Олег Марков
Правило относительных импортов в Go relative-imports
Введение
Правило относительных импортов (relative-imports) в Go — одна из тех тем, о которую часто спотыкаются даже опытные разработчики, переходящие на Go из других языков. В C, Python, JavaScript и многих других экосистемах относительные импорты — это привычный способ организовать модули: вы пишете ../utils или ./helpers и получаете доступ к нужному коду.
В Go подход другой. Язык сознательно ограничивает использование относительных импортов для пользовательского кода и продвигает строгое правило: импортировать нужно по полному пути пакета, а не относительно текущей директории. В реальных проектах это правило влияет на:
- структуру каталогов
- использование
GOPATHи модулей Go - настройку сборки и запуск тестов
- переносимость и воспроизводимость сборки
Давайте разберем, как именно работает правило относительных импортов в Go, почему они считаются плохой практикой, чем их заменять и как избегать типичных ошибок при настройке импортов в проектах.
Что такое относительные импорты и почему они вообще появились
Абсолютные и относительные импорты на интуитивном уровне
Когда вы подключаете один пакет к другому, у вас есть два варианта указать путь:
Абсолютный путь пакета (в терминах языка)
В Go это путь с точки зренияmodule pathилиGOPATH. Например:import "github.com/username/project/pkg/utils" // абсолютный путь пакетаЗдесь: // github.com/username/project — путь модуля // pkg/utils — путь до пакета внутри модуля
Относительный путь (относительно текущей директории)
В некоторых языках это выглядит так:from ..utils import helpers # пример относительного импорта в PythonНо в Go такой стиль импорта в продакшн-коде запрещен.
Как относительные импорты выглядели в старых версиях Go
Исторически в самых ранних версиях Go существовала возможность писать что-то вроде:
// ПРИМЕР ИСТОРИЧЕСКОГО ПОВЕДЕНИЯ - ТАК СЕЙЧАС ДЕЛАТЬ НЕЛЬЗЯ
import "../utils" // относительный путь к пакету
// Тогда компилятор искал пакет в файловой системе относительно текущего каталога файла
Сейчас компилятор и инструменты Go такую запись не принимают. Если вы попробуете импортировать пакет с помощью ../, получите ошибку наподобие:
imports ../utils: relative import paths not supported in module mode
Именно это и называют правилом относительных импортов: запрет использовать относительные пути вида ../ и ./ в импортах обычных Go-пакетов.
Как Go на самом деле ищет пакеты при импорте
Чтобы понять, зачем нужно это правило, давайте разберем, как Go вообще находит пакеты, когда вы пишете import "project/pkg/utils".
Поиск пакетов в старой модели GOPATH
До появления модулей Go основной моделью была GOPATH. Смотрите, как это работает:
Ваша переменная окружения
GOPATH, например:- GOPATH=/home/user/go
Внутри GOPATH есть набор стандартных директорий:
/home/user/go/src— исходники пакетов/home/user/go/pkg— скомпилированные пакеты/home/user/go/bin— бинарные файлы
Если вы пишете:
import "project/pkg/utils"То компилятор будет искать папку:
$GOPATH/src/project/pkg/utils// Здесь предполагается что исходный код пакета находится в этом пути
То есть абсолютный путь для Go — это путь относительно GOPATH/src, а не файловой системы целиком.
Поиск пакетов в модульной системе (Go modules)
С появлением модулей (go mod init, go.mod) ситуация изменилась. Теперь основой для поиска пакетов стал модульный путь, указанный в go.mod.
Пример go.mod:
module github.com/username/project
go 1.22
Представим структуру:
project/
go.mod
main.go
pkg/
utils/
math.go
internal/
config/
cfg.go
Если в main.go вы пишете:
package main
import (
"fmt"
"github.com/username/project/pkg/utils" // импорт публичного пакета
"github.com/username/project/internal/config" // импорт внутреннего пакета
)
func main() {
fmt.Println(utils.Add(2, 3)) // вызываем функцию из пакета utils
cfg := config.Load() // используем пакет internal/config
_ = cfg // чтобы не было неиспользуемой переменной
}
// Здесь мы импортируем пакеты исходя из module path и структуры каталогов внутри репозитория
Go сопоставляет:
- модуль
github.com/username/project→ корень репозитория (где лежитgo.mod) pkg/utils→ каталог./pkg/utilsв корне модуляinternal/config→ каталог./internal/config
Ключевой момент: путь в import — это логическое имя пакета, а не «путь от текущего файла».
Почему относительные пути запрещены
Причины запрета относительных импортов в Go:
Прозрачность и предсказуемость
Любой человек (и любой инструмент) по строке импорта сразу понимает, где искать пакет — либо в модуле, либо в GOPATH, либо скачать его черезgo get. Не нужно знать, где физически лежит текущий файл.Переносимость кода
Проект можно склонировать в любую директорию, и импорты продолжат работать, потому что они не завязаны на конкретное местоположение файлов в файловой системе.Упрощение инструментов и сборки
go build,go test,gopls, статические анализаторы, IDE — все опираются на единый механизм поиска пакетов. Им не нужно вычислять относительные пути.Избежание «цепочек относительных путей»
В больших проектах относительные импорты часто приводят к сложным зависимостям вида../../shared/utils, которые сложно поддерживать и рефакторить.
Именно поэтому в Go действует правило: импортировать пакеты можно только по их полному импортному пути, без использования . и ...
Структура проекта и импорт без относительных путей
Чтобы не тянуться к ../ и ./, проект нужно выстраивать так, чтобы импорты естественно получались абсолютными в терминах Go.
Базовый пример структуры проекта на модулях
Давайте разберем минимальный пример:
myapp/
go.mod
cmd/
myapp/
main.go
pkg/
math/
add.go
strings/
format.go
Файл go.mod:
module github.com/username/myapp
go 1.22
Файл pkg/math/add.go:
package math
// Add складывает два целых числа и возвращает результат
func Add(a, b int) int {
return a + b
}
Файл pkg/strings/format.go:
package strings
import "fmt"
// FormatSum форматирует сумму двух чисел в строку
func FormatSum(a, b int) string {
// Здесь мы используем fmt.Sprintf чтобы собрать строку
return fmt.Sprintf("%d + %d = %d", a, b, a+b)
}
Файл cmd/myapp/main.go:
package main
import (
"fmt"
// Импортируем наши пакеты по полному пути в рамках модуля
mymath "github.com/username/myapp/pkg/math"
mystrings "github.com/username/myapp/pkg/strings"
)
func main() {
// Вызываем функцию Add из пакета math
sum := mymath.Add(2, 3)
// Форматируем строку с помощью пакета strings
msg := mystrings.FormatSum(2, 3)
fmt.Println("Sum is:", sum) // Выведет: Sum is: 5
fmt.Println(msg) // Выведет: 2 + 3 = 5
}
// Обратите внимание мы нигде не используем относительные импорты вроде ./ или ../ // Все импорты опираются на module path github.com/username/myapp
Чтобы запустить программу, вы просто выполняете:
go run ./cmd/myapp
// Здесь относительный путь используется только для команды go run а не в импортах пакетов
Это важное различие: в командной строке относительные пути к каталогам допустимы и нормальны, в импортах пакетов — нет.
Правило «одна директория — один пакет»
В Go действует простое правило, которое помогает избегать путаницы с импортами:
- каждый каталог в проекте содержит один пакет (одно имя
package) - имя пакета совпадает с названием каталога (как хорошая практика, хотя это не строго обязательно)
Например:
pkg/math→package mathinternal/config→package configcmd/myapp→package main
Так структура директорий естественно превращается в структуру импортов:
github.com/username/myapp/pkg/mathgithub.com/username/myapp/internal/configgithub.com/username/myapp/cmd/myapp
И все эти пути абсолютны для Go, даже если физически ваш проект лежит где-то глубоко в /home/user/work/src/....
Типичные ошибки при использовании относительных импортов и как их исправить
Ошибка «relative import paths not supported»
Одна из самых частых проблем — попытка импортировать соседний каталог так, как вы делали бы в других языках:
// ПЛОХО - ТАК ДЕЛАТЬ НЕЛЬЗЯ
import "../utils"
Результат:
imports ../utils: relative import paths not supported
Как исправить:
- Убедиться, что у вас есть
go.modв корне проекта. - Определить module path (например,
github.com/username/project). - Импортировать пакет через этот module path:
// ХОРОШО - импорт по полному пути
import "github.com/username/project/utils"
Если пакет лежит в подпапке pkg/utils, путь будет:
import "github.com/username/project/pkg/utils"
Ошибка при запуске кода из подкаталога без go.mod
Иногда вы заходите в какую-то вложенную директорию и пытаетесь там собрать или запустить код:
cd project/pkg/utils
go run .
Если проект не настроен как модуль, вы можете столкнуться с проблемами, особенно если используете новую версию Go (где модульный режим включен по умолчанию).
Решение:
В корне проекта выполнить:
go mod init github.com/username/project go mod tidy// go mod init создает go.mod с указанным module path // go mod tidy подтягивает зависимости и чистит неиспользуемые
Запускать команды всегда от корня модуля или использовать относительный путь к каталогу:
go run ./cmd/myapp// Здесь go понимает что находитесь в модуле и корректно обрабатывает импорты
Конфликт имен пакетов и директорий
Еще одна неприятная ситуация — вы создаете каталог utils, а внутри объявляете package helpers:
// файл: utils/file.go
package helpers
// ...
И пытаетесь импортировать:
import "github.com/username/project/utils"
Технически Go разрешит такое несоответствие, но это ухудшает читаемость и может запутать других разработчиков. Они ожидают, что пакет utils объявлен как package utils.
Рекомендация:
- совмещайте имя каталога и имя пакета.
- не используйте относительные костыли вида
../helpers, если можно просто переименовать каталог или пакет.
Как правильно организовать проект, чтобы не хотелось использовать относительные импорты
Деление на cmd/, pkg/, internal/
Часто желание использовать ../ возникает, когда структура проекта хаотична. Хороший шаблон, который я рекомендую, выглядит так:
project/
go.mod
cmd/
app1/
main.go
app2/
main.go
pkg/
api/
storage/
service/
internal/
config/
auth/
testdata/
Роли директорий:
cmd/— точки входа (main-пакеты), здесь почти нет логики, только связывание.pkg/— общедоступные пакеты, которые теоретически могут переиспользоваться в других проектах.internal/— внутренние пакеты, которые можно импортировать только внутри этого модуля (Go это контролирует).testdata/— данные для тестов, не попадают в сборку.
При такой структуре:
- любой код в
cmd/может честно импортировать пакеты изpkg/иinternal/по полным путям - вам не нужно лезть в
../для доступа к общим компонентам - зависимости между пакетами становятся явно видимыми по import-путям
Пример переписывания относительных импортов на модульные
Представьте, что у вас был неудачный проект (или код, мигрированный с очень старой версии Go), где встречались относительные импорты:
legacy/
main.go
lib/
math/
add.go
strings/
format.go
И в main.go было что-то вроде:
// ПЛОХО - ИСТОРИЧЕСКИЙ ПРИМЕР
package main
import (
"../lib/math"
"../lib/strings"
)
Чтобы привести это к современному стилю:
В корне
legacy/создаем модуль:cd legacy go mod init example.com/legacyПриводим структуру к рекомендуемой:
legacy/ go.mod cmd/ app/ main.go pkg/ math/ add.go strings/ format.goОбновляем импорты в
cmd/app/main.go:package main import ( "fmt" "example.com/legacy/pkg/math" formatstrings "example.com/legacy/pkg/strings" ) func main() { // Вызываем функцию Add из пакета math s := math.Add(10, 20) // Используем пакет strings для форматирования msg := formatstrings.FormatSum(10, 20) fmt.Println("Sum:", s) // Выведет: Sum: 30 fmt.Println(msg) // Выведет отформатированную строку }// Здесь вместо относительных импортов мы используем модульный путь example.com/legacy
Теперь команда:
go run ./cmd/appкорректно собирает и запускает приложение без каких-либо относительных импортов.
Как относительные импорты связаны с workspace, monorepo и несколькими модулями
Несколько модулей в одном репозитории
В крупных репозиториях (monorepo) часто встречается такая структура:
repo/
service-a/
go.mod (module example.com/service-a)
service-b/
go.mod (module example.com/service-b)
libs/
common/
go.mod (module example.com/common)
Здесь может возникнуть соблазн в service-a импортировать ../libs/common напрямую, как относительный путь. Но в Go так нельзя.
Вместо этого:
В
service-a/go.modвы добавляете зависимость:module example.com/service-a go 1.22 require example.com/common v0.0.0 replace example.com/common => ../libs/common// require объявляет зависимость от модуля example.com/common // replace говорит go что этот модуль нужно брать из локальной директории ../libs/common
В коде
service-aвы импортируете:import "example.com/common/pkg/helpers"а не
../libs/common/pkg/helpers.Благодаря директиве
replaceGo понимает, что модуль нужно искать в локальной файловой системе, но имя пакета остается абсолютным, и вы не используете относительный путь в импорте.
Workspace (go.work) и организация разработки
С появлением go work стало проще работать с несколькими модулями без злоупотребления replace. Пример:
repo/
go.work
service-a/
go.mod
service-b/
go.mod
libs/common/
go.mod
Файл go.work:
go 1.22
use (
./service-a
./service-b
./libs/common
)
// Команда go work use указывает какие модули участвуют в рабочем пространстве
Теперь:
- каждый модуль имеет свой
module path - модули могут импортировать друг друга по этим module path
- инструменты Go автоматически сопоставляют модули с локальными директориями согласно
go.work - и нигде не требуется
../в импорт-путях
Это еще один пример, как экосистема Go поддерживает абсолютные (логические) импорт-пути и не поощряет относительные.
Работа с тестами и относительными путями
Импорт тестируемых пакетов из тестов
Иногда разработчики пытаются в тестах «обойти» правило и использовать относительные импорты, особенно если тесты лежат в отдельном каталоге. Но в Go для этого есть разные механизмы:
Тесты в том же пакете
Например, у вас есть:
pkg/math/add.go pkg/math/add_test.goФайлы:
// add.go package math // Add складывает два числа func Add(a, b int) int { return a + b }// add_test.go package math import "testing" func TestAdd(t *testing.T) { // Здесь мы напрямую вызываем функцию Add if Add(2, 3) != 5 { t.Fatal("expected 5") } }// Здесь тесты находятся в том же пакете math поэтому никакого импорта пакета math не нужно
Внешние тесты (
package xxx_test)Иногда нужно тестировать пакет как «чужой», чтобы проверять только его публичный API:
// add_external_test.go package math_test import ( "testing" "github.com/username/project/pkg/math" ) func TestAddExternal(t *testing.T) { // Здесь мы импортируем пакет math по его полному пути if math.Add(2, 3) != 5 { t.Fatal("expected 5") } }// Обратите внимание мы снова используем абсолютный путь пакета в модуле
Тесты собираются и запускаются той же системой модулей, поэтому правило относительно импортов остается прежним: ../ и ./ в import-путях использовать нельзя.
Использование относительных путей в тестовых данных
Относительные пути в Go все же могут встречаться — но не в импортах, а в работе с файлами. Например, в тестах вы можете читать файлы из каталога testdata:
package math
import (
"os"
"path/filepath"
"testing"
)
func TestLoadTable(t *testing.T) {
// Определяем путь до файла в testdata относительно текущего файла
path := filepath.Join("testdata", "table.txt")
// Открываем файл для чтения
data, err := os.ReadFile(path)
if err != nil {
t.Fatalf("failed to read file: %v", err)
}
// Здесь вы работаете с содержимым файла data
_ = data
}
// Это законно потому что вы работаете с файловой системой а не с импортом пакетов
Важное различие: относительные пути к файлам — нормальная практика, относительные пути в импортах пакетов — запрещены.
Практические рекомендации по работе с импортами без относительных путей
1. Сразу инициализируйте модуль в корне проекта
Как только создаете новый проект:
mkdir myapp
cd myapp
go mod init github.com/username/myapp
// Это задает module path который вы будете использовать в импортах
После этого любые новые пакеты вы создаете только внутри этого модуля, и импортируете их по путям вида github.com/username/myapp/....
2. Проектируйте структуру директорий под будущие импорты
Перед тем, как создать каталог, подумайте, как вы будете его импортировать:
если пакет должен быть общедоступным: кладите его в
pkg/
импорт:github.com/username/myapp/pkg/nameесли пакет «внутренний» и не должен использоваться снаружи: в
internal/
импорт:github.com/username/myapp/internal/nameесли это исполняемая программа: в
cmd/appname(package main)
импорт:github.com/username/myapp/cmd/appname
Так вам не придется пытаться «дотянуться» до нужного кода с помощью ../.
3. Не бойтесь длинных импортных путей
Многих смущает длина строк импорта:
import "github.com/very-long-company-name/complex-project-name/pkg/some/submodule"
Но это цена за:
- однозначность
- отсутствие скрытой логики «относительности»
- удобную навигацию в IDE
Если путь действительно слишком длинный, можно использовать алиасы:
import (
sub "github.com/company/project/pkg/some/submodule"
)
func main() {
// Здесь мы используем короткое имя sub вместо полного имени пакета
sub.DoSomething()
}
// Алиас sub помогает сократить длину обращений к пакету без нарушения правил импортов
4. Используйте go fmt и goimports для автоматической организации импортов
Инструменты go fmt и особенно goimports помогают автоматически:
- сортировать импорты
- удалять неиспользуемые
- подставлять правильные пути пакетов
Это еще одна причина, почему в Go не хотят иметь относительные импорты: инструменты проще и надежнее работают с абсолютными путями.
Заключение
Правило относительных импортов в Go — это не «каприз» языка, а важная часть философии экосистемы: делать сборку кода максимально предсказуемой, переносимой и удобной для инструментов. Вместо импорта вида ../utils вы всегда работаете с логическими именами пакетов, основанными на module path (go.mod) или GOPATH.
Ключевые моменты, которые стоит запомнить:
- относительные пути
../и./в import-путях пакетов использовать нельзя - импорты всегда строятся от module path или от корня GOPATH
- структура каталогов должна быть согласована с логикой импортов (одна директория — один пакет)
- для нескольких модулей используйте
replaceилиgo.work, а не относительные импорты - относительные пути допустимы только при работе с файлами (например, в тестовых данных), но не при импорте пакетов
Если ориентироваться на модульную модель Go и грамотно организовывать проект, необходимость в относительных импортах просто исчезает, а код становится проще для сопровождения и сборки.
Частозадаваемые технические вопросы по теме и ответы
Как импортировать локальный пакет без публикации в GitHub
Если вы не хотите или не можете публиковать код на GitHub, задайте в go.mod любой уникальный module path, например:
module example.local/myproject
Структура:
myproject/
go.mod
pkg/utils/
Импорт:
import "example.local/myproject/pkg/utils"
Go не будет пытаться скачивать модуль из сети, если вы работаете с локальным кодом, находящимся внутри этого модуля.
Что делать, если нужно временно использовать код из соседней директории другого проекта
Используйте директиву replace в go.mod:
require example.com/other v0.0.0
replace example.com/other => ../other-project
Теперь вы можете импортировать:
import "example.com/other/pkg/something"
Go возьмет код из ../other-project, но в импортах вы по-прежнему используете полный путь.
Как быть, если IDE «ругается» на импорты после переноса проекта в другую папку
Если вы перенесли проект в другое место или поменяли имя репозитория:
- Обновите
moduleвgo.modна актуальный путь. - Запустите
go mod tidy. - В IDE выполните «Reimport project» или аналогичную команду.
- Убедитесь, что GOPATH и настройки Go SDK корректны (особенно в старых проектах).
После этого импорты, основанные на module path, будут снова корректно распознаваться.
Можно ли в одном каталоге иметь несколько пакетов и импортировать их как-то «относительно»
Технически в одной директории могут быть файлы с разными package, но Go этого не рекомендует. Импорты все равно должны ссылаться на каталог, а не на «часть каталога». Безопасный подход:
- один каталог — один пакет
- разные варианты пакета (например, с build tags) размещайте в тех же файлах с разными тэгами, а не делите каталог на несколько пакетов
Как отладить проблему, когда Go не находит пакет по корректному на вид пути
Алгоритм проверки:
- Убедитесь, что путь в
importсоответствует структуре каталогов относительно корня модуля (go.mod). - Проверьте, что
go.modлежит в корне проекта, а вы запускаете команды из этого корня или ниже. - Выполните
go list ./...и посмотрите, все ли пакеты видны. - Если используете
go.work, убедитесь, что нужный модуль включен вuseи путь к нему верный. - Проверьте, что имя пакета (
package name) в файлах совпадает с именем каталога (часто это источник путаницы).