TL;DR: Кодогенератор.
Представьте, что вы разрабатываете некоторую библиотеку, которая поддерживает различные драйвера для хранения данных: Postgresql, Scylla, JSON и какие-либо другие.
У каждого из этих драйверов есть внешние зависимости от других библиотек. Postgresql — database/sql, github.com/lib/pq. Scylla — github.com/scylladb/gocql или стандартный github.com/gocql/gocql. JSON — github.com/mailru/easyjson или github.com/json-iterator/go, или encoding/json.
Обычный пользователь подключает вашу библиотеку и получает неявное включение всех этих внешних библиотек.
В большинстве случаев это нормально, но не идеально: иногда пользователю нужен только Postgres драйвер, но вместе с ним он неявно получает огромное количество кода из внешних библиотек, которые ему никогда не будут нужны.
Попробуем это исправить, используя замечательный механизм кодогенерации.
Кодогенерация
Разбейте файлы на части
Разбейте файлы вашей библиотеки на логические части, которые реализуют каждую отдельную конкретную зависимость. Пример: sql.go, cql.go, json.go.
Это позволит вам сгенерировать для пользователя только ту часть вашей библиотеки, которую он запросит.
Упакуйте файлы
Упакуйте файлы через pkger или любое другое решение для хранения их в бинарном файле.
Для включения файлов через pkger — в исходном коде просто нужно их использовать
files := make([]string, 0)
if *cql {
files = append(files, pkger.Include("/cql.go"))
}
if *json {
files = append(files, pkger.Include("/json.go"))
}
if *sql {
files = append(files, pkger.Include("/sql.go"))
}
Далее запустите
pkger -o <путь к исходнику кодогенератора>
И после этого вы получите статический исходник с упакованными файлами, который будут включены в бинарный файл.
Удобно добавить генерацию в go generate
//go:generate pkger -o <путь к исходнику кодогенератора>
Упаковка исходных кодов в бинарный файл нужна потому, что кодогенератор будет использоваться независимо от вашей библиотеки.
Напишите кодогенератор
Кодогенератор — это обычная программа на Go, которая принимает аргументы для генерации и записывает исходные файлы для выбранных типов в проект пользователя.
for i := range files {
_, filename := filepath.Split(files[i])
in, err := pkger.Open(files[i])
if err != nil {
panic(err)
}
src := []byte(fmt.Sprintf(genStr, strings.Join(os.Args[1:], " ")))
reader := bufio.NewReader(in)
for {
line, err := reader.ReadBytes('\n')
if strings.HasPrefix(string(line), "package ") {
line = []byte(fmt.Sprintf("package %s\n", *pkgName))
}
src = append(src, line...)
if err == io.EOF {
break
}
if err != nil {
panic(err)
}
}
if err := ioutil.WriteFile(filename, src, 0644); err != nil {
panic(err)
}
in.Close()
}
Разместите исходный код вашего кодогенератора в подпапке проекта вида “cmd/<имя вашего проекта>”. Это позволит пользователю получить бинарный файл кодогенератора с тем же названием, что и название вашей библиотеки.
Использование
Пользователь устанавливает кодогенератор
go get github.com/<ваш профиль>/<ваша библиотека>/<путь к кодогенератору>
И затем запускает кодогенератор в своём проекте
<имя бинарного файла вашего кодогенератора> -sql
Когда нужно обновить вашу библиотеку — пользователь обновляет кодогенератор
go get -u github.com/<ваш профиль>/<ваша библиотека>/<путь к кодогенератору>
И затем запускает генерацию заново.
Пример данного подхода к использованию зависимостей я и creker реализовали в библиотеке nan: с библиотекой можно работать как в обычном режиме, через import, так и в варианте кодогенерации.