Встраивание ресурсов в приложение
Перевод статьи "Embedding Assets in Go"
В этой статье я продемонстрирую, как можно встраивать ресурсы в само Go приложение. В частности, я покажу пример встраивания shell скрипта для chef-runner(одного из моих открытых проектов). Моя задача состоит в том, чтобы подтолкнуть вас к использованию этой замечательной технологии в ваших проектах. Кроме того, я постараюсь дать вам представлении о выборе правильных инструментов для подобных задач. Давайте начнем.
Встречайте chef-runner. Быстрый способ запуска Chef Cookbooks.
Самый большой плюс chef-runner'a -это ускорение разработки и тестирования Chef cookbooks. Изначально я разрабатывал этот инструмент как быструю альтернативу медленной команде vagrant provision
. С тех пор прошло много времени и теперь chef-runner может использоваться как для настройки виртуальных машин в Vagrant так и удаленных хостов, например инстансов EC2.
Одно из преимуществ chef-runner'a в том что он может установить Chef на машину до ее полного резервирования. Это позволяет использовать его на голых серверах без ПО, только с операционной системой. Для реализации этой возможности chef-runner использует скаченный в локальную папку Omnibus installer(также известный как install.sh
скрипт), который будет скопирован на целевую машину где он должен будет установить Chef.
Чуть позже я решил немного изменить схему работы. Вместо скачивания скрипта из интернета будет лучше встроить его как часть исходника chef-runner'a. Это дает множество преимуществ:
- Простота. Код становится проще, не нужно реализовывать логику загрузки, не нужно реализовывать кеширование.
- Прозрачность. Всегда используется тот скрипт, который вы добавили в репозиторий.
- Скорость. Нет необходимости что-то скачивать для каждого проекта.
К тому же, скачивание инсталятора было необходимо для поддержания актуальной версии, но от этого было мало пользы, так как он обновлялся очень редко.
chef-runner написан на Go. Я знал, что есть возможность встроить ресурсы в Go приложение и решил ознакомиться с различными вариантами и пакетами для этого. В конце концов, я решил использовать комбинацию go-bindata
и go generate
.
Встраивание ресурсов с go-bindata
go-bindata конвертирует все текстовые или бинарные файлы в исходники Go. И это действительно замечательный инструмент для встраивания различных данных в ваше приложение. go-bindata
можно использовать для встраивания CSS, JavaScript и изображений в ваше веб-приложение. В результате, у вас будет единственный бинарный файл, который можно просто деплоить как и любую другую Go программу.
Я использовал go-bindata
для добавления Omnibus installer как ресурса к chef-runner'у в виде пакета omnibus
. Все что мне нужно сделать для этого - скачать скрипт и выполнить команду go-bindata
для генерирования Go кода. Более точно все выглядит так:
# inside $GOPATH/src/github.com/mlafeldt/chef-runner
$ cd chef/omnibus/
$ mkdir assets
$ curl https://www.chef.io/chef/install.sh > assets/install.sh
$ go-bindata -pkg omnibus -o assets.go assets/
После этого я должен адаптировать chef/omnibus/omnibus.go
для использования директории с ресурсами напрямую, не загружая их. Данные из ресурсов доступны через функцию Asset
, которая определена в сгенерированном файле assets.go. В результате код будет выглядеть вот так:
// chef/omnibus/omnibus.go
package omnibus
type Installer struct {
ChefVersion string
SandboxPath string
RootPath string
Sudo bool
}
func (i Installer) writeOmnibusScript() error {
script := path.Join(i.SandboxPath, "install.sh")
log.Debugf("Writing Omnibus script to %s\n", script)
data, err := Asset("assets/install.sh")
if err != nil {
return err
}
return ioutil.WriteFile(script, []byte(data), 0644)
}
И это все что мне нужно было сделать. Я даже был удивлен, что понадобилось так мало усилий.
Тем не менее, есть небольшой момент, который мне не нравится в функции Asset
, сгенерированной go-bindata
. Дело в том, что эта функция будет отображаться в документации к пакету, что немного сбивает с толку. Один из вариантов, это хранить ресурсы в отдельном пакете.
Если вы хотите узнать больше о фичах go-bindata
, то сходите и почитайте README. Есть очень полезная возможность включить режим отладки, в котором данные будут загружены из исходного файла на диске с использованием того же API. Это удобно использовать во время разработки.
Еще один взаимосвязанный проект - go-bindata-assetfs. Его можно использовать для раздачи файлов через net/http
, которые были встроены с помощью go-bindata
(внутри все реализованно через интерфейс http.FileSystem
). Этот пакет может быть полезен для множества разных кейсов.
go generate: используем кодогенерацию
Хотя такие инструменты как go-bindata могут работать сами по себе, порой возникает необходимость интегрировать их в свой собственный бил процесс. Для этого вы можете использовать например Make, который будет работать как клей в вашей системе сборки. Но было бы намного круче, если бы Go сам предоставлял необходимые инструменты для генерации кода. И, на самом деле, так и есть!
В Go 1.4 появился новый инструмент go generate
, который позволяет автоматизировать генерацию исходного кода перед его компиляцией. Этот инструмент сканирует ваш проект на наличие специальных комментариев, которые определяют порядок генерации. Это дает возможность указывать инструкции сборки прямо в вашем коде, что делает сборку более структурированной.
Пример комментария, который я добавил в пакет omnibus
:
// chef/omnibus/omnibus.go
package omnibus
//go:generate go-bindata -pkg $GOPACKAGE -o assets.go assets/
Теперь при запуске go generate
выполниться команда go-bindata
с указанными параметрами ($GOPACKAGE
будет заменено на текущее имя пакета, например "omnibus"). Давайте сгенерируем немного кода:
$ go generate -x ./chef/omnibus
go-bindata -pkg omnibus -o assets.go assets/
Флаг -x
означает, что во время выполнения go generate
будут выводится все выполняемые ею действия. И эти действия должны быть вам знакомы.
Как и в большинстве Go инструментов, вы можете запустить go generate ./...
для прохода по всем пакетам в вашем проекте. И если так случилось, что у вас есть Makefile, то сейчас самое время добавить цель make generate
для сокращения и для использования в других целях в Makefile.
Есть только одни неудобный момент работы с go generate
, про котрый вы должны помнить: нет никакой интеграции с go get
, как можно было бы ожидать. Если вы хотите, чтобы ваш проект был "go gettable", то вам нужно положить в систему контроля версий и сгенерированный файлы, как это сделал я с assets.go.
Для более детальной информации по go generate
используйте go help generate
или, что еще лучше, почитайте эту статью в блоге Go.
Заключение
В целом, в Go есть удобные средства для встраивания ресурсов, которыми легко пользоваться благодаря существующим инструментам. Это и go-bindata
, которая позволяет конвертировать любые данные в встраиваемые ресурсы, и go generate
, который позволяет автоматизировать генерацию Go кода красивым способом. Я уверен, в недалеком будущем мы будем использовать эти инструменты еще чаще.
И еще кое-что. В одном из своих открытых проектах я использовал Python script для генерации Go map'ы со списком дистров, поддерживаемых Packagecloud. Так как я генерировал эту map еще до компиляции, то на этапы исполнения у меня получилось сэкономить на API вызовах. Я рекомендую вам ознакомиться с исходным кодом проекта, чтобы быть в курсе какие еще бывают практики встраивания ресурсов в свои приложения.