Введение
При создании пакета в Go конечная цель обычно заключается в том, чтобы сделать пакет доступным для использования другими разработчиками, либо в пакетах более высокого уровня, либо в целых программах. Импортируя пакет, ваш фрагмент кода может служить строительным блоком для других, более сложных инструментов. Однако для импорта доступны только определенные пакеты. Это определяется видимостью пакета.
Видимость в данном контексте означает пространство файлов, из которого пакет или другой конструкт могут быть упомянуты. Например, если мы определим переменную в функции, видимость (область действия) этой переменной будет только внутри функции, в которой она была определена. Аналогично, если вы определите переменную в пакете, вы можете сделать ее видимой только для этого пакета или разрешить ей быть видимой за пределами пакета.
Тщательное управление видимостью пакетов важно при написании удобного кода, особенно когда речь идет о будущих изменениях, которые вы можете захотеть внести в свой пакет. Если вам нужно исправить ошибку, улучшить производительность или изменить функциональность, вы захотите сделать это таким образом, чтобы не нарушить работу кода, используемого другими разработчиками. Один из способов минимизировать разрушительные изменения — разрешить доступ только к частям вашего пакета, которые необходимы для его правильного использования. Ограничивая доступ, вы можете вносить изменения внутри своего пакета с меньшим риском повлиять на то, как другие разработчики используют ваш пакет.
В этой статье вы узнаете, как контролировать видимость пакетов, а также как защищать части вашего кода, которые должны использоваться только внутри вашего пакета. Для этого мы создадим базовый логгер для логирования и отладки сообщений, используя пакеты с различными уровнями видимости элементов.
Предварительные требования
Для выполнения примеров в этой статье вам потребуется:
- Настроенная рабочая среда Go, следуя инструкциям Как установить Go и настроить локальную программную среду. В этом руководстве будет использоваться следующая структура файлов:
.
├── bin
│
└── src
└── github.com
└── gopherguides
Экспортируемые и неэкспортируемые элементы
В отличие от других языков программирования, таких как Java и Python, которые используют модификаторы доступа типа public
, private
или protected
для определения области видимости, Go определяет, является ли элемент экспортируемым
или неэкспортируемым
через способ его объявления. Экспортирование элемента в данном случае делает его видимым
за пределами текущего пакета. Если элемент не экспортируется, он виден и может быть использован только внутри пакета, в котором он был определен.
Эта внешняя видимость контролируется заглавной буквой в начале объявления элемента. Все объявления, такие как Типы
, Переменные
, Константы
, Функции
и т.д., начинающиеся с заглавной буквы, видны за пределами текущего пакета.
Рассмотрим следующий код, обращая особое внимание на заглавные буквы:
package greet
import "fmt"
var Greeting string
func Hello(name string) string {
return fmt.Sprintf(Greeting, name)
}
Этот код объявляет, что он находится в пакете greet
. Затем он объявляет два символа: переменную с именем Greeting
и функцию с именем Hello
. Поскольку оба они начинаются с заглавной буквы, они оба экспортируемы
и доступны любой внешней программе. Как упоминалось ранее, создание пакета, ограничивающего доступ, позволит лучше спроектировать API и упростит обновление вашего пакета изнутри, не нарушая работу кода, который зависит от вашего пакета.
Определение видимости пакетов
Чтобы более подробно рассмотреть, как работает видимость пакетов в программе, давайте создадим пакет logging
, учитывая, что мы хотим сделать видимым за пределами нашего пакета, а что нет. Этот пакет logging
будет отвечать за запись любых сообщений нашей программы в консоль. Он также будет учитывать, на каком уровне мы ведем логирование. Уровень описывает тип лога и будет одним из трех статусов: info
, warning
или error
.
Сначала, в вашем каталоге src
, создадим каталог с именем logging
, чтобы разместить в нем наши файлы логирования:
Затем перейдите в этот каталог:
Далее, используя редактор, например nano, создайте файл с именем logging.go
:
Поместите следующий код в файл logging.go
, который мы только что создали:
Первая строка этого кода объявляет пакет под названием logging
. В этом пакете есть две экспортируемые
функции: Debug
и Log
. Эти функции могут быть вызваны любым другим пакетом, который импортирует пакет logging
. Также есть приватная переменная под названием debug
. Эта переменная доступна только изнутри пакета logging
. Важно отметить, что хотя функция Debug
и переменная debug
имеют одинаковое написание, функция начинается с заглавной буквы, а переменная — нет. Это делает их различными объявлениями с разными областями видимости.
Сохраните и закройте файл.
Чтобы использовать этот пакет в других частях нашего кода, мы можем импортировать
его в новый пакет. Мы создадим этот новый пакет, но сначала нам нужно создать новую директорию для хранения исходных файлов.
Давайте выйдем из директории logging
, создадим новую директорию под названием cmd
и перейдём в неё:
Создайте файл под названием main.go
в только что созданной директории cmd
:
Теперь мы можем добавить следующий код:
Мы теперь полностью написали нашу программу. Однако прежде чем мы сможем запустить эту программу, нам нужно также создать несколько конфигурационных файлов, чтобы наш код работал правильно. Go использует Go Modules для настройки зависимостей пакетов для импорта ресурсов. Go modules — это конфигурационные файлы, размещенные в директории вашего пакета, которые сообщают компилятору, откуда импортировать пакеты. Хотя изучение модулей выходит за рамки этой статьи, мы можем написать всего несколько строк конфигурации, чтобы этот пример работал локально.
Откройте следующий файл go.mod
в директории cmd
:
Затем поместите в файл следующее содержимое:
module github.com/gopherguides/cmd
replace github.com/gopherguides/logging => ../logging
Первая строка этого файла сообщает компилятору, что пакет cmd
имеет путь к файлу github.com/gopherguides/cmd
. Вторая строка сообщает компилятору, что пакет github.com/gopherguides/logging
можно найти локально на диске в директории ../logging
.
Нам также понадобится файл go.mod
для нашего пакета logging
. Вернемся в директорию logging
и создадим файл go.mod
:
Добавьте в файл следующее содержимое:
module github.com/gopherguides/logging
Это сообщает компилятору, что созданный нами пакет logging
на самом деле является пакетом github.com/gopherguides/logging
. Это позволяет импортировать пакет в нашем пакете main
с помощью следующей строки, которую мы написали ранее:
package main
import "github.com/gopherguides/logging"
func main() {
logging.Debug(true)
logging.Log("This is a debug statement...")
}
Теперь у вас должна быть следующая структура директорий и расположение файлов:
├── cmd
│ ├── go.mod
│ └── main.go
└── logging
├── go.mod
└── logging.go
Теперь, когда все настройки завершены, мы можем запустить программу main
из пакета cmd
с помощью следующих команд:
Вы получите вывод, подобный следующему:
Output2019-08-28T11:36:09-05:00 This is a debug statement...
Программа выведет текущее время в формате RFC 3339, за которым последует любое сообщение, которое мы отправили в логгер. RFC 3339 — это формат времени, разработанный для представления времени в интернете и широко используемый в файлах журналов.
Поскольку функции Debug
и Log
экспортируются из пакета логирования, мы можем использовать их в нашем пакете main
. Однако переменная debug
в пакете logging
не экспортируется. Попытка обратиться к неэкспортируемому объявлению приведет к ошибке компиляции.
Добавьте следующую выделенную строку в main.go
:
package main
import "github.com/gopherguides/logging"
func main() {
logging.Debug(true)
logging.Log("This is a debug statement...")
fmt.Println(logging.debug)
}
Сохраните и запустите файл. Вы получите ошибку, подобную следующей:
Output. . .
./main.go:10:14: cannot refer to unexported name logging.debug
Теперь, когда мы увидели, как ведут себя экспортируемые и неэкспортируемые элементы в пакетах, мы рассмотрим, как могут быть экспортированы поля
и методы
из структур
.
Видимость в структурах
Хотя схема видимости в регистраторе, который мы создали в последнем разделе, может работать для простых программ, она разделяет слишком много состояний, чтобы быть полезной из нескольких пакетов. Это происходит потому, что экспортируемые переменные доступны для нескольких пакетов, которые могут изменять переменные в противоречивые состояния. Позволение изменять состояние вашего пакета таким образом затрудняет предсказание поведения вашей программы. Например, с текущей структурой один пакет может установить переменную Debug
в true
, а другой может установить её в false
в одном и том же экземпляре. Это создаст проблему, так как оба пакета, импортирующие пакет logging
, будут затронуты.
Мы можем изолировать регистратор, создав структуру, а затем добавив к ней методы. Это позволит нам создать экземпляр регистратора для независимого использования в каждом пакете, который его потребляет.
Измените пакет logging
следующим образом, чтобы рефакторить код и изолировать регистратор:
В этом коде мы создали структуру Logger
. Эта структура будет хранить наше неэкспортируемое состояние, включая формат времени для вывода и переменную debug
, установленную в true
или false
. Функция New
устанавливает начальное состояние для создания логгера, такое как формат времени и состояние отладки. Затем она сохраняет переданные значения во внутренних неэкспортируемых переменных timeFormat
и debug
. Мы также создали метод Log
для типа Logger
, который принимает выражение, которое мы хотим вывести. Внутри метода Log
есть ссылка на локальную переменную метода l
, чтобы получить доступ к его внутренним полям, таким как l.timeFormat
и l.debug
.
Этот подход позволит нам создавать Logger
в различных пакетах и использовать его независимо от того, как используют его другие пакеты.
Чтобы использовать его в другом пакете, изменим cmd/main.go
следующим образом:
Запуск этой программы даст вам следующий вывод:
Output2019-08-28T11:56:49-05:00 This is a debug statement...
В этом коде мы создали экземпляр логгера, вызвав экспортируемую функцию New
. Мы сохранили ссылку на этот экземпляр в переменной logger
. Теперь мы можем вызывать logging.Log
для вывода выражений.
Если мы попытаемся обратиться к неэкспортируемому полю структуры Logger
, такому как поле timeFormat
, мы получим ошибку компиляции. Попробуйте добавить следующую выделенную строку и запустить cmd/main.go
:
package main
import (
"time"
"github.com/gopherguides/logging"
)
func main() {
logger := logging.New(time.RFC3339, true)
logger.Log("This is a debug statement...")
fmt.Println(logger.timeFormat)
}
Это вызовет следующую ошибку:
Output. . .
cmd/main.go:14:20: logger.timeFormat undefined (cannot refer to unexported field or method timeFormat)
Компилятор распознает, что logger.timeFormat
не экспортируется, и поэтому его нельзя получить из пакета logging
.
Видимость в методах
Так же, как и поля структур, методы могут быть экспортируемыми или неэкспортируемыми.
Для иллюстрации этого давайте добавим логирование по уровням в наш логгер. Логирование по уровням — это способ категоризации ваших логов, чтобы вы могли искать логи по определенным типам событий. Уровни, которые мы добавим в наш логгер, следующие:
-
Уровень
info
, который представляет события информационного типа, информирующие пользователя о каком-либо действии, напримерProgram started
илиEmail sent
. Они помогают нам отлаживать и отслеживать части нашей программы, чтобы убедиться, что происходит ожидаемое поведение. -
Уровень
warning
. Такие события идентифицируют моменты, когда что-то происходит неожиданно, но это не ошибка, напримерEmail failed to send, retrying
. Они помогают нам видеть части нашей программы, которые не работают так гладко, как мы ожидали. -
Уровень
error
, что означает, что программа столкнулась с проблемой, например,File not found
. Это часто приводит к сбою работы программы.
Возможно, вы захотите включать и выключать определенные уровни логирования, особенно если ваша программа не работает так, как ожидалось, и вы хотели бы отладить её. Мы добавим эту функциональность, изменив программу так, чтобы при установке debug
в true
она выводила все уровни сообщений. В противном случае, если оно false
, будут выводиться только сообщения об ошибках.
Добавьте уровневое логирование, внеся следующие изменения в logging/logging.go
:
В этом примере мы ввели новый аргумент в метод Log
. Теперь мы можем передавать уровень level
сообщения журнала. Метод Log
определяет, какой уровень сообщения. Если это сообщение info
или warning
, и поле debug
равно true
, то оно записывает сообщение. В противном случае оно игнорирует сообщение. Если это любой другой уровень, например, error
, оно выведет сообщение в любом случае.
Большая часть логики для определения, будет ли сообщение выведено, находится в методе Log
. Мы также ввели неэкспортируемый метод под названием write
. Метод write
фактически выводит сообщение журнала.
Теперь мы можем использовать эту структурированную регистрацию в нашем другом пакете, изменив cmd/main.go
следующим образом:
Запуск этого даст вам:
Output[info] 2019-09-23T20:53:38Z starting up service
[warning] 2019-09-23T20:53:38Z no tasks found
[error] 2019-09-23T20:53:38Z exiting: no work performed
В этом примере cmd/main.go
успешно использовал экспортируемый метод Log
.
Теперь мы можем передавать level
каждого сообщения, переключая debug
на false
:
Теперь мы увидим, что выводятся только сообщения уровня error
:
Output[error] 2019-08-28T13:58:52-05:00 exiting: no work performed
Если мы попытаемся вызвать метод write
извне пакета logging
, мы получим ошибку времени компиляции:
package main
import (
"time"
"github.com/gopherguides/logging"
)
func main() {
logger := logging.New(time.RFC3339, true)
logger.Log("info", "starting up service")
logger.Log("warning", "no tasks found")
logger.Log("error", "exiting: no work performed")
logger.write("error", "log this message...")
}
Outputcmd/main.go:16:8: logger.write undefined (cannot refer to unexported field or method logging.(*Logger).write)
Когда компилятор видит, что вы пытаетесь обратиться к чему-то из другого пакета, начинающемуся со строчной буквы, он понимает, что это не экспортируется, и, следовательно, выдает ошибку компиляции.
Логгер в этом уроке демонстрирует, как мы можем написать код, который открывает только те части, которые мы хотим, чтобы другие пакеты использовали. Поскольку мы контролируем, какие части пакета видны за его пределами, теперь мы можем вносить будущие изменения, не затрагивая код, который зависит от нашего пакета. Например, если бы мы хотели отключить сообщения уровня info
, когда debug
установлено в false, вы могли бы сделать это изменение, не затрагивая другие части вашего API. Мы также могли бы безопасно изменять сообщения журнала, чтобы включать больше информации, например, директорию, в которой программа выполнялась.
Заключение
Эта статья показала, как делиться кодом между пакетами, одновременно защищая детали реализации вашего пакета. Это позволяет вам экспортировать простой API, который редко будет меняться для обеспечения обратной совместимости, но позволит вносить изменения в вашем пакете приватно, по мере необходимости, чтобы улучшить его работу в будущем. Это считается лучшей практикой при создании пакетов и их соответствующих API.
Чтобы узнать больше о пакетах в Go, ознакомьтесь с нашими статьями Импортирование пакетов в Go и Как писать пакеты в Go, или изучите нашу полную серию Как программировать на Go.
Source:
https://www.digitalocean.com/community/tutorials/understanding-package-visibility-in-go