Опирается на правила:
GO-3.1…GO-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.2 | import "math" + math.Pi |
import _ "pkg" вне main без комментария | GO-3.3 | перенести в main или добавить комментарий зачем |
| Ручная расстановка групп импортов | GO-3.1 | goimports в пресейв-хуке или CI |
Импорт из internal/ другого модуля | GO-3.4 | вынести API в публичный пакет |
package orderutils / package helper | GO-2.X1 | package order / доменный пакет |
order.OrderCreate(...) | GO-2.X2 | order.Create(...) |
| Пакет >500 строк без чёткой роли | GO-3.5 | разбить по доменной ответственности |
| Циклические зависимости между пакетами | GO-3.X1 | инверсия через интерфейс или общий пакет |
Куда дальше
- Именование — правила для имён пакетов, типов, методов в Go.
- Управляющие структуры — guard clause, switch, defer.
- golangci-lint — конфигурация
goimports,revive,staticcheck. - Обработка ошибок — Go — как
apperrиinternal/сочетаются.