Menu

Структура проекта на Golang: cmd, internal и раскладка пакетов

Как организовать проект на Go: начать с плоской структуры, делить на пакеты, когда есть причина, использовать cmd/ для нескольких бинарников и internal/ для кода, который никто больше не должен импортировать, хорошо называть пакеты и держать тесты рядом с кодом.

На этой странице есть исполняемые редакторы: меняйте, запускайте и сразу видите результат.

Начните с одного файла

Программа на Go может целиком жить в одном main.go, и маленьким утилитам так и стоит. Эта полная программа разбирает ввод, выполняет работу и печатает отчёт в одном пакете:

Когда она вырастет, разбейте её на несколько файлов в той же папке и том же пакете (parse.go, report.go, main.go). Файлы одного пакета делят все идентификаторы, так что ничего не нужно ни экспортировать, ни импортировать. Часто это вся структура, которая нужна проекту на несколько тысяч строк.

Помните, что package main из нескольких файлов нужно запускать как пакет: go run ., а не go run main.go. Иначе файлы компилируются по одному, и вы получите ошибки undefined для имён из других файлов.

Обязательной структуры нет

Go не требует никакой структуры папок, кроме правила «одна папка это один пакет». Официальные рекомендации это страница Organizing a Go module на go.dev, и она описывает несколько форм, а не правила.

Репозиторий golang-standards/project-layout на GitHub широко копируют и часто принимают за стандарт. Это собранные сообществом соглашения из больших проектов, и Расс Кокс, в то время технический руководитель Go, открыл там issue с заявлением, что это не стандарт Go. Большинство его папок (pkg/, api/, build/, deployments/) имеют смысл только для больших кодовых баз. Если скопировать весь скелет в новый проект, получатся пустые папки и длинные пути импорта без всякой пользы.

Правила, которые действительно проверяет утилита, короткие:

  • Одна папка это один пакет. Все файлы .go в ней (кроме файлов _test.go с суффиксом _test в имени пакета) имеют одно имя пакета.
  • Путь импорта это путь модуля плюс путь папки.
  • Папка с именем internal ограничивает, кто может импортировать то, что внутри.
  • Папки с именем testdata, а также начинающиеся с . или _, утилита go игнорирует.

Распространённые формы

Одна команда

todo/
├── go.mod        module github.com/you/todo
├── main.go
├── parse.go
├── report.go
└── parse_test.go

Плоско, всё в package main. go install github.com/you/todo@latest работает, а бинарник называется todo.

Библиотека

slug/
├── go.mod        module github.com/you/slug
├── slug.go       package slug
├── slug_test.go
├── example_test.go
└── internal/
    └── table/    helpers the public package uses, hidden from users

Пакет лежит в корне модуля, поэтому пользователи импортируют github.com/you/slug и вызывают slug.Make(...). Всё, что вы не хотите поддерживать как публичный API, идёт в internal/.

Сервис или несколько бинарников

shop/
├── go.mod                module github.com/you/shop
├── cmd/
│   ├── shop-api/
│   │   └── main.go       package main: flags, config, wiring
│   └── shop-worker/
│       └── main.go
├── internal/
│   ├── order/            package order: domain types and logic
│   │   ├── order.go
│   │   └── order_test.go
│   ├── store/            package store: database access
│   └── httpapi/          package httpapi: handlers, routing
├── migrations/
└── README.md

cmd/<name>/main.go это одна папка на бинарник, и имя папки становится именем бинарника (go build ./cmd/shop-api даёт shop-api). Каждый main остаётся тонким: прочитать конфигурацию, собрать зависимости, запустить сервер. Настоящий код живёт в пакетах внутри internal/, где его могут использовать оба бинарника и не может ни один другой модуль.

К этой структуре приходит большинство сервисов на Go. Переходите к ней, когда появился второй бинарник или реальная причина делить пакеты, а не в первый же день.

internal/ проверяет команда go

Пакет внутри internal/ может импортировать только код из дерева с корнем в родителе internal. Из другого модуля сборка падает:

package example.com/b
	main.go:6:2: use of internal package example.com/a/internal/secret not allowed

Поэтому internal/ это инструмент, который держит публичный интерфейс маленьким. Код там можно свободно менять, потому что вы знаете, что все вызывающие находятся в вашем же репозитории. Для приложения разумно положить в internal/ почти всё. Для библиотеки это отделяет то, что вы обещаете сохранять стабильным, от того, что не обещаете.

internal работает на любой глубине: shop/internal/order виден всему shop/, а shop/internal/order/internal/pricing виден только внутри shop/internal/order/.

Именование пакетов

Имя пакета входит в каждое место вызова, поэтому оно важнее дерева папок.

  • Коротко, строчными буквами, одно слово: order, store, httpapi. Без подчёркиваний и без mixedCaps.
  • Называйте пакет по тому, что он даёт, а не по тому, что в нём лежит. util, common, helpers, misc и models ничего не говорят и превращаются в свалки; руководство по стилю от команды Go прямо их не рекомендует. Кладите помощник в пакет, который его использует, или в пакет, названный по назначению (slug, retry).
  • Избегайте повторов. Вызывающий код пишет order.Order и order.New, поэтому не называйте что-то order.OrderService или order.NewOrder. Стандартная библиотека последовательно так делает: http.Server, а не http.HTTPServer.
  • Имя папки и имя пакета должны совпадать, кроме package main в папках команд. Когда они различаются, читателю приходится открыть файл, чтобы узнать, какой идентификатор вводит импорт.

Делите пакеты по ответственности, а не по виду кода. Разделение на models/, controllers/, services/ (обычное в других экосистемах) заставляет каждую функцию затрагивать три пакета и склонно порождать циклические импорты, которые Go запрещает. Пакеты вокруг понятия предметной области (order, payment, user) держат свои типы и логику вместе.

Где лежат тесты

Тесты живут рядом с кодом в той же папке, в файлах *_test.go. Дерева tests/ нет.

  • package order в order_test.go: внутренний тест, который может использовать неэкспортированные идентификаторы.
  • package order_test в той же папке: внешний тест, который видит только экспортированный API, как вызывающий код. Полезен для примеров и чтобы избежать циклических импортов в тестах.
  • Файлы с тестовыми данными идут в папку testdata/ рядом с тестами. Утилита go её игнорирует, а тесты запускаются с папкой пакета в качестве рабочей, так что относительные пути вроде testdata/big.json работают.

Другие файлы в корне

Файл или папкаНазначение
go.mod, go.sumопределение модуля и контрольные суммы зависимостей, всегда в корне
README.md, LICENSEкак в любом проекте
Makefile или Taskfile.ymlнеобязательные сокращения для сборки
Dockerfileу сервисов обычно в корне
migrations/, web/, docs/ресурсы не на Go, названные по содержимому
tools.goстарый способ зафиксировать зависимости-инструменты; в Go 1.24 его заменяют строки tool в go.mod (go get -tool)
go.workрабочее пространство для локальной разработки нескольких модулей вместе; в проектах из одного модуля обычно не коммитится

Частые ошибки

  • Большой шаблон для маленького проекта. Начинайте плоско и добавляйте папки, когда появляется второй бинарник или настоящая граница.
  • Пакеты с именами utils или common. Называйте пакеты по тому, что они делают.
  • Пакет на каждый файл или тип. Пакеты Go задуманы как более крупные единицы, чем классы Java. Пакет из десяти файлов это нормально.
  • Циклические импорты. Два пакета не могут импортировать друг друга. Обычно это значит, что они должны быть вместе, что общий тип нужно перенести в более низкоуровневый пакет или что одна сторона должна зависеть от маленького интерфейса, а не от другого пакета.
  • pkg/ по привычке. Добавляет сегмент в каждый путь импорта, не добавляя смысла.
  • Тесты в отдельной папке. Там они не видят неэкспортированный код, и инструменты их там не ожидают.

Часто задаваемые вопросы

Есть ли официальная структура проекта на Go?

Обязательной нет. Команда Go публикует рекомендации на go.dev/doc/modules/layout, где описано несколько распространённых форм (один пакет, одна команда, несколько команд с internal/). Популярный репозиторий golang-standards/project-layout это проект сообщества, а не стандарт Go, и команда Go заявляла об этом публично.

Что такое папка internal в Go?

Пакет, путь импорта которого содержит элемент internal, может импортировать только код с корнем в родителе этой папки internal. example.com/app/internal/store можно импортировать где угодно внутри example.com/app/, а импорты из любого другого модуля команда go отвергает с сообщением use of internal package ... not allowed.

Нужна ли в Go папка pkg?

Она необязательна и добавляет сегмент пути, не добавляя смысла. Некоторые большие проекты используют pkg/, чтобы отделить публичный библиотечный код от остального, но стандартная библиотека Go и большинство современных проектов так не делают. Кладите импортируемые пакеты в корень модуля или в именованные папки, а всё приватное в internal/.

Где лежат файлы тестов в проекте на Go?

В той же папке, что и тестируемый код, с именами *_test.go. Отдельного дерева для тестов в Go нет. Файлы с тестовыми данными кладутся в папку testdata рядом с тестами, и утилита go игнорирует её при поиске пакетов.

Coddy programming languages illustration

Учитесь программировать с Coddy

НАЧАТЬ