出自 温铭 -OpenResty从入门到实战 专栏

在前面内容中,我们提到了一些 Lua 和其他开发语言不同的点,比如下标从 0 开始、默认全局变量等等。在 OpenResty 实际的代码开发中,我们还会遇到更多和 Lua、 LuaJIT 相关的问题点, 下面我会讲其中一些比较常见的。
这里要先提醒一下,即使你知道了所有的 ,但不可避免的,估计还是要自己踩过之后才能印象深刻。当然,不同的是,你能够更块地从坑里面爬出来,并找到症结所在。

下标从 0 开始还是从 1 开始

第一个坑,Lua 的下标是从 1 开始的,这点我们之前反复提及过。但我不得不说,这并非事实的全部。
因为在 LuaJIT 中,使用 ffi.new 创建的数组,下标又是从 0 开始的:

  1. local buf = ffi_new("char[?]", 128)

所以,如果你要访问上面这段代码中 buf 这个 cdata,请记得下标从 0 开始,而不是 1。在使用 FFI 和 C 交互的时候,一定要特别注意这个地方。

正则模式匹配

第二个坑,正则模式匹配问题。OpenResty 中并行着两套字符串匹配方法:Lua 自带的 sting 库,以及 OpenResty 提供的 ngx.re.* API。
其中, Lua 正则模式匹配是自己独有的格式,和 PCRE 的写法不同。下面是一个简单的示例:

resty -e 'print(string.match("foo 123 bar", "%d%d%d"))'  — 123

这段代码从字符串中提取了数字部分,你会发现,它和我们的熟悉的正则表达式完全不同。Lua 自带的正则匹配库,不仅代码维护成本高,而且性能低——不能被 JIT,而且被编译过一次的模式也不会被缓存。
所以,在你使用 Lua 内置的 string 库去做 find、match 等操作时,如果有类似正则这样的需求,不用犹豫,请直接使用 OpenResty 提供的 ngx.re 来替代。只有在查找固定字符串的时候,我们才考虑使用 plain 模式来调用 string 库。
这里我有一个建议:在 OpenResty 中,我们总是优先使用 OpenResty 的 API,然后是 LuaJIT 的 API,使用 Lua 库则需要慎之又慎

json 编码时无法区分 array 和 dict

第三个坑,json 编码时无法区分 array 和 dict。由于 Lua 中只有 table 这一个数据结构,所以在 json 对空 table 编码的时候,自然就无法确定编码为数组还是字典:

resty -e 'local cjson = require "cjson"
local t = {}
print(cjson.encode(t))
'

比如上面这段代码,它的输出是 {},由此可见, OpenResty 的 cjson 库,默认把空 table 当做字典来编码。当然,我们可以通过 encode_empty_table_as_object 这个函数,来修改这个全局的默认值:

resty -e 'local cjson = require "cjson"
cjson.encode_empty_table_as_object(false)
local t = {}
print(cjson.encode(t))
'

这次,空 table 就被编码为了数组:[]
不过,全局这种设置的影响面比较大,那能不能指定某个 table 的编码规则呢?答案自然是可以的,我们有两种方法可以做到。
第一种方法,把 cjson.empty_array 这个 userdata 赋值给指定 table。这样,在 json 编码的时候,它就会被当做空数组来处理:

$ resty -e 'local cjson = require "cjson"
local t = cjson.empty_array
print(cjson.encode(t))
'

不过,有时候我们并不确定,这个指定的 table 是否一直为空。我们希望当它为空的时候编码为数组,那么就要用到 cjson.empty_array_mt 这个函数,也就是我们的第二个方法。
它会标记好指定的 table,当 table 为空时编码为数组。从cjson.empty_array_mt 这个命名你也可以看出,它是通过 metatable 的方式进行设置的,比如下面这段代码操作:

$ resty -e 'local cjson = require "cjson"
local t = {}
setmetatable(t, cjson.empty_array_mt)
print(cjson.encode(t))
t = {123}
print(cjson.encode(t))
'

你可以在本地执行一下这段代码,看看输出和你预期的是否一致。

变量的个数限制

再来看第四个坑,变量的个数限制问题。 Lua 中,一个函数的局部变量的个数,和 upvalue 的个数都是有上限的,你可以从 Lua 的源码中得到印证:

/*
@@ LUAI_MAXVARS is the maximum number of local variables per function
@* (must be smaller than 250).
*/
#define LUAI_MAXVARS            200
/*
@@ LUAI_MAXUPVALUES is the maximum number of upvalues per function
@* (must be smaller than 250).
*/
#define LUAI_MAXUPVALUES        60

这两个阈值,分别被硬编码为 200 和 60。虽说你可以手动修改源码来调整这两个值,不过最大也只能设置为 250。
一般情况下,我们不会超过这个阈值,但写 OpenResty 代码的时候,你还是要留意这个事情,不要过多地使用局部变量和 upvalue,而是要尽可能地使用 do .. end 做一层封装,来减少局部变量和 upvalue 的个数。
比如我们来看下面这段伪码:

local re_find = ngx.re.find
  function foo() ... end
function bar() ... end
function fn() ... end

如果只有函数 foo 使用到了 re_find, 那么我们可以这样改造下:

do
     local re_find = ngx.re.find
     function foo() ... end
end
function bar() ... end
function fn() ... end

这样一来,在 main 函数的层面上,就少了 re_find 这个局部变量。这在单个的大的 Lua 文件中,算是一个优化技巧。