При создании пакета в 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
, где будут размещены наши файлы регистрации:
- mkdir logging
Перейдите в этот каталог:
- cd logging
Используйте nano или другой редактор для создания файла logging.go
:
- nano logging.go
Поместите следующий код в созданный нами файл logging.go
:
package logging
import (
"fmt"
"time"
)
var debug bool
func Debug(b bool) {
debug = b
}
func Log(statement string) {
if !debug {
return
}
fmt.Printf("%s %s\n", time.Now().Format(time.RFC3339), statement)
}
В первой строчке этого кода декларируется пакет с именем logging
. В этом пакете содержится две экспортируемые
функции: Debug
и Log
. Эти функции сможет вызывать любой другой пакет, импортирующий пакет logging
. Также существует private переменная с именем debug
. Эта переменная доступна только из пакета logging
. Важно отметить, что функция Debug
и переменная debug
имеют одинаковое написание, но имя функции начинается с заглавной буквы, а имя переменной — нет. Это обеспечивает отдельное декларирование с разной сферой действия.
Сохраните и закройте файл.
Чтобы использовать этот пакет в других частях нашего кода, мы можем импортировать
его в новый пакет. Мы создадим этот новый пакет, но вначале нам потребуется новый каталог для хранения этих исходных файлов.
Выйдем из каталога logging
, создадим новый каталог cmd
и перейдем в этот новый каталог:
- cd ..
- mkdir cmd
- cd cmd
Создайте файл с именем main.go
в каталоге cmd
, который мы только что создали:
- nano main.go
Теперь мы можем добавить следующий код:
package main
import "github.com/gopherguides/logging"
func main() {
logging.Debug(true)
logging.Log("This is a debug statement...")
}
Теперь у нас написана вся программа. Однако перед запуском этой программы нам также потребуется создать несколько файлов конфигурации, чтобы обеспечить правильную работу нашего кода. Go использует Go Modules для настройки зависимостей пакетов при импорте ресурсов. Модули Go представляют собой файлы конфигурации в каталоге вашего пакета, которые указывают компилятору, откуда импортировать пакеты. Хотя изучение модулей не входит в состав настоящей статьи, мы можем написать несколько строк конфигурации, чтобы этот пример работал локально.
Откройте следующий файл go.mod
в каталоге cmd
:
- nano go.mod
Затем поместите в файл следующий код:
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
:
- cd ../logging
- nano 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
с помощью следующих команд:
- cd ../cmd
- go run main.go
Результат должен выглядеть примерно следующим образом:
Output2019-08-28T11:36:09-05:00 This is a debug statement...
Программа выведет текущее время в формате RFC 3339, а затем выражение, которое мы отправили в регистратор. RFC 3339 — это формат времени, разработанный для представления времени в интернете и обычно используемый в файлах журналов.
Поскольку функции Debug
и Log
экспортированы из пакета logging, мы можем использовать их в нашем пакете 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
на следующее, чтобы исправить код и изолировать регистратор:
package logging
import (
"fmt"
"time"
)
type Logger struct {
timeFormat string
debug bool
}
func New(timeFormat string, debug bool) *Logger {
return &Logger{
timeFormat: timeFormat,
debug: debug,
}
}
func (l *Logger) Log(s string) {
if !l.debug {
return
}
fmt.Printf("%s %s\n", time.Now().Format(l.timeFormat), s)
}
В этом коде мы создали структуру Logger
. В этой структуре будет размещено неэкспортированное состояние, включая формат времени для вывода и значение переменной debug
— true
или false
. Функция New
задает начальное состояние для создания регистратора, в частности формат времени и статус отладки. Она сохранит присвоенные внутренние значения в неэкспортированные переменные timeFormat
и debug
. Также мы создали метод Log
типа Logger
, который принимает выражение, которое мы хотим вывести. В методе Log
содержится ссылка на переменную локального метода l
для получения доступа к таким его внутренним полям, как l.timeFormat
и l.debug
.
Этот подход позволит нам создавать Logger
в разных пакетах и использовать его независимо от его использования другими пакетами.
Чтобы использовать его в другом пакете, изменим 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...")
}
При запуске этой программы будут выведены следующие результаты:
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
:
package logging
import (
"fmt"
"strings"
"time"
)
type Logger struct {
timeFormat string
debug bool
}
func New(timeFormat string, debug bool) *Logger {
return &Logger{
timeFormat: timeFormat,
debug: debug,
}
}
func (l *Logger) Log(level string, s string) {
level = strings.ToLower(level)
switch level {
case "info", "warning":
if l.debug {
l.write(level, s)
}
default:
l.write(level, s)
}
}
func (l *Logger) write(level string, s string) {
fmt.Printf("[%s] %s %s\n", level, time.Now().Format(l.timeFormat), s)
}
В этом примере мы ввели новый аргумент для метода Log
. Теперь мы можем передать уровень
сообщения журнала. Метод Log
определяет уровень сообщения. Если это сообщение типа info
или warning
, и если поле debug
имеет значение true
, выполняется запись сообщения. В противном случае, сообщение игнорируется. Если это сообщение любого другого уровня, например, error
, оно будет выведено в любом случае.
Основная логика определения необходимости вывода сообщения содержится в методе Log.
Также мы представили неэкспортированный метод с именем write
. Метод write
фактически выполняет вывод сообщения журнала.
Теперь мы можем использовать многоуровневую регистрацию в другом пакете, изменив cmd/main.go
следующим образом:
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")
}
При запуске вы увидите следующее:
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
.
Мы можем передать уровень
каждого сообщения, изменив значение debug
на false
:
package main
import (
"time"
"github.com/gopherguides/logging"
)
func main() {
logger := logging.New(time.RFC3339, false)
logger.Log("info", "starting up service")
logger.Log("warning", "no tasks found")
logger.Log("error", "exiting: no work performed")
}
Теперь мы видим, что выводятся только сообщения уровня 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.
Thanks for learning with the DigitalOcean Community. Check out our offerings for compute, storage, networking, and managed databases.
This textbox defaults to using Markdown to format your answer.
You can type !ref in this text area to quickly search our full set of tutorials, documentation & marketplace offerings and insert the link!