Это статья об основах программирования на Go. На канале я рассказываю об опыте перехода в IT с нуля, структурирую информацию и делюсь мнением.
Хой, джедаи и амазонки!
Публикация о применении языка скриптов Lua в Golang: для чего нужно встраивать другой язык в Go, немного о Lua и примеры кода. Go!
1. Введение в Lua
1.1. Общая информация
Lua - это язык скриптов, созданный в Бразилии в 1993 г. тремя ребятами из католического университета Рио-де-Жанейро. Написан на языке Си, а с португальского Lua означает Луна.
По идеологии Lua ближе всего к JavaScript.
Отличительная особенность Lua - баланс между простым синтаксисом и широкими возможностями; а также относительно низкое потребление памяти.
Для Go-разработчика Lua может быть интересен в виде интеграции в основной код на Go.
В основе интеграции Lua идея встраивания интерпретатора Lua в основное приложение. Основное приложение во время исполнения кода вызывает Lua-скрипт для расширения функционала.
Здесь интересный момент, на мой взгляд такой подход - это классный пример принципа O в SOLID - открытости/закрытости. За счёт такого подхода мы можем расширять функциональность основного приложения без его изменяется: нам не потребуется перекомпилировать приложение, и, например, создать новый докер-образ, загрузить его на сервер через CI/CD и т.д.
Т.е. смысл в том, что поменять Lua-скрипт или добавить новый проще, чем вносить изменения в основной код. При этом функциональность приложения изменяется.
1.2. Процесс интеграции Lua в Go
- Приложение на Go включает в себя интерпретатор Lua в виде какой-либо библиотеки. При подготовке к запуску Lua-скрипта, создаётся окружение для языка Lua;
- Предоставляются данные/функции/методы из Go в Lua-окружение;
- Загрузка в Go Lua-скрипта и его выполнение в необходимый момент;
- Обмен данными между окружением Lua и языком Go.
В итоге можно обновить функционал приложения без перекомпиляции: за счёт изменения содержимого lua-скриптов, что обычно намного проще, чем доставить код на сервер, например в виде докер-образа и перезапустить работающий контейнер, тем самым на время останавливая работу сервера.
Посмотрим на пример интеграции Lua в Go. Я нашёл две библиотеки для создания окружения Lua в Go: gopher-lua и go-lua. Будем пользоваться библиотекой gopher-lua.
2. Практикуем Lua
2.1. Hello, Lua
Напишем супер-простой код, чтобы понять логику. В Lua-скрипте будет выводится в терминал одна фраза, и после код завершается:
Скрипт на Lua я вынес в константу. Строка l:=lua.NewState создаёт виртуальную машину для Lua с необходимым окружением.
Исполнение скрипта выполняется через метод DoString с возвратом ошибки. Пока всё просто. Усложним скрипт и добавим структуру.
2.2. Добавляем в код структуру
2.2.1. Lua-скрипт
Добавим структуру и усложним скрипт: пусть теперь он не печатает текст, а выполняет какую-то логическую работу.
Синтаксис Lua похож на Паскаль, с которым занимался в школе, и на Бэйсик - занимался в институте. Ключевое слово end используется в Lua для закрытия блоков кода: условий, циклов, функций.
Без end интерпретатор Lua не будет знать, где заканчивается условная конструкция, и выдаст ошибку. Забегая вперёд, покажу результат без end:
Вернём end на своё место и продолжим писать код.
2.2.2. Основной код
Основной код выглядит так:
Сперва запустим его, потом разберём:
По логике и комментариям в коде всё должно быть понятно. Интерес вызывает пожалуй построчное преобразование полей структуры в "таблицу" Lua.
Единственно, обратите внимание, как я вывожу в терминал информацию о полях структуры: через спецификатор со знаком "плюс".
2.2.3. Таблица Lua
Таблица Lua - это хеш-таблица, тип данных аналогичный карте в Go. С помощью таблицы в Lua представляются такие типы данных Go, как массивы и срезы, структуры и карты.
Для массивов и срезов, ключ хеш-таблицы Lua - это индекс массива/среза +1 из Go, т.е. индексация в Lua с единицы.
У нас в примере - структура. Разберём её.
Когда мы помещаем структуру Go в таблицу Lua, мы по сути представляем поля структуры как пары ключ-значение в таблице.
Имя поля экземпляра структуры становится ключом в таблице, а значение поля экземпляра структуры становится соответствующим значением.
Для полноты картины напомню типы данных в структуре:
Т.е. в библиотеке для Lua есть отдельные функции для преобразования разных типов данных в Lua-данные.
Обратное преобразование из хеш-таблицы Lua в экземпляр структуры Go:
Здесь интересно, какие ещё бывают типы преобразования данных в Lua-код и обратно.
2.2.4. Типы данных между Go и Lua
В библиотеке Lua есть такой код:
Через эти константы происходит преобразование типов данных в библиотеке Lua между Go и Lua.
Детально изучать это не будем, только ознакомительно. Сейчас перейдём дальше - добавим в lua-код функцию.
2.3. Пишем Lua-функцию
Lua-скрипт я пишу внутри Go как многострочную строку. Для серьёзных приложений логично писать скрипты в отдельных файлах с соответствующей подсветкой IDE синтаксиса.
Как добавить функцию Go в Lua мы разобрались. Сейчас напишем простую функцию в скрипте, чтобы просто "потрогать" механику.
Оператор .. означает сложение двух строк, т.е. это аналог "плюса" в Go.
В Lua-коде мы вызываем функцию, присваивая её результат имени пользователя. Результат:
Пойдём дальше, и добавим возможность Lua взаимодействовать с функциями, написанными на Go.
2.4. Добавляем в Lua-скрипт Go-функцию
2.4.1. Общие сведения
Этот подход имеет смысл в том случае, когда наша Go-функция будет как-бы библиотекой, которая выполняет стандартные операции, необходимые во многих сценариях Lua-скриптов. Т.е. может быть одним из способов оптимизации.
Иначе получится не вполне оптимальная ситуация:
Исполняется код на Go ->
исполняется скрипт Lua ->
выполняется кусочек Go-кода для Lua-скрипта ->
завершается исполнение Lua-скрипта -> возвращается основное исполнение Go-кода.
Накрученная ситуация. Но возможно будет полезной в некоторых сценариях.
2.4.2. Пишем скрипт
Возвращаемся к коду. Доработаем структуру и скрипт:
Обратите внимание, как пишутся комментарии в Luа: через двойной минус, как в SQL.
Теперь наш скрипт будет считать количество лет до выхода на пенсию.
2.4.3. Go-функция для Lua
Далее напишем код новой функции, которую зарегистрируем для Lua, у неё особая сигнатура:
Сигнатура функций, предназначенных для понимания Lua вполне конкретная: принимает в параметре состояние окружения Lua, а возвращает количество значений, загружаемых в Lua.
2.4.4. Основной код
Код основной функции также немного изменится с учётом новых полей структуры и регистрации функции для Lua:
Основное здесь - регистрация функции в виде глобальной переменной внутри окружения Lua.
2.4.5. Результат
Вот как выглядит результат работы кода:
Теперь посмотрим как добавляется Go-метод в Lua.
2.5. Добавляем Go-метод в Lua
Изменим написанную ранее функцию calculateYearsToRetire так, чтобы это был метод.
Допишем скрипт:
В Lua такой синтаксис для описания метода - через двоеточие.
Так выглядит код метода, преобразованного из функции:
А так выглядит основной код, основное я выделил:
Мы обернули Go-метод в требуемую сигнатуру функции, которая принимает состояние Lua и возвращает количество значений.
Метод Push библиотеки gother-lua добавляет в стек окружения Lua полученное значение. Стек - это структура данных, в нём хранятся данные во время выполнения кода. У Lua свой стек, у каждой горутины в Go также есть свой стек.
В Lua-скрипте есть строка:
person.YearsToRetire = person:calculateYearsToRetire()
Левой части выражения присвоится значение из верхушки стека Lua.
А сейчас хочу развить эту тему и используя методы, инкапсулировать все данные из го-структуры, предоставляя луа-скрипту только методы.
2.6. Инкапсулируем данные для Lua
Предоставим Lua-скрипту только методы взаимодействия с данными. При этом доступ к данным напрямую запретим.
А также сделаем вложенность структуры, чтобы посмотреть как это можно реализовать в Lua.
Ещё я убрал часть рассмотренного выше кода, который не нужен для изучения темы инкапсуляции.
2.6.1. Lua-скрипт
Структура и Lua-скрипт теперь выглядят так:
Структура в Go расширена эмбендингом. А в скрипте у нас нет прямого обращения к хеш-таблице. Вместо этого используются только методы: для определения возраста, установки статуса и витиеватая конструкция "поиск адреса" - "поиск города":
city = person:findAdress():findCity()
С оператором двоеточие мы познакомились в предыдущем разделе, но здесь он используется последовательно. Это что-то вроде синтаксического сахара. В го такая запись была бы примерно такой:
city := person.findAdress().findCity()
2.6.2. Пишем методы
Мы напишем обычные го-методы, без сигнатур для луа:
Естественно у методов может быть и более сложная логика.
2.7.3. Основной код
Основной код выглядит так:
и продолжение:
Что здесь интересного:
- Мы не передаём в lua-окружение данные из структуры;
- Мы не читаем из lua-окружения данные для обновления структуры;
- Мы в один из ключей исходной хеш-таблицы lua добавили в виде значения для одного ключа другую хеш-таблицу. Так обеспечил наследование по-свойствам (данным) из go-структуры с эмбендингом.
- При регистрации метода findCity в lua-окружении я указал соответствующую хеш-таблицу, а именно addressLua, а не personLua как для других методов.
2.7.4. Вывод в терминал
Результат работы программы:
Выделенные две строки - печать из Lua-скрипта для демонстрации что в нём происходит при вызове методов внутри Lua-окружения.
В третьей строке вывода в терминал видим, что для поля IsSenior установлено значение true: скрипт отработал.
В целом, на этом всё с разбором синтаксиса Lua и его интеграции в Go.
Остался интересный момент: где хранить Lua-скрипты?
3. Хранение Lua-скриптов
Какие есть способы хранения Lua-скриптов? Вариант с константой неприемлем для боевого кода, т.к. вся затея интеграции Lua в Go в том, что скрипт можно легко поменять, не трогая основной код.
Есть вариант хранить скрипты в файлах, но если файлов уже сотни, можно самим запутаться. Можно хранить скрипты в БД. Да-да, полноценный скрипт, который может состоять из тысяч, а то и десятков тысяч символов может быть удобно хранить в БД.
Здесь не будет примера, просто решил поделиться мыслями на счёт как и где хранить скрипты.
4. Выводы
Подведём итоги:
4.1. Именования
Названия переменных/функций/методов должны быть одинаковыми в Lua и Go, чтобы мог быть обмен данными.
Т.е. если в Go мы регистрируем функцию с именем calculateYearsToRetire:
То в Lua-скрипте у этой функции должно быть такое же имя:
При этом go-компилятор не увидит ошибки, если названия отличаются. Нужно внимательно за этим следить самостоятельно.
4.2. Обмен данными Go-Lua-Go
В Lua можно передавать данные из Go, можно предоставлять функции и методы для работы с данными в Go, можно комбинировать эти две возможности: например часть данных в Lua можно читать напрямую, а другие читать/изменять только через методы.
4.3. Принцип открытости/закрытости
Интеграция Lua в Go может быть полезной, когда требуется соблюдать принцип открытости/закрытости по SOLID: мы легко можем изменить функциональность приложения, не изменяя код.
4.4. Хранение скриптов
Хорошая идея - хранить скрипты в БД.
4.5. Эмбендинг в Lua
В Lua можно передавать связанные go-структуры и работать с ними в Lua соответственно, например вызывая метод от метода от экземпляра структуры:
4.6. Функции внутри Lua
В Lua можно написать функции на языке Lua, а можно использовать Go-функции, предварительно зарегистрированные в Go для их применения в окружении Lua.
4.7. Почему Lua?
Синтаксис Lua похож на Паскаль/Бейсик/SQL. Он считается простым и малозатратным по расходам памяти. Интеграция языка Lua в Go может быть полезна, если нужно развить принцип открытости/закрытости SOLID в приложении.
На этом у меня всё. Я напомню, что стажировку нашёл благодаря акселерации после курса Яндекс Практикума "Go разработчик с нуля", в которой изредка попадаются вакансии от компаний-партнёров, которые не ищут себе специалистов на hh.ru или других агрегаторах, а ищут в более нишевых источниках: попасть в такие компании проще, т.к. конкуренция не 1,5-2 тысячи откликов на место, а в разы меньше. Если интересно узнать подробнее - напиши в ТГ.
Благодарю, что дочитали публикацию до конца. Успехов, и будем на связи.
Бро, ты уже здесь? 👉 Подпишись на канал для начинающих IT-специалистов «Войти в IT» в Telegram, будем изучать IT вместе 👨💻👩💻👨💻