Правило относительных импортов в Go relative-imports

05 января 2026
Автор

Олег Марков

Введение

Правило относительных импортов (relative-imports) в Go — одна из тех тем, о которую часто спотыкаются даже опытные разработчики, переходящие на Go из других языков. В C, Python, JavaScript и многих других экосистемах относительные импорты — это привычный способ организовать модули: вы пишете ../utils или ./helpers и получаете доступ к нужному коду.

В Go подход другой. Язык сознательно ограничивает использование относительных импортов для пользовательского кода и продвигает строгое правило: импортировать нужно по полному пути пакета, а не относительно текущей директории. В реальных проектах это правило влияет на:

  • структуру каталогов
  • использование GOPATH и модулей Go
  • настройку сборки и запуск тестов
  • переносимость и воспроизводимость сборки

Давайте разберем, как именно работает правило относительных импортов в Go, почему они считаются плохой практикой, чем их заменять и как избегать типичных ошибок при настройке импортов в проектах.

Что такое относительные импорты и почему они вообще появились

Абсолютные и относительные импорты на интуитивном уровне

Когда вы подключаете один пакет к другому, у вас есть два варианта указать путь:

  1. Абсолютный путь пакета (в терминах языка)
    В Go это путь с точки зрения module path или GOPATH. Например:

    import "github.com/username/project/pkg/utils" // абсолютный путь пакета
    

    Здесь: // github.com/username/project — путь модуля // pkg/utils — путь до пакета внутри модуля

  2. Относительный путь (относительно текущей директории)
    В некоторых языках это выглядит так:

    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. Смотрите, как это работает:

  1. Ваша переменная окружения GOPATH, например:

    • GOPATH=/home/user/go
  2. Внутри GOPATH есть набор стандартных директорий:

    • /home/user/go/src — исходники пакетов
    • /home/user/go/pkg — скомпилированные пакеты
    • /home/user/go/bin — бинарные файлы
  3. Если вы пишете:

    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:

  1. Прозрачность и предсказуемость
    Любой человек (и любой инструмент) по строке импорта сразу понимает, где искать пакет — либо в модуле, либо в GOPATH, либо скачать его через go get. Не нужно знать, где физически лежит текущий файл.

  2. Переносимость кода
    Проект можно склонировать в любую директорию, и импорты продолжат работать, потому что они не завязаны на конкретное местоположение файлов в файловой системе.

  3. Упрощение инструментов и сборки
    go build, go test, gopls, статические анализаторы, IDE — все опираются на единый механизм поиска пакетов. Им не нужно вычислять относительные пути.

  4. Избежание «цепочек относительных путей»
    В больших проектах относительные импорты часто приводят к сложным зависимостям вида ../../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/mathpackage math
  • internal/configpackage config
  • cmd/myapppackage main

Так структура директорий естественно превращается в структуру импортов:

  • github.com/username/myapp/pkg/math
  • github.com/username/myapp/internal/config
  • github.com/username/myapp/cmd/myapp

И все эти пути абсолютны для Go, даже если физически ваш проект лежит где-то глубоко в /home/user/work/src/....

Типичные ошибки при использовании относительных импортов и как их исправить

Ошибка «relative import paths not supported»

Одна из самых частых проблем — попытка импортировать соседний каталог так, как вы делали бы в других языках:

// ПЛОХО - ТАК ДЕЛАТЬ НЕЛЬЗЯ
import "../utils"

Результат:

imports ../utils: relative import paths not supported

Как исправить:

  1. Убедиться, что у вас есть go.mod в корне проекта.
  2. Определить module path (например, github.com/username/project).
  3. Импортировать пакет через этот 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 (где модульный режим включен по умолчанию).

Решение:

  1. В корне проекта выполнить:

    go mod init github.com/username/project
    go mod tidy
    

    // go mod init создает go.mod с указанным module path // go mod tidy подтягивает зависимости и чистит неиспользуемые

  2. Запускать команды всегда от корня модуля или использовать относительный путь к каталогу:

    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"
)

Чтобы привести это к современному стилю:

  1. В корне legacy/ создаем модуль:

    cd legacy
    go mod init example.com/legacy
    
  2. Приводим структуру к рекомендуемой:

    legacy/
      go.mod
      cmd/
        app/
          main.go
      pkg/
        math/
          add.go
        strings/
          format.go
    
  3. Обновляем импорты в 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

  4. Теперь команда:

    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 так нельзя.

Вместо этого:

  1. В 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

  2. В коде service-a вы импортируете:

    import "example.com/common/pkg/helpers"
    

    а не ../libs/common/pkg/helpers.

  3. Благодаря директиве replace Go понимает, что модуль нужно искать в локальной файловой системе, но имя пакета остается абсолютным, и вы не используете относительный путь в импорте.

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 для этого есть разные механизмы:

  1. Тесты в том же пакете

    Например, у вас есть:

    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 не нужно

  2. Внешние тесты (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 «ругается» на импорты после переноса проекта в другую папку

Если вы перенесли проект в другое место или поменяли имя репозитория:

  1. Обновите module в go.mod на актуальный путь.
  2. Запустите go mod tidy.
  3. В IDE выполните «Reimport project» или аналогичную команду.
  4. Убедитесь, что GOPATH и настройки Go SDK корректны (особенно в старых проектах).

После этого импорты, основанные на module path, будут снова корректно распознаваться.

Можно ли в одном каталоге иметь несколько пакетов и импортировать их как-то «относительно»

Технически в одной директории могут быть файлы с разными package, но Go этого не рекомендует. Импорты все равно должны ссылаться на каталог, а не на «часть каталога». Безопасный подход:

  • один каталог — один пакет
  • разные варианты пакета (например, с build tags) размещайте в тех же файлах с разными тэгами, а не делите каталог на несколько пакетов

Как отладить проблему, когда Go не находит пакет по корректному на вид пути

Алгоритм проверки:

  1. Убедитесь, что путь в import соответствует структуре каталогов относительно корня модуля (go.mod).
  2. Проверьте, что go.mod лежит в корне проекта, а вы запускаете команды из этого корня или ниже.
  3. Выполните go list ./... и посмотрите, все ли пакеты видны.
  4. Если используете go.work, убедитесь, что нужный модуль включен в use и путь к нему верный.
  5. Проверьте, что имя пакета (package name) в файлах совпадает с именем каталога (часто это источник путаницы).
Реэкспорт в index.ts - как упростить структуру модулей в TypeScript и JavaScriptСтрелочка вправо

Все гайды по Fsd

Открыть базу знаний

Отправить комментарий