По-настоящему живая перезагрузка кода в golang
Подготовка
Итак, перед тем, как начать, нам понадобятся:
- go1.8beta2 или новее
- Linux (в go1.9 должно работать и в macOS)
- Пакет github.com/bouk/monkey
Основная идея
Для go есть библиотека для monkey patching’а кода, которая позволяет подменять тело функций и методов «на лету» под названием monkey. Она работает путем переписывания тела функции и вставки туда своего кода. Подробнее на английском о реализации можно почитать здесь: bouk.co/blog/monkey-patching-in-go. Чтобы эта библиотека работала правильно, проект должен быть собран с отключенным inlining кода: «go build -gcflags=-l». Эта библиотека нам подходит, но есть проблема: откуда взять указатель на метод с новым кодом вместо старого?
Плагины в go1.8
В версии go1.8 появится механизм плагинов — возможность скомпилировать код на go в качестве .so-файла, который можно динамически загрузить в уже работающее приложение и начать его использовать. Подробнее вы можете почитать про это здесь: tip.golang.org/pkg/plugin. Плагин собирается с динамической линковкой и может немного отличаться по тому, как он работает, от обычного кода на go. Тем не менее, как правило, никаких отличий в работе нет.
Собираем всё вместе
Создадим веб-сервер с двумя пакетами: main и handlers (импорты опущены для краткости).
// файл main.go
package main
func main() {
http.HandleFunc("/example", handlers.Example)
http.HandleFunc("/reload", handlers.Reload)
http.ListenAndServe(":9999", nil)
}
// файл handlers/example.go
package handlers
func Example(rw http.ResponseWriter, req *http.Request) {
req.ParseForm()
fmt.Fprintf(rw, "Hello, %s!", req.Form.Get("name"))
}
func Reload(rw http.ResponseWriter, req *http.Request) {
p, _ := plugin.Open("handlers.so")
sym, _ := p.Lookup("Example")
monkey.Patch(Example, sym)
}
Запустим этот веб-сервер и проверим, что он работает:
$ curl 'http://localhost:9999/example?name=Yuriy'
Hello, Yuriy!
Создадим ещё один пакет, куда положим плагин (местоположение директории не имеет значения, в примере я положу этот файл в handlers/plug/main.go):
package main
import (
"fmt"
"net/http"
)
import "C"
func Example(rw http.ResponseWriter, req *http.Request) {
req.ParseForm()
fmt.Fprintf(rw, "Hello modified, %s!", req.Form.Get("name"))
}
Обратите внимание на import "C"
, которого не было в оригинальном пакете. Этот импорт нужен для корректной работы плагина.
Соберем этот плагин следующей командой:
$ go build -buildmode plugin -o handlers.so
Файл handlers.so нужно поместить в директорию, из которой вы запускали веб-сервер, поскольку в функции handlers.Reload указан относительный путь до handlers.so.
Теперь попробуем осуществить горячую замену кода:
$ curl 'http://localhost:9999/reload'
Если никаких ошибок в логе веб-сервера не было, значит всё прошло успешно и можно проверить, что код обновился:
$ curl 'http://localhost:9999/example?name=Yuriy'
Hello modified, Yuriy!
Заключение
Я выложил полный код примера на github: github.com/YuriyNasretdinov/hotreload-example. Хотел бы отметить, что это лишь proof-of-concept и есть много вещей, которые бы стоило доработать перед тем, как этим можно было бы реально пользоваться:
- Нет простого способа пропатчить таким образом любую функцию или метод — нужно заранее иметь список функций, которые можно передать в monkey.Patch
- Собранные плагины занимают столько же, сколько соответствующая программа на go и в некоторых случаях компиляция плагина может занимать сравнимое время с компиляцией всей программы. С этим сложно что-то поделать, кроме как путем уменьшения связности в приложении
- monkey.Patch не является thread-safe и в теории может вызвать SEGFAULT в приложении
Тем не менее, я надеюсь, вам понравилась эта статья и вы узнали что-то новое для себя. Поздравляю всех с наступлением новогодних каникул :)!
Комментарии (1)
2 января 2017 в 13:13
0↑
↓
Милота. Не хватает разве что возвращения многомерных массивов SQL-запросами.