Introducción a Lua

API7.ai

September 23, 2022

OpenResty (NGINX + Lua)

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.

Share article link