强曰为道

与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

第 04 章 - Lua 语言基础

第 04 章 - Lua 语言基础

4.1 为什么是 Lua?

OpenResty 选择 Lua 作为嵌入语言有以下原因:

特性说明
极轻量LuaJIT 运行时 < 1MB,启动纳秒级
快速 FFI直接调用 C 函数,无需绑定层
低 GC 停顿增量式 GC,停顿微秒级
JIT 编译LuaJIT 将热点代码编译为机器码
协程支持原生协程,配合 OpenResty 实现同步风格的异步编程
简单易学语法精简,几小时可上手

4.2 基本语法

4.2.1 变量与类型

-- 全局变量(不需要声明,直接赋值)
name = "OpenResty"
version = 1.25
is_stable = true

-- 局部变量(推荐使用 local,性能更好且作用域更小)
local port = 8080
local host = "localhost"
local enabled = true

-- nil 表示"无值"
local result = nil  -- 等同于未定义

-- 类型检查
print(type(name))      -- "string"
print(type(version))   -- "number"
print(type(is_stable)) -- "boolean"
print(type(result))    -- "nil"

-- 字符串操作
local str = "Hello, OpenResty!"
print(string.len(str))         -- 18
print(string.upper(str))       -- "HELLO, OPENRESTY!"
print(string.find(str, "Open")) -- 8 11
print(string.sub(str, 1, 5))   -- "Hello"
print(string.format("Port: %d", port)) -- "Port: 8080"

-- 多行字符串(Lua 特有)
local template = [[
<html>
<head><title>Gateway</title></head>
<body><h1>Welcome</h1></body>
</html>
]]

注意:Lua 中只有 falsenil 是假值,其他所有值(包括 0"")都是真值。这与 Python 不同。

4.2.2 数字与精度

-- Lua 5.1 的数字都是双精度浮点数
local a = 10
local b = 3.14

-- LuaJIT 支持整数模式
local big_int = 9007199254740991  -- 安全整数范围

-- 位运算(LuaJIT 扩展)
local bit = require "bit"
local flags = bit.bor(0x01, 0x02)   -- 按位或: 3
local masked = bit.band(flags, 0x01) -- 按位与: 1
local shifted = bit.lshift(1, 10)   -- 左移: 1024

4.2.3 控制结构

-- if-elseif-else
local status = 200
if status == 200 then
    print("OK")
elseif status == 301 then
    print("Moved Permanently")
elseif status == 404 then
    print("Not Found")
else
    print("Status: " .. status)
end

-- 三元运算符模拟(Lua 没有三元运算符)
local msg = (status == 200) and "OK" or "Error"

-- for 循环(数值型)
for i = 1, 10 do
    print(i)
end

for i = 10, 1, -1 do  -- 递减
    print(i)
end

-- for 循环(泛型,遍历表)
local config = {host = "localhost", port = 8080, debug = true}
for k, v in pairs(config) do
    print(k .. " = " .. tostring(v))
end

-- while 循环
local retry = 0
while retry < 3 do
    retry = retry + 1
    -- 执行重试逻辑
end

-- repeat-until(至少执行一次)
local attempts = 0
repeat
    attempts = attempts + 1
until attempts >= 3

4.3 表(Table):Lua 的核心数据结构

Table 是 Lua 中唯一的数据结构,可以同时作为数组、字典、对象使用。

4.3.1 数组模式

-- 数组(索引从 1 开始,这是 Lua 的约定)
local fruits = {"apple", "banana", "cherry"}

print(fruits[1])  -- "apple"(不是 fruits[0])
print(#fruits)    -- 3(长度操作符)

-- 添加元素
table.insert(fruits, "date")        -- 追加到末尾
table.insert(fruits, 2, "avocado")  -- 插入到位置 2

-- 删除元素
table.remove(fruits, 1)  -- 删除第一个元素

-- 遍历数组(推荐 ipairs)
for i, fruit in ipairs(fruits) do
    print(i, fruit)
end

4.3.2 字典模式

-- 字典(键值对)
local user = {
    id = 12345,
    name = "张三",
    email = "[email protected]",
    roles = {"admin", "user"},
}

-- 访问
print(user.name)        -- "张三"
print(user["email"])    -- "[email protected]"

-- 动态键
local key = "id"
print(user[key])        -- 12345

-- 添加/修改
user.last_login = os.time()
user.email = "[email protected]"

-- 删除
user.email = nil

-- 遍历字典(使用 pairs)
for k, v in pairs(user) do
    print(k, ":", v)
end

4.3.3 混合模式

-- 同时作为数组和字典
local config = {
    -- 字典部分
    host = "localhost",
    port = 8080,

    -- 数组部分
    "default_server",   -- [1]
    "backup_server",    -- [2]

    -- 嵌套表
    ssl = {
        enabled = true,
        cert = "/etc/ssl/cert.pem",
    },
}

print(config[1])              -- "default_server"
print(config.host)            -- "localhost"
print(config.ssl.cert)        -- "/etc/ssl/cert.pem"

4.3.4 表操作技巧

-- 表浅拷贝
local function shallow_copy(t)
    local copy = {}
    for k, v in pairs(t) do
        copy[k] = v
    end
    return copy
end

-- 表深拷贝
local function deep_copy(t)
    if type(t) ~= "table" then return t end
    local copy = {}
    for k, v in pairs(t) do
        copy[deep_copy(k)] = deep_copy(v)
    end
    return setmetatable(copy, getmetatable(t))
end

-- 表合并
local function merge_tables(t1, t2)
    local result = {}
    for k, v in pairs(t1) do result[k] = v end
    for k, v in pairs(t2) do result[k] = v end
    return result
end

-- 检查表是否包含值
local function has_value(t, val)
    for _, v in pairs(t) do
        if v == val then return true end
    end
    return false
end

-- 表转字符串(调试用)
local function dump_table(t, indent)
    indent = indent or ""
    local parts = {}
    for k, v in pairs(t) do
        if type(v) == "table" then
            table.insert(parts, indent .. tostring(k) .. " = {")
            table.insert(parts, dump_table(v, indent .. "  "))
            table.insert(parts, indent .. "}")
        else
            table.insert(parts, indent .. tostring(k) .. " = " .. tostring(v))
        end
    end
    return table.concat(parts, "\n")
end

4.4 函数

4.4.1 基本函数

-- 标准定义
local function add(a, b)
    return a + b
end

-- 函数变量(等价写法)
local add = function(a, b)
    return a + b
end

-- 多返回值
local function split_uri(uri)
    local path = uri:match("^([^%?]*)")  -- 路径部分
    local query = uri:match("%?(.+)$")    -- 查询字符串
    return path, query
end

local path, query = split_uri("/api/users?page=1")
-- path  = "/api/users"
-- query = "page=1"

-- 可变参数
local function log(level, ...)
    local args = {...}
    local parts = {}
    for i, v in ipairs(args) do
        table.insert(parts, tostring(v))
    end
    print("[" .. level .. "] " .. table.concat(parts, " "))
end

log("INFO", "Request from", "192.168.1.1", "status:", 200)

4.4.2 闭包(Closure)

闭包是 Lua 函数式编程的基础,也是 OpenResty 中常用模式:

-- 闭包示例:生成计数器
local function make_counter(initial)
    local count = initial or 0
    return function()
        count = count + 1
        return count
    end
end

local counter = make_counter(0)
print(counter())  -- 1
print(counter())  -- 2
print(counter())  -- 3

-- 实际应用:限流器工厂
local function make_rate_limiter(max_requests, window_seconds)
    local requests = {}
    local window = window_seconds

    return function(key)
        local now = os.time()
        -- 清理过期记录
        if requests[key] then
            local new_requests = {}
            for _, ts in ipairs(requests[key]) do
                if now - ts < window then
                    table.insert(new_requests, ts)
                end
            end
            requests[key] = new_requests
        else
            requests[key] = {}
        end

        -- 检查限流
        if #requests[key] >= max_requests then
            return false, "Rate limit exceeded"
        end

        table.insert(requests[key], now)
        return true, "OK"
    end
end

local limiter = make_rate_limiter(100, 60)  -- 100次/分钟
local ok, msg = limiter("user_123")

4.4.3 方法(冒号语法)

-- 冒号语法:自动传递 self 参数
local Router = {}
Router.__index = Router

function Router.new()
    local self = setmetatable({}, Router)
    self.routes = {}
    return self
end

-- 冒号定义:自动有 self 参数
function Router:add(path, handler)
    self.routes[path] = handler
end

function Router:match(path)
    return self.routes[path]
end

-- 使用
local router = Router.new()
router:add("/api/users", function() return "users handler" end)  -- 冒号调用
router:add("/api/orders", function() return "orders handler" end)

local handler = router:match("/api/users")
print(handler())  -- "users handler"

4.5 元表(Metatable)

元表是 Lua 实现面向对象和运算符重载的机制。

-- __index: 访问不存在的键时调用
local defaults = {
    timeout = 5000,
    retries = 3,
    debug = false,
}

local config = {timeout = 10000}
setmetatable(config, {__index = defaults})

print(config.timeout)  -- 10000 (config 自己的值)
print(config.retries)  -- 3 (从 defaults 获取)

-- __tostring: 自定义字符串表示
local Response = {}
Response.__index = Response

function Response.new(status, body)
    return setmetatable({status = status, body = body}, Response)
end

function Response:__tostring()
    return string.format("Response(%d, %s)", self.status, self.body)
end

local res = Response.new(200, '{"ok":true}')
print(tostring(res))  -- "Response(200, {"ok":true})"

-- __call: 让表可以像函数一样被调用
local Middleware = {}
Middleware.__index = Middleware

function Middleware.new(name, fn)
    return setmetatable({name = name, fn = fn}, Middleware)
end

function Middleware:__call(...)
    ngx.log(ngx.INFO, "Middleware: ", self.name)
    return self.fn(...)
end

local auth = Middleware.new("auth", function(req)
    return req.headers["Authorization"] ~= nil
end)

-- 可以像函数一样调用
local ok = auth({headers = {Authorization = "Bearer xxx"}})

4.6 面向对象编程

-- 类定义模式
local BasePlugin = {}
BasePlugin.__index = BasePlugin

function BasePlugin.new(name, priority)
    local self = setmetatable({}, BasePlugin)
    self.name = name
    self.priority = priority or 0
    self.enabled = true
    return self
end

function BasePlugin:execute(ctx)
    error("execute() not implemented")
end

function BasePlugin:enable()
    self.enabled = true
end

function BasePlugin:disable()
    self.enabled = false
end

-- 继承
local RateLimitPlugin = setmetatable({}, {__index = BasePlugin})
RateLimitPlugin.__index = RateLimitPlugin

function RateLimitPlugin.new(max_req, window)
    local self = BasePlugin.new("rate-limit", 100)
    setmetatable(self, RateLimitPlugin)
    self.max_requests = max_req
    self.window = window
    return self
end

function RateLimitPlugin:execute(ctx)
    if not self.enabled then return true end
    -- 限流逻辑
    local key = ctx.client_ip
    local count = ngx.shared.rate_limit:get(key) or 0
    if count >= self.max_requests then
        ctx.status = 429
        ctx.body = '{"error":"Rate limit exceeded"}'
        return false
    end
    ngx.shared.rate_limit:incr(key, 1, 0, self.window)
    return true
end

-- 使用
local limiter = RateLimitPlugin.new(100, 60)
local ok = limiter:execute({client_ip = "192.168.1.1"})

4.7 模块系统

-- 定义模块: /usr/local/openresty/lua/gateway/utils.lua
local _M = {}
_M._VERSION = "1.0.0"

local function private_func()  -- 私有函数(local)
    return "private"
end

function _M.public_func()  -- 公有函数
    return "public: " .. private_func()
end

return _M

-- 使用模块
local utils = require "gateway.utils"
print(utils.public_func())

注意:OpenResty 中使用 require 加载的模块会被缓存,整个 Worker 进程共享同一份。如果需要独立实例,使用工厂函数模式。

4.8 协程(Coroutine)

Lua 的协程是 OpenResty 实现同步风格异步编程的关键。

-- 基本协程
local co = coroutine.create(function()
    coroutine.yield("step 1")
    coroutine.yield("step 2")
    return "step 3"
end)

print(coroutine.resume(co))  -- true, "step 1"
print(coroutine.resume(co))  -- true, "step 2"
print(coroutine.resume(co))  -- true, "step 3"
print(coroutine.status(co))  -- "dead"

-- 生产者-消费者模式
local function producer()
    return coroutine.create(function()
        for i = 1, 5 do
            coroutine.yield(i * 10)
        end
    end)
end

local co = producer()
while true do
    local ok, value = coroutine.resume(co)
    if not ok or value == nil then break end
    print("Received:", value)
end

OpenResty 中的协程应用

-- OpenResty 利用协程将异步 API 包装为同步风格
-- 底层原理:ngx.socket.tcp 等 API 在 I/O 等待时 yield,完成后 resume

local sock = ngx.socket.tcp()
sock:settimeout(3000)

-- 这看起来是同步代码,但底层是异步的
-- 1. 调用 connect → yield(等待连接完成)
-- 2. 连接完成 → resume(继续执行)
local ok, err = sock:connect("127.0.0.1", 6379)

-- 发送数据 → yield → 发送完成 → resume
local bytes, err = sock:send("PING\r\n")

-- 接收数据 → yield → 接收完成 → resume
local data, err = sock:receive()

sock:setkeepalive()

4.9 OpenResty 核心 API

4.9.1 ngx.var — Nginx 变量

-- 读取 Nginx 内置变量
local method = ngx.var.request_method
local uri = ngx.var.uri
local host = ngx.var.host
local client_ip = ngx.var.remote_addr
local args = ngx.var.args

-- 读取自定义变量(需要在 nginx.conf 中用 set 定义)
-- local user_id = ngx.var.user_id

-- 设置变量(仅在 rewrite 和 access 阶段有效)
ngx.var.my_custom_var = "some_value"

4.9.2 ngx.req — 请求操作

-- 获取请求方法
local method = ngx.req.get_method()

-- 获取请求头
local headers = ngx.req.get_headers()
local content_type = headers["content-type"]
local auth = headers["authorization"]

-- 获取 URI 参数
local args = ngx.req.get_uri_args()
local page = args.page or 1
local limit = args.limit or 20

-- 读取请求体
ngx.req.read_body()
local body = ngx.req.get_body_data()

-- 获取请求体(文件上传场景)
local file = ngx.req.get_body_file()

-- 修改请求
ngx.req.set_method(ngx.HTTP_POST)
ngx.req.set_uri("/new/path", true)  -- true 表示内部重定向
ngx.req.set_header("X-Forwarded-For", ngx.var.remote_addr)

4.9.3 ngx.resp — 响应操作

-- 获取响应头
local resp_headers = ngx.resp.get_headers()
local content_type = resp_headers["content-type"]

4.9.4 ngx.shared — 共享内存

# nginx.conf 中定义
lua_shared_dict my_cache 10m;
local cache = ngx.shared.my_cache

-- 写入(带过期时间)
cache:set("key1", "value1", 60)         -- 60 秒过期
cache:set("key2", 100, 300)

-- 仅当不存在时写入
local ok, err = cache:add("key3", "value3", 60)

-- 读取
local value, flags = cache:get("key1")

-- 递增(常用于计数器)
local new_val, err = cache:incr("key2", 1, 0, 60)  -- +1,初始值 0,TTL 60s

-- 删除
cache:delete("key1")

-- 清空
cache:flush_all()

-- 获取使用情况
local capacity = cache:capacity()      -- 总容量
local free = cache:free_space()        -- 剩余空间

4.9.5 ngx.timer — 定时器

-- 一次性定时器
local function delayed_task(premature, arg1, arg2)
    if premature then
        return  -- Worker 正在退出,放弃任务
    end
    ngx.log(ngx.INFO, "Delayed task: ", arg1, ", ", arg2)
end

local ok, err = ngx.timer.at(5, delayed_task, "hello", 42)

-- 周期性定时器(OpenResty 1.19.3+)
local function periodic_task(premature)
    if premature then return end
    -- 定期清理过期数据
    ngx.shared.cache:flush_expired(100)  -- 最多清理 100 个
end

ngx.timer.every(30, periodic_task)

4.10 LuaJIT FFI

FFI(Foreign Function Interface)允许 Lua 直接调用 C 库函数,无需编写 C 绑定层。

local ffi = require "ffi"

-- 声明 C 函数
ffi.cdef[[
    int getpid(void);
    int atoi(const char *nptr);
    typedef struct { double x; double y; } point_t;
]]

-- 调用 C 函数
local pid = ffi.C.getpid()
ngx.log(ngx.INFO, "PID: ", pid)

local num = ffi.C.atoi("12345")

-- 创建 C 结构体
local point = ffi.new("point_t")
point.x = 3.14
point.y = 2.71

FFI 实际应用:高性能字符串处理

local ffi = require "ffi"
local C = ffi.C

ffi.cdef[[
    void *memcpy(void *dest, const void *src, size_t n);
    int memcmp(const void *s1, const void *s2, size_t n);
    size_t strlen(const char *s);
]]

-- 快速字符串比较(避免 Lua 字符串对象创建)
local function fast_compare(s1, s2)
    if #s1 ~= #s2 then return false end
    return C.memcmp(s1, s2, #s1) == 0
end

-- FFI 调用比纯 Lua 快数十倍

FFI 库加载

-- 加载动态库
local lib = ffi.load("ssl")  -- 加载 libssl.so

ffi.cdef[[
    // OpenSSL 函数声明
    typedef struct ssl_ctx_st SSL_CTX;
    SSL_CTX *SSL_CTX_new(void *method);
]]

-- 使用
local ctx = lib.SSL_CTX_new(nil)

4.11 错误处理

-- pcall: 保护调用,捕获错误
local ok, result = pcall(function()
    error("something went wrong")
end)

if not ok then
    ngx.log(ngx.ERR, "Error: ", result)
end

-- xpcall: 带错误处理函数的保护调用
local function error_handler(err)
    return debug.traceback(err, 2)
end

local ok, err = xpcall(function()
    error("test error")
end, error_handler)

if not ok then
    ngx.log(ngx.ERR, "Error with trace:\n", err)
end

-- 常见模式:安全调用外部服务
local function safe_redis_call(red, cmd, ...)
    local ok, err = pcall(function(...)
        return red[cmd](red, ...)
    end, ...)

    if not ok then
        ngx.log(ngx.ERR, "Redis error: ", err)
        return nil, err
    end

    return ok
end

4.12 性能优化技巧

技巧说明示例
使用 local局部变量比全局变量快local x = 1 vs x = 1
避免表创建复用表对象减少 GC预分配、对象池
使用 table.concat避免 .. 拼接大量字符串table.concat(parts, ",")
避免 string.gsub对简单场景使用 string.find + string.sub
使用 FFI计算密集型操作用 FFI 调用 C
避免 pairs有序遍历用 ipairs,数组操作更快
JIT 友好避免在热路径中使用 pcallrequire
-- ❌ 不推荐:循环中拼接字符串
local result = ""
for i = 1, 1000 do
    result = result .. tostring(i) .. ","  -- 每次创建新字符串
end

-- ✅ 推荐:使用 table.concat
local parts = {}
for i = 1, 1000 do
    parts[#parts + 1] = tostring(i)
end
local result = table.concat(parts, ",")

-- ❌ 不推荐:循环中创建表
for i = 1, 1000 do
    local t = {key = "value"}  -- 每次创建新表
    process(t)
end

-- ✅ 推荐:复用表
local t = {}
for i = 1, 1000 do
    t.key = "value"
    process(t)
    t.key = nil
end

4.13 常用字符串操作速查

操作函数示例
长度#strstring.len(str)#"hello" → 5
查找string.find(str, pattern)string.find("hello", "ell") → 2,4
截取string.sub(str, i, j)string.sub("hello", 2, 4) → “ell”
替换string.gsub(str, pat, repl)string.gsub("hello", "l", "L") → “heLLo”
格式化string.format(fmt, ...)string.format("id=%d", 42) → “id=42”
匹配string.match(str, pattern)string.match("v123", "%d+") → “123”
全匹配string.gmatch(str, pattern)迭代器
大写string.upper(str)string.upper("hello") → “HELLO”
小写string.lower(str)string.lower("HELLO") → “hello”
重复string.rep(str, n)string.rep("ab", 3) → “ababab”
反转string.reverse(str)string.reverse("hello") → “olleh”
字节码string.byte(str, i)string.byte("A") → 65
字符string.char(n)string.char(65) → “A”

上一章← 第 03 章 - Nginx 与 Lua 执行阶段 下一章第 05 章 - 路由与动态路由 →