Названия пакетов

33 minute read

Перевод статьи "Package names" (автор Sameer Ajmani)

Введение

Go код организован в пакеты. Внутри пакета можно обращаться к любому идентификатору(имени) определенному в этом пакете, а клиент может обращаться только к экспортируемым типам, функциям, константам и переменным. Такие обращения всегда содержат имя пакета в качестве префикса: foo.Bar ссылается на экспортируемое имя Bar из пакета foo.

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

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

Названия пакетов

Хорошие названия должны быть короткими и "чистыми". Они пишутся в нижнем регистре без _подчеркиваний и смешАногоРегистра. Как правило, это просто существительные, такие как:

  • time (предоставляет функционал для измерения и отображения времени)
  • list (реализация двусвязного списка)
  • http (предоставляет реализацию HTTP сервера и клиета)

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

  • computeServiceClient
  • priority_queue

Go пакет может экспортировать несколько типов и функций. Например, пакет compute может экспортировать тип Client с методами для использования сервиса и функции для разбиения вычислительных задач на несколько клиентов.

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

  • strconv string conversion - преобразование строк
  • syscall system call - системные вызовы
  • fmt formatted I/O - форматирование ввода/вывода

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

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

Названия внутри пакетов

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

Избегайте дублирования. Так как клиент использует имя пакета в качестве префикса, то нужно чтобы все используемые названия в пакете не дублировали его имя. HTTP сервер, предоставляемый пакетом http называется просто Server, а не HTTPServer. Клиентский код может использовать его как http.Server - такое название не двусмысленное и нет лишних повторений.

Упрощайте названия функций. Когда функция из пакета pkg возвращает значение типа pkg.Pkg (или *pkg.Pkg), то в названии функции имя типа может не упоминаться:

start := time.Now()  // start это переменная типа time.Time
t, err := time.Parse(time.Kitchen, "6:06PM")  // t тип time.Time

Функция с именем New в пакете pkg возвращает значение типа pkg.Pkg. Это стандартная точка входа для клиентского кода:

q := list.New()  // q это переменная типа *list.List

Когда функция возвращает значение типа pkg.T, где T это не тип Pkg, то название функции должно включать название типа T что бы клиенту было понятно с чем приходится работать. Типичная ситуация, это пакет с несколькими New-что-там функциями:

d, err := time.ParseDuration("10s")  // d переменная типа time.Duration
elapsed := time.Since(start)         // elapsed переменная типа time.Duration
ticker := time.NewTicker(d)          // ticker переменная типа *time.Ticker
timer := time.NewTimer(d)            // timer переменная типа *time.Timer

Типы в разных пакетах могут иметь одинаковые имена, потому что для клиента они будут разделены по названию пакета. Для примера, в стандартной библиотеке есть пакеты с разными вариациями Reader: jpeg.Reader, bufio.Reader и csv.Reader. Префикс согласуется с Reader и мы получаем хорошее имя для типа.

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

Пакеты и пути

В Go есть как название пакета так и путь к нему. Название указано в исходниках самого пакета, именно его клиентский код использует как префикс для содержимого. Клиент использует путь пакета во время импорта. По соглашению, последний элемент пути соответствует его названию:

import (
    "fmt"                       // пакет fmt
    "os/exec"                   // пакет exec
    "golang.org/x/net/context"  // пакет context
)

Инструменты сборки сопоставляют пути пакетов с файловой структурой. Go инструменты используют переменную окружения GOPATH для поиска исходников пакета "github.com/user/hello" в директории $GOPATH/src/github.com/user/hello. (Конечно, вы это, скорее всего, знаете. Но нам нужно определиться с терминологией.)

Директории. Стандартная библиотека использует директории для группировки связанных протоколов и алгоритмов, например crypto, container, encoding и image. Нет никакой зависимости между пакетами в одной из таких директорий, это просто способ организовать исходные файлы. Любой пакет может импортировать любой другой пакет при условии что не будет цикличного импорта.

Так же как типы в различных пакетах могут иметь одинаковые имена, так и пакеты в различных директориях могут называться одинаково. Например, runtime/pprof предоставляет данные профилирования в формате для инструмента pprof, а net/http/pprof предоставляет HTTP интерфейс для данных профилирования. Клиентский код использует разные пути импорта, так что нет никакой путаницы. Если нам необходимо импортировать больше одного пакета с одинаковыми названиями, то можно [локально переименовать]((https://golang.org/ref/spec#Import_declarations) один или несколько из них. При локальном переименовании необходимо следовать правилам для именования пакетов(нижний регистр, никаких _подчеркиваний и МешанинРегистров).

Плохие имена для пакетов

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

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

Разделяйте обобщенные пакеты. Для этого посмотрите на типы и функции, описанные в пакете, у которых есть общие элементы и выделите их в отдельные пакеты. Для примера, если у вас есть

package util
func NewStringSet(...string) map[string]bool {...}
func SortStringSet(map[string]bool) []string {...}

то клиентский код выглядит так

set := util.NewStringSet("c", "a", "b")
fmt.Println(util.SortStringSet(set))

Переместите эти функции из util в новый пакет, с более подходящим именем:

package stringset
func New(...string) map[string]bool {...}
func Sort(map[string]bool) []string {...}

тогда клиентский код будет выглядеть так

set := stringset.New("c", "a", "b")``
fmt.Println(stringset.Sort(set))

После таких изменений значительно проще увидеть, как можно улучшить новый пакет:

package stringset
type Set map[string]bool
func New(...string) Set {...}
func (s Set) Sort() []string {...}

что позволяет писать еще более простой клиентский код:

set := stringset.New("c", "a", "b")
fmt.Println(set.Sort())

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

Не используйте один пакет для всех ваших API. Многие программисты часто горят желанием собрать все интерфейсы, предоставляемые их кодом, в один пакет с названием api, types, или interfaces, предполагая, что таким образом облегчают доступ к их коду. Это ошибка. Такие пакеты страдают теми же проблемами, что и пакеты с названиями util или common, разрастаются, используют все больше зависимостей и конфликтуют с другими аналогичными пакетами и клиентским кодом. Разделяйте такие пакеты с использованием директорий и с учетом реализации.

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

Заключение

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

Почитать