Перейти к содержимому
Теория · От файла к проекту · урок 1 из 10

Пакет и модуль

Чему вы научитесь

Чему вы научитесь:

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

До сих пор каждая ваша программа помещалась в один файл. Настоящий проект на Go — это десятки и сотни файлов, и их нужно как-то разложить по местам. В этой теме мы пройдём путь от одного файла к проекту: как Go делит код на части, как части находят друг друга и как проверить, что всё работает.

Представьте завод. У завода есть адрес на воротах. Внутри — цеха, у каждого цеха своё назначение, а в цехах стоят станки. В Go всё устроено похоже:

  • модуль — это завод с адресом на воротах;
  • пакет — это цех, то есть каталог с файлами;
  • файл — это станок в цехе.

Пакеты вы уже встречали в первой теме. Каждая программа начиналась со строки package main, а fmt и strings — это пакеты стандартной библиотеки:

GO
package main

import (
    "fmt"
    "strings"
)

func main() {
    fmt.Println(strings.ToUpper("go"))   // GO
    fmt.Println(strings.Repeat("ab", 3)) // ababab
}

Давайте разберём программу по строкам:

  1. package main говорит, к какому пакету относится файл.
  2. import подключает два пакета: fmt и strings.
  3. strings.ToUpper("go") вызывает функцию ToUpper из пакета strings и печатает GO.
  4. strings.Repeat("ab", 3) повторяет строку три раза и печатает ababab.

Перед именем функции стоит имя пакета и точка. Так Go понимает, из какого цеха взять станок.

Пакет — это каталог

Пакет — это все файлы .go из одного каталога, у которых в первой строке одно и то же имя после package. Вот правила, которые из этого следуют:

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

Экспортированные имена

Цех не отдаёт наружу всё подряд. Что-то он выставляет в окно выдачи, а что-то остаётся внутри. В Go это решает первая буква имени:

  • имя с заглавной буквы экспортировано: его видят другие пакеты;
  • имя со строчной буквы остаётся внутри своего пакета.

Вот пакет store, который считает сумму покупки со скидкой:

GO
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 на диске, а справа — пути импорта его пакетов:

TEXT
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:

GO
package main

import (
    "fmt"

    "example.com/shop/store"
)

func main() {
    fmt.Println(store.Total([]int{100, 250})) // 315
}

Разберём вызов по шагам:

  1. Импорт "example.com/shop/store" подключает пакет из каталога store/.
  2. В коде к нему обращаются по имени из строки package, то есть store.
  3. store.Total складывает 100 и 250, вычитает скидку 35 и возвращает 315.

Стандартные пакеты вроде fmt импортируют по короткому пути без адреса: они приходят вместе с Go. Свои пакеты всегда начинаются с пути модуля.

Здесь картинка с заводом перестаёт работать. На заводе два цеха могут передавать детали туда и обратно. В Go так нельзя: если store импортирует пакет report, а report импортирует store, сборка остановится с ошибкой import cycle not allowed. Импорты всегда идут в одну сторону.

Частая ошибка

Из main вызвали функцию, которая начинается со строчной буквы:

GO
fmt.Println(store.discount(350)) // ✗
TEXT
name discount not exported by package store

Программа не компилируется. Функция discount существует, но она внутри пакета store, а main — другой пакет. Если функция нужна снаружи, её переименовывают с заглавной буквы: Discount.

Главное

  • Пакет — это каталог файлов с одним именем в строке package; main — программа, остальные пакеты импортируют.
  • Имя с заглавной буквы экспортировано, со строчной — видно только внутри пакета.
  • Модуль — дерево пакетов с go.mod в корне; первая строка go.mod задаёт путь модуля.
  • Путь импорта — это путь модуля плюс каталог пакета: "example.com/shop/store".

Подпишись, чтобы пройти курс

Курс бесплатный. Войди через Telegram и подпишись на канал — откроются все уроки.

Войти другим способом