Найти в Дзене
Я, Golang-инженер

#78. Lua в Golang

Это статья об основах программирования на Go. На канале я рассказываю об опыте перехода в IT с нуля, структурирую информацию и делюсь мнением. Хой, джедаи и амазонки! Публикация о применении языка скриптов Lua в Golang: для чего нужно встраивать другой язык в Go, немного о Lua и примеры кода. Go! Lua - это язык скриптов, созданный в Бразилии в 1993 г. тремя ребятами из католического университета Рио-де-Жанейро. Написан на языке Си, а с португальского Lua означает Луна. По идеологии Lua ближе всего к JavaScript. Отличительная особенность Lua - баланс между простым синтаксисом и широкими возможностями; а также относительно низкое потребление памяти. Для Go-разработчика Lua может быть интересен в виде интеграции в основной код на Go. В основе интеграции Lua идея встраивания интерпретатора Lua в основное приложение. Основное приложение во время исполнения кода вызывает Lua-скрипт для расширения функционала. Здесь интересный момент, на мой взгляд такой подход - это классный пример принципа
Оглавление

Это статья об основах программирования на 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

  1. Приложение на Go включает в себя интерпретатор Lua в виде какой-либо библиотеки. При подготовке к запуску Lua-скрипта, создаётся окружение для языка Lua;
  2. Предоставляются данные/функции/методы из Go в Lua-окружение;
  3. Загрузка в Go Lua-скрипта и его выполнение в необходимый момент;
  4. Обмен данными между окружением 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:

Ошибка синтаксиса в Lua-коде
Ошибка синтаксиса в Lua-коде

Вернём 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 мы разобрались. Сейчас напишем простую функцию в скрипте, чтобы просто "потрогать" механику.

Lua-код
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 такой синтаксис для описания метода - через двоеточие.

Так выглядит код метода, преобразованного из функции:

Код
Код

А так выглядит основной код, основное я выделил:

-18

Мы обернули 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. Основной код

Основной код выглядит так:

Код
Код

и продолжение:

Код
Код

Что здесь интересного:

  1. Мы не передаём в lua-окружение данные из структуры;
  2. Мы не читаем из lua-окружения данные для обновления структуры;
  3. Мы в один из ключей исходной хеш-таблицы lua добавили в виде значения для одного ключа другую хеш-таблицу. Так обеспечил наследование по-свойствам (данным) из go-структуры с эмбендингом.
  4. При регистрации метода 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 тысячи откликов на место, а в разы меньше. Если интересно узнать подробнее - напиши в ТГ.

Благодарю, что дочитали публикацию до конца. Успехов, и будем на связи.

https://ru.freepik.com
https://ru.freepik.com

Бро, ты уже здесь? 👉 Подпишись на канал для начинающих IT-специалистов «Войти в IT» в Telegram, будем изучать IT вместе 👨‍💻👩‍💻👨‍💻