---
title: "Пакеты и модули"
description: "Любая программа на Go состоит из пакетов. Даже первая программа курса — это пакет main. Пока код помещается в один файл, об этом можно не думать. Но сервис из курса «Backend на…"
canonical: https://graphlms.ru/go-basics/book/packages-modules
course: "Основы Go"
updated: 2026-10-12
lang: ru
publisher: GraphLMS
---

# Пакеты и модули

> Источник: https://graphlms.ru/go-basics/book/packages-modules — курс «Основы Go», GraphLMS.

## О чём глава

Любая программа на Go состоит из **пакетов**. Даже первая программа курса —
это пакет `main`. Пока код помещается в один файл, об этом можно не думать. Но
сервис из курса «Backend на Go» — это десятки файлов: обработка HTTP, очередь
заданий, хранилище. Свалить их в один `main.go` — значит потом полчаса искать
нужную функцию. Go решает это строго: код группируют по пакетам, а пакеты живут
внутри **модуля**.

В этой главе разберём три уровня этой системы. **Пакет** — папка с файлами,
которые работают вместе. **Импорт** — как один пакет берёт код другого.
**Модуль** — единица, которой пакеты распространяются и версионируются, со своим
файлом `go.mod`. Заодно поймём, почему `Println` пишут с большой буквы, и
напишем первый автоматический тест.

Большинство примеров здесь без кнопки запуска: в браузере выполняется только один
файл, а модуль из нескольких пакетов и тесты (пакета `testing` там нет) нужно
запускать локально. Если вы ещё не установили Go, вернитесь к разделу о первом запуске в
[первой главе](/go-basics/book/hello).

## Что такое пакет

Пакет — это просто **папка с `.go`-файлами**, у которых в первой строке одно и
то же объявление `package`. Все файлы одного пакета делят общее пространство
имён: функция, объявленная в одном файле, видна в другом без всякого импорта.
Делить пакет на несколько файлов — нормально и полезно: складываете связанные
части по смыслу.

```go
// файл user.go
package account

type User struct {
    Name string
}
```

```go
// файл balance.go — тот же пакет account, другой файл
package account

func NewUser(name string) *User {
    return &User{Name: name} // User виден без импорта: тот же пакет
}
```

Имя пакета по соглашению короткое, в нижнем регистре, без подчёркиваний:
`account`, `http`, `strconv`. Обычно оно совпадает с именем папки — так проще
ориентироваться. Особый случай — пакет `main`: он не библиотека, а точка входа.
Именно в нём живёт `func main()`, с которой стартует исполняемая программа.

## Импорты

Чтобы воспользоваться кодом другого пакета, его **импортируют**. Один импорт
пишут в строку, несколько — группируют в блок `import ( ... )`:

```go
import (
    "fmt"
    "strings"
)
```

В кавычках указывают **путь импорта** — это не имя пакета, а его адрес. Для
стандартной библиотеки путь короткий: `fmt`, `strings`, `math/rand`. Для чужого
кода путь обычно начинается с домена репозитория, например
`github.com/google/uuid`. А обращаются к содержимому всегда по **имени пакета**
(оно обычно совпадает с последним элементом пути): импортировав `math/rand`, вызываете `rand.Intn(...)`.

Go придирчив к импортам: **неиспользуемый импорт — это ошибка компиляции**, а не
предупреждение. Сначала раздражает, потом привыкаешь: лишних зависимостей в
коде не накапливается. `gofmt` форматирует код и сортирует импорты внутри групп. `goimports`
дополнительно добавляет нужные импорты и удаляет неиспользуемые.

Иногда удобно дать импорту **псевдоним** — например, когда два пакета называются
одинаково или имя слишком длинное:

```go
import (
    crand "crypto/rand" // теперь обращаемся как crand.Read(...)
    "math/rand"         // а это просто rand
)
```

## Экспорт: всё решает заглавная буква

Правило видимости в Go умещается в одно предложение.
**Идентификатор (имя функции, типа, поля, переменной) экспортируется (виден снаружи пакета) тогда и только тогда,
когда начинается с заглавной буквы.** С маленькой — он приватный, доступен лишь
внутри своего пакета.

```go
package account

type User struct {
    Name    string // экспортируется: видно снаружи
    balance int    // с маленькой — приватное поле, снаружи недоступно
}

func New(name string) *User { ... } // New виден другим пакетам
func validate(name string) bool { ... } // validate — только внутри account
```

Никаких ключевых слов `public`/`private` — регистр первой буквы и есть модификатор
доступа. Это работает для всего: функций, типов, методов, полей структур,
констант, переменных. Поэтому `fmt.Println` пишется с большой `P` (его вызывают
извне), а внутренние помощники пакета `fmt` — с маленькой, и вы их просто не
видите.

Практический смысл: то, что с заглавной, — это **публичный контракт** пакета,
его API. То, что с маленькой, — внутренняя кухня, которую автор волен менять, не
ломая пользователей. Когда проектируете пакет, делайте экспортируемым только
необходимый минимум: чем меньше публичная поверхность, тем легче её
поддерживать.

## Модуль и go.mod

Пакеты не висят в воздухе — они принадлежат **модулю**. Модуль — это дерево
пакетов, которые версионируются и распространяются как одно целое. У модуля есть
корневая папка и в ней файл **`go.mod`**, который объявляет путь модуля и его
зависимости.

Создаётся модуль одной командой в корне проекта:

```bash
go mod init github.com/me/shop
```

Она генерирует минимальный `go.mod`:

```text
module github.com/me/shop

go 1.25.6
```

Вторая строка — версия языка, под которую написан модуль. Go 1.25 и старше
подставляют версию установленного тулчейна. Начиная с Go 1.26 команда
записывает версию на один релиз ниже (Go 1.26 пишет `go 1.25.0`).
Поэтому число у вас может отличаться.

Первая строка — **путь модуля**. Он становится префиксом для путей импорта всех
пакетов внутри. Если в модуле `github.com/me/shop` есть папка `cart`, то её
пакет импортируют как `github.com/me/shop/cart`. Так Go однозначно понимает, где
искать код, и не путает ваш `cart` с чужим.

Когда вы добавляете внешнюю зависимость и запускаете `go mod tidy`, Go дописывает
её в `go.mod` в блок `require`. Выбор версий задают `go.mod` и граф зависимостей,
а `go.sum` хранит контрольные суммы скачанных модулей и их файлов `go.mod`.
Это не файл-«замок» версий: он помогает проверить целостность зависимостей.

## Как раскладывают код по пакетам

Посмотрим, как это выглядит целиком. Типичный небольшой проект:

```text
shop/
├── go.mod                      module github.com/me/shop
├── go.sum
├── main.go                     package main
├── cart/
│   ├── cart.go                 package cart
│   └── cart_test.go            package cart
└── internal/
    └── pricing/
        └── pricing.go          package pricing
```

Что здесь важно увидеть:

- **`main.go` в корне** — это `package main` с `func main()`. Точка входа,
  которую собирают в исполняемый файл командой `go build`.
- **`cart/`** — обычный библиотечный пакет. Файл `cart.go` начинается с
  `package cart`; из `main.go` его берут импортом
  `github.com/me/shop/cart`.
- **`cart_test.go`** лежит рядом с кодом, который тестирует, — в Go тесты
  держат в том же пакете, в файлах с суффиксом `_test.go`.
- **`internal/`** — особая папка. Пакеты под ней может импортировать только код
  из дерева каталогов, корнем которого является родитель `internal`. Это встроенный в Go способ сказать «это деталь реализации,
  снаружи трогать нельзя».

Когда исполняемых программ в модуле несколько (например, сервер и утилита
миграций), их `package main` кладут в отдельные папки под `cmd/`:
`cmd/server/main.go`, `cmd/migrate/main.go`. Так устроен и стартовый проект
курса «Backend на Go»: точка входа в `cmd/server`, логика в `internal/jobs`.

Главный принцип группировки: **по смыслу, а не по типу**. Не делайте папки
`models`, `services`, `utils` со свалкой всего подряд. Лучше пакет `cart`, в
котором и тип корзины, и операции над ней. Имя пакета — часть API: вызов
`cart.Add(...)` читается лучше, чем `utils.AddToCart(...)`.

## Стандартная библиотека

Вместе с Go устанавливается большая **стандартная библиотека** — набор пакетов,
доступных без скачивания. Часть из них вы уже встречали; вот самые ходовые:

- `fmt` — форматированный ввод-вывод: `Println`, `Printf`, `Sprintf`.
- `strings` — операции над строками: `Contains`, `Split`, `ToUpper`, `Join`.
- `strconv` — преобразования строк и чисел: `Atoi`, `Itoa`, `ParseFloat`.
- `sort` — сортировка срезов и пользовательских коллекций.
- `errors` — создание и разбор ошибок: `New`, `Is`, `As`.
- `time` — время, длительности, таймеры.
- `math`, `math/rand` — математика и псевдослучайные числа.
- `os` — аргументы, переменные окружения, работа с файлами (в браузере недоступен).

Всё это обычные пакеты: импортируете по короткому пути и обращаетесь по имени,
так же, как к своим.

Соберём небольшую программу, которая задействует сразу три пакета стандартной
библиотеки: `strings` разбивает строку, `strconv` превращает текст в числа, а
`sort` их упорядочивает.

```go play
package main

import (
	"fmt"
	"sort"
	"strconv"
	"strings"
)

func main() {
	raw := "42, 7, 13, 99, 1"

	parts := strings.Split(raw, ",") // ["42", " 7", ...]
	nums := make([]int, 0, len(parts))
	for _, p := range parts {
		n, _ := strconv.Atoi(strings.TrimSpace(p)) // текст -> число
		nums = append(nums, n)
	}

	sort.Ints(nums) // сортируем по возрастанию

	fmt.Println("отсортировано:", nums)
	fmt.Println("минимум:", nums[0], "максимум:", nums[len(nums)-1])
}
```

Вывод:

```
отсортировано: [1 7 13 42 99]
минимум: 1 максимум: 99
```

`strings.Split` оставляет пробел перед каждым числом (`" 7"`), и
`strconv.Atoi` такую строку не разберёт. Поэтому сначала `strings.TrimSpace`.
Ошибку `Atoi` здесь отбрасываем через `_` только потому, что входная строка
написана прямо в коде; для данных от пользователя так делать нельзя.

Когда строку собирают из многих кусков, используют `strings.Builder`: он
дописывает куски в один буфер, не создавая новую строку на каждом шаге.
`fmt.Fprintf` пишет отформатированный текст прямо в него:

```go play
package main

import (
	"fmt"
	"strings"
)

func main() {
	var b strings.Builder
	for i := 1; i <= 3; i++ {
		fmt.Fprintf(&b, "шаг %d; ", i) // пишем в Builder, а не на экран
	}
	line := strings.TrimSuffix(b.String(), "; ")

	fmt.Println("собрано:", line)
	fmt.Println("в верхнем регистре:", strings.ToUpper(line))
}
```

Вывод:

```text
собрано: шаг 1; шаг 2; шаг 3
в верхнем регистре: ШАГ 1; ШАГ 2; ШАГ 3
```

`&b` передаёт адрес билдера: `Fprintf` должен дописывать в нашу переменную, а
не в её копию — ровно та ситуация из [главы об указателях](/go-basics/book/pointers).
`TrimSuffix` срезает лишний разделитель в конце.

## Первый тест: проверяем функцию

До сих пор результат программы вы проверяли глазами: запустили, посмотрели на вывод. Это работает, пока функция одна и проверок три. Тест — это код, который делает такую проверку за вас: вызывает функцию с известными входами и сравнивает результат с ожидаемым. Запускается одной командой, сколько угодно раз, после каждой правки.

В учебной папке с `go.mod` создайте `sum.go`:

```go
package main

func Sum(a, b int) int {
    return a + b
}
```

Рядом создайте файл `sum_test.go`:

```go
package main

import "testing"

func TestSum(t *testing.T) {
    got := Sum(2, 3)
    if got != 5 {
        t.Fatalf("Sum(2, 3) = %d; хотим 5", got)
    }
}
```

Суффикс `_test.go`, имя с `Test` и параметр `*testing.T` позволяют команде найти проверку. `t.Fatalf` отмечает ошибку и останавливает этот тест. Если в папке остался `main.go` из предыдущей главы, он может оставаться: `Sum` там повторно объявлять не нужно. Функции `main` для `go test` не требуется.

```bash
go test .
go test ./...
```

Точка проверяет пакет в текущей папке, `./...` — пакеты в ней и во всех вложенных папках. При правильном результате появится строка вида:

```text
ok  	example.com/go-study	0.214s
```

Иногда вместо времени стоит `(cached)`: Go не стал перезапускать тест, потому что код не менялся. Чтобы запустить заново, добавьте флаг: `go test -count=1 .`.

Замените `+` на `-` в `Sum` и запустите тест снова:

```text
--- FAIL: TestSum (0.00s)
    sum_test.go:8: Sum(2, 3) = -1; хотим 5
FAIL
FAIL	example.com/go-study	0.180s
FAIL
```

Сообщение называет файл, строку и полученное значение — для этого мы и писали его в `Fatalf`. Верните `+`. Обратите внимание: ошибка компиляции выглядит иначе. Если код не собрался, тест даже не начал выполняться, и в выводе будет `[build failed]`.

### Табличный тест

Проверок обычно больше одной, и копировать блок `if` для каждой утомительно. В Go принято собирать случаи в таблицу — срез анонимных структур из [главы о структурах](/go-basics/book/structs) — и проходить по ней циклом:

```go
func TestSumTable(t *testing.T) {
    cases := []struct {
        name string
        a, b int
        want int
    }{
        {"положительные", 2, 3, 5},
        {"с отрицательным", -2, 2, 0},
        {"с нулём", 0, 7, 7},
    }
    for _, tc := range cases {
        got := Sum(tc.a, tc.b)
        if got != tc.want {
            t.Errorf("%s: Sum(%d, %d) = %d; хотим %d", tc.name, tc.a, tc.b, got, tc.want)
        }
    }
}
```

Новый случай — одна строка в таблице. Здесь `t.Errorf` вместо `t.Fatalf`: он отмечает провал, но не останавливает тест, и вы увидите все неверные строки таблицы сразу, а не только первую. Именно такой тест вас попросят написать в первом же этапе курса «Backend на Go».

Когда появятся горутины с общей памятью, проверяйте тесты также через `go test -race .`. Этот флаг ищет гонки данных на выполненных путях. Он не заменяет сравнение результатов, проверку завершения и тесты ошибочных входов. Для `-race` нужен локальный Go на поддерживаемой платформе и необходимые инструменты C; если команда сообщает об отсутствии компилятора, установите инструменты разработки для своей ОС.

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

- **Путают путь импорта и имя пакета.** В кавычках пишут адрес
  (`math/rand`), а обращаются по последнему элементу (`rand.Intn`). Если имя
  пакета внутри файла отличается от папки — ориентируйтесь на строку `package`,
  именно она задаёт, как к пакету обращаться.
- **Забывают про заглавную букву.** Объявили `func parseConfig(...)` с маленькой
  и удивляются, почему из другого пакета его не видно. Хотите экспортировать —
  `ParseConfig`. И наоборот: не делайте публичным то, что должно быть внутренним.
- **Неиспользуемый импорт.** Go не соберёт код с импортом, который вы не
  применяете. Удалите строку (или дайте `goimports` сделать это за вас). Если
  пакет нужен только ради его побочного эффекта — есть приём `import _ "путь"`,
  но это редкий случай.
- **Свалка в `utils`.** Один гигантский пакет «на всё» быстро превращается в
  клубок зависимостей. Группируйте по смыслу и держите публичный API узким.

## Попробуйте сами

Добавьте в `sum.go` функцию `CountWords(s string) int`, которая возвращает число слов в строке (подойдёт `strings.Fields` из [главы о коллекциях](/go-basics/book/collections)). Напишите для неё табличный тест минимум на четыре случая: обычная фраза, лишние пробелы между словами, пустая строка, строка из одних пробелов. Перед запуском ответьте: нужен ли в тесте импорт собственного `sum.go`? Затем намеренно испортите функцию (например, верните `len(s)`), убедитесь, что тест падает и показывает все неверные случаи, и восстановите её.

### Проверка

```go
// sum.go
func CountWords(s string) int {
    return len(strings.Fields(s))
}
```

```go
// sum_test.go
func TestCountWords(t *testing.T) {
    cases := []struct {
        in   string
        want int
    }{
        {"привет мир", 2},
        {"  много   пробелов  ", 2},
        {"", 0},
        {"   ", 0},
    }
    for _, tc := range cases {
        if got := CountWords(tc.in); got != tc.want {
            t.Errorf("CountWords(%q) = %d; хотим %d", tc.in, got, tc.want)
        }
    }
}
```

В `sum.go` нужен `import "strings"`. Импорт `sum.go` в тесте не нужен: оба файла объявляют один пакет `main`, а внутри пакета все имена общие. Импорт подключает другой пакет, а не соседний файл. С испорченной версией `len(s)` провалятся все четыре случая, и благодаря `t.Errorf` вы увидите их все. Глагол `%q` в сообщении показывает строку в кавычках, иначе пустую строку и строку из пробелов не различить.

## Что дальше

В следующей главе [горутины и каналы](/go-basics/book/goroutines-intro) добавят независимое выполнение: результат придётся не только вычислить, но и дождаться его. Проверки `error` из [главы об ошибках](/go-basics/book/errors) остаются нужны при работе с пакетами и вызовами, которые могут не завершиться успешно.
