По-настоящему живая перезагрузка кода в golang


74fd257305d14a4d94fe2e70eb68dc5d.png Если вы разрабатываете веб-приложения на го, то эта статья, возможно, будет вам интересна. До того, как перейти на go, я в основном программировал на PHP и мне всегда нравилось то, что можно сохранить файл, перезагрузить страницу и увидеть результат, который сгенерирован уже новым кодом. Большие программы на go могут компилироваться несколько десятков секунд, что весьма быстро, но всё равно ощутимо. Возможно ли сделать аналог Java hotswap (замена тела метода в runtime), ведь Go компилируется в нативный код? Ответ — да, возможно, но только для разработки. В данный момент мне неизвестно о готовых инструментах, которые бы позволяли это автоматизировать. В этой статье я хотел бы продемонстрировать proof-of-concept «живой перезагрузки» с использованием пакета plugin в go1.8beta2 и пакета github.com/bouk/monkey. Пытливый читатель скорее всего уже догадывается, что мы будем делать.

Подготовка


Итак, перед тем, как начать, нам понадобятся:
  1. go1.8beta2 или новее
  2. Linux (в go1.9 должно работать и в macOS)
  3. Пакет 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 и есть много вещей, которые бы стоило доработать перед тем, как этим можно было бы реально пользоваться:
  1. Нет простого способа пропатчить таким образом любую функцию или метод — нужно заранее иметь список функций, которые можно передать в monkey.Patch
  2. Собранные плагины занимают столько же, сколько соответствующая программа на go и в некоторых случаях компиляция плагина может занимать сравнимое время с компиляцией всей программы. С этим сложно что-то поделать, кроме как путем уменьшения связности в приложении
  3. monkey.Patch не является thread-safe и в теории может вызвать SEGFAULT в приложении

Тем не менее, я надеюсь, вам понравилась эта статья и вы узнали что-то новое для себя. Поздравляю всех с наступлением новогодних каникул :)!

Комментарии (1)

  • 2 января 2017 в 13:13

    0

    Милота. Не хватает разве что возвращения многомерных массивов SQL-запросами.

© Habrahabr.ru