Создание и импорт пакетов Go

GolangBeginner
Практиковаться сейчас

Введение

В предыдущем разделе вы завершили простую программу на Go, которая содержала следующие строки кода:

package main
import "fmt"

Как понять эти две строки кода? Как эффективно использовать инструкции package и import?

В этом лабораторном занятии (LabEx) вы научитесь создавать и импортировать пакеты в Go. Это позволит вам организовать свой код в переиспользуемые модули, сделав ваши проекты на Go более поддерживаемыми и масштабируемыми.

Основные темы:

  • Определение и объявление пакета
  • Понимание экспортируемых (публичных) и неэкспортируемых (частных) идентификаторов
  • Различные формы импорта пакетов: одиночный, групповой, точечный, с псевдонимом и анонимный импорт

Объявление и определение пакетов

На этом шаге вы объявите переиспользуемый пакет и научитесь отличать экспортируемые идентификаторы от идентификаторов, доступных только внутри пакета.

Пакет в Go похож на модули в Python или библиотеки в C. Это набор файлов исходного кода, используемых для организации и повторного использования кода. Каждый файл на Go должен объявлять пакет в начале файла.

Примечание: Исполняемый файл Go собирается из пакета main, содержащего функцию main. Переиспользуемым пакетам такая точка входа не нужна. Модуль может содержать несколько каталогов команд, каждый со своим пакетом main.

Основные моменты:

Экспортируемые (публичные) идентификаторы: Идентификаторы (переменные, функции, типы и т.д.), начинающиеся с заглавной буквы, доступны из других пакетов. Их можно рассматривать как публичный интерфейс вашего пакета.

Неэкспортируемые (частные) идентификаторы: Идентификаторы, начинающиеся с строчной буквы, доступны только внутри того же пакета. Они считаются внутренними деталями реализации пакета.

Сцепление пакета: Все файлы в одной папке должны принадлежать одному и тому же пакету. Это обеспечивает сохранение связанного кода вместе.

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

Давайте создадим собственный пользовательский пакет:

Создайте папку с именем propagandist и файл propagandist.go внутри нее:

mkdir ~/project/propagandist
touch ~/project/propagandist/propagandist.go

Напишите следующий код в файле propagandist.go:

package propagandist

var Shout = "I Love LabEx" // Public variable
var secret = "I love the dress" // Private variable

func Hit() string {
    return "Don't hit me, please!"
}
  • Shout является публичной переменной, так как начинается с заглавной буквы. Это означает, что вы можете получить доступ к ней из других пакетов, которые импортируют propagandist.
  • secret является частной переменной, так как начинается со строчной буквы. Она может быть использована только внутри пакета propagandist.
  • Hit - это публичная функция, доступная из других пакетов.

Инициализируйте модуль Go для пакета:

cd ~/project/propagandist
go mod init propagandist

Эта команда инициализирует новый модуль Go в директории propagandist, который помогает управлять зависимостями пакета.

Импорт одного пакета

На этом шаге вы импортируете и используете локальный пакет с помощью отдельных объявлений import.

Для использования пакета propagandist создадим новую программу на Go. Этот шаг продемонстрирует, как импортировать и использовать один пакет в вашем коде на Go.

Создайте новый файл на Go с именем pacExercise.go в папке проекта:

touch ~/project/pacExercise.go

Инициализируйте модуль Go для программы:

cd ~/project
go mod init pacExercise

Обновите файл go.mod, чтобы включить зависимость от локального пакета. Выполните следующую команду в терминале:

echo "replace propagandist => ./propagandist" >> go.mod

Важно: Эта команда добавляет директиву replace в ваш файл go.mod. Это очень важно, так как она сообщает Go, что пакет propagandist должен быть взят из локальной директории ./propagandist, а не пытаться скачать его из удаленного репозитория. Вы должны выполнить эту команду в терминале, которая добавит строку replace propagandist => ./propagandist в ваш файл go.mod. Не пишите эту строку напрямую в файл вручную.

Напишите следующий код в файле pacExercise.go, чтобы импортировать и использовать пакет propagandist:

package main

import "fmt"
import "propagandist"

func main() {
    fmt.Println(propagandist.Shout) // Access the public variable
}
  • Этот код импортирует пакет fmt для вывода информации и пакет propagandist.
  • Затем он обращается к публичной переменной Shout из пакета propagandist с помощью propagandist.Shout.

Запустите программу:

go mod tidy
go run pacExercise.go

Команда go mod tidy гарантирует, что ваш файл go.mod обновлен с учетом любых новых зависимостей. Команда go run pacExercise.go компилирует и выполняет программу.

Ожидаемый вывод:

I Love LabEx

Групповой импорт

На этом шаге вы сгруппируете импорты пакетов, не меняя поведение программы.

При импорте нескольких пакетов можно использовать групповой импорт для улучшения читаемости и организации кода. Это вопрос стиля и не влияет на функциональность вашего кода.

Измените файл pacExercise.go, чтобы использовать групповой импорт:

```go
package main

import (
    "fmt"
    "propagandist"
)

func main() {
    fmt.Println(propagandist.Shout)
}
```

В приведенном выше фрагменте кода пакеты fmt и propagandist импортируются внутри одного блока import, заключенного в скобки. Это упрощает чтение и управление импортом нескольких пакетов. В отличие от отдельных объявлений import на предыдущем шаге, здесь оба пути пакетов сгруппированы в одном объявлении. Поведение программы остаётся прежним.

Запустите программу, чтобы убедиться, что она по-прежнему работает:

```bash
go run pacExercise.go
```

Программа должна выполниться без ошибок и выдать тот же результат, что и раньше.

Точечный импорт

На этом шаге вы рассмотрите точечный импорт и связанные с ним компромиссы в использовании пространства имён.

При использовании точкового импорта можно опустить префикс имени пакета при вызове его функций или переменных. Это часто не рекомендуется в пользу явных имен пакетов, так как это может привести к конфликтам пространств имен и снизить читаемость. Однако полезно знать, что это такое.

Измените файл pacExercise.go, чтобы использовать точковый импорт для пакета fmt:

```go
package main

import . "fmt"
import "propagandist"

func main() {
    Println(propagandist.Shout) // No `fmt.` prefix needed
}
```
  • Здесь import . "fmt" означает, что вы можете использовать функции и переменные из пакета fmt напрямую, без префикса fmt..
  • Например, вы используете Println вместо fmt.Println.

Запустите программу:

```bash
go run pacExercise.go
```

Ожидаемый вывод:

```bash
I Love LabEx
```

Импорт с псевдонимом

На этом шаге вы зададите импортируемому пакету локальный псевдоним.

Вы можете назначить псевдоним импортируемому пакету для ясности или чтобы избежать конфликтов, когда два пакета имеют похожие имена. Это помогает сделать ваш код более читаемым и управлять коллизиями пространств имен.

Измените файл pacExercise.go, чтобы назначить псевдоним io пакету fmt:

```go
package main

import io "fmt"
import "propagandist"

func main() {
    io.Println(propagandist.Shout) // Use the alias `io` instead of `fmt`
}
```
  • import io "fmt" создает псевдоним io для пакета fmt.
  • Теперь вы используете io.Println вместо fmt.Println.

Запустите программу:

```bash
go run pacExercise.go
```

Анонимный импорт

На этом шаге вы импортируете пакет только ради побочных эффектов его инициализации.

Анонимный импорт используется для импорта пакета ради его побочных эффектов, таких как выполнение его функции init(), без необходимости напрямую ссылаться на его экспортируемые идентификаторы. Это полезно для пакетов, которые регистрируют драйверы или выполняют другие задачи инициализации.

Измените файл pacExercise.go, чтобы включить анонимный импорт пакета time:

```go
package main

import (
    "fmt"
    "propagandist"
    _ "time" // Anonymous import
)

func main() {
    fmt.Println(propagandist.Shout)
}
```
  • import _ "time" - это анонимный импорт. Подчеркивание _ используется как пустой идентификатор, сообщающий компилятору, что вы импортируете пакет ради его побочных эффектов и не будете напрямую ссылаться на его содержимое в своем коде.
  • Функция init() пакета time будет выполнена при запуске этой программы. Пакет time не имеет каких - то особых видимых побочных эффектов здесь, однако многие пакеты используют этот подход для регистрации драйверов баз данных или настройки конфигурации.

Запустите программу:

```bash
go run pacExercise.go
```

Ожидаемый вывод:

```bash
I Love LabEx
```

Функция init() и порядок инициализации

На этом шаге вы проследите инициализацию переменных пакетов и выполнение функций init() до запуска main().

Функция init() — особая функция Go, которая играет важную роль в инициализации пакетов. Go автоматически выполняет её при импорте пакета, прежде чем начнёт выполняться код функции main. В этом разделе вы подробно рассмотрите функции init() и их место в процессе инициализации Go.

Основные сведения о функциях init():

Определение и назначение:

  • Функция init() не имеет параметров и возвращаемых значений: func init() {}.
  • Она используется для задач инициализации пакета: задания начального состояния, регистрации драйверов или проверки предварительных условий.

Порядок выполнения:

  • Go гарантирует, что пакет инициализируется только один раз, даже если он импортирован несколько раз.

  • Инициализация выполняется в определённом порядке:

  • Сначала инициализируются переменные уровня пакета.

  • Затем выполняются функции init().

  • Наконец, выполняется функция main() (только в пакете main).

Несколько функций init():

  • Один файл Go может содержать несколько функций init().
  • У разных файлов одного пакета могут быть собственные функции init().
  • Все эти функции init() выполняются в порядке объявления в исходном коде с учётом порядка, в котором файлы переданы компилятору. Инструменты Go передают файлы пакета в лексикографическом порядке имён файлов; не полагайтесь на порядок файлов для выражения зависимостей.

Цепочка зависимостей:

  • Когда пакеты импортируют другие пакеты, Go обеспечивает выполнение функций init() в зависимостях в первую очередь.
  • Так возникает поток инициализации снизу вверх: первыми инициализируются самые глубокие зависимости.

Создадим практический пример, показывающий работу функций init():

Сначала добавим функцию init() в пакет propagandist. Обновите propagandist.go:

package propagandist

import "fmt"

var Shout = "I Love LabEx" // Public variable
var secret = "I love the dress" // Private variable
var initialized bool

func init() {
    fmt.Println("Initializing propagandist package...")
    initialized = true
}

func Hit() string {
    return "Don't hit me, please!"
}

func IsInitialized() bool {
    return initialized
}

Теперь создадим ещё один файл в пакете propagandist, чтобы продемонстрировать несколько функций init():

touch ~/project/propagandist/second.go

Добавьте в файл следующее содержимое:

package propagandist

import "fmt"

func init() {
    fmt.Println("Second init function in propagandist package...")
}

Создайте новый вспомогательный пакет для демонстрации порядка инициализации:

mkdir -p ~/project/helper
touch ~/project/helper/helper.go

Добавьте в файл следующее содержимое:

package helper

import "fmt"

var Message = "Helper package is ready"

func init() {
    fmt.Println("Initializing helper package...")
}

func GetMessage() string {
    return Message
}

Добавьте файл модуля для пакета helper:

cd ~/project/helper
go mod init helper

Обновите pacExercise.go, чтобы использовать оба пакета и продемонстрировать порядок инициализации:

package main

import (
    "fmt"
    "helper"
    "propagandist"
)

func init() {
    fmt.Println("Initializing main package...")
}

func main() {
    fmt.Println("Main function is running")
    fmt.Println(propagandist.Shout)
    fmt.Println(helper.Message)
    fmt.Printf("Propagandist initialized: %v\n", propagandist.IsInitialized())
}

Обновите файл go.mod основного проекта, добавив локальный пакет helper:

cd ~/project
echo "replace helper => ./helper" >> go.mod
go mod tidy

Запустите программу и проследите порядок инициализации:

go run pacExercise.go

Ожидаемый вывод при использовании предоставленных имён файлов и инструментов Go:

Initializing helper package...
Initializing propagandist package...
Second init function in propagandist package...
Initializing main package...
Main function is running
I Love LabEx
Helper package is ready
Propagandist initialized: true

Этот вывод показывает основные особенности процесса инициализации Go:

Пакеты-зависимости инициализируются до пакетов, которые их импортируют.

Внутри одного пакета все функции init() выполняются в порядке объявления с учётом того, как исходные файлы переданы компилятору.

Функция main() выполняется только после завершения инициализации всех пакетов.

Переменные уровня пакета инициализируются до выполнения любых функций init().

Этот порядок инициализации важно учитывать при проектировании пакетов Go, особенно при управлении зависимостями или выполнении подготовительных операций, которые должны происходить в определённой последовательности.

Резюме

В этом практическом занятии вы узнали:

  1. Как создавать и определять пользовательские пакеты в Go, инкапсулируя повторно используемый код.

  2. Разницу между публичными (экспортируемыми) и приватными (неэкспортируемыми) идентификаторами и то, как они влияют на доступность.

  3. Различные способы импорта пакетов и их области применения:

    • Импорт одного пакета за раз: Импортирование по одному пакету за раз.
    • Групповой импорт: Импортирование нескольких пакетов в одном блоке для лучшей организации.
    • Точковый импорт: Импортирование пакета и использование его идентификаторов напрямую без префикса имени пакета. (Используйте с осторожностью)
    • Импорт с псевдонимом: Переименование импортированных пакетов для лучшей читаемости или избежания конфликтов имен.
    • Анонимный импорт: Импортирование пакета только ради его побочных эффектов, таких как инициализация.
  4. Роль функции init() в пакетах и то, как анонимный импорт может вызвать ее выполнение.

  5. Подробности процесса инициализации Go:

    • Инициализацию переменных уровня пакета до функций init().
    • Гарантированный порядок выполнения функций init() в пакетах-зависимостях.
    • Работу нескольких функций init() внутри одного пакета.
    • Полный процесс инициализации от пакетов-зависимостей до функции main.

Завершив это практическое занятие, вы теперь можете эффективно структурировать и управлять проектами на Go с использованием пакетов. Вы можете создавать повторно используемые модули, контролировать доступ к идентификаторам, лучше организовать свой код и понимать процесс инициализации, что приведет к созданию более поддерживаемых и масштабируемых приложений на Go.