Shanraq.org Shanraq.org
Пакеты и модули в Go: go mod и заглавная буква
IT

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, — вы уже знаете, куда её класть.

И последнее. Не начинайте с папок. Пока файл один и в нём двести строк — оставьте как есть. Делить стоит тогда, когда вы уже видите, где проходит граница, а не заранее.

Карта урока

Карта урока: пакет — это папка, заглавная буква — дверь наружу

Скажите своими словами

Не подглядывая, ответьте вслух или на бумаге. Ответы — в конце урока.

  1. Почему main.go не может обратиться к s.items, хотя оба файла лежат в одной программе?
  2. Чем go.mod отличается от go.sum и зачем нужны оба?
  3. Почему blog.Article, а не blog.BlogArticle?
  4. Чем internal/ отличается от pkg/?

Задание

Обязательное. Возьмите свою программу из урока про ошибки и разложите её на два пакета: blog с типом, хранилищем и ошибками — и main, который только вызывает. Поле с данными оставьте со строчной буквы и добавьте метод Len(). Убедитесь, что go run . работает, а обращение к внутреннему полю из main не собирается.

По желанию.

  • Подключите github.com/google/uuid и выдайте каждой статье идентификатор.
  • Удалите строку require из go.mod и выполните go mod tidy. Посмотрите, что произойдёт.
  • Переименуйте папку blog в internal/blog и поправьте импорт. Программа должна работать, как раньше.

Куда это встанет в блоге

Так устроен и этот сайт: сервер в одном пакете, статьи в другом, хранилище в третьем, и каждое общается с остальными через несколько имён с заглавной буквы. Состояние блога после этого урока — step-6: тот же самый блог, разложенный по папкам.

Ответы

Показать ответы
  1. Потому что items написано со строчной буквы, а значит, видно только внутри пакета blog. Программа одна, но пакеты разные, и граница проходит по первой букве имени. Наружу выходит метод Len() — умение, а не устройство.
  2. go.mod — список того, что вы взяли и каких версий; его читает человек. go.sum — контрольные суммы скачанного; его читает Go, чтобы заметить подмену. Оба файла хранят в репозитории.
  3. Потому что имя пакета читается в месте вызова: снаружи выходит blog.Article. Повторять имя пакета внутри — заикание, которое видно на каждой строке чужого кода.
  4. internal — правило языка: чужой модуль, который туда полез, просто не соберётся, и это говорит компилятор. pkg — обычное имя папки и привычка части сообщества; Go про него не знает ничего и ничего им не запрещает.

Источники

Если вы нашли ошибку или опечатку в тексте статьи, то сообщите нам об этом

Проверить задание

Сначала решите и запустите в VS Code — редактор покажет ошибку на месте. Готовое решение вставьте сюда. Проверяет модель: она укажет на ошибку, но не даст готовый ответ.

Чтобы проверить, нужно войти. Войти

Комментарии (0)

Пока нет комментариев. Будьте первым.