0%

【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

因为比较简单,这里直接用一段代码加上注释进行说明:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
-- 单行注释
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 个基本类型:

1. nil 表示一个无效值,在条件中相当于 false
2. boolean 布尔类型 true / false
3. number 双精度实浮点数
4. string 字符串
5. userdata 由 C 或 Lua 编写的函数
6. function 表示任意存储在变量中的 C 数据结构
7. thread 表示执行的独立线程,用于执行协同程序
8. table 实际上是一个关联数组,通过构造表达式来完成,索引可以是数字、字符串或表类型

关于变量

+ Lua 变量有三种类型:全局变量、局部变量、表中的域
+ Lua 中的变量全是全局变量,哪怕是语句块或是函数里,除非用 local 显式声明为局部变量
+ 局部变量的作用域为从声明位置开始到所在语句块结束
+ 变量的默认值均为 nil
+ 可以在一行内进行交换 `x, y = y, x`
+ 对 table 的索引用方括号访问,如果是字符串,也可以使用 `t.i` 这种简化写法

关于循环:提供了 while, for, repeat until

关于流程控制:提供了 if, if else

本节源码如下:

```lua
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

和其他语言的函数没有特别大的区别,格式如下:

1
2
3
4
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语言函数可以返回多个值,每个值以逗号隔开

其他一些特性有多返回值和可变参数,具体可以参考如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
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 的运算符也和大部分语言类似,基本上该有的都提供了,这部分也比较简单,直接看代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
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 中字符串可以用双引号,单引号和 [[]] 来定义,更多可参考如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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,这里直接看代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
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 的一种用法,更多的用法可以参考下面的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
-- 简单的 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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
-- 定义一个名为 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("<模块名>"),可以给模块重命名

1
2
3
4
5
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 来处理错误,具体参考下面的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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,格式如下:

1
2
3
4
5
if pcall(function_name, ….) then
-- 没有错误
else
-- 一些错误
end

pcall 以一种”保护模式”来调用第一个参数,因此 pcall 可以捕获函数执行中的任何错误。通常在错误发生时,希望落得更多的调试信息,而不只是发生错误的位置。但 pcall 返回时,它已经销毁了调用桟的部分内容。

Lua提供了 xpcall 函数,xpcall 接收第二个参数——一个错误处理函数,当错误发生时,Lua 会在调用桟展开(unwind)前调用错误处理函数,于是就可以在这个函数中使用 debug 库来获取关于错误的额外信息了。具体可以参考下面的代码

1
2
3
4
5
6
7
8
9
10
11
print("xpcall 例子")
function myfunction ()
n = n/nil
end

function myerrorhandler( err )
print( "ERROR:", err )
end

status = xpcall( myfunction, myerrorhandler )
print(status)

其他更多的内容可见参考资料,这里不再赘述。

参考材料