Если вы разрабатываете веб-приложения на го, то эта статья, возможно, будет вам интересна. До того, как перейти на 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 в приложении.

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

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


  1. handicraftsman
    02.01.2017 13:13

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


  1. andboson
    02.01.2017 18:36
    -2

    Хот релоад без костылей в коде:
    https://github.com/tockins/realize

    к примеру
    Есть и другие.


    1. youROCK
      02.01.2017 18:37

      С остальными подходами есть один нюанс: они все требуют перезапуска приложения. Мой подход работает без перезапуска


      1. andboson
        02.01.2017 23:27

        Я уважаю ваш труд. Но вы же не пишете без ошибок? Паники — ваши постоянные друзья будут. Т.е. просто вручную придется запускать опять )


        1. youROCK
          02.01.2017 23:32

          Ну, строго говоря, panic в веб-обработчике будет просто отдаваться клиенту как 500 ошибка. Вам нужно посадить panic в отдельной горутине, чтобы приложение вышло. В большинстве случаев, в моей практике, паники все-таки происходят локализовано и не роняют приложение, даже во время разработки.


  1. datacompboy
    03.01.2017 03:34
    +1

    В свое время я так же создал живое обновление кода для Перла, в связи с чем открыл для себя erlang...


  1. miolini
    03.01.2017 10:51
    +1

    Не вижу кода, который выгружает предыдущую версию. Когда именно это происходит?


    1. youROCK
      03.01.2017 10:52

      Предыдущую версию выгрузить в go нельзя


      1. rustler2000
        03.01.2017 11:22

        Блин — а я так надеялся, что можно сделать gouchdb :S


      1. miolini
        03.01.2017 11:27

        Ну т.е. много раз такое не сделать, иначе ресурсы закончатся в системе. Процесс придется перезагружать.


        1. youROCK
          03.01.2017 12:09

          Да. Перезагружать процесс периодически все равно будет нужно, если вы захотите поменять значение константы, добавить новый тип и многое другое :).


      1. equand
        03.01.2017 12:59

        Почему нельзя сделать парент процесс, который всего лишь рутер соединений и форкнутые процессы обработчики и форкать его каждый раз когда файлы меняются?


        Парент процесс будет держать соединение и отправлять его в новый форк, Старый форк соединения принимать не будет и будет по мере заканчивания очереди отрубаться чисто сам. Примерно так и работает mod_php mod_python и иные (только там фактически форк на каждый запущенный скрипт).


        1. youROCK
          03.01.2017 14:46

          Да можно, конечно, только весь смысл hot swap кода заключается в том, что полная перекомпиляция не требуется и соответственно время цикла сохранил-подождал_пока_скомпилится-проверил сокращается и благодаря этому ускоряется разработка и дебаг. Ваша же идея описывает "плавный рестарт" и нужна в продакшене, а во время разработки она ничего не ускоряет


          1. equand
            03.01.2017 15:21

            Я писал подобный Вашему релоадер на Python. Основная проблема с которой я столкнулся это не загрузка нового, а выгрузка старого — по-крайней мере с JIT Python это не работает верно. Дело в том, что GC там удаляет "когда посчитает нужным". В итоге нужно обходить все локали и удалять их, но появляется другая проблема — закрытие соединений и файлов. И вот так раз за разом идет куча ликов.
            Помимо этого проблема с форсированной загрузкой из файла, а не из прекомпилированного кода тоже появляется. Помимо этого reload(module) работает неконсистентно, то есть его вообще надо не использовать, а нельзя, иначе импорт эррор.


            Я понимаю, что это Питоновские заморочки, но более стабильного метода я не вижу. С форком просто плавный переход от бывшей машины к новой.