Опирается на правила: GO-3.1GO-3.X1 из Go Style Guide → раздел 3. Пакеты и импорты.

Важно знать

  • goimports группирует импорты за вас: stdlib / внешние / internal — пустая строка между группами, руками не трогать.
  • dot-импорт (import . "pkg") запрещён — нарушает навигацию и линтер.
  • blank-импорт (import _ "pkg") допустим только в main или init-пакете, с явным комментарием зачем.
  • internal/ — встроенный барьер видимости; нарушение ловит компилятор, не ревью.
  • Имя пакета = имя директории, одно слово, lowercase, описывает контент (order, не orderutils).
  • Пакет-бог (>500 строк без чёткой роли) — сигнал разбить по доменной ответственности.
  • Циклические зависимости компилятор запрещает; если возникли — пересмотри разбиение.
  • Суффиксы util/helper/common в имени пакета — признак нарушения, см. GO-2.X1.

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

Группировка импортов: goimports делает это за вас

GO-3.1: три группы, пустая строка между ними.

import (
    "context"
    "fmt"
    "net/http"

    "github.com/go-chi/chi/v5"
    "github.com/jackc/pgx/v5"

    "github.com/myorg/orderservice/internal/order"
    "github.com/myorg/orderservice/internal/apperr"
)

goimports (или goimports-reviser) расставляет группы автоматически при сохранении файла. Конфигурация в .golangci.yml:

linters-settings:
  goimports:
    local-prefixes: github.com/myorg/orderservice

После этого инструмент знает, что github.com/myorg/orderservice/* — internal-группа, и разделит её от внешних зависимостей пустой строкой.

Ручная расстановка импортов — источник noise в diff и споров на ревью. goimports снимает эту задачу полностью.

Dot-импорт запрещён

GO-3.2: import . "pkg" ломает навигацию.

import . "math"

func circleArea(r float64) float64 {
    return Pi * r * r
}

Pi — откуда оно? Читатель не видит источника, IDE не может перейти к определению одним кликом. golangci-lint с revive помечает это как ошибку.

Правильно:

import "math"

func circleArea(r float64) float64 {
    return math.Pi * r * r
}

Имя пакета — часть API. math.Pi, order.New, customer.Repository — читается как документация без дополнительных комментариев.

Blank-импорт только в main

GO-3.3: import _ "pkg" вне main или init-пакета запрещён.

package main

import (
    "database/sql"

    _ "github.com/jackc/pgx/v5/stdlib"
)

Blank-импорт выполняет init() пакета — регистрирует драйвер, codec, провайдера. Это побочный эффект без явной зависимости. В main это приемлемо: точка входа должна связать все компоненты.

В internal/order или internal/customer blank-импорт создаёт скрытую связность — пакет неявно зависит от чего-то, что не видно в его сигнатуре.

Если blank-импорт всё же нужен вне main, добавляй комментарий:

import (
    // регистрирует prometheus-метрики при инициализации пакета
    _ "github.com/myorg/orderservice/internal/metrics"
)

internal/ как барьер видимости

GO-3.4: компилятор запрещает импорт из internal/ вне пакета-владельца.

Типичная структура сервиса заказов:

orderservice/
├── cmd/
│   └── main.go
├── internal/
│   ├── order/
│   │   ├── handler.go
│   │   ├── usecase.go
│   │   └── repository.go
│   ├── customer/
│   │   └── repository.go
│   └── apperr/
│       └── kind.go
└── go.mod

Попытка импортировать internal/order из другого репозитория:

cannot use internal package github.com/myorg/orderservice/internal/order

Компилятор отклоняет это на этапе сборки. Не нужны интерфейсы-заглушки, // не вызывать снаружи или тесты на «неправильный импорт» — Go встраивает барьер в язык.

Что идёт в internal/: домен (order, customer, product), адаптеры (postgres, kafka), инфраструктура (apperr, middleware). Что не идёт: контракты, которые должны быть видны другим сервисам — они в отдельном модуле или публичном пакете.

Пакет-бог — сигнал, не правило

GO-3.5: пакет >500 строк без чёткой роли требует разбиения.

internal/
└── service/          ← god-пакет: Order, Customer, Payment, Inventory — всё тут
    ├── orders.go     (400 строк)
    ├── customers.go  (300 строк)
    ├── payments.go   (350 строк)
    └── inventory.go  (200 строк)

Признаки god-пакета:

  • Имя service, handler, manager без уточнения домена.
  • Более двух несвязанных доменных понятий в одном пакете.
  • Все файлы пакета импортируют разные внешние библиотеки (pgx, kafka, redis) — функции не связаны.

После разбиения по доменной ответственности:

internal/
├── order/
│   ├── handler.go
│   ├── usecase.go
│   └── repository.go
├── customer/
│   └── repository.go
├── payment/
│   └── gateway.go
└── inventory/
    └── stock.go

Каждый пакет теперь описывает один домен. Имя пакета — order, customer — само объясняет контент без комментариев и README.

Циклические зависимости: компилятор не пропустит

GO-3.X1: цикл между пакетами — ошибка компиляции.

import cycle not allowed:
    github.com/myorg/orderservice/internal/order
    → github.com/myorg/orderservice/internal/customer
    → github.com/myorg/orderservice/internal/order

Цикл появляется когда order знает о customer, а customer знает об order. Решение — выделить общий пакет или инвертировать зависимость через интерфейс:

// internal/order/usecase.go
type CustomerRepository interface {
    FindByID(ctx context.Context, id string) (*Customer, error)
}

type CreateOrderUseCase struct {
    customers CustomerRepository
}

Теперь order зависит от своего интерфейса, а customer реализует его — цикл разорван. Пакет order ничего не знает о пакете customer.

Имя пакета = имя директории

GO-2.1, GO-2.X1: одно слово, lowercase, описывает контент.

package order    // ✓ — директория internal/order/
package product  // ✓ — директория internal/product/
package sber     // ✓ — директория internal/sber/ (внешняя интеграция)

package orderUtils   // ✗ — camelCase
package order_utils  // ✗ — snake_case
package util         // ✗ — не описывает контент
package helper       // ✗ — то же

Суффикс util, helper, common — сигнал что пакет является мусорной корзиной: в него складывают то, для чего не нашлось правильного места. Каждая функция в таком пакете принадлежит какому-то домену — найди его и перенеси туда.

Имя метода не повторяет имя пакета (GO-2.X2):

// ✗
order.OrderCreate(ctx, cmd)

// ✓
order.Create(ctx, cmd)

order.Create читается как «создать заказ» — префикс пакета уже несёт контекст.

Что запрещено

АнтипаттернПравилоЧто взамен
import . "math"GO-3.2import "math" + math.Pi
import _ "pkg" вне main без комментарияGO-3.3перенести в main или добавить комментарий зачем
Ручная расстановка групп импортовGO-3.1goimports в пресейв-хуке или CI
Импорт из internal/ другого модуляGO-3.4вынести API в публичный пакет
package orderutils / package helperGO-2.X1package order / доменный пакет
order.OrderCreate(...)GO-2.X2order.Create(...)
Пакет >500 строк без чёткой ролиGO-3.5разбить по доменной ответственности
Циклические зависимости между пакетамиGO-3.X1инверсия через интерфейс или общий пакет

Куда дальше

  • Именование — правила для имён пакетов, типов, методов в Go.
  • Управляющие структуры — guard clause, switch, defer.
  • golangci-lint — конфигурация goimports, revive, staticcheck.
  • Обработка ошибок — Go — как apperr и internal/ сочетаются.