【Lua 之旅】01 语言简介
作为系列的第一篇,本文会介绍 Lua 的基本概念和语法,最终的目标是帮助大家更好的为 APISIX 编写插件。
更新历史
- 2022.03.27: 完成第一版
简介与环境安装
Lua 是用 C 编写的脚本语言,设计目的是嵌入在应用程序中提供灵活的扩展和定制功能。关键词:轻量级 + 可扩展 + 自动内存管理。
在 mac 的安装很简单 brew install lua 即可,版本是 5.4.3。安装完成后,当然少不了来个 Hello World,代码很简单:print("Hello World! Welcome to wdxtub.com"),运行也很简单 lua 01-hello-world.lua
注:本文的全部代码放在 gitee.com/wdxtub/lua-tour 中的 tutorial 文件夹,感兴趣可以自取。
基本语法
本节对应代码:01-hello-world.lua
因为比较简单,这里直接用一段代码加上注释进行说明:
-- 单行注释
print("Good Day!")
--[[
多行注释
--]]
print("Another Good Day!")
-- 以下划线开头连接一串大写字母的名字(比如 _VERSION)被保留用于 Lua 内部全局变量
--[[
保留字
and, break, do, else, elseif, end, false, for
function, if, in, local, nil, not, or, repeat
return, then, true, until, while, goto
--]]
-- 直接赋值就是全局变量
a = 10
-- 赋值为 nil 等于删除变量
a = nil
数据类型与流程控制
本节对应代码:02-data-and-flow.lua
Lua 中有 8 个基本类型:
- nil 表示一个无效值,在条件中相当于 false
- boolean 布尔类型 true / false
- number 双精度实浮点数
- string 字符串
- userdata 由 C 或 Lua 编写的函数
- function 表示任意存储在变量中的 C 数据结构
- thread 表示执行的独立线程,用于执行协同程序
- table 实际上是一个关联数组,通过构造表达式来完成,索引可以是数字、字符串或表类型
关于变量
- Lua 变量有三种类型:全局变量、局部变量、表中的域
- Lua 中的变量全是全局变量,哪怕是语句块或是函数里,除非用 local 显式声明为局部变量
- 局部变量的作用域为从声明位置开始到所在语句块结束
- 变量的默认值均为 nil
- 可以在一行内进行交换
x, y = y, x - 对 table 的索引用方括号访问,如果是字符串,也可以使用
t.i这种简化写法
关于循环:提供了 while, for, repeat until。
关于流程控制:提供了 if, if else。
本节源码如下:
print("Type Test")
print("-----------")
print("type(\"Hello world\")=" .. type("Hello world"))
print("type(10.4*3) =" .. type(10.4*3))
print("type(print) =" .. type(print))
print("type(type) =" .. type(type))
print("type(true) =" .. type(true))
print("type(nil) =" .. type(nil))
print("type(type(X)) =" .. type(type(X)))
print("-----------")
print("nil 作比较时应该加上双引号")
print("type(X) = " .. type(X))
print("type(X)==nil =", (type(X)==nil))
print("type(X)==\"nil\" =", (type(X)=="nil"))
print("-----------")
print("布尔值")
print("type(true) = " .. type(true))
print("type(false) = " .. type(false))
print("type(nil) = " .. type(nil))
if false or nil then
print("至少有一个是 true")
else
print("false 和 nil 都为 false")
end
if 0 then
print("数字 0 是 true")
else
print("数字 0 为 false")
end
print("-----------")
print("Lua 默认只有一种 number 类型 -- double 类型")
print("type(2) = " .. type(2))
print("type(2.2) = " .. type(2.2))
print("type(0.2) = " .. type(0.2))
print("type(2e+1) = " .. type(2e+1))
print("type(0.2e-1) = " .. type(0.2e-1))
print("type(7.8263692594256e-06) = " .. type(7.8263692594256e-06))
print("-----------")
print("字符串由一对双引号或单引号来表示,也可以用 2 个方括号 [[]] 表示多行字符串")
b = [[
Welcome to wdxtub.com.
Welcome again.
Have a nice day.
Bye.
]]
print(b)
print("在对一个数字字符串上进行算术操作时,Lua 会尝试将这个数字字符串转成一个数字")
print("\"2\"+6 = ", "2" + 6)
print("\"2\"+ \"6\" = ","2" + "6")
print("\"2 + 6\" = ", "2 + 6")
print("\"-2e2\" * \"6\"" ,"-2e2" * "6")
print("使用 # 来计算字符串的长度")
c = "wdxtub.com"
print("#c = ", #c)
print("#\"wdxtub.com\" =", #"wdxtub.com")
print("-----------")
print("table 的创建是通过\"构造表达式\"来完成,最简单构造表达式是{},用来创建一个空表。也可以在表里添加一些数据,直接初始化表")
d = {}
d["key"] = "value"
key = 10
d[key] = 22
d[key] = d[key] + 11
for k, v in pairs(d) do
print(k .. " : " .. v)
end
print("在 Lua 里表的默认初始索引一般以 1 开始")
e = {"a","b","c","d"}
for key, val in pairs(e) do
print("Key", key)
end
print("table 不会固定长度大小,有新数据添加时 table 长度会自动增长,没初始的 table 都是 nil")
print("-----------")
print("函数可以存在变量里")
function coolFunc(n)
if n == 0 then
return 1
else
return n + coolFunc(n - 1)
end
end
print(coolFunc(5))
coolFuncNew = coolFunc
print(coolFuncNew(5))
print("function 可以以匿名函数的方式通过参数传递")
function testFun(tab,fun)
for k ,v in pairs(tab) do
print(fun(k,v));
end
end
tab={key1="val1",key2="val2"}
testFun(tab,
function(key,val) --匿名函数
return key.."="..val
end
)
print("-----------")
print("while 循环")
a=10
while( a < 20 )
do
print("a 的值为:", a)
a = a+1
end
print("for 循环")
print("for的三个表达式在循环开始前一次性求值,以后不再进行求值")
for i=10,1,-1 do
print(i)
end
print("泛型 for 循环通过一个迭代器函数来遍历所有值")
a = {"one", "two", "three"}
for i, v in ipairs(a) do
print(i, v)
end
print("repeat...until 循环的条件语句在当前循环结束后判断")
a = 10
repeat
print("a的值为:", a)
a = a + 1
until( a > 15 )
print("-----------")
print("if 例子")
a = 10;
if( a < 20 )
then
print("a 小于 20" )
end
print("a 的值为:", a)
print("if elseif else 例子")
a = 100
if( a == 10 )
then
print("a 的值为 10" )
elseif( a == 20 )
then
print("a 的值为 20" )
elseif( a == 30 )
then
print("a 的值为 30" )
else
print("没有匹配 a 的值" )
end
print("a 的真实值为: ", a )
函数与运算符
本节对应代码:03-function-and-operator.lua
和其他语言的函数没有特别大的区别,格式如下:
optional_function_scope function function_name( argument1, argument2, argument3..., argumentn)
function_body
return result_params_comma_separated
end
具体说明:
optional_function_scope: 该参数是可选的制定函数是全局函数还是局部函数,未设置该参数默认为全局函数,如果你需要设置函数为局部函数需要使用关键字 local。function_name: 指定函数名称。argument1, argument2, argument3..., argumentn: 函数参数,多个参数以逗号隔开,函数也可以不带参数。function_body: 函数体,函数中需要执行的代码语句块。result_params_comma_separated: 函数返回值,Lua语言函数可以返回多个值,每个值以逗号隔开
其他一些特性有多返回值和可变参数,具体可以参考如下代码:
function min(num1, num2)
if (num1 < num2) then
result = num1;
else
result = num2;
end
return result;
end
print("调用函数")
print("两值比较最小值为 ",min(20,22))
print("两值比较最小值为 ",min(14,15))
print("可以将函数作为参数传递")
myprint = function(param)
print("[PRINT] -> ", param)
end
function sumCustom(num1, num2, func)
result = num1 + num2
func(result)
end
sumCustom(2020,20, myprint)
print("函数可以有多个返回值")
function maximum(a)
local mi = 1 -- 最大值索引
local m = a[mi] -- 最大值
for i,val in ipairs(a) do
if val > m then
mi = i
m = val
end
end
return m, mi
end
print(maximum({1997,7,1,1949,10,1}))
print("函数可以接受可变数目的参数,用 ... 表示")
function sum(...)
local s = 0
for i, v in ipairs{...} do --> {...} 表示一个由所有变长参数构成的数组
s = s + v
end
return s
end
print(sum(1,2,3,4,5,6,7))
print("可以通过 select(\"#\"...) 来获取参数个数")
function average(...)
result = 0
local arg={...}
for i,v in ipairs(arg) do
result = result + v
end
print("总共传入 " .. select("#",...) .. " 个数")
return result/select("#",...)
end
print("平均值为", average(1,3,5,7,9))
print("调用 select 时,必须传入一个固定实参 selector(选择开关) 和一系列变长参数。如果 selector 为数字 n,那么 select 返回参数列表中从索引 n 开始到结束位置的所有参数列表")
function f(...)
a = select(3,...) -->从第三个位置开始,变量 a 对应右边变量列表的第一个参数
print (a)
print (select(3,...)) -->打印所有列表参数
end
f(0,1,2,3,4,5)
Lua 的运算符也和大部分语言类似,基本上该有的都提供了,这部分也比较简单,直接看代码
print("计算运算符展示")
a = 14
b = 5
print("a=",a)
print("b=",b)
c = a + b
print("a + b 的值为 ", c )
c = a - b
print("a - b 的值为 ", c )
c = a * b
print("a * b 的值为 ", c )
c = a / b
print("a / b 的值为 ", c )
c = a % b
print("a % b 的值为 ", c )
c = a // b
print("a // b 的值为 ", c )
c = a^2
print("a^2 的值为 ", c )
c = -a
print("-a 的值为 ", c )
print("关系运算符展示")
print("a=",a)
print("b=",b)
if( a == b )
then
print("a 等于 b" )
else
print("a 不等于 b" )
end
if( a ~= b )
then
print("a 不等于 b" )
else
print("a 等于 b" )
end
if ( a < b )
then
print("a 小于 b" )
else
print("a 大于等于 b" )
end
if ( a > b )
then
print("a 大于 b" )
else
print("a 小于等于 b" )
end
print("逻辑运算符展示")
a = false
b = true
if ( a and b )
then
print("a and b - 条件为 true" )
else
print("a and b - 条件为 false" )
end
if ( not( a and b) )
then
print("not( a and b) - 条件为 true" )
else
print("not( a and b) - 条件为 false" )
end
print("运算符优先级展示")
a = 20
b = 10
c = 15
d = 5
e = (a + b) * c / d;-- ( 30 * 15 ) / 5
print("(a + b) * c / d 运算值为 :",e )
e = ((a + b) * c) / d; -- (30 * 15 ) / 5
print("((a + b) * c) / d 运算值为 :",e )
e = (a + b) * (c / d);-- (30) * (15/5)
print("(a + b) * (c / d) 运算值为 :",e )
e = a + (b * c) / d; -- 20 + (150/5)
print("a + (b * c) / d 运算值为 :",e )
字符串、迭代器与 table
本节对应代码:04-string-iterator-table.lua
Lua 中字符串可以用双引号,单引号和 [[]] 来定义,更多可参考如下代码:
string1 = "Lua"
print("\"字符串 1 是\"",string1)
string2 = 'wdxtub.com'
print("字符串 2 是",string2)
string3 = [[
"Lua 教程"
没有什么能够阻挡
我对自由的向往
]]
print("字符串 3 是",string3)
astr = "OH! wdxtub.com"
print("字符串操作示例,原始字符串", astr)
print("全部转为大写", string.upper(astr))
print("全部转为小写", string.lower(astr))
print("替换字符串", string.gsub(astr, "OH", "WOW"))
print("查找字符串", string.find(astr, "wdxtub"))
print("字符串反转", string.reverse(astr))
print("字符串格式化输出", string.format("%s:%d", astr, 4))
print("将整型数字转成字符串并拼接", string.char(100,99,98,97))
print("将字符转成整型数值(默认第一个字符)", string.byte("abcd"), string.byte("abcd",4))
print("计算字符串长度", string.len(astr))
print("重复字符串", string.rep(astr, 2, "#"))
print("连接字符串.." .. astr)
print("匹配字符串")
for word in string.gmatch(astr, "%a+") do print(word) end
print("字符串截取", string.sub(astr, 5))
数组与迭代器部分也比较简单,本质上也是 table,这里直接看代码
print("一维数组")
array = {"Lua", "Tutorial"}
for i= 0, 2 do
print(array[i])
end
print("多维数组")
-- 初始化数组
array = {}
for i=1,3 do
array[i] = {}
for j=1,3 do
array[i][j] = i*j
end
end
-- 访问数组
for i=1,3 do
for j=1,3 do
print(array[i][j])
end
end
print("-------------")
print("迭代器")
array = {"wdxtub", "com"}
for key,value in ipairs(array)
do
print(key, value)
end
前面提到数组实际上只是 table 的一种用法,更多的用法可以参考下面的代码:
-- 简单的 table
mytable = {}
print("mytable 的类型是 ",type(mytable))
mytable[1]= "Lua"
mytable["wow"] = "修改前"
print("mytable 索引为 1 的元素是 ", mytable[1])
print("mytable 索引为 wow 的元素是 ", mytable["wow"])
-- alternatetable和mytable的是指同一个 table
alternatetable = mytable
print("alternatetable 索引为 1 的元素是 ", alternatetable[1])
print("mytable 索引为 wow 的元素是 ", alternatetable["wow"])
alternatetable["wow"] = "修改后"
print("mytable 索引为 wow 的元素是 ", mytable["wow"])
-- 释放变量
alternatetable = nil
print("alternatetable 是 ", alternatetable)
-- mytable 仍然可以访问
print("mytable 索引为 wow 的元素是 ", mytable["wow"])
mytable = nil
print("mytable 是 ", mytable)
print("Table 函数")
fruits = {"banana","orange","apple"}
-- 返回 table 连接后的字符串
print("连接后的字符串 ",table.concat(fruits))
-- 指定连接字符
print("连接后的字符串 ",table.concat(fruits,", "))
-- 指定索引来连接 table
print("连接后的字符串 ",table.concat(fruits,", ", 2,3))
-- 在末尾插入
table.insert(fruits,"mango")
print("索引为 4 的元素为 ",fruits[4])
-- 在索引为 2 的键处插入
table.insert(fruits,2,"grapes")
print("索引为 2 的元素为 ",fruits[2])
print("最后一个元素为 ",fruits[5])
table.remove(fruits)
print("移除后最后一个元素为 ",fruits[5])
print("排序前")
for k,v in ipairs(fruits) do
print(k,v)
end
table.sort(fruits)
print("排序后")
for k,v in ipairs(fruits) do
print(k,v)
end
模块与包
Lua 的模块是由变量、函数等已知元素组成的 table,因此创建一个模块很简单,就是创建一个 table,然后把需要导出的常量、函数放入其中,最后返回这个 table 就行。以下为创建自定义模块 module.lua
-- 定义一个名为 module 的模块
module = {}
-- 定义一个常量
module.constant = "这是一个常量"
-- 定义一个函数
function module.func1()
io.write("这是一个公有函数!\n")
end
local function func2()
print("这是一个私有函数!")
end
function module.func3()
func2()
end
return module
模块的结构就是一个 table 的结构,因此可以像操作调用 table 里的元素那样来操作调用模块里的常量或函数。
上面的 func2 声明为程序块的局部变量,即表示一个私有函数,因此是不能从外部访问模块里的这个私有函数,必须通过模块里的公有函数来调用。
加载模块使用 require 函数,如 require("<模块名>"),可以给模块重命名
local m = require("module")
print(m.constant)
m.func3()
对于自定义的模块,模块文件不是放在哪个文件目录都行,函数 require 有它自己的文件路径加载策略,它会尝试从 Lua 文件或 C 程序库中加载模块。
require 用于搜索 Lua 文件的路径是存放在全局变量 package.path 中,当 Lua 启动后,会以环境变量 LUA_PATH 的值来初始这个环境变量。如果没有找到该环境变量,则使用一个编译时定义的默认路径来初始化。
当然,如果没有 LUA_PATH 这个环境变量,也可以自定义设置,在当前用户根目录下打开 .profile 文件(没有则创建,打开 .bashrc 文件也可以)
错误处理
本节对应代码:05-error-handling.lua
我们可以使用两个函数:assert 和 error 来处理错误,具体参考下面的代码
print("assert 来进行检查")
local function add(a,b)
assert(type(a) == "number", "a 不是一个数字")
assert(type(b) == "number", "b 不是一个数字")
return a+b
end
add(10,50)
print("error 来处理错误,后续代码不会执行")
local function func1()
print("start")
error("出错了")
print("你不会看到这句话")
end
func1()
也可以使用函数 pcall(protected call)来包装需要执行的代码。pcall 接收一个函数和要传递给后者的参数,并执行,执行结果:有错误、无错误;返回值true或者或false, errorinfo,格式如下:
if pcall(function_name, ….) then
-- 没有错误
else
-- 一些错误
end
pcall 以一种"保护模式"来调用第一个参数,因此 pcall 可以捕获函数执行中的任何错误。通常在错误发生时,希望落得更多的调试信息,而不只是发生错误的位置。但 pcall 返回时,它已经销毁了调用桟的部分内容。
Lua提供了 xpcall 函数,xpcall 接收第二个参数——一个错误处理函数,当错误发生时,Lua 会在调用桟展开(unwind)前调用错误处理函数,于是就可以在这个函数中使用 debug 库来获取关于错误的额外信息了。具体可以参考下面的代码
print("xpcall 例子")
function myfunction ()
n = n/nil
end
function myerrorhandler( err )
print( "ERROR:", err )
end
status = xpcall( myfunction, myerrorhandler )
print(status)
其他更多的内容可见参考资料,这里不再赘述。