Introducción a Lua
API7.ai
September 23, 2022
Después de comprender los conceptos básicos de NGINX, profundizaremos en Lua. Es el lenguaje de programación utilizado en OpenResty, y es necesario dominar su sintaxis básica.
Lua es un lenguaje de scripting pequeño y sutil, nacido en un laboratorio universitario en Brasil, cuyo nombre significa "luna hermosa" en portugués. NGINX nació en Rusia, Lua en Brasil, y OpenResty en China, el país del autor. Curiosamente, estas tres tecnologías de código abierto igualmente inteligentes provienen de países BRICS, no de Europa o América.
Lua fue diseñado para posicionarse como un lenguaje de pegamento simple, ligero y embebible que no sigue el camino de ser grande y audaz. Aunque es posible que no escribas código Lua directamente en tu trabajo diario, Lua es ampliamente utilizado. Muchos juegos en línea, como World of Warcraft, usan Lua para escribir complementos; Redis, la base de datos clave-valor, tiene Lua integrado para controlar la lógica.
Por otro lado, aunque la biblioteca de Lua es relativamente simple, puede llamar fácilmente a una biblioteca del lenguaje de programación C, y muchos códigos maduros del lenguaje C pueden ser utilizados para ello. Por ejemplo, en OpenResty, a menudo necesitarás llamar a funciones del lenguaje C de NGINX y OpenSSL, gracias a la capacidad de Lua y LuaJIT para acceder fácilmente a las bibliotecas C.
Aquí, te guiaré a través de una familiarización rápida con los tipos de datos y la sintaxis de Lua para que puedas aprender OpenResty más fácilmente más adelante.
Entorno y hola mundo
No necesitamos instalar específicamente un entorno estándar de Lua 5.1 porque OpenResty ya no soporta Lua estándar, solo LuaJIT. Ten en cuenta que la sintaxis de Lua que presento aquí también es compatible con LuaJIT y no se basa en la última versión de Lua 5.3.
Puedes encontrar el directorio y el ejecutable de LuaJIT en el directorio de instalación de OpenResty. Estoy en un entorno Mac y usé brew para instalar OpenResty, por lo que tu ruta local probablemente difiera de la siguiente.
$ 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
También puedes encontrarlo en el directorio de archivos ejecutables del sistema.
$ which luajit
/usr/local/bin/luajit
Verifica la versión de LuaJIT.
$ luajit -v
LuaJIT 2.1.0-beta2 -- Copyright (C) 2005-2017 Mike Pall. http://luajit.org/
Después de verificar esta información, puedes crear un nuevo archivo 1.lua
y usar LuaJIT para ejecutar el código hola mundo
.
$ cat 1.lua
print("hola mundo")
$ luajit 1.lua
hola mundo
Por supuesto, también puedes usar resty
para ejecutarlo directamente, sabiendo que finalmente se ejecuta con LuaJIT.
$ resty -e 'print("hola mundo")'
hola mundo
Ambas formas de ejecutar hola mundo
son posibles. Prefiero el enfoque de resty
porque mucho código de OpenResty también se ejecuta con resty
más adelante.
Tipos de datos
No hay muchos tipos de datos en Lua, y puedes devolver el tipo de un valor con la función type
, como en el siguiente ejemplo.
$ resty -e 'print(type("hola mundo"))
print(type(print))
print(type(true))
print(type(360.0))
print(type({}))
print(type(nil))
'
Se imprimirá la siguiente información.
string
function
boolean
number
table
nil
Estos son los tipos de datos básicos en Lua. Vamos a presentarlos brevemente.
Cadena de texto
En Lua, la cadena de texto es un valor inmutable. Si deseas modificar una cadena, debes crear una nueva. Este enfoque tiene ventajas y desventajas: la ventaja es que incluso si la misma cadena aparece muchas veces, solo hay una copia en memoria, pero la desventaja también es evidente: si deseas modificar y concatenar cadenas, creas muchas cadenas adicionales innecesarias.
Tomemos un ejemplo para ilustrar esta desventaja. En Lua, usamos dos puntos para indicar la adición de cadenas. El siguiente código une los números del 1 al 10 como cadenas.
$ resty -e 'local s = ""
for i = 1, 10 do
s = s .. tostring(i)
end
print(s)'
Aquí hacemos un bucle diez veces, y solo el último resultado es lo que necesitamos; las nueve cadenas nuevas intermedias son inútiles. No solo ocupan espacio adicional, sino que también consumen operaciones de CPU innecesarias.
Por supuesto, tendremos una solución para esto más adelante en la sección de optimización de rendimiento.
Además, en Lua, tienes tres formas de expresar una cadena: comillas simples, comillas dobles y corchetes largos ([[]]
). Las dos primeras son relativamente fáciles de entender y se usan comúnmente en otros lenguajes, entonces, ¿para qué sirven los corchetes largos?
Veamos un ejemplo concreto.
$ resty -e 'print([[cadena tiene \n y \r]])'
cadena tiene \n y \r
Puedes ver que las cadenas en los corchetes largos no se escapan de ninguna manera.
Puedes preguntarte: ¿Qué pasa si la cadena anterior incluye los corchetes largos? La respuesta es simple: agrega uno o más símbolos =
en medio de los corchetes largos.
$ resty -e 'print([=[ cadena tiene un [[]]. ]=])'
cadena tiene un [[]].
Booleano
Este es simple, true
y false
. En Lua, solo nil
y false
son falsos; todo lo demás es verdadero, incluidos el 0 y la cadena vacía. Podemos verificar esto con el siguiente código.
$ resty -e 'local a = 0
if a then
print("true")
end
a = ""
if a then
print("true")
end'
Este tipo de juicio es inconsistente con muchos lenguajes de desarrollo comunes, por lo que para evitar errores en tales asuntos, puedes escribir explícitamente el objeto de comparación, como en el siguiente ejemplo.
$ resty -e 'local a = 0
if a == false then
print("true")
end
'
Número
El tipo de número en Lua se implementa como un número de punto flotante de doble precisión. Vale la pena mencionar que LuaJIT soporta el modo dual-number
, lo que significa que LuaJIT almacena enteros como enteros y números de punto flotante como números de doble precisión, dependiendo del contexto.
Además, LuaJIT soporta enteros largos
para números grandes, como en el siguiente ejemplo.
$ resty -e 'print(9223372036854775807LL - 1)'
9223372036854775806LL
Función
Las funciones son ciudadanos de primera clase en Lua, y puedes almacenar una función en una variable o usarla como referencia de entrada y salida a otra función.
Por ejemplo, las siguientes dos declaraciones de función son exactamente equivalentes.
function foo()
end
y
foo = function ()
end
Tabla
La tabla es la única estructura de datos en Lua y es naturalmente muy importante, por lo que le dedicaré una sección especial más adelante. Podemos comenzar mirando un ejemplo de código simple.
$ resty -e 'local color = {first = "red"}
print(color["first"])'
red
Valor nulo
En Lua, un valor nulo es nil
. Si defines una variable pero no le asignas un valor, su valor predeterminado es nil
.
$ resty -e 'local a
print(type(a))'
nil
Cuando entres en el sistema OpenResty, encontrarás muchos valores nulos, como ngx.null
, y otros. Hablaremos más sobre esto más adelante.
Los tipos de datos de Lua, principalmente los presentaré hasta aquí, para darte una base. Continuaremos aprendiendo lo que necesitas dominar más adelante en el artículo. Aprender a través de la práctica y el uso es siempre la forma más conveniente de absorber nuevos conocimientos.
Bibliotecas estándar comunes
A menudo, aprender un lenguaje se trata realmente de aprender sus bibliotecas estándar.
Lua es relativamente pequeño y no tiene muchas bibliotecas estándar integradas. Además, en el entorno de OpenResty, la biblioteca estándar de Lua tiene una prioridad muy baja. Para la misma función, recomiendo usar primero la API de OpenResty, luego las funciones de la biblioteca de LuaJIT, y finalmente las funciones normales de Lua.
API de OpenResty > funciones de la biblioteca de LuaJIT > funciones estándar de Lua
es una prioridad que se mencionará repetidamente en términos de usabilidad y rendimiento.
Sin embargo, a pesar de esto, inevitablemente usaremos algunas bibliotecas de Lua en nuestros proyectos reales. Aquí, he seleccionado algunas de las bibliotecas estándar más utilizadas para presentarlas, y si deseas saber más, puedes consultar la documentación oficial de Lua.
Biblioteca de cadenas
La manipulación de cadenas es algo que usamos con frecuencia y donde hay más trampas.
Una regla simple es que si se involucran expresiones regulares, asegúrate de usar ngx.re.*
proporcionado por OpenResty para resolverlas, no el procesamiento string.*
de Lua. Esto se debe a que las expresiones regulares de Lua son únicas y no se ajustan a la especificación PCRE, y creo que la mayoría de los ingenieros no podrán manejarlas.
Una de las funciones de la biblioteca de cadenas más utilizadas es string.byte(s [, i [, j ]])
, que devuelve el código ASCII correspondiente a los caracteres s[i], s[i + 1], s[i + 2], ------, s[j]
. El valor predeterminado de i
es 1, el primer byte, y el valor predeterminado de j
es i
.
Veamos un código de ejemplo.
$ resty -e 'print(string.byte("abc", 1, 3))
print(string.byte("abc", 3)) -- Falta el tercer parámetro, el tercer parámetro es igual al segundo por defecto, que es 3
print(string.byte("abc")) -- Faltan el segundo y tercer parámetros, ambos predeterminados a 1
'
Su salida es:
979899
99
97
Biblioteca de tablas
En el contexto de OpenResty, no recomiendo usar la mayoría de las bibliotecas de tablas que vienen con Lua, excepto algunas funciones como table.concat
y table.sort
. En cuanto a sus detalles, los dejaremos para el capítulo de LuaJIT.
Aquí mencionaré brevemente table.concat
. table.concat
se usa generalmente en escenarios de concatenación de cadenas, como en el siguiente ejemplo. Puede evitar generar muchas cadenas inútiles.
$ resty -e 'local a = {"A", "b", "C"}
print(table.concat(a))'
Biblioteca matemática
La biblioteca matemática de Lua consiste en un conjunto estándar de funciones matemáticas. La introducción de la biblioteca matemática enriquece el lenguaje de programación Lua y facilita la escritura de programas.
En los proyectos de OpenResty, rara vez usamos Lua para hacer operaciones matemáticas. Sin embargo, dos funciones relacionadas con números aleatorios, math.random()
y math.randomseed()
, se usan comúnmente, como en el siguiente código, que puede generar dos números aleatorios en un rango especificado.
$ resty -e 'math.randomseed (os.time())
print(math.random())
print(math.random(100))'
Variables ficticias
Después de entender estas bibliotecas estándar compartidas, aprendamos un nuevo concepto: las variables ficticias.
Imagina un escenario donde una función devuelve múltiples valores, algunos de los cuales no necesitamos, entonces, ¿cómo deberíamos recibir estos valores?
No sé cómo te sientes al respecto, pero para mí, al menos, es tortuoso intentar dar nombres significativos a estas variables no utilizadas.
Afortunadamente, Lua tiene una solución perfecta para esto, proporcionando el concepto de una variable ficticia convencionalmente nombrada con un guion bajo para descartar valores no necesarios y servir como un marcador de posición.
Tomemos la función de la biblioteca estándar string.find
como ejemplo para ver el uso de variables ficticias. Esta función de la biblioteca estándar devuelve dos valores que representan los subíndices de inicio y fin, respectivamente.
Si solo necesitamos obtener el subíndice de inicio, es simple declarar una variable para recibir el valor de retorno de string.find
como sigue.
$ resty -e 'local start = string.find("hello", "he")
print(start)'
1
Pero si solo deseas obtener el subíndice de fin, entonces debes usar la variable ficticia.
$ resty -e 'local _, end_pos = string.find("hello", "he")
print(end_pos)'
2
Además de usarse en el valor de retorno, las variables ficticias se usan a menudo en bucles, como en el siguiente ejemplo.
$ resty -e 'for _, v in ipairs({4,5,6}) do
print(v)
end'
4
5
6
Y cuando hay múltiples valores de retorno para ignorar, puedes reutilizar la misma variable ficticia. No daré un ejemplo aquí. ¿Puedes intentar escribir un código de ejemplo como este tú mismo? Te invito a publicar el código en la sección de comentarios para compartir e intercambiar conmigo.
Resumen
Hoy, hemos echado un vistazo rápido a las estructuras de datos y la sintaxis de Lua estándar, y estoy seguro de que has tenido una primera impresión de este lenguaje simple y compacto. En la próxima lección, te llevaré a través de la relación entre Lua y LuaJIT, con LuaJIT siendo el enfoque principal de OpenResty y vale la pena profundizar en él.
Finalmente, quiero dejarte con una pregunta más para reflexionar.
¿Recuerdas el código que aprendiste en esta publicación cuando hablamos de la biblioteca matemática? Genera dos números aleatorios en un rango especificado.
$ resty -e 'math.randomseed (os.time())
print(math.random())
print(math.random(100))'
Sin embargo, es posible que hayas notado que el código se siembra con la marca de tiempo actual. ¿Hay algún problema con este enfoque? ¿Y cómo deberíamos generar buenas semillas? A menudo, los números aleatorios que desarrollamos no son aleatorios y tienen grandes riesgos de seguridad.
Te invito a compartir tus opiniones con nosotros, y también a compartir esta publicación con tus colegas y amigos. Comunicémonos y mejoremos juntos.