Go: с нуля до своего блога Урок 15 из 50
Пакеты и модули в Go: go mod и заглавная буква
Пятнадцатый урок курса по Go. Пакет — это папка, а заглавная буква в имени — дверь наружу. Как разложить программу по папкам, что записано в go.mod и go.sum, как подключить чужой пакет командой go get, зачем нужна папка internal и почему go mod tidy запускают перед отправкой кода.
Зачем это нужно
main.go разросся: тип, методы, хранилище, ошибки — всё в одном файле. Пора разложить.
И вторая половина урока: рано или поздно вам понадобится чужой код. Писать самому генератор идентификаторов или разбор JSON — трата жизни, когда это уже написано и проверено тысячами людей.
Сразу целиком
Новая папка sabaq-14, и в ней ещё одна — blog. Три файла:
sabaq-14/
go.mod
main.go
blog/
article.go
store.go
blog/article.go:
package blog
import "fmt"
type Article struct {
Title string
Words int
}
func (a Article) ReadingTime() int {
return (a.Words + 199) / 200
}
func (a Article) String() string {
return fmt.Sprintf("%s (%d мин)", a.Title, a.ReadingTime())
}
blog/store.go:
package blog
import (
"errors"
"fmt"
)
var ErrNotFound = errors.New("статья не найдена")
type Store struct {
items map[string]Article
}
func NewStore() *Store {
return &Store{items: map[string]Article{}}
}
func (s *Store) Add(slug string, a Article) {
s.items[slug] = a
}
func (s *Store) Get(slug string) (Article, error) {
a, ok := s.items[slug]
if !ok {
return Article{}, fmt.Errorf("get %q: %w", slug, ErrNotFound)
}
return a, nil
}
func (s *Store) Len() int { return len(s.items) }
main.go:
package main
import (
"errors"
"fmt"
"github.com/google/uuid"
"sabaq14/blog"
)
func main() {
s := blog.NewStore()
s.Add("dala", blog.Article{Title: "О степи", Words: 400})
a, err := s.Get("dala")
if err != nil {
fmt.Println(err)
return
}
fmt.Println(a)
fmt.Println("статей:", s.Len())
if _, err := s.Get("kokek"); errors.Is(err, blog.ErrNotFound) {
fmt.Println("404:", err)
}
fmt.Println("новый id:", uuid.NewSHA1(uuid.NameSpaceURL, []byte("shanraq.org/dala")))
}
Три команды по порядку:
go mod init sabaq14
go get github.com/google/uuid
go run .
Прежде чем запускать — не заглядывая ниже, скажите вслух, что напечатает программа. Потом запустите и сверьте.
О степи (2 мин)
статей: 1
404: get "kokek": статья не найдена
новый id: 7046abf7-a993-54e5-8960-558b4e3d88d9
Идентификатор у вас будет ровно такой же: он вычислен из адреса, а не из случайности.
Разбор
Пакет — это папка
Все файлы одной папки принадлежат одному пакету, и в каждом из них первая строка одинаковая: package blog. Разбивать пакет на файлы можно как угодно — компилятору всё равно, а человеку удобнее по смыслу: тип в одном файле, хранилище в другом.
Имя пакета обычно совпадает с именем папки. Внутри пакета файлы видят друг друга целиком, безо всяких импортов: store.go пользуется типом Article из соседнего файла и ничего для этого не делает.
Образ. Комната в доме. Внутри комнаты всё под рукой. Наружу, в коридор, выходит только то, что вы сами вынесли к двери.
Заглавная буква — дверь наружу
Теперь понятно, почему в уроке про структуры поля назывались Title и Words.
Правило одно и без исключений: имя с заглавной буквы видно из других пакетов, со строчной — нет. Это касается всего: типов, функций, методов, полей, переменных.
В нашем хранилище поле items написано со строчной, и это не случайность. Попробуйте из main.go:
len(s.items)
Программа не соберётся:
s.items undefined (cannot refer to unexported field items)
Именно поэтому у Store есть метод Len(): наружу выходит умение, а не устройство. Захотите завтра заменить словарь на базу — снаружи ничего не сломается.
go mod init и что в go.mod
go mod init sabaq14
Эта команда объявляет модуль — единицу, которую можно собрать и опубликовать. Модуль — это дерево папок с файлом go.mod в корне; пакеты — папки внутри него.
После go get файл выглядит так:
module sabaq14
go 1.27.1
require github.com/google/uuid v1.6.0
Первая строка — имя модуля, оно же начало всех внутренних путей импорта. Вторая — версия языка, которой вы собираете; у вас будет своя. Третья — список того, что вы взяли у других, с точными версиями.
Импорт своего пакета
import "sabaq14/blog"
Путь начинается с имени модуля, дальше — путь папки внутри него. Не относительный ./blog, не абсолютный путь на диске: только так.
Обратите внимание на порядок в блоке import: сначала стандартная библиотека, потом чужие пакеты, потом свои, между группами пустая строка. Это не требование языка, а общая привычка, и редактор расставит всё сам при сохранении.
Чужой пакет: go get, go.sum, go mod tidy
go get github.com/google/uuid делает три вещи: скачивает пакет, добавляет строку в go.mod и записывает отпечаток скачанного в go.sum.
go.sum — не список зависимостей, а список контрольных сумм. При следующей сборке Go проверит, что скачанное совпадает с записанным. Оба файла кладут в репозиторий.
Образ. Расписка с отпечатком. Список взятых книг — это
go.mod. Аgo.sum— отметка, по которой видно, что вернули ту же самую книгу, а не похожую с другим текстом внутри.
go mod tidy наводит порядок: добавляет то, что вы импортировали и забыли объявить, и убирает то, что больше не используется. Запускайте её перед тем, как отправлять код: сразу после go get наш пакет был помечен // indirect, и только tidy убрал эту пометку.
Имя пакета читается в месте вызова
Снаружи пишут blog.Article и blog.NewStore. Поэтому внутри пакета не надо повторять его имя: blog.BlogArticle — заикание, а blog.Article читается как фраза.
По той же причине имена пакетов короткие и в одно слово, строчными буквами, без подчёркиваний: http, fmt, errors.
internal/ — папка, закрытая снаружи
Если папку назвать internal, её содержимое смогут импортировать только соседи по модулю. Чужой проект, подключивший ваш модуль, до неё не доберётся — это проверяет сам компилятор.
Проверить это можно за минуту. Два модуля: в первом пакет лежит в internal, второй пробует его импортировать.
$ cd lib && go build ./... # свой модуль читает свой internal
свой модуль: собралось
$ cd ../app && go build ./... # чужой модуль лезет в тот же пакет
package example.com/app
main.go:6:2: use of internal package example.com/lib/internal/store not allowed
Компилятор не советует и не предупреждает — он отказывается собирать. internal — единственное имя папки, которое Go понимает как правило языка, а не как привычку.
Удобно, когда вы хотите иметь пакеты, но не хотите обещать чужим людям, что они не изменятся.
Образ. Дверь с надписью «служебное помещение». Она в том же здании и не заперта для сотрудников, но посетитель за неё не заходит.
Дерево проекта: cmd, internal и pkg
У нашего блога плоское дерево: main.go рядом с папкой blog, и никаких cmd с internal. Это не упрощение ради урока — так советует сам Go для программы такого размера: пока модуль — одна программа, лишние папки только удлиняют пути импорта и ничего не добавляют.
Три имени, которые встретятся вам в чужих репозиториях:
cmd/— когда программ в модуле больше одной.cmd/app,cmd/migrate,cmd/export: у каждой свойmain, а общий код лежит рядом и импортируется всеми.internal/— то, что нельзя импортировать снаружи. Правило языка, которое мы только что измерили.pkg/— папка, куда складывают код, рассчитанный на чужие проекты. Это не правило Go, а привычка части сообщества: компилятор проpkgне знает ничего.
Сайт, на котором вы читаете этот урок, разложен так: четыре программы в cmd/ — app, migrate, export и adminctl, — пять пакетов в internal/ и семнадцать в pkg/. Это дерево выросло из плоского, и выросло тогда, когда появилась вторая программа, а не заранее.
Страдает ли блог курса от того, что у него этого нет? Нет: у него одна программа и один общий пакет, и любое из этих имён добавило бы пустую папку. Но когда у вашего блога появится вторая программа — скажем, отдельный migrate, — вы уже знаете, куда её класть.
И последнее. Не начинайте с папок. Пока файл один и в нём двести строк — оставьте как есть. Делить стоит тогда, когда вы уже видите, где проходит граница, а не заранее.
Карта урока
Скажите своими словами
Не подглядывая, ответьте вслух или на бумаге. Ответы — в конце урока.
- Почему
main.goне может обратиться кs.items, хотя оба файла лежат в одной программе? - Чем
go.modотличается отgo.sumи зачем нужны оба? - Почему
blog.Article, а неblog.BlogArticle? - Чем
internal/отличается отpkg/?
Задание
Обязательное. Возьмите свою программу из урока про ошибки и разложите её на два пакета: blog с типом, хранилищем и ошибками — и main, который только вызывает. Поле с данными оставьте со строчной буквы и добавьте метод Len(). Убедитесь, что go run . работает, а обращение к внутреннему полю из main не собирается.
По желанию.
- Подключите
github.com/google/uuidи выдайте каждой статье идентификатор. - Удалите строку
requireизgo.modи выполнитеgo mod tidy. Посмотрите, что произойдёт. - Переименуйте папку
blogвinternal/blogи поправьте импорт. Программа должна работать, как раньше.
Куда это встанет в блоге
Так устроен и этот сайт: сервер в одном пакете, статьи в другом, хранилище в третьем, и каждое общается с остальными через несколько имён с заглавной буквы. Состояние блога после этого урока — step-6: тот же самый блог, разложенный по папкам.
Ответы
Показать ответы
- Потому что
itemsнаписано со строчной буквы, а значит, видно только внутри пакетаblog. Программа одна, но пакеты разные, и граница проходит по первой букве имени. Наружу выходит методLen()— умение, а не устройство. go.mod— список того, что вы взяли и каких версий; его читает человек.go.sum— контрольные суммы скачанного; его читает Go, чтобы заметить подмену. Оба файла хранят в репозитории.- Потому что имя пакета читается в месте вызова: снаружи выходит
blog.Article. Повторять имя пакета внутри — заикание, которое видно на каждой строке чужого кода. internal— правило языка: чужой модуль, который туда полез, просто не соберётся, и это говорит компилятор.pkg— обычное имя папки и привычка части сообщества; Go про него не знает ничего и ничего им не запрещает.
Источники
Если вы нашли ошибку или опечатку в тексте статьи, то сообщите нам об этом
Комментарии (0)
Войдите, чтобы оставить комментарий →
Пока нет комментариев. Будьте первым.