Пакет и модуль
Чему вы научитесь:
- отличать пакет от модуля и составлять путь импорта своего пакета;
- читать go.mod и понимать, что делает
go mod tidy; - писать тесты в файлах
_test.goи читать отчётgo test -v; - собирать табличные тесты с
t.Runи выбирать междуt.Errorfиt.Fatalf.

До сих пор каждая ваша программа помещалась в один файл. Настоящий проект на Go — это десятки и сотни файлов, и их нужно как-то разложить по местам. В этой теме мы пройдём путь от одного файла к проекту: как Go делит код на части, как части находят друг друга и как проверить, что всё работает.
Представьте завод. У завода есть адрес на воротах. Внутри — цеха, у каждого цеха своё назначение, а в цехах стоят станки. В Go всё устроено похоже:
- модуль — это завод с адресом на воротах;
- пакет — это цех, то есть каталог с файлами;
- файл — это станок в цехе.
Пакеты вы уже встречали в первой теме. Каждая программа начиналась со строки package main, а fmt и strings — это пакеты стандартной библиотеки:
package main
import (
"fmt"
"strings"
)
func main() {
fmt.Println(strings.ToUpper("go")) // GO
fmt.Println(strings.Repeat("ab", 3)) // ababab
}Давайте разберём программу по строкам:
package mainговорит, к какому пакету относится файл.importподключает два пакета:fmtиstrings.strings.ToUpper("go")вызывает функциюToUpperиз пакетаstringsи печатаетGO.strings.Repeat("ab", 3)повторяет строку три раза и печатаетababab.
Перед именем функции стоит имя пакета и точка. Так Go понимает, из какого цеха взять станок.
Пакет — это каталог
Пакет — это все файлы .go из одного каталога, у которых в первой строке одно и то же имя после package. Вот правила, которые из этого следуют:
- в одном каталоге живёт ровно один пакет;
- файлы пакета видят функции, типы и переменные друг друга без всякого импорта;
- пакет
mainс функциейmain— это программа, которую можно запустить; - пакет с любым другим именем — это код для других пакетов: его импортируют, но не запускают.
Экспортированные имена
Цех не отдаёт наружу всё подряд. Что-то он выставляет в окно выдачи, а что-то остаётся внутри. В Go это решает первая буква имени:
- имя с заглавной буквы экспортировано: его видят другие пакеты;
- имя со строчной буквы остаётся внутри своего пакета.
Вот пакет store, который считает сумму покупки со скидкой:
package store
func Total(prices []int) int {
sum := 0
for _, p := range prices {
sum += p
}
return sum - discount(sum)
}
func discount(sum int) int {
return sum / 10
}Функция Total начинается с заглавной буквы, поэтому её можно вызвать из другого пакета. Функция discount — внутренняя деталь: Total вызывает её, потому что они в одном пакете, а снаружи её не видно.
Модуль и путь импорта
Модуль — это дерево каталогов с пакетами и файлом go.mod в корне. Первая строка go.mod — адрес завода на воротах: module example.com/shop. Вот как выглядит модуль shop на диске, а справа — пути импорта его пакетов:
shop/
├── go.mod module example.com/shop
├── main.go package main
└── store/
└── store.go package store → "example.com/shop/store"Путь импорта пакета — это путь модуля и каталог пакета внутри модуля. Пакет из каталога store/ импортируют как "example.com/shop/store". Пакет main в корне никто не импортирует: это сама программа.
Вот main.go, который пользуется пакетом store:
package main
import (
"fmt"
"example.com/shop/store"
)
func main() {
fmt.Println(store.Total([]int{100, 250})) // 315
}Разберём вызов по шагам:
- Импорт
"example.com/shop/store"подключает пакет из каталогаstore/. - В коде к нему обращаются по имени из строки
package, то естьstore. store.Totalскладывает100и250, вычитает скидку35и возвращает315.
Стандартные пакеты вроде fmt импортируют по короткому пути без адреса: они приходят вместе с Go. Свои пакеты всегда начинаются с пути модуля.
Здесь картинка с заводом перестаёт работать. На заводе два цеха могут передавать детали туда и обратно. В Go так нельзя: если store импортирует пакет report, а report импортирует store, сборка остановится с ошибкой import cycle not allowed. Импорты всегда идут в одну сторону.
Частая ошибка
Из main вызвали функцию, которая начинается со строчной буквы:
fmt.Println(store.discount(350)) // ✗name discount not exported by package storeПрограмма не компилируется. Функция discount существует, но она внутри пакета store, а main — другой пакет. Если функция нужна снаружи, её переименовывают с заглавной буквы: Discount.
Главное
- Пакет — это каталог файлов с одним именем в строке
package;main— программа, остальные пакеты импортируют. - Имя с заглавной буквы экспортировано, со строчной — видно только внутри пакета.
- Модуль — дерево пакетов с go.mod в корне; первая строка go.mod задаёт путь модуля.
- Путь импорта — это путь модуля плюс каталог пакета:
"example.com/shop/store".
Подпишись, чтобы пройти курс
Курс бесплатный. Войди через Telegram и подпишись на канал — откроются все уроки.