Пишем модульную Go программу с плагинами
Перевод статьи “Writing Modular Go Programs with Plugins”
Среди всех фич, которые появились в Go 1.8 есть система плагинов. С ее помощью можно создавать модульные программы используя пакеты как динамически загружаемые в рантайме библиотеки.
Это открывает большие возможности. Наверняка вы замечали, что разработчикам больших систем на Go неизбежно приходится структурировать по модулям свое приложение. Мы можем использовать различные инструменты для мудуляризации нашего приложения, такие как системные вызовы, сокеты, RPC/gRPC и т.д. Несмотря на то, что перечисленные подходы работают, все это говорит о том, что не плохо было бы иметь нативную поддержку системы плагинов.
В этой статье я хочу показать небольшой пример создания модульного приложения с использованием систем Go плагинов. Я постараюсь рассказать о всех деталях, знание которых вам понадобится для создание полно функционального примера и затрону тему проектирования более серьезных вещей.
Плагины в Go
Плагины в Go, по своей сути, это пакеты, скомпилированные с указанием флага -buildmode=plugin
в общие динамические библиотеки (файлы .so). Экспортируемые функции и переменные в этом пакете остаются открытыми как ELF символы, которые могут быть использованы в рантайме с помощью пакета plugin
В одной из моих прошлых статей я рассказывал, что Go компилятор, при использовании флага
-buildmode=c-shared
, может делать совместимые с сишными динамические библиотеками.
Ограничения
В версии Go 1.8 плагины доступны доступны только для Linux. Возможно, в будущем что-то поменяется, особенно если к этой фиче проявят много интереса.
Простая программа с плагинами
В этом разделе посмотрим как написать маленькую программу использующую плагины, которая печает в консоли приветствие на различных языках. Каждый язык в этой программе был реализован через плагин.
Можете сразу посмотреть на пример реализации - https://github.com/vladimirvivien/go-plugin-example
Эта программа, greeter.go, использует плагины, которые реализуются пакетами ./eng
и ./chi
, для вывода приветствия на английском и китайском, соответственно. На картинке снизу показана примерная структура программы.
Прежде всего, рассмотрим код eng/greeter.go
который выводит сообщение на английском языке.
Код находится в файле ./eng/greeter.go
1package main
2
3import "fmt"
4
5type greeting string
6
7func (g greeting) Greet() {
8 fmt.Println("Hello Universe")
9}
10
11// экспортируется как символ с именем "Greeter"
12var Greeter greeting
Код выше это все содержимое пакета. Вам нужно учитывать несколько вещей:
- Сам пакет, не зависимо от папки а которой он лежит, должен называться main
- Экспортируемые функции и переменные становятся доступными символами в динамической библиотеке. В примере выше экспортируемая переменная
Greeter
экспортируется как символ в динамической библиотеке.
Компилирование плагина
Плагины компилируются с помощью команд ниже:
go build -buildmode=plugin -o eng/eng.so eng/greeter.go
go build -buildmode=plugin -o chi/chi.so chi/greeter.go
Использование плагинов
Плагины загружаются динамически с использованием специального пакета plugin
. Клиентская программа ./greeter.go использует заранее скомпилированные плагины как указано ниже:
Файл ./greeter.go
package main
import "plugin"; ...
type Greeter interface {
Greet()
}
func main() {
// определяем пакет для загрузки
lang := "english"
if len(os.Args) == 2 {
lang = os.Args[1]
}
var mod string
switch lang {
case "english":
mod = "./eng/eng.so"
case "chinese":
mod = "./chi/chi.so"
default:
fmt.Println("don't speak that language")
os.Exit(1)
}
// загружаем плагин
// 1. открываем .so файл для загрузки символов
plug, err := plugin.Open(mod)
if err != nil {
fmt.Println(err)
os.Exit(1)
}
// 2. выполняем поиск символов(экспортированных функций или переменных)
// в нашем случае, это переменная Greeter
symGreeter, err := plug.Lookup("Greeter")
if err != nil {
fmt.Println(err)
os.Exit(1)
}
// 3. делаем возможным работу с этими символами в нашем коде
// нужно не забывать про тип экспортированных символов
var greeter Greeter
greeter, ok := symGreeter.(Greeter)
if !ok {
fmt.Println("unexpected type from module symbol")
os.Exit(1)
}
// 4. используем загруженный плагин
greeter.Greet()
}
Как видно из кода выше, нужно выполнить несколько определенных шагов для загрузки плагина и работы с его интерфейсами в вашем коде.
- Прежде всего, нужно определится с названием плагина, который нужно загрузить. В нашем случае имя плагина передается через аргументы
os.Args
. - Необходимо добраться до нужного символа “Greeter” с помощью вызова
plguin.Lookup("Greeter")
. Название символа совпадает с названием экспортируемых переменных и функций, определенных в пакете плагина. - Приводим найденный символ к нужному интерфейсу с помощью конструкции
symGreeter.(Greeter)
. - Теперь можем спокойно вызывать
Greet()
, который выведет приветствие на английском.
Запускаем программу
Программа выводит в консоль приветствие на английском или китайском, в зависимости от того, какой параметр указан при запуске, как показано ниже.
> go run greeter.go english
Hello Universe
> go run greeter.go chinese
你好宇宙
Самое главное преимущество такого подхода к проектированию приложения, это возможность в рантайме изменять логику работы приложения(вывод приветствия) без необходимости перекомпилирования самого приложения.
Модульный дизайн Go приложений
Создание модульных приложений на основе Go плагинов требует не менее строгого подход к разработке, чем при проектировании обычных приложений. Тем не менее, благодаря своей разделяющей сущности, плагины позволяют использовать некоторые новые концепции.
ПОнятная интеграция
Когда вы проектируете расширяемое приложение, очень важно чтобы вы заранее продумали как плагины будут интегрироваться в вашу систему. Вы должны предоставить простые и удобные интерфейсы для встраивания плагинов. С другой стороны, разработчики плагинов должны относится к вашей системе как к черному ящику и руководствоваться только только предоставленной вами спецификацией.
Независимость плагинов
Плагин должен быть отдельным и самодостаточным компонентом, никак не имеющим лишних связей с другими компонентами системы. Это позволяет разрабатывать плгины как отдельные приложения со своим собственным циклом разработки.
Идеология Unix
Плагин должен хорошо выполнять одну и только одну задачу.
Понятная документация
Если плагины работают как независимы компоненты, загруженные в рантайме, то очень важно, чтобы они были максимально задокументированы. Например, имена экспортируемых функций и переменных должны быть описаны в документации, это поможет избежать ошибок при их использовании в приложении, это поможет избежать большего количества ошибок.
Испольуйте интерфейсные типы как границы
Go могут экспортировать как функции так и переменные из пакета, независимо от их типа. Вы можете разрабатывать свой плагин, реализующий определенную логику, как набор функций. Но в таком случае вам прийдется загружать и биндить отдельно каждую функцию.
Есть более оптимальный подход - использование интерфейсных типов. Вы можете создать некоторый интерфейс, который будет четко определять функциональность и взаимодействие с ним будет более лаконичным. Загрузка экспортируемого символа, который определен как интерфейс, предоставляет доступ ко всем его методам, а не только к одной функции.
Новая парадигма деплоя
Использование плагинов может повлиять на то, как собираются и деплоятся приложения на Go. Например, автор библиотеки может распространять свой код как скомпилированные компоненты, которые могут быть использованы в рантайме. А это уже довольно сильное отклонение от стандартного цикла go get
, сборки и линковки.
Доверие и безопасность
Если Go сообщество примет идею распространения скомпилированных плагинов и бинарников, то безопасность и доверие станет проблемой. К счастью, сообщество уже довольно зрелое и вполне может помочь с решением проблемы распространения таких библиотек.
Версионирование
Go плагины независимые и самостоятельные сущности, которые должны версионироваться так, чтобы было понятно, какая функциональность включена в какую версию. Я рекомендую использовать семантическое версионирование и добавлять номер версии к названию самого .so файла. Например, файл может называться eng.so.1.0.0
, где 1.0.0
и есть номер версии.
Gosh: модульная командная оболочка
Я хочу представить проект, который я недавно начал. Как только стало известно о появлении плагинов в Go, я захотел написать расширяемый фреймворк для создания интерактивной командной оболочки, в которой все команды будут реализованы с помощью плагинов. В итоге у меня получился Gosh (Go shell).
Узнать больше о Gosh можно вот в этой статье
Gosh использует драйвер оболочки для загрузки плагинов различных команд прям в рантайме. Когда пользовать набирает команду в консоли, драйвер выбирает необходимый плагин, необходимый для обработки этой команды. Конечно, это только первые шаги, но уже виден потенциал и мощь системы плагинов в Go.
Заключение
Меня очень радует идея добавления плагинов в Go. Я считаю, что это одно из самых важных изменений, которое очень сильно повлияет на сбоку и дистрибуцию Go приложений. Плагины позволяют создавать Go программы нового типа, с более динамической связанностью, реализованной через загрузку разделяемых библиотек. И одним из первых примеров такого приложения является Gosh. Теперь мы можем делать более распределенные системы, загружая бинарки плагинов на ноды или используя их в контейнерах при необходимости.
Конечно, плагины не решают всех проблем, а порой добавляют новых. Плагины Например, использование плагинов может увеличить размер вашего приложения. А если посмотреть на использование пакетов в других языках, то вероятно нас ожидает еще один “ад плагинов” связанные с их версионированием и распространением. Конечно, для решения всех этих вопросов необходима Go платформа, поддерживающая как модульную стрктуру приложения так и монолитные программы
Как всегда, если вам понравилась эта статья, жду комментариев и репостов.
И не забудьте купить мою книгу о Go: “Learning Go Programming from Packt Publishing".