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

19 февраля 2026
Автор

Олег Марков

Введение

Правило относительных импортов (relative-imports) в Go связано с тем, как компилятор и инструменты экосистемы ищут пакеты при импорте. Как только вы начинаете разносить код по нескольким директориям, вопрос «как правильно импортировать соседний пакет» встает довольно остро.

Давайте разберемся, что такое относительные импорты, чем они отличаются от «обычных» (абсолютных) импортов, почему в Go они официально считаются нежелательными, как все же включить их использование в некоторых сценариях и какие практики лучше применять в современных проектах с модулями.

Здесь я буду опираться на реальные примеры, чтобы вы могли сразу видеть, как это выглядит в коде и что именно происходит «под капотом».


Что такое относительные импорты в Go

Абсолютные и относительные импорты — в чем разница

Сначала давайте зафиксируем терминологию.

В Go под импортом пакета обычно понимают строку вида:

import "example.com/myproj/pkg/utils"

Это абсолютный импорт:

  • путь начинается не с точки;
  • он задается относительно GOPATH/src (в старой модели) или относительно корня модуля (в Go modules);
  • такой импорт однозначен и не зависит от того, где физически находится файл, который его использует.

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

import "./utils"   // относительный импорт
import "../core"   // переход на директорию выше

Такие импорты:

  • зависят от текущей директории файла;
  • меняются, если вы переносите файл в другую директорию;
  • плохо вписываются в концепцию модулей и стабильных путей пакетов.

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

Почему относительные импорты считаются плохой практикой

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

  1. Хрупкость структуры

Если вы перенесете файл service.go из одного пакета в другой, строка:

import "../core"

скорее всего перестанет быть корректной. Вам придется вручную починить путь. При абсолютном импорте:

import "example.com/myproj/core"

перенос файла внутри проекта не ломает путь к пакету.

  1. Сложности с переиспользованием

Код, который полагается на относительные импорты, сложно переиспользовать в других модулях или в другой структуре проекта. Путь ../core нигде за пределами вашего дерева директорий не имеет смысла.

  1. Инструменты и модули

Современный Go ориентирован на работу с модулями (go.mod). Инструменты — go build, go test, go list — ожидают, что вы будете использовать модульные пути, а не относительные. В контексте модулей относительные импорты ломают ожидаемую модель разрешения путей.

  1. Непрозрачность для читателя

Когда вы видите:

import "../auth"

с первого взгляда не понятно, каков «модульный» или «репозиторный» путь этого пакета. Абсолютный импорт:

import "example.com/myproj/internal/auth"

сразу показывает, где в проекте находится пакет и как его можно импортировать снаружи (если он не internal).


Как Go реально относится к относительным импортам

Поведение go build и go tool compile

Смотрите, как это устроено на практике:

  • высокоуровневые команды go build, go test, go install запрещают относительные импорты;
  • при попытке использовать import "./pkg" вы получите ошибку уровня build вида:

    relative import "./pkg" not supported in module mode
    
  • низкоуровневый инструмент go tool compile (и некоторые старые сценарии) все еще технически умеет работать с относительными путями, но для нормальной разработки в современных версиях Go это не считается поддерживаемым способом.

Иначе говоря: формально понятие relative-imports в языке осталось, но для обычной разработки через go build и модули это правило заключено в том, что относительные импорты считаются ошибкой.

Связь с GOPATH и модулями

До появления модулей (GO111MODULE=on по умолчанию):

  • пакеты искались в GOPATH/src;
  • абсолютный импорт «пример.ком/проект/пакет» трактовался как путь GOPATH/src/пример.ком/проект/пакет.

Относительные импорты в этот момент выглядели чуть менее странно, но даже тогда они не поощрялись.

С переходом на modules:

  • базовой единицей стал модуль, описанный в go.mod;
  • любой импорт должен приводить к валидному пути внутри или вне модуля;
  • относительные пути, зависящие от физического расположения файла, конфликтуют с идеей декларативного пути пакета.

Поэтому правило relative-imports в современной экосистеме можно сформулировать так: «В нормальной работе через go build относительные импорты запрещены, используйте только модульные/абсолютные пути».


Как правильно подключать соседние пакеты вместо относительных импортов

Теперь давайте посмотрим, как делать то, для чего чаще всего пытаются использовать относительные импорты: импортировать соседние пакеты в одном проекте.

Типичная структура проекта с модулями

Представим структуру:

myproj/
  go.mod
  cmd/
    app/
      main.go
  internal/
    core/
      core.go
    api/
      handler.go
  pkg/
    util/
      util.go

Содержимое go.mod:

module example.com/myproj

go 1.22

Теперь вы хотите в internal/api/handler.go использовать код из internal/core.

Вместо относительного импорта вроде:

// Так делать не нужно
import "../core"

делайте абсолютный модульный импорт:

package api

import (
    "example.com/myproj/internal/core"
)

В этом случае:

  • example.com/myproj — модульный путь из go.mod;
  • internal/core — путь к пакету внутри модуля.

Как видите, код больше не зависит от того, откуда вы запустили go build, и не зависит от структуры директорий относительно файла handler.go — он зависит только от структуры проекта как модуля.

Пример: использование internal вместо относительных импортов

Покажу вам, как это выглядит в коде.

Файл internal/core/core.go:

package core

// Service - простая структура сервиса
type Service struct {
    Name string
}

// NewService - конструктор сервиса
func NewService(name string) *Service {
    // Здесь мы создаем и возвращаем указатель на структуру Service
    return &Service{Name: name}
}

Файл internal/api/handler.go:

package api

import (
    // Здесь мы импортируем пакет core по модульному пути
    "example.com/myproj/internal/core"
)

// Handler - обработчик, который использует core.Service
type Handler struct {
    service *core.Service
}

// NewHandler - конструктор обработчика
func NewHandler() *Handler {
    // Здесь мы создаем сервис через функцию NewService из пакета core
    s := core.NewService("main-service")

    // Возвращаем указатель на Handler с инициализированным полем service
    return &Handler{service: s}
}

Здесь я показываю пример, чтобы вам было проще увидеть, как модульные пути заменяют собой относительные. Ни одной точки в начале пути, но при этом api и core — соседние директории внутри одного модуля.


Когда относительные импорты все еще могут встречаться

Старая кодовая база без модулей

В старых репозиториях, написанных до повсеместного появления модулей, вы можете увидеть файлы с относительными импортами. Например:

package main

import (
    "./lib"
)

или

import "../common"

Чаще всего это:

  • старые учебные примеры;
  • внутренняя кодовая база, которую долго не обновляли;
  • примеры, написанные до Go 1.11 (до появления модулей).

Если вы столкнулись с таким кодом, хорошей практикой будет:

  1. Добавить go.mod в корень проекта.
  2. Пронумеровать/определить модульный путь — например, module example.com/oldproj.
  3. Заменить относительные импорты на модульные, соответствующие структуре директорий.

То есть:

// Было
import "../common"

стало:

import "example.com/oldproj/common"

и так далее, пока весь код не перестанет использовать относительные пути.

Сценарии с go tool compile и специальной сборкой

Иногда вам может встретиться рекомендация использовать низкоуровневую команду:

go tool compile -I . -o main.o main.go

Теоретически, при таком способе сборки можно обойти некоторые ограничения и заставить компилятор принять относительные импорты. Но:

  • это не поддерживаемый путь для обычной разработки;
  • инструменты go build, go test, go vet и прочие работать корректно не будут;
  • вы усложните жизнь себе и другим разработчикам.

Поэтому относительные импорты в таких сценариях — это скорее побочный эффект использования очень низкоуровневого инструментария, а не рекомендуемая практика.


Почему в некоторых языках относительные импорты нормальны, а в Go — нет

Разработчики, приходящие из Python, JavaScript или других языков, часто удивляются, почему import "./pkg" в Go считается проблемой. Давайте разберемся.

Сопоставление с Python

В Python относительные импорты активно используются внутри пакетов, например:

from . import utils
from ..core import service

Там они частично решают проблему пакетов и окружений виртуальных сред.

В Go архитектурное решение иное:

  • структура пакетов жестко привязана к структуре директорий;
  • модульный путь выполняет роль «корневого пространства имен»;
  • мульти-модульность и работа с удаленными репозиториями опираются на стабильные абсолютные пути.

Если бы Go активно поощрял относительные импорты, стало бы гораздо сложнее:

  • однозначно идентифицировать пакеты;
  • делать кросс-проектное переиспользование;
  • стабильно кэшировать сборки и модули.

Поэтому в экосистеме Go относительные импорты сознательно ограничены: в обмен вы получаете более предсказуемую и стабильную модель зависимостей.


Практические рекомендации по структуре проекта вместо relative-imports

Теперь давайте посмотрим, какие подходы помогают избегать желания использовать относительные импорты.

Ясная структура модулей

Обратите внимание, как структура проекта позволяет избежать относительных импортов сама по себе. Общие рекомендации:

  • один модуль на репозиторий (в большинстве случаев);
  • пакет cmd — для entrypoint-приложений;
  • пакеты internal — для внутренней логики, которую не нужно экспортировать наружу;
  • пакеты pkg — для переиспользуемых компонентов, которые потенциально могут быть использованы другими модулями.

Схема:

myproj/
  go.mod              // module example.com/myproj
  cmd/
    app/
      main.go
  internal/
    auth/
    core/
    api/
  pkg/
    logger/
    config/

Все импорты внутри этого модуля будут вида:

import (
    "example.com/myproj/internal/auth"
    "example.com/myproj/pkg/logger"
)

Ни одной точки в начале, никаких ../.

Избегайте глубоких и запутанных вложенностей

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

project/
  service/
    api/
      http/
        v1/
          handlers/
            user/
              create/
                handler.go

Когда вы находитесь на уровне create/handler.go, рука сама тянется к чему-то вроде:

import "../../../../core"

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

  1. Упростите структуру директорий.
  2. Подумайте, нельзя ли перенести общую логику на уровень выше, в четко именованный пакет.
  3. Используйте модульные пути, отражающие логическую структуру пакетов, а не подробную вложенность.

Например:

internal/
  core/
  http/
    v1/
      user/
        create.go

и импорт:

import "example.com/myproj/internal/core"

Относительные пути в других командах Go и их отличие от relative-imports

Важно не перепутать понятия: относительные импорты и относительные пути для пакетов в командах go — это разные вещи.

Относительный путь в командах go build, go test, go list

Вы можете запустить команду:

go build ./...

или

go test ./internal/...

Здесь ./... — относительный путь, но это:

  • путь к пакету или набору пакетов для инструмента go;
  • не строка импорта внутри кода.

То есть виcимая разница:

  • import "./pkg" — запрещенный относительный импорт в исходном коде;
  • go test ./pkg — нормальный относительный путь к каталогу pkg от текущей директории для утилиты go.

Давайте посмотрим на пример.

cd myproj
go test ./internal/api

Здесь:

  • ./internal/api — относительный путь в файловой системе;
  • пакет, который реально тестируется, имеет модульный путь example.com/myproj/internal/api;
  • внутри кода пакета api вы по-прежнему используете только модульные импорты, без точек в начале.

Как мигрировать проект с относительных импортов

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

Исходная ситуация

Пусть у нас есть старый проект:

oldproj/
  main.go
  lib/
    util.go

Файл main.go:

package main

import (
    "./lib"
)

func main() {
    // Здесь мы вызываем функцию из пакета lib
    lib.Run()
}

Файл lib/util.go:

package lib

import (
    "../shared"
)

func Run() {
    // Здесь вызывается функция из пакета shared
    shared.Do()
}

Видно, что здесь используются относительные импорты ./lib и ../shared.

Шаг 1 — определить модуль

Добавляем go.mod в корень:

cd oldproj
go mod init example.com/oldproj

Теперь структура:

oldproj/
  go.mod     // module example.com/oldproj
  main.go
  lib/
    util.go
  shared/
    shared.go

Шаг 2 — заменить относительные импорты

Изменяем main.go:

package main

import (
    // Импортируем пакет lib по модульному пути
    "example.com/oldproj/lib"
)

func main() {
    // Вызываем Run из пакета lib
    lib.Run()
}

Изменяем lib/util.go:

package lib

import (
    // Импортируем пакет shared по модульному пути
    "example.com/oldproj/shared"
)

func Run() {
    // Вызываем функцию Do из пакета shared
    shared.Do()
}

Файл shared/shared.go:

package shared

import "fmt"

// Do - простая функция для демонстрации
func Do() {
    // Печатаем сообщение в стандартный вывод
    fmt.Println("shared.Do called")
}

Теперь проект не использует правила относительных импортов, а полностью опирается на модульные пути.

Шаг 3 — проверка

Запускаем:

go build ./...
go test ./...

Если все импорты исправлены, никаких ошибок о relative-imports больше не будет.


Типичные ошибки, связанные с relative-imports, и как их читать

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

Ошибка в режиме модулей

Сообщение может выглядеть так:

relative import "./lib" not supported in module mode

Расшифровка:

  • Go запущен в режиме модулей (обычно по умолчанию);
  • в вашем коде в разделе import есть путь вида ./lib;
  • такие импорты не поддерживаются — нужно заменить на модульный путь.

Решение:

  1. Открыть файл, упомянутый в ошибке.
  2. Найти строку import "./lib".
  3. Заменить на что-то вроде:

    import "example.com/myproj/lib"
    

    с учетом вашего module в go.mod.

Ошибка при использовании GOPATH без модулей

Если вы работаете в старой среде, иногда можно увидеть что-то вроде:

cannot find package "./lib" in any of:
    /usr/local/go/src./lib (from $GOROOT)
    /home/user/go/src./lib (from $GOPATH)

Здесь Go пытается трактовать ./lib как путь в рамках GOPATH, но не находит его.

Решение в современном контексте чаще всего то же самое: добавить go.mod и перейти на модульные импорты.


Краткое резюме по правилу relative-imports

  • В Go относительные импорты — это импорты, начинающиеся с . или .. в строках import.
  • Такие импорты официально считаются плохой практикой и по сути запрещены для нормальной разработки с использованием go build и модулей.
  • Современная модель Go строится вокруг модульных/абсолютных путей: module example.com/myproj + import "example.com/myproj/internal/core".
  • Вместо relative-imports стоит:
    • грамотно организовать структуру модулей и пакетов;
    • использовать internal и pkg для внутреннего и публичного кода;
    • избегать чрезмерной глубины вложенности директорий.
  • Если вы встречаете старый код с относительными импортами:
    • добавьте go.mod;
    • замените относительные пути на модульные;
    • проверьте проект с помощью go build ./... и go test ./....

Частозадаваемые технические вопросы по теме и ответы

Как импортировать локальный пакет при разработке нескольких модулей в одном репозитории без относительных импортов

Используйте replace в go.mod. Например, у вас есть два модуля:

repo/
  service/
    go.mod      // module example.com/service
  lib/
    go.mod      // module example.com/lib

В service/go.mod:

module example.com/service

go 1.22

require example.com/lib v0.0.0

replace example.com/lib => ../lib

Теперь в коде вы можете писать:

import "example.com/lib"

и при этом физически использовать код из ../lib.

Как понять, какой именно модульный путь нужно указать вместо относительного импорта

Откройте go.mod в корне проекта и посмотрите строку module. Допустим, там:

module example.com/myproj

Далее найдите путь к директории пакета относительно корня модуля. Если файл находится в internal/core, то импорт будет:

import "example.com/myproj/internal/core"

То есть: module + относительный путь директории пакета.

Можно ли включить поддержку относительных импортов только для одного файла или пакета

Нет, в нормальном режиме работы go build и модулей такой опции нет. Поддержка относительных импортов рассматривается как устаревшая и не предназначена для выборочного включения. Если их хочется использовать, это явный сигнал, что лучше перепроектировать структуру модулей/пакетов.

Как организовать тесты, чтобы не возникало желания использовать относительные импорты

Размещайте тесты рядом с кодом и используйте тот же пакет или package xxx_test. Например:

internal/core/
  core.go
  core_test.go

В core_test.go:

package core

// или package core_test, если нужен внешний импорт

Если используете core_test, импортируйте:

import "example.com/myproj/internal/core"

Так отношение между тестами и кодом будет очевидным, без относительных путей.

Почему go run ./cmd/app работает, хотя внутри кода нельзя писать import "./pkg"

Команда go run ./cmd/app использует относительный путь в файловой системе для указания каталога с пакетом, который нужно запустить. Это внешний путь для утилиты go. Внутри же исходников импорты по-прежнему должны быть модульными, например:

import "example.com/myproj/internal/core"

То есть относительный путь в командной строке и относительный импорт в коде — разные механизмы, и только второй запрещен.

Реэкспорт в index.ts - реэкспорт модулей в TypeScript и JavaScriptСтрелочка вправо

Все гайды по Feature-sliced_design

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

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