[Перевод] Лучшие практики Go, шесть лет в деле
В 2014 году я выступил на открытии конференции GopherCon с докладом под названием «Go: Best Practices for Production Environments». В SoundCloud мы были одними из первых пользователей Go и к тому времени уже два года писали на нём и поддерживали Go в бою в той или иной форме. За это время мы кое-чему научились, и я попытался поделиться частью этого опыта.
С тех пор я продолжал программировать на Go в течение всего рабочего дня, сначала в командах SoundCloud, отвечающих за операционную деятельность и инфраструктуру, а теперь работаю в компании Weaveworks над Weave Scope и Weave Mesh. Также я усердно трудился над Go kit, набором инструментов для микросервисов с открытым исходным кодом. И всё это время я принимал активное участие в развитии сообщества Go-программистов, встречался со многими разработчиками на митапах и конференциях по всей Европе и в США, коллекционируя их истории успехов и провалов.
В ноябре 2015-го, на шестую годовщину релиза Go, я вспоминал то своё первое выступление. Какие из лучших практик прошли проверку временем? Какие из них устарели или стали неэффективными? Появились ли какие-то новые методики? В марте мне представилась возможность выступить на конференции QCon London, где я рассказал о лучших практиках 2014 года и дальнейшем развитии Go до 2016 года. В этом посте представлена выжимка из моего выступления.
Ключевые положения я выделил в тексте в виде Top Tips — лучших советов.
А вот и cодержание:
- Среда разработки
- Структура репозитория
- Форматирование и стиль
- Конфигурация
- Разработка программ
- Логирование и метрики
- Тестирование
- Управление зависимостями
- Сборка и развёртывание
- Заключение
Соглашения среды разработки Go основаны на использовании GOPATH. В 2014 году я отстаивал точку зрения, что должна быть единственная глобальная переменная GOPATH. С тех пор моя позиция несколько смягчилась. Я до сих пор считаю, что при прочих равных это наилучший вариант, но многое также зависит от особенностей вашего проекта, команды и прочих вещей.
Если вы или ваша компания создаёте в основном исполняемые двоичные файлы (binaries), то использование отдельного GOPATH для каждого проекта может дать определённые преимущества. Для таких случаев можно воспользоваться новой утилитой gb от Дейва Чейни (Dave Cheney) и контрибьютеров, заменяющей стандартные инструменты go
для этих целей. На утилиту есть уже множество положительных отзывов.
Некоторые разработчики используют GOPATH с двумя директориями (two-entry), например $HOME/go/external:$HOME/go/internal
. Go-команда всегда знала, как обрабатывать такие случаи: go get
скачает зависимость в директорию по первому пути, поэтому такое решение может быть полезным, если вам нужно строго отделить внутренний код от стороннего.
Я заметил, что некоторые разработчики забывают помещать GOPATH/bin
в свой PATH
. А ведь это позволяет легко запускать получаемые вами посредством go get
исполняемые файлы, а также облегчает работу с (предпочтительным) механизмом сборки кода go install
. Нет ни одной причины этого не делать.
✪ Top Tip — Помещайте $GOPATH/bin
в свой $PATH
, это облегчит доступ к установленным программам.
Благодаря всевозможным редакторам и IDE среда разработки непрерывно улучшалась. Если вы поклонник vim, то для вас всё сложилось как нельзя лучше: благодаря неустанному и невероятно эффективному труду Фатиха Арслана (Fatih Arslan) плагин vim-go превратился в настоящее произведение искусства, лучший инструмент в своём классе. Я не так хорошо знаком с Emacs, но в этой сфере всё ещё правит go-mode.el Доминика Хоннефа (Dominik Honnef).
Двигаясь дальше, многие всё ещё успешно используют связку Sublime Text + GoSublime. По скорости с ней трудно соперничать. Но судя по всему, в последнее время всё больше внимания уделяется редакторам на базе Electron. Немало поклонников у связки Atom + go-plus, особенно среди разработчиков, которым часто приходится переключаться с какого-нибудь языка на JavaScript. Связка Visual Studio Code + vscode-go была тёмной лошадкой: она работает медленнее Sublime Text, но заметно быстрее Atom«а, а заодно по умолчанию прекрасно поддерживает важные для меня возможности, вроде click-to-definition (переход к месту определения объекта по клику). Я уже полгода ежедневно пользуюсь этой связкой, с тех пор как Томас Адам (Thomas Adam) познакомил меня с ней. Отличная вещь.
Что касается полноценных IDE, то можно упомянуть специально созданный LiteIDE, который регулярно обновляется и имеет свою аудиторию поклонников. Также есть интересный плагин для Go Intellij, который постоянно улучшается.
У нас было достаточно времени для того, чтобы проекты стали более зрелыми, и в результате выработался ряд чётких подходов. От того, чем является ваш проект, зависит то, как вы структурируете свой репозиторий. Если речь идёт о закрытом проекте или внутреннем проекте компании, то можно уйти в отрыв: пусть в нём будет собственный GOPATH, используйте кастомный инструмент для сборки, делайте что угодно, если это приносит вам удовольствие и повышает вашу производительность.
Но если это публичный проект (например, open source), то правила становятся строже. Ваш код должен быть совместим с go get
, поскольку именно таким способом большинство Go-разработчиков захотят воспользоваться вашей работой.
Идеальная структура репозитория зависит от типов ваших сущностей. Если это исключительно исполняемые бинарные файлы или библиотеки, тогда нужно быть уверенным, что потребители смогут использовать go get
или импортировать по базовому пути. Так что поместите package main или основной код для импорта в github.com/name/repo
, а для вспомогательных пакетов используйте вложенные папки.
Если ваш репозиторий представляет собой комбинацию двоичных файлов и библиотек, то вам следует определить основную сущность и положить её в корень репозитория. Например, если ваш репозиторий по большей части состоит из исполняемых файлов, но также может использоваться как библиотека, то вы наверняка предпочтёте структурировать его так:
github.com/peterbourgon/foo/
main.go // package main
main_test.go // package main
lib/
foo.go // package foo
foo_test.go // package foo
Полезный совет: во вложенной папке lib/
лучше именовать пакет в соответствии с названием библиотеки, а не самой папки; т. е. в этом примере — package foo
вместо package lib
. Это исключение из довольно строгих Go-идиом, но на практике это очень удобно для пользователей. Подобным образом устроен замечательный репозиторий tsenart/vegeta, инструмент для нагрузочного тестирования HTTP-сервисов.
✪ Top Tip — Если ваш репозиторий foo в основном состоит из исполняемых бинарных файлов, то поместите код библиотеки во вложенную папку lib/
и назовите package foo
.
Если ваш репозиторий в основном библиотека, но также включает в себя одну-две исполняемых программы, то структура может быть такой:
github.com/peterbourgon/foo
foo.go // package foo
foo_test.go // package foo
cmd/
foo/
main.go // package main
main_test.go // package main
Получается инвертированная структура, когда код библиотеки кладётся в корень, а во вложенной папке cmd/foo/
хранится код исполняемых программ. Промежуточный уровень cmd/
удобен по двум причинам:
- Инструментарий Go автоматически именует двоичные файлы по названию папки, в которой находится package main, так что мы получаем наилучшие имена файлов без возможных конфликтов с другими пакетами в репозитории.
- Если ваши пользователи применяют
go get
на путь, в котором содержится/cmd/
, они сразу понимают, что получили. Подобным образом устроен репозиторий сборочной утилиты gb.
✪ Top Tip — Если основное предназначение вашего репозитория — библиотека, то поместите код исполняемых программ во вложенные папки внутри cmd/
.
Основная мысль здесь: заботьтесь о пользователях — упрощайте использование основной функциональности вашего проекта. Мне кажется, что эта абстрактная идея — фокус на потребностях пользователей — отвечает самому духу языка Go.
Здесь мало что изменилось. Это одно из тех мест, где Go пошёл по правильной дороге, и я очень ценю соглашения в сообществе и стабильность языка в отношении этого. Комментарии Code Review Comments великолепны и должны быть минимальным набором необходимых для соблюдения критериев в ходе ревизии кода. А если у вас в наименованиях встречаются спорные ситуации или противоречия, то можете воспользоваться прекрасным набором идиоматических соглашений о наименованиях Эндрю Герранда (Andrew Gerrand).
✪ Top Tip — Воспользуйтесь соглашениями о наименованиях Эндрю Герранда.
А что касается инструментария, то здесь всё стало только лучше. Сконфигурируйте свой редактор так, чтобы при сохранении инициировался gofmt, а лучше goimports (надеюсь, здесь ни у кого не возникнет возражений). Использование утилиты go vet почти не приводит к ложноположительным срабатываниям, так что вы вполне можете сделать её частью вашего pre-commit-хука (pre-commit hook). И обратите внимание на замечательную утилиту контроля качества кода gometalinter. У неё могут быть ложноположительные срабатывания, так что имеет смысл как-то обозначить свои собственные соглашения.
Конфигурация пролегает между runtime-средой и процессом. Она должна быть явной и хорошо задокументированной. Я всё ещё использую и рекомендую использовать пакет flag, но всё же предпочёл бы, чтобы конфигурация была более привычной. Хотелось бы получить стандартный синтаксис аргументов в getopts-стиле, чтобы были подробная и краткая формы аргументов. Также хочется, чтобы текст использования (usage text) был гораздо компактнее.
Приложения, следующие соглашениям The Twelve-Factor App, мотивируют использовать для конфигурирования переменные окружения, и я думаю, что это нормально, при условии, что каждая переменная также определена как флаг. Здесь важна явность: изменение runtime-поведения приложения должно выполняться легко обнаруживаемым и задокументированным путём.
Я уже говорил в 2014 году, но считаю необходимым повториться: определяйте и разбирайте флаги внутри func main (). Только у func main()
есть право решать, какие флаги будут доступны пользователю. Если ваша библиотека позволяет конфигурировать своё поведение, то параметры конфигурации должны быть частью конструкторов типов. Перенос конфигурации в глобальную область видимости пакетов создаёт иллюзию выгоды, но экономия получается ложной: вы ломаете модульность кода, поэтому другим разработчикам будет труднее понять отношения зависимостей, к тому же станет куда сложнее писать независимые параллелизуемые тесты.
✪ Top Tip — Только у func main()
есть право решать, какие флаги будут доступны пользователю.
Думаю, сообщество вполне может создать всеобъемлющий пакет флагов, в котором будут сочетаться все эти свойства. Возможно, он уже существует. Если да, то дайте мне знать. Я бы точно воспользовался им.
В беседе я использовал конфигурацию как отправную точку для обсуждения ряда других аспектов разработки программы (я не поднимал эту тему в 2014-м). Для начала давайте посмотрим на конструкторы. Если мы правильно параметризуем все наши зависимости, то конструкторы могут стать весьма большими.
foo, err := newFoo(
*fooKey,
bar,
100 * time.Millisecond,
nil,
)
if err != nil {
log.Fatal(err)
}
defer foo.close()
Иногда подобную конструкцию лучше выразить с помощью объекта конфигурации: структуры, принимающей необязательные параметры, определяющие поведение конструируемого объекта. Предположим, что параметр fooKey
обязательный, а все остальные либо имеют разумные значения по умолчанию, либо необязательны.
Мне часто встречаются проекты, в которых объекты конфигурации конструируются как-то разрозненно:
// Не делайте этого
cfg := fooConfig{}
cfg.Bar = bar
cfg.Period = 100 * time.Millisecond
cfg.Output = nil
foo, err := newFoo(*fooKey, cfg)
if err != nil {
log.Fatal(err)
}
defer foo.close()
Но куда лучше конструировать объект за один раз, с помощью одного выражения, воспользовавшись так называемым синтаксисом инициализации структуры (struct initialization syntax).
// Вот это лучше
cfg := fooConfig{
Bar: bar,
Period: 100 * time.Millisecond,
Output: nil,
}
foo, err := newFoo(*fooKey, cfg)
if err != nil {
log.Fatal(err)
}
defer foo.close()
Здесь нет никаких выражений, когда объект находится в промежуточном, неправильном состоянии. При этом все поля красиво разграничены и выделены отступами, отражая определение fooConfig
.
Обратите внимание, что объект cfg
мы конструируем и сразу же используем. В этом случае, напрямую встроив объявление структуры в конструктор newFoo
, мы можем избежать ещё одной ступени промежуточного состояния и сберечь ещё одну строку кода.
// Это ещё лучше
foo, err := newFoo(*fooKey, fooConfig{
Bar: bar,
Period: 100 * time.Millisecond,
Output: nil,
})
if err != nil {
log.Fatal(err)
}
defer foo.close()
Отлично.
✪ Top Tip — Чтобы избежать неправильного промежуточного состояния, используйте инициализацию литерала структуры. Везде, где возможно, встраивайте объявления структуры.
Теперь обратимся к теме разумных умолчаний. Заметьте, что параметр Output
может принимать значение nil
.
Предположим, что это io.Writer
. Если не делать ничего особенного, то, когда мы захотим использовать его в нашем объекте foo
, нам сначала придётся осуществить проверку на nil.
func (f *foo) process() {
if f.Output != nil {
fmt.Fprintf(f.Output, "start\n")
}
// ...
}
Это не здорово. Гораздо лучше и безопаснее иметь возможность использовать выходное значение без проверки на его существование.
func (f *foo) process() {
fmt.Fprintf(f.Output, "start\n")
// ...
}
Итак, здесь нам нужно по умолчанию предоставлять что-то полезное. Благодаря интерфейсным типам у нас есть возможность передать что-либо, что обеспечивает no-op-реализацию (т. е. реализацию, не делающую никаких операций, заглушку. — Прим. переводчика) интерфейса. Поэтому пакет stdlib ioutil поставляется с no-op io.Writer
, который называется ioutil.Discard
.
✪ Top Tip — Избегайте проверок на nil с помощью no-op-реализаций по умолчанию.
Можно было бы передать это в объект fooConfig
, но это довольно хрупкое решение. Если вызывающий код забудет сделать это в месте вызова, то у нас опять получится параметр nil
. Вместо этого мы можем обезопасить себя внутри конструктора.
func newFoo(..., cfg fooConfig) *foo {
if cfg.Output == nil {
cfg.Output = ioutil.Discard
}
// ...
}
Это всего лишь применение Go-идиомы «делайте нулевое значение полезным». То есть мы позволяем нулевому значению (nil
) предоставлять хорошее поведение по умолчанию (no-op).
✪ Top Tip — Делайте нулевое значение полезным, особенно в объектах конфигурации.
Вновь обратимся к конструктору. Параметры fooKey
, bar
, period
и output
являются зависимостями. Успешность запуска и работы объекта foo
зависит от каждого из них. Чему я точно научился за шесть лет ежедневного программирования на Go и наблюдения за большими проектами, так это тому, что нужно делать зависимости явными.
✪ Top Tip — Делайте зависимости явными!
Я считаю, что неоднозначные или неявные зависимости являются причиной невероятного объёма трудозатрат на техническую поддержку, путаницы, багов и неоплаченного технического долга. Рассмотрим метод process () типа foo
:
func (f *foo) process() {
fmt.Fprintf(f.Output, "start\n")
result := f.Bar.compute()
log.Printf("bar: %v", result) // Whoops!
// ...
}
fmt.Printf
автономен, не влияет и не зависит от глобального состояния. В функциональных терминах он обладает чем-то вроде ссылочной прозрачности (referential transparency). Так что это не зависимость. Очевидно, что ею является f.Bar
. Любопытно, что log.Printf
оказывает влияние на глобальный (в рамках пакета) объект-логгер, это просто неочевидно из-за свободной функции Printf
. Так что это тоже зависимость.
Что нам делать со всеми этим зависимостями? Сделаем их явными. Поскольку метод process () пишет в лог в процессе своей работы, то либо метод, либо сам объект foo
должны принимать объект логирования в качестве зависимости. Например, log.Printf
должен стать f.Logger.Printf
.
func (f *foo) process() {
fmt.Fprintf(f.Output, "start\n")
result := f.Bar.compute()
f.Logger.Printf("bar: %v", result) // Лучше.
// ...
}
Мы привыкли считать побочными определённые виды работ вроде логирования. Поэтому мы рады использовать вспомогательные библиотеки вроде глобальных логгеров для облегчения своего бремени. Но логирование, как и метрики, часто играет решающую роль в функционировании сервиса. И скрывание зависимостей в глобальном пространстве видимости может — и сделает это — ударить по нам же, либо в виде чего-то внешне безобидного, как логирование, либо в виде какого-то другого, более важного, предметного компонента, о параметризации которого мы не позаботились. Защитите себя от боли в будущем с помощью строгого правила: делать явными все свои зависимости.
✪ Top Tip — Логгеры являются зависимостями, так же как и ссылки на другие компоненты, клиенты баз данных, аргументы командной строки и т. д.
Безусловно, нам нужно позаботиться и о получении разумного умолчания для нашего логгера.
func newFoo(..., cfg fooConfig) *foo {
// ...
if cfg.Logger == nil {
cfg.Logger = log.New(ioutil.Discard, ...)
}
// ...
}
Говоря о проблеме в целом: с логированием у меня было намного больше опыта в бою, что лишь усилило моё уважительное отношение к проблеме. Логирование — дорогое, гораздо дороже, чем вы думаете, и может быстро превратиться в узкое место вашей системы. Я подробно осветил эту тему в отдельном посте, но если вкратце:
- Логируйте только ту информацию, которая даёт основание для действий, считываемую человеком или машиной.
- Избегайте слишком подробного журналирования, возможно, вам будет достаточно общей информации и данных для отладки.
- Применяйте структурированное логирование. Хоть я и пристрастен, но рекомендую go-kit/log.
- Логгеры — это зависимости!
Там, где логирование стоит дорого, метрики дёшевы. Снимайте метрики с любого существенного компонента вашей кодовой базы. Если это ресурс, наподобие очереди, то измеряйте его по методу USE Брендана Грегга: Utilization, Saturation, Error count (rate). Если это какая-то конечная точка (endpoint), то измеряйте по методу RED Тома Уилки: Request count (rate), Error count (rate), Duration.
Если в этом вопросе у вас есть возможность выбирать, то в качестве измерительной системы рекомендую использовать Prometheus. И конечно же, метрики также являются зависимостями!
Давайте отвлечёмся от логгеров и метрик и посмотрим непосредственно на глобальное состояние. Вот несколько фактов про Go:
log.Print
использует фиксированный глобальныйlog.Logger
.http.Get
использует фиксированный глобальныйhttp.Client
.http.Server
по умолчанию использует фиксированный глобальныйlog.Logger
.database/sql
использует фиксированный глобальный реестр драйверов.func init
существует только для того, чтобы оказывать побочный эффект на глобальное состояние пакета.
Эти факты терпимы по отдельности, но затруднительны в целом. То есть как мы можем протестировать выходные данные, передаваемые в лог компонентами, использующими фиксированный глобальный логгер? Придётся перенаправлять эти данные, но как их параллельно тестировать? Никак? Ответ неудовлетворительный. Или, скажем, есть два независимых компонента, генерирующих HTTP-запросы с разными требованиями, как нам этим управлять? С помощью стандартного глобального http.Client
делать это довольно трудно. Посмотрите пример:
func foo() {
resp, err := http.Get("http://zombo.com")
// ...
}
http.Get вызывает глобал в пакете http. У него неявная глобальная зависимость, от которой мы можем довольно легко избавиться:
func foo(client *http.Client) {
resp, err := client.Get("http://zombo.com")
// ...
}
Просто передайте http.Client
в качестве параметра. Но это конкретный тип (concrete type), так что если мы хотим протестировать данную функцию, то нам придётся предоставить конкретный http.Client, который наверняка заставит нас установить фактическое соединение через HTTP. Это нехорошо. Можно поступить лучше: передать интерфейс, который может выполнять (Do
) HTTP-запросы.
type Doer interface {
Do(*http.Request) (*http.Response, error)
}
func foo(d Doer) {
req, _ := http.NewRequest("GET", "http://zombo.com", nil)
resp, err := d.Do(req)
// ...
}
http.Client
автоматически удовлетворяет интерфейсу Doer
, но теперь мы вольны передать в наш тест свою реализацию Doer
. И это прекрасно: модульный тест для функции foo
предназначен для тестирования только поведения foo
, и теперь можно спокойно предполагать, что http.Client
будет работать так, как заявлено.
Раз мы заговорили о тестировании…
В 2014 году я размышлял о нашем опыте работы с разными фреймворками для тестирования и вспомогательными библиотеками и пришёл к заключению, что все они не принесли какой-то особой пользы. Поэтому я рекомендовал обычный (stdlib) подход к тестированию пакетов с помощью тестов на базе таблиц. В целом я всё ещё считаю это наилучшим советом. Относительно тестирования в Go важно помнить, что это просто программирование. Здесь нет столь серьёзных отличий от программирования других аспектов вашей программы, чтобы можно было говорить о своём собственном метаязыке. И потому пакет testing хорошо подходит для этой задачи.
Пакеты TDD/BDD предлагают нам новые, незнакомые DSL и управляющие структуры, что увеличивает когнитивную нагрузку на вас и тех, кто потом будет поддерживать ваш код. Лично мне не попадались кодовые базы, в которых полученные преимущества окупили бы затраты. Я считаю, что подобные пакеты, как и глобальное состояние, дают фальшивую экономию и гораздо чаще являются результатом культа карго, приходя из других языков и экосистем. When in Go, do as Gophers do (когда программируешь на Go, делай так, как принято у гоферов): у нас уже есть язык для написания простых и выразительных тестов — он называется Go, и вы, вероятно, хорошо им владеете.
Учитывая сказанное, я осознаю свой собственный контекст и пристрастия. Как и в случае с моим мнением по поводу GOPATH, за прошедшее время моя позиция смягчилась, и я стал лучше понимать команды и компании, для которых может иметь смысл использование тестовых DSL и фреймворков. Если вы знаете, что хотите использовать пакет, то используйте. Главное, чтобы на то были веские причины.
С тестами связана ещё одна невероятно интересная тема. Митчелл Хашимото (Mitchell Hashimoto) недавно выступил по ней с прекрасным докладом в Берлине (SpeakerDeck, YouTube), обязательно посмотрите.
В общих чертах: похоже, что лучше всего писать на Go в функциональном стиле, когда при каждой возможности зависимости перечисляются явным образом и представляются в виде маленьких интерфейсов с маленькой областью видимости. Помимо того, что это хорошо дисциплинирует в программном инжиниринге, так ещё и автоматически оптимизирует ваш код, облегчая его тестирование.
✪ Top Tip — Используйте многочисленные маленькие интерфейсы для моделирования зависимостей.
Как и в примере с http.Client
, помните, что модульные тесты должны писаться только для тестирования какого-то конкретного функционала, и больше ни для чего. Если вы тестируете какую-то функцию-обработчик, то нет смысла тестировать здесь ещё и HTTP-транспорт, в который пришёл запрос, или путь на диске, по которому записываются результаты. Передавайте входные и выходные данные в качестве фальшивых реализаций параметров-интерфейсов и сконцентрируйтесь исключительно на бизнес-логике метода или компонента.
✪ Top Tip — Тесты должны тестировать только то, что тестируется.
Всегда горячая тема. В 2014 году всё ещё только зарождалось, и мой практически единственный внятный совет относился к использованию вендоринга (vendor). Это по-прежнему актуально: вендоринг до сих пор позволяет решать проблему управления зависимостями для бинарных файлов. В частности, в Go 1.6 GO15VENDOREXPERIMENT и сопутствующая этой переменной окружения vendor/ поддиректория используется по умолчанию. Так что вы будете использовать такую схему. И, к счастью, инструментарий значительно улучшился. Вот что я могу порекомендовать:
- FiloSottile/gvt использует минималистский подход. По сути, просто извлекает из утилиты gb подкоманду для вендоринга, чтобы использовать её отдельно.
- Masterminds/glide использует максималистский подход: пытается воссоздать ощущение и мелкие детали полноценного инструмента управления зависимостями. Внутри используется вендоринг.
- kardianos/govendor находится примерно посередине, предоставляя, вероятно, богатейший интерфейс для специфичных для вендоринга вещей. И сводит разговор к файлу-манифесту (не до конца понятно, что автор имеет в виду, возможно — файл vendor.json. — Прим. переводчика).
- constabulary/gb отказывается от инструментария go в пользу другой структуры репозитория и механизма сборки. Отлично подходит для случаев, когда вы создаёте исполняемые бинарные файлы и можете управлять средой сборки, например в корпоративной среде.
✪ Top Tip — Используйте лучший инструмент для вендоринга зависимостей ваших исполняемых бинарных файлов.
Важное предостережение относительно библиотек. В Go управление зависимостями является заботой автора исполняемого бинарного файла. Очень трудно использовать библиотеки с завендоренными зависимостями, практически невозможно. В течение нескольких месяцев после того, как в версии 1.5 был представлен вендоринг, были выявлены многочисленные тупиковые ситуации и граничные условия. Если вас интересуют подробности, можете изучить пару постов на форуме: 1, 2. Если вкратце, то вывод очевиден: никогда не применяйте вендоринг зависимостей в библиотеках.
✪ Top Tip — Библиотеки никогда не должны вендорить свои зависимости.
Вы можете сделать для себя исключение, если ваша библиотека герметично запечатала свои зависимости и ни одна из них не сможет проникнуть в экспортированный (публичный) слой API. Никакие экспортируемые функции, сигнатуры методов и структуры не ссылаются на зависимые типы.
Если перед вами стоит общая задача поддержки open source репозитория, состоящего из двоичных файлов и библиотек, то вы оказались между молотом и наковальней. С одной стороны, вы захотите вендорить зависимости своих двоичных файлов, но для библиотек этого делать нельзя. А GO15VENDOREXPERIMENT не имеет такого уровня детализации, что представляется мне недосмотром со стороны разработчиков.
Честно говоря, у меня нет совета для такой ситуации. В Etcd используют хак, в котором решают проблему с помощью символических ссылок, но я не могу его порекомендовать, потому что симлинки плохо поддерживаются пакетом инструментов Go и окончательно ломаются под Windows. То, что у них это работает, скорее счастливая случайность, чем результат хорошего подхода. Я, как и ряд других программистов, поднял этот вопрос перед разработчиками и надеюсь, что в ближайшем будущем что-нибудь будет сделано.
Что касается сборки, то здесь можно порекомендовать (спасибо Дейву Чейни) использовать go install
вместо go build
. Команда install
кеширует в $GOPATH/pkg
артефакты сборки из зависимостей, что ускоряет процесс сборки. Также эта команда кладёт в $GOPATH/bin
исполняемые бинарные файлы, поэтому их легче найти и использовать.
✪ Top Tip — Используйте go install
вместо go build
.
Если вы создаёте двоичный файл, попробуйте воспользоваться новыми инструментами сборки, например gb. Это может помочь существенно снизить когнитивную нагрузку. В то же время нужно помнить, что начиная с Go 1.5 кросс-компиляция доступна «из коробки». Просто настройте соответствующие переменные среды GOOS и GOARCH, а затем введите нужную go-команду. Никакие дополнительные инструменты здесь больше не требуются.
Что касается процесса развёртывания, то у нас, гоферов, он весьма прост по сравнению с такими языками, как Ruby, или Python, или даже JVM. Одно замечание: если вы осуществляете развёртывание в контейнерах, то следуйте совету Келси Хайтауэр — используйте FROM scratch. Go предоставляет нам прекрасную возможность, и стыдно ею не пользоваться.
В качестве более общего совета могу сказать: тщательно всё продумайте, прежде чем начать выбирать платформу или систему оркестрации, — если вы вообще выберете что-либо. То же самое относится и к запрыгиванию на подножку микросервисов. Элегантный монолит, развёрнутый в виде AMI в автомасштабируемой группе EC2, является очень производительным решением для маленьких команд. Сопротивляйтесь шумихе и навязчивой рекламе или как минимум очень внимательно анализируйте.
Top Tips:
- Помещайте
$GOPATH/bin
в свой$PATH
, это облегчит доступ к установленным исполняемым бинарным файлам. - Если ваш репозиторий foo в основном состоит из исполняемых бинарных файлов, то поместите код библиотеки во вложенную папку lib/ и назовите
package foo
. - Если основное предназначение вашего репозитория — библиотека, то поместите код исполняемых программ во вложенные папки внутри cmd/.
- Воспользуйтесь соглашениями о наименованиях Эндрю Герранда.
- Только у
func main()
есть право решать, какие флаги будут доступны пользователю. - Чтобы избежать неправильного промежуточного состояния, используйте инициализацию литерала структуры. Везде, где возможно, встраивайте объявления структуры.
- Избегайте проверок на nil с помощью no-op-реализаций по умолчанию.
- Делайте нулевое значение полезным, особенно в объектах конфигурации.
- Делайте зависимости явными!
- Логгеры являются зависимостями, так же как и ссылки на другие компоненты, обработчики баз данных, флаги командной строки и т. д.
- Используйте многочисленные маленькие интерфейсы для моделирования зависимостей.
- Тесты должны тестировать только то, что тестируется.
- Используйте лучший инструмент для вендоринга зависимостей ваших исполняемых бинарных файлов.
- Библиотеки никогда не должны вендорить свои зависимости.
- Используйте
go install
вместоgo build
.
Go всегда был консервативным языком, и процесс его развития преподнёс нам немного сюрпризов, без каких-либо серьёзных изменений. В результате — и это было предсказуемо — в сообществе не отмечено сильных сдвигов в представлениях о лучших практиках. Вместо этого мы наблюдали овеществление метафор и пословиц (Go Proverbs), которые были хорошо известны в ранние годы, а также постепенное движение «вверх по стеку» (up the stack) по мере того, как шаблоны разработки, библиотеки и программные структуры развивались и трансформировались в идиоматичный Go.
Переходим к следующим шести годам весёлого и продуктивного программирования на Go.