Начало работы с Lua

API7.ai

September 23, 2022

OpenResty (NGINX + Lua)

После общего понимания основ NGINX мы продолжим изучение Lua. Это язык программирования, используемый в OpenResty, и необходимо освоить его базовый синтаксис.

Lua — это небольшой и изящный скриптовый язык, созданный в университетской лаборатории в Бразилии, чьё название означает "красивая луна" на португальском. NGINX был создан в России, Lua — в Бразилии, а OpenResty — в Китае, стране автора. Интересно, что эти три одинаково умные технологии с открытым исходным кодом появились в странах БРИКС, а не в Европе или Америке.

Lua был разработан как простой, лёгкий и встраиваемый язык-клей, который не стремится быть большим и мощным. Хотя вы, возможно, не будете писать код на Lua в своей повседневной работе, Lua широко используется. Многие онлайн-игры, такие как World of Warcraft, используют Lua для написания плагинов; Redis, база данных ключ-значение, имеет встроенный Lua для управления логикой.

С другой стороны, хотя библиотека Lua относительно проста, она легко может вызывать библиотеки на языке C, и многие зрелые коды на языке C могут быть использованы для неё. Например, в OpenResty вам часто нужно будет вызывать функции NGINX и OpenSSL на языке C, благодаря способности Lua и LuaJIT легко взаимодействовать с библиотеками C.

Здесь я проведу вас через быстрое ознакомление с типами данных и синтаксисом Lua, чтобы вы могли более плавно изучать OpenResty в дальнейшем.

Среда и hello world

Нам не нужно специально устанавливать стандартную среду Lua 5.1, потому что OpenResty больше не поддерживает стандартный Lua, только LuaJIT. Обратите внимание, что синтаксис Lua, который я представляю здесь, также совместим с LuaJIT и не основан на последней версии Lua 5.3.

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

$ ll /usr/local/Cellar/openresty/1.13.6.2/luajit/bin/luajit lrwxr-xr-x 1 ming admin 18B 4 2 14:54 /usr/local/Cellar/openresty/1.13.6.2/luajit/bin/luajit -> luajit-2.1.0-beta3

Вы также можете найти его в каталоге исполняемых файлов системы.

$ which luajit /usr/local/bin/luajit

Проверьте версию LuaJIT.

$ luajit -v LuaJIT 2.1.0-beta2 -- Copyright (C) 2005-2017 Mike Pall. http://luajit.org/

После проверки этой информации вы можете создать новый файл 1.lua и использовать LuaJIT для запуска кода hello world.

$ cat 1.lua print("hello world") $ luajit 1.lua hello world

Конечно, вы также можете использовать resty для его запуска, зная, что он в конечном итоге выполняется с помощью LuaJIT.

$ resty -e 'print("hello world")' hello world

Оба способа запуска hello world возможны. Я предпочитаю подход с использованием resty, потому что много кода OpenResty также запускается с помощью resty позже.

Типы данных

В Lua не так много типов данных, и вы можете вернуть тип значения с помощью функции type, например, следующей.

$ resty -e 'print(type("hello world")) print(type(print)) print(type(true)) print(type(360.0)) print(type({})) print(type(nil)) '

Будет выведена следующая информация.

string function boolean number table nil

Это основные типы данных в Lua. Давайте кратко их представим.

Строка

В Lua строка является неизменяемым значением. Если вы хотите изменить строку, вы должны создать новую. Этот подход имеет свои преимущества и недостатки: преимущество в том, что даже если одна и та же строка появляется много раз, в памяти будет только одна её копия, но недостаток также очевиден: если вы хотите изменять и объединять строки, вы создаёте много лишних ненужных строк.

Давайте рассмотрим пример, чтобы проиллюстрировать этот недостаток. В Lua мы используем две точки для обозначения сложения строк. Следующий код объединяет числа от 1 до 10 в виде строк.

$ resty -e 'local s = "" for i = 1, 10 do s = s .. tostring(i) end print(s)'

Здесь мы выполняем цикл десять раз, и только последний результат — это то, что нам нужно; девять новых строк между ними бесполезны. Они не только занимают дополнительное пространство, но и потребляют ненужные операции CPU.

Конечно, у нас будет решение для этого позже в разделе оптимизации производительности.

Кроме того, в Lua у вас есть три способа выразить строку: одинарные кавычки, двойные кавычки и длинные скобки ([[]]). Первые два относительно легко понять и они обычно используются в других языках, так зачем нужны длинные скобки?

Давайте посмотрим на конкретный пример.

$ resty -e 'print([[string has \n and \r]])' string has \n and \r

Вы можете видеть, что строки в длинных скобках не экранируются никаким образом.

Вы можете задать ещё один вопрос: что делать, если строка включает длинные скобки? Ответ прост: добавьте один или несколько символов = в середину длинных скобок.

$ resty -e 'print([=[ string has a [[]]. ]=])' string has a [[]].

Логический тип

Это простой тип, true и false. В Lua только nil и false являются ложными; всё остальное — истина, включая 0 и пустую строку. Мы можем проверить это с помощью следующего кода.

$ resty -e 'local a = 0 if a then print("true") end a = "" if a then print("true") end'

Такой тип суждения не соответствует многим распространённым языкам разработки, поэтому, чтобы избежать ошибок в таких вопросах, вы можете явно указать объект сравнения, как в следующем примере.

$ resty -e 'local a = 0 if a == false then print("true") end '

Число

Числовой тип в Lua реализован как число с плавающей точкой двойной точности. Стоит отметить, что LuaJIT поддерживает режим dual-number, что означает, что LuaJIT хранит целые числа как целые числа, а числа с плавающей точкой — как числа с плавающей точкой двойной точности, в зависимости от контекста.

Кроме того, LuaJIT поддерживает long-long integers для больших целых чисел, как в следующем примере.

$ resty -e 'print(9223372036854775807LL - 1)' 9223372036854775806LL

Функция

Функции являются объектами первого класса в Lua, и вы можете хранить функцию в переменной или использовать её как входящий и исходящий аргумент другой функции.

Например, следующие два объявления функций абсолютно эквивалентны.

function foo() end

и

foo = function () end

Таблица

Таблица — это единственная структура данных в Lua и, естественно, очень важна, поэтому я посвящу ей отдельный раздел позже. Мы можем начать с рассмотрения простого примера кода.

$ resty -e 'local color = {first = "red"} print(color["first"])' red

Пустое значение

В Lua пустое значение — это nil. Если вы определяете переменную, но не присваиваете ей значение, её значение по умолчанию — nil.

$ resty -e 'local a print(type(a))' nil

Когда вы войдёте в систему OpenResty, вы найдёте много пустых значений, таких как ngx.null, и так далее. Мы поговорим об этом позже.

Типы данных в Lua я в основном представил, чтобы дать вам основу. Мы продолжим изучать то, что вам нужно освоить позже в статье. Обучение через практику и использование всегда является наиболее удобным способом усвоения новых знаний.

Стандартные библиотеки

Часто изучение языка — это действительно изучение его стандартных библиотек.

Lua относительно мал и не имеет много встроенных стандартных библиотек. Кроме того, в среде OpenResty стандартная библиотека Lua имеет очень низкий приоритет. Для той же функции я рекомендую сначала использовать API OpenResty, затем функции библиотеки LuaJIT и, наконец, стандартные функции Lua.

API OpenResty > функции библиотеки LuaJIT > стандартные функции Lua — это приоритет, который будет упоминаться неоднократно с точки зрения удобства использования и производительности.

Однако, несмотря на это, мы неизбежно будем использовать некоторые библиотеки Lua в наших реальных проектах. Здесь я выбрал несколько наиболее часто используемых стандартных библиотек, чтобы представить их, и если вы хотите узнать больше, вы можете ознакомиться с официальной документацией Lua.

Библиотека строк

Манипуляции со строками — это то, что мы часто используем, и где больше всего подводных камней.

Одно простое правило: если задействованы регулярные выражения, обязательно используйте ngx.re.*, предоставляемый OpenResty, а не обработку string.* в Lua. Это связано с тем, что регулярные выражения в Lua уникальны и не соответствуют спецификации PCRE, и я уверен, что большинство инженеров не смогут с ними справиться.

Одна из наиболее часто используемых функций библиотеки строк — это string.byte(s [, i [, j ]]), которая возвращает ASCII-код, соответствующий символам s[i], s[i + 1], s[i + 2], ------, s[j]. Значение по умолчанию для i — 1, первый байт, а значение по умолчанию для ji.

Давайте посмотрим на пример кода.

$ resty -e 'print(string.byte("abc", 1, 3)) print(string.byte("abc", 3)) -- Отсутствует третий параметр, третий параметр по умолчанию равен второму, то есть 3 print(string.byte("abc")) -- Отсутствуют второй и третий параметры, оба по умолчанию равны 1 '

Его вывод:

979899 99 97

Библиотека таблиц

В контексте OpenResty я не рекомендую использовать большинство библиотек таблиц, входящих в состав Lua, за исключением нескольких функций, таких как table.concat и table.sort. Что касается их деталей, мы оставим их для главы о LuaJIT.

Здесь я кратко упомяну table.concat. table.concat обычно используется в сценариях объединения строк, как в примере ниже. Это позволяет избежать создания множества бесполезных строк.

$ resty -e 'local a = {"A", "b", "C"} print(table.concat(a))'

Математическая библиотека

Математическая библиотека Lua состоит из стандартного набора математических функций. Введение математической библиотеки обогащает язык программирования Lua и упрощает написание программ.

В проектах OpenResty мы редко используем Lua для математических операций. Тем не менее, две функции, связанные с генерацией случайных чисел, math.random() и math.randomseed(), часто используются, как в следующем коде, который может генерировать два случайных числа в заданном диапазоне.

$ resty -e 'math.randomseed (os.time()) print(math.random()) print(math.random(100))'

Фиктивные переменные

После понимания этих общих стандартных библиотек давайте изучим новую концепцию — фиктивные переменные.

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

Не знаю, как вы к этому относитесь, но для меня, по крайней мере, пытаться дать осмысленные имена этим неиспользуемым переменным — это пытка.

К счастью, в Lua есть идеальное решение для этого, предоставляющее концепцию фиктивной переменной, условно именуемой с использованием подчёркивания, чтобы отбросить ненужные значения и служить заполнителем.

Давайте возьмём стандартную библиотечную функцию string.find в качестве примера, чтобы увидеть использование фиктивных переменных. Эта стандартная библиотечная функция возвращает два значения, представляющие начальный и конечный индексы соответственно.

Если нам нужно только получить начальный индекс, достаточно объявить переменную для получения возвращаемого значения string.find, как показано ниже.

$ resty -e 'local start = string.find("hello", "he") print(start)' 1

Но если вы хотите получить только конечный индекс, то вам нужно использовать фиктивную переменную.

$ resty -e 'local _, end_pos = string.find("hello", "he") print(end_pos)' 2

Кроме использования в возвращаемых значениях, фиктивные переменные часто используются в циклах, как в следующем примере.

$ resty -e 'for _, v in ipairs({4,5,6}) do print(v) end' 4 5 6

И когда нужно игнорировать несколько возвращаемых значений, вы можете повторно использовать одну и ту же фиктивную переменную. Я не буду приводить пример здесь. Можете ли вы попробовать написать пример кода самостоятельно? Вы можете поделиться кодом в разделе комментариев, чтобы обменяться мнениями со мной.

Итог

Сегодня мы быстро рассмотрели структуры данных и синтаксис стандартного Lua, и я уверен, что вы получили первое представление об этом простом и компактном языке. В следующем уроке я расскажу вам о связи между Lua и LuaJIT, где LuaJIT является основной частью OpenResty и заслуживает более глубокого изучения.

Наконец, я хочу оставить вам ещё один вопрос для размышления.

Помните код, который вы изучили в этом посте, когда мы говорили о математической библиотеке? Он генерирует два случайных числа в заданном диапазоне.

$ resty -e 'math.randomseed (os.time()) print(math.random()) print(math.random(100))'

Однако вы могли заметить, что код использует текущую метку времени в качестве начального значения. Есть ли проблема с этим подходом? И как мы должны генерировать хорошие начальные значения? Часто случайные числа, которые мы разрабатываем, не являются случайными и имеют серьёзные проблемы с безопасностью.

Поделитесь своими мнениями с нами, а также поделитесь этим постом с коллегами и друзьями. Давайте общаться и улучшаться вместе.