小土刀4.0

【Lua 之旅】01 语言简介

约 14 分钟

作为系列的第一篇,本文会介绍 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 个基本类型:

  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。

本节源码如下:

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)

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

参考材料