Начало работы с Lua
API7.ai
September 23, 2022
После общего понимания основ 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, первый байт, а значение по умолчанию для j — i.
Давайте посмотрим на пример кода.
$ 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))'
Однако вы могли заметить, что код использует текущую метку времени в качестве начального значения. Есть ли проблема с этим подходом? И как мы должны генерировать хорошие начальные значения? Часто случайные числа, которые мы разрабатываем, не являются случайными и имеют серьёзные проблемы с безопасностью.
Поделитесь своими мнениями с нами, а также поделитесь этим постом с коллегами и друзьями. Давайте общаться и улучшаться вместе.