Понимание видимости пакета в Go

Вступление

При созданииpackage in Go конечная цель обычно состоит в том, чтобы сделать пакет доступным для использования другими разработчиками, будь то пакеты более высокого порядка или целые программы. Поimporting the package ваш фрагмент кода может служить строительным блоком для других, более сложных инструментов. Однако для импорта доступны только определенные пакеты. Это определяется видимостью пакета.

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

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

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

Предпосылки

Чтобы следовать примерам в этой статье, вам понадобится:

.
├── bin
│
└── src
    └── github.com
        └── gopherguides

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

В отличие от других языков программирования, таких как Java иPython, которые используютaccess modifiers, такие какpublic,private илиprotected, для указания области действия, Go определяет, является ли элементexported иunexported через то, как он объявлен. Экспорт элемента в этом случае делает егоvisible вне текущего пакета. Если он не экспортируется, он виден и может использоваться только в том пакете, который был определен.

Эта внешняя видимость контролируется заглавными буквами первой объявленной позиции. Все объявления, такие какTypes,Variables,Constants,Functions и т. Д., Начинающиеся с заглавной буквы, видны вне текущего пакета.

Давайте посмотрим на следующий код, обращая особое внимание на заглавные буквы:

greet.go

package greet

import "fmt"

var Greeting string

func Hello(name string) string {
    return fmt.Sprintf(Greeting, name)
}

Этот код объявляет, что он находится в пакетеgreet. Затем он объявляет два символа: переменную с именемGreeting и функцию с именемHello. Поскольку оба они начинаются с заглавной буквы, они оба являютсяexported и доступны для любой внешней программы. Как указывалось ранее, создание пакета, ограничивающего доступ, позволит улучшить дизайн API и упростить внутреннее обновление вашего пакета, не нарушая чей-либо код, который зависит от вашего пакета.

Определение видимости пакета

Чтобы более подробно рассмотреть, как работает видимость пакетов в программе, давайте создадим пакетlogging, помня, что мы хотим сделать видимыми вне нашего пакета, а что не будем делать видимыми. Этот пакет регистрации будет отвечать за запись любых наших программных сообщений на консоль. Он также будет смотреть на то, в какойlevel мы регистрируемся. Уровень описывает тип журнала и может быть одним из трех статусов:info,warning илиerror.

Во-первых, в вашем каталогеsrc давайте создадим каталог с именемlogging для размещения наших файлов журналов:

mkdir logging

Перейдите в этот каталог дальше:

cd logging

Затем с помощью редактора, такого как nano, создайте файл с именемlogging.go:

nano logging.go

Поместите следующий код в только что созданный файлlogging.go:

logging/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. В этом пакете есть две функцииexported:Debug иLog. Эти функции могут быть вызваны любым другим пакетом, который импортирует пакетlogging. Также существует закрытая переменнаяdebug. Эта переменная доступна только из пакетаlogging. Важно отметить, что хотя функцияDebug и переменнаяdebug имеют одинаковое написание, функция пишется с заглавной буквы, а переменная - нет. Это делает их разными объявлениями с разными областями применения.

Сохраните и выйдите из файла.

Чтобы использовать этот пакет в других областях нашего кода, мы можемimport it into a new package. Мы создадим этот новый пакет, но нам понадобится новый каталог, чтобы сначала сохранить эти исходные файлы.

Давайте выйдем из каталогаlogging, создадим новый каталог с именемcmd и перейдем в этот новый каталог:

cd ..
mkdir cmd
cd cmd

Создайте файл с именемmain.go в только что созданном каталогеcmd:

nano main.go

Теперь мы можем добавить следующий код:

cmd/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

Затем поместите следующее содержимое в файл:

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

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

go.mod

module github.com/gopherguides/logging

Это сообщает компилятору, что созданный нами пакетlogging на самом деле является пакетомgithub.com/gopherguides/logging. Это позволяет импортировать пакет в наш пакетmain со следующей строкой, которую мы написали ранее:

cmd/main.go

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 экспортируются из пакета протоколирования, мы можем использовать их в нашем пакетеmain. Однако переменнаяdebug в пакетеlogging не экспортируется. Попытка ссылки на неэкспортированное объявление приведет к ошибке времени компиляции.

Добавьте следующую выделенную строку вmain.go:

cmd/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

Теперь, когда мы увидели, как ведут себя элементыexported иunexported в пакетах, мы теперь рассмотрим, какfields иmethods можно экспортировать изstructs.

Видимость в структурах

Хотя схема видимости в логгере, который мы создали в последнем разделе, может работать для простых программ, она имеет слишком много общего состояния, чтобы быть полезной из нескольких пакетов. Это связано с тем, что экспортируемые переменные доступны нескольким пакетам, которые могут изменять переменные в противоречивые состояния. Если вы позволите изменить состояние вашего пакета таким образом, вам будет трудно предсказать, как ваша программа будет вести себя. В текущем дизайне, например, один пакет может установить для переменнойDebug значениеtrue, а другой может установить ее вfalse в том же экземпляре. Это создало бы проблему, так как затронуты оба пакета, импортирующие пакетlogging.

Мы можем сделать регистратор изолированным, создав структуру и затем повесив на нее методы. Это позволит нам создатьinstance регистратора, который будет использоваться независимо в каждом пакете, который его использует.

Измените пакетlogging на следующий, чтобы провести рефакторинг кода и изолировать регистратор:

logging/logging.go

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. Эта структура будет содержать наше неэкспортированное состояние, включая формат времени для печати и настройку переменнойdebugtrue илиfalse. ФункцияNew устанавливает начальное состояние для создания регистратора, такое как формат времени и состояние отладки. Затем он сохраняет значения, которые мы ему присвоили, в неэкспортированных переменныхtimeFormat иdebug. Мы также создали метод под названиемLog для типаLogger, который принимает оператор, который мы хотим распечатать. В методеLog есть ссылка на его локальную переменную методаl, чтобы получить доступ к своим внутренним полям, таким какl.timeFormat иl.debug.

Такой подход позволит нам создатьLogger во многих разных пакетах и ​​использовать его независимо от того, как его используют другие пакеты.

Чтобы использовать его в другом пакете, изменимcmd/main.go, чтобы он выглядел следующим образом:

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:

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.

Видимость в методах

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

Чтобы проиллюстрировать это, давайте добавим записьleveled в наш регистратор. Уровневое ведение журнала - это средство классификации ваших журналов, чтобы вы могли искать в своих журналах определенные типы событий. Уровни, которые мы добавим в наш логгер:

  • Уровеньinfo, который представляет события информационного типа, которые информируют пользователя о действии, напримерProgram started илиEmail sent. Они помогают нам отлаживать и отслеживать части нашей программы, чтобы увидеть, происходит ли ожидаемое поведение.

  • Уровеньwarning. Эти типы событий определяют, когда происходит что-то неожиданное, не являющееся ошибкой, напримерEmail failed to send, retrying. Они помогают нам увидеть части нашей программы, которые идут не так гладко, как мы ожидали.

  • Уровеньerror, что означает, что программа обнаружила проблему, напримерFile not found. Это часто приводит к сбою работы программы.

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

Добавьте выровненное ведение журнала, внеся следующие изменения вlogging/logging.go:

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. Теперь мы можем передатьlevel сообщения журнала. МетодLog определяет, какой это уровень сообщения. Если это сообщениеinfo илиwarning, а полеdebug -true, то сообщение записывается. В противном случае он игнорирует сообщение. Если это какой-либо другой уровень, напримерerror, он напишет сообщение независимо.

Большая часть логики для определения того, распечатано ли сообщение, существует в методеLog. Мы также представили неэкспортированный метод под названиемwrite. Методwrite - это то, что фактически выводит сообщение журнала.

Теперь мы можем использовать это уровневое ведение журнала в нашем другом пакете, изменивcmd/main.go, чтобы он выглядел следующим образом:

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.

Теперь мы можем передатьlevel каждого сообщения, переключивdebug наfalse:

main.go

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, мы получим ошибку времени компиляции:

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")

    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 ложно, вы можете внести это изменение, не затрагивая другие части вашего API. Мы также могли бы безопасно внести изменения в сообщение журнала, чтобы включить в него дополнительную информацию, такую ​​как каталог, из которого запускалась программа.

Заключение

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

Чтобы узнать больше о пакетах в Go, ознакомьтесь с нашими статьямиImporting Packages in Go иHow To Write Packages in Go или изучите весь нашHow To Code in Go series.