1. Lua 解决的问题

Lua 是一种轻量级、可嵌入的脚本语言,核心解决的是"如何在宿主程序(尤其是 C/C++ 应用)中灵活、高效地扩展逻辑"的问题。

1.1 配置与业务逻辑分离

传统软件常把业务规则硬编码在 C/C++ 中,每次修改都需重新编译。Lua 允许将配置、规则、算法写成脚本,由主程序动态加载执行,实现不改源码、不重启程序即可调整行为。

1.2 嵌入式脚本需求

游戏引擎、数据库、Web 服务器等程序需要一种体积小、速度快、易于与宿主语言交互的脚本语言。Lua 解释器仅约 200KB,极易嵌入;通过 C API 可无缝调用宿主函数、操作宿主数据。

1.3 游戏开发与热更新

  • 逻辑脚本化:将战斗系统、AI 行为、UI 交互从引擎核心剥离到 Lua。

  • 热更新:玩家无需重新下载客户端,仅更新 Lua 脚本即可修复 Bug 或调整数值。

1.4 替代笨重的配置文件

相比 XML/JSON/YAML,Lua 是一门完整的编程语言,解决了"配置文件表达能力不足"的问题——可在配置中写函数、做条件判断、引用变量。

1.5 高性能与低资源占用

Lua 采用基于寄存器的虚拟机增量垃圾回收,运行效率接近原生代码,解决了脚本语言"慢"和"吃内存"的问题。

1.6 跨平台与可移植性

Lua 完全由 ANSI C 编写,可在任何有 C 编译器的平台上运行(包括嵌入式设备),解决了环境不一致问题。


2. Lua 环境搭建

  1. 访问下载页:https://luabinaries.sourceforge.net/

  2. 选择对应系统的版本(Windows 建议下载 lua-5.4.x_Win64_bin.zip

  3. 解压后将目录添加到系统 PATH 环境变量

  4. 打开终端(CMD / PowerShell),输入以下命令验证

    lua -v
    # 或带版本号的命令,如:lua54 -v
  5. 进入 Lua 交互模式测试

    lua
    > print("hello world")
    hello world

3. IDE 插件配置

GoLand(或其他 JetBrains 系列 IDE)中编写 Lua 脚本,建议安装 EmmyLua 插件,可提供语法高亮、代码补全、断点调试等功能。


4. Lua 语法基础

4.1 变量与动态类型

Lua 变量分为全局变量局部变量。使用 local 声明局部变量;省略 local 则默认为全局变量。变量类型在运行时动态确定。

-- 变量定义与类型动态变化
name = "学习笔记"
print(name, type(name))        -- 输出: 学习笔记    string

name = 1                       -- 同一变量可被重新赋值为不同类型
print(name, type(name))        -- 输出: 1    number

-- 多变量并行赋值
age, addr = 15, "成都"
print(age, type(age), addr, type(addr))
-- 输出: 15    number    成都    string

4.2 代码块与作用域

使用 do...end 定义代码块。local 声明的局部变量仅在当前代码块内有效,离开作用域后其值变为 nil

-- 使用 do...end 定义代码块
do
    local a = 10   -- 局部变量:仅在 do...end 内可见
    b = 20         -- 全局变量:省略 local,在整个脚本中可见
    print("代码块内 a:", a, type(a))              -- 10    number
    print("代码块内 name + a:", name + a, type(name + a))  -- 11    number
end

-- 代码块外部
print("代码块外 a:", a, type(a))  -- nil    nil(局部变量在块外不可访问)

-- 错误示范:字符串与数字不能直接用 + 相加
-- print("学习笔记" + b)  -- 报错:attempt to perform arithmetic on a string value

-- 正确做法:字符串拼接应使用 ..
name = "学习笔记"
print(name .. b, type(name .. b))  -- 学习笔记20    string

4.3 数组(Array)与表(Table)

Lua 没有独立的数组类型,数组本质上是表(table)的一种特殊形式——通过数字索引来模拟数组行为。需要特别注意:Lua 的默认索引从 1 开始,而非其他语言常见的 0。

# 运算符的局限性# 只能正确计算连续数字索引的元素个数。如果数组中间存在 nil 空洞或非连续索引,结果将不准确,此时需手动维护长度。


4.3.1 数组的定义与遍历
do
    -- 数组(表的特殊形式),默认索引从 1 开始
    local array = {"A", nil, "B", "C", "D", 1, 2, 3, 4}
    
    print(array, type(array))      -- 输出表地址及类型:table
    print(#array)                  -- 输出长度(受 nil 影响可能不准确):1
    print(array[#array])           -- 尝试获取最后一个元素(结果可能不符合预期)
    
    print("------------ 三种遍历方式对比 --------------")
    
    -- 方式 1:按索引遍历(for i = 1, #array)
    -- 适用于已知连续范围的数组,遇到 nil 会输出 nil 但不会中断
    print("-------- 按索引遍历 -------------")
    for i = 1, #array do
        print(i, array[i])
    end
    
    -- 方式 2:pairs 遍历
    -- 遍历所有键值对(包括非连续索引和字符串键),遇到 nil 值会跳过
    print("---------- pairs 遍历 -----------")
    for k, v in pairs(array) do
        print("pairs 键:", k, "值:", v)
    end
    
    -- 方式 3:ipairs 遍历
    -- 仅遍历从 1 开始的连续数字索引,遇到第一个 nil 立即终止
    print("---------- ipairs 遍历 ----------")
    for k, v in ipairs(array) do
        print("ipairs 键:", k, "值:", v)
    end
end

三种遍历方式对比总结:

遍历方式 索引范围 遇到 nil 的行为 适用场景
for i = 1, #array 1 到 #array 输出 nil,继续执行 连续索引的数组
pairs 所有键值对 跳过该键值对 字典、混合表、稀疏数组
ipairs 从 1 开始的连续整数 终止遍历 标准连续数组

4.3.2 表的定义方式
do
    -- 方式 1:空表
    local t1 = {}
    
    -- 方式 2:初始化键值对(推荐)
    
    -- ① 隐式数字索引(即数组形式,默认从 1 开始)
    local t2 = {"Lua", "Python", "Java"}
    -- 等价于:t2[1]="Lua", t2[2]="Python", t2[3]="Java"
    
    -- ② 显式键(支持任意类型作为键)
    local t3 = {
        name = "张三",              -- 字符串键(等价于 t3["name"])
        age = 20,                   -- 值为数字
        is_student = true,         -- 值为布尔
        [100] = "分数",             -- 显式数字键
        ["hobby-list"] = {"读书", "运动"}  -- 含特殊字符的字符串键,值为嵌套表
    }
    
    -- ③ 混合键(数组部分 + 字典部分共存)
    local t4 = {
        10, 20, 30,                -- 隐式数字键:1、2、3
        name = "李四",             -- 显式字符串键
        [false] = "布尔键"         -- 显式布尔键(Lua 中 boolean 也可作为键)
    }
end

4.3.3 访问表元素
do
    local t = {name = "张三", age = 20, [1] = "hello"}
    
    -- 方式 1:点语法(.)
    -- 仅适用于符合标识符规则的字符串键(字母、数字、下划线,且不能以数字开头)
    print(t.name)   -- 输出:张三
    print(t.age)    -- 输出:20
    -- print(t.1)    -- 语法错误!点语法不能用于数字键或含特殊字符的键
    
    -- 方式 2:方括号语法([])
    -- 通用访问方式,支持所有类型的键
    print(t["name"])  -- 输出:张三
    print(t[1])       -- 输出:hello
    print(t["age"])   -- 输出:20
    
    -- 访问不存在的键,返回 nil(不会报错)
    print(t.gender)   -- 输出:nil
end

4.3.4 修改表元素
do
    local t = {name = "张三"}
    
    -- 1. 添加新键值对
    t.age = 20                -- 添加字符串键
    t[100] = "满分"           -- 添加数字键
    t["hobby"] = "跑步"       -- 显式字符串键
    
    print("添加后:")
    for k, v in pairs(t) do
        print(k, v)
    end
    
    -- 2. 修改已有键值对
    t.name = "李四"
    print("修改后 name:", t.name)  -- 输出:李四
    
    -- 3. 删除键值对(赋值为 nil)
    t.age = nil
    print("删除后 age:", t.age)    -- 输出:nil
end

4.3.5 关键注意事项
  1. 数字键与字符串键严格区分
    t[1]t["1"] 是两个完全不同的键,访问时切勿混淆。

  2. # 运算符的适用范围
    仅对连续数字索引的数组有效。字典型表或稀疏数组不要使用 # 求长度,结果不可预期。

  1. nil 对遍历的影响

    • ipairs() 遇到 nil立即终止,后续元素不再遍历。

    • 如需保留"空位",建议用 0 或空字符串 "" 占位,而非 nil

  2. 表的引用特性(非值拷贝)

    local t1 = {a = 1}
    local t2 = t1      -- t2 只是 t1 的引用,指向同一块内存
    t2.a = 2
    print(t1.a)        -- 输出:2(修改 t2 会影响 t1)
    
    -- 如需独立拷贝,需手动深拷贝或使用第三方库
  3. 索引起点
    Lua 数组索引默认从 1 开始。虽然可以手动指定 t[0],但标准库函数(如 table.insertipairs)均按 1 处理,建议遵循惯例。

5. Lua 流程控制

5.1 条件判断(if 语句)

Lua 的 if 语句与其他语言逻辑相似,但有两个特殊点:

  • 没有 elifelse if,必须用连写的 elseif

  • 必须以 end 结尾,不可省略

local score = 75

-- 单分支
if score >= 60 then
    print("及格")
end

-- 双分支
if score >= 90 then
    print("优秀")
else
    print("非优秀")
end

-- 多分支(elseif 连写)
if score >= 90 then
    print("优秀")
elseif score >= 80 then
    print("良好")
elseif score >= 60 then
    print("及格")
else
    print("不及格")
end

5.2 循环语句

Lua 提供三种核心循环结构,覆盖所有场景:

5.2.1 while 循环(当型循环)

先判断条件,再执行循环体。适合循环次数不确定的场景,必须手动更新循环变量,否则会造成死循环。

local sum = 0
local i = 1
while i <= 10 do
    sum = sum + i
    i = i + 1  -- 手动递增,避免死循环
end
print(sum)  -- 输出:55
5.2.2 for 循环

Lua 的 for 分为两类:数值 for泛型 for

① 数值 for(遍历数字范围)

格式:for 变量 = 起始值, 结束值, 步长 do ... end

  • 步长可省略,默认为 1

  • 范围是闭区间(包含起始值和结束值)

  • 步长可以是负数,用于倒序遍历

-- 示例1:默认步长 1,遍历 1~5
for i = 1, 5 do
    print(i)  -- 输出:1 2 3 4 5
end

-- 示例2:指定步长 2,遍历奇数
for i = 1, 10, 2 do
    print(i)  -- 输出:1 3 5 7 9
end

-- 示例3:步长为 -1,倒序遍历(闭区间,包含 1)
for i = 5, 1, -1 do
    print(i)  -- 输出:5 4 3 2 1
end

⚠️ 注意:数值 for 的循环变量是局部常量,在循环体内修改无效。

② 泛型 for(遍历集合)

配合迭代器函数使用,最常用的是 ipairspairs

-- ipairs:仅遍历连续数字索引(从 1 开始),遇到 nil 终止
local arr = {"Lua", "Python", "Java"}
for k, v in ipairs(arr) do
    print("索引:", k, "值:", v)
end
-- 输出:1 Lua / 2 Python / 3 Java

-- pairs:遍历所有键值对(顺序不固定)
local dict = {name = "张三", age = 20}
for k, v in pairs(dict) do
    print("键:", k, "值:", v)
end
-- 输出:name 张三 / age 20(顺序不固定)

5.2.3 repeat...until 循环(直到型循环)

先执行循环体,再判断条件,保证循环体至少执行一次。条件为时退出(与 while 的逻辑相反)。

-- 示例:不断读取输入,直到输入大于 100
local num = 0
repeat
    print("请输入一个数字:")
    num = tonumber(io.read())  -- 读取输入并转为数字
until num > 100  -- 条件为真时退出

print("你输入了大于 100 的数字:", num)

5.3 流程跳转语句

语句 作用 说明
break 跳出当前循环 只能跳出一层循环
return 退出当前函数 可携带返回值
goto 跳转到指定标签 慎用,易造成代码混乱

特别说明:

  • Lua 没有 continue 关键字,如需跳过当前迭代,通常用 if 包裹代码或重构逻辑

  • 不等于运算符是 ~=,不是 !=

goto 示例(仅供了解):

local i = 1
::loop_start::  -- 定义标签
if i > 5 then
    goto loop_end  -- 跳转到结束标签
end

print("当前 i 的值:", i)
i = i + 1
goto loop_start  -- 跳回循环开头

::loop_end::
print("循环结束,最终 i 的值:", i)

💡 建议:除非处理极其特殊的状态机逻辑,否则优先使用 while / for / repeat 等结构化循环,避免使用 goto

6. Lua 函数

6.1 函数定义

Lua 函数通过 function ... end 定义,有三种常见形式:

-- 方式1:全局函数(不推荐,易污染全局命名空间)
function add(a, b)
    return a + b, a, b  -- 支持返回多个值
end

-- 方式2:局部函数(推荐)
local function add1(a, b)
    return a + b
end

-- 方式3:匿名函数赋值给变量(最灵活,常用于回调)
local sub = function(a, b)
    return a - b
end

6.2 函数参数

6.2.1 参数个数不匹配

Lua 不校验参数个数。实参少于形参时,多余形参补 nil;实参多于形参时,多余实参被忽略。

function test(a, b)
    print(a, b)
end

test(1)        -- 输出:1    nil(b 补 nil)
test(1, 2, 3)  -- 输出:1    2(第三个参数被忽略)
6.2.2 默认参数

Lua 无原生默认参数语法,通过 or 运算符手动实现:

function greet(name)
    name = name or "游客"  -- name 为 nil 时取默认值
    print("你好," .. name)
end

greet()        -- 输出:你好,游客
greet("张三")  -- 输出:你好,张三
6.2.3 可变参数

使用 ... 接收任意数量参数,可通过 {...} 转为表,或用 select() 精确操作:

-- 示例1:求和任意个数字
function sum(...)
    local args = {...}  -- 转为表
    local total = 0
    for _, v in ipairs(args) do
        total = total + v
    end
    return total
end

print(sum(1, 2, 3))       -- 输出:6
print(sum(10, 20, 30, 40)) -- 输出:100

-- 示例2:select 精确获取参数
function test(...)
    print(select("#", ...))   -- 获取参数总个数:4
    print(select(2, ...))     -- 获取第 2 个及之后的参数:2 3 4
end
test(1, 2, 3, 4)

6.3 多返回值

Lua 函数可返回多个值,调用时可选择性接收:

function get_user()
    return "张三", 20, "男"
end

-- 接收全部返回值
local name, age, gender = get_user()
print(name, age, gender)  -- 输出:张三    20    男

-- 只接收第一个(多余的自动丢弃)
local name_only = get_user()
print(name_only)  -- 输出:张三

-- 用下划线 _ 占位,跳过不需要的值
local _, age_only = get_user()
print(age_only)  -- 输出:20

6.4 函数高级特性

6.4.1 函数作为"一等公民"

Lua 中函数是第一类值,可赋值给变量、存入表中、作为参数传递:

-- 1. 赋值给变量
local f = add
print(f(4, 5))  -- 输出:9

-- 2. 作为表的字段(模拟方法)
local user = {
    name = "李四",
    say = function(self)
        print("我是" .. self.name)
    end
}
user.say(user)   -- 用 . 调用需手动传 self
user:say()      -- 用 : 调用自动将 user 作为 self 传入

-- 3. 作为参数传递(回调函数)
function calculate(a, b, func)
    return func(a, b)
end
local result = calculate(10, 5, sub)
print(result)  -- 输出:5
6.4.2 闭包(Closure)

闭包是能记住并访问其外部作用域变量的函数。即使外部函数已执行完毕,闭包仍持有那些变量的引用:

function create_counter(init)
    local count = init or 0  -- 外部局部变量,被闭包捕获
    return function()
        count = count + 1
        return count
    end
end

-- 创建两个独立的计数器(各自持有独立的 count)
local counter1 = create_counter(0)   -- 从 0 开始
local counter2 = create_counter(10)  -- 从 10 开始

print(counter1())  -- 输出:1
print(counter1())  -- 输出:2
print(counter2())  -- 输出:11
print(counter2())  -- 输出:12

每个闭包都是独立的。counter1counter2 虽然来自同一个工厂函数,但各自维护独立的 count 状态。

6.4.3 递归

函数调用自身即为递归,必须设置终止条件

function factorial(n)
    if n <= 1 then
        return 1  -- 终止条件
    end
    return n * factorial(n - 1)  -- 递归调用
end

print(factorial(5))  -- 输出:120

6.5 表方法与对象方法

Lua 没有真正的"类"和"对象",通过表 + 函数来模拟。核心区别在于调用语法

表方法(用 . 调用)

本质是"表的某个键对应的函数",不自动传递 self,适用于纯工具函数:

local utils = {}

-- 定义:普通函数赋值给表字段
utils.add = function(a, b)
    return a + b
end

-- 调用:用 . 传入普通参数
print(utils.add(1, 2))  -- 输出:3
对象方法(用 : 调用)

模拟 OOP 中的"方法",隐式接收 self 参数,用于访问对象自身属性:

local person = {
    name = "李四",
    age = 21
}

-- 定义时用 :,自动添加 self 参数
function person:say_hello()
    print("我是" .. self.name .. ",今年" .. self.age .. "岁")
end

function person:grow_up()
    self.age = self.age + 1
end

-- 调用时用 :,自动将 person 作为 self 传入
person:say_hello()  -- 输出:我是李四,今年21岁
person:grow_up()
person:say_hello()  -- 输出:我是李四,今年22岁

.: 的等价关系:

-- 以下两种调用完全等价
person.say_hello(person)  -- 用 . 需手动传 self
person:say_hello()         -- 用 : 自动传 self

-- 以下两种定义完全等价
function person.say_hello(self) end   -- 显式写 self
function person:say_hello() end       -- : 语法糖自动加 self
调用符 是否自动传 self 适用场景
. 工具函数、不需要访问对象属性的场景
: 是(将表本身作为第一个参数) 需要访问/修改对象自身属性的场景

7. 元表(Metatable)与元方法(Metamethod)

元表是 Lua 最核心、最灵活的特性之一。它本质上是一张特殊的表,用来为普通表(称为基础表对象)定义自定义行为——例如让表支持加减运算、自定义索引查找规则、控制打印格式等。

核心概念

术语 说明
元表(Metatable) 一张普通表,但其键值对用于定义"元方法"
元方法(Metamethod) 元表中以双下划线 __ 开头的键(如 __add__index),对应特定的自定义行为
基础表 被元表修饰的普通表,触发特定操作时会查询其元表

核心 API

lua

复制

setmetatable(t, mt)   -- 给表 t 设置元表 mt,返回 t
getmetatable(t)       -- 获取表 t 的元表,若无则返回 nil
rawget(t, key)        -- 绕过元方法,直接访问 t[key]
rawset(t, key, val)   -- 绕过元方法,直接赋值 t[key] = val

7.1 __index — 索引查找

当访问基础表中不存在的键时,Lua 会转向元表的 __index 查找。这是实现继承默认值委托模式的核心机制。

__index 可以是(优先在该表中查找键),也可以是函数(自定义查找逻辑)。

-- 示例1:__index 为表(模拟继承/原型链)
local proto = {name = "默认名称", age = 18}  -- 原型表
local obj = {gender = "男"}                  -- 基础表

setmetatable(obj, {__index = proto})

print(obj.name)    -- 输出:默认名称(obj 无 name,从 proto 继承)
print(obj.age)     -- 输出:18
print(obj.gender)  -- 输出:男(obj 自身有,直接返回)

-- 示例2:__index 为函数(自定义查找逻辑)
local t = {abc = "123"}

setmetatable(t, {
    __index = function(table, key)
        return "键 [" .. key .. "] 不存在,返回默认值"
    end
})

print(t.abc)       -- 输出:123(自身存在,不触发 __index)
print(t.xyz)       -- 输出:键 [xyz] 不存在,返回默认值

⚠️ 注意__index 仅在访问不存在的键时触发。若基础表本身有该键,直接返回,不走元方法。


7.2 __newindex — 索引赋值

当给基础表中不存在的键赋值时,Lua 会调用 __newindex,而非直接写入基础表。可用于限制赋值记录日志重定向存储目标

-- 示例:记录赋值日志并允许写入
local t = {name = "张三"}

setmetatable(t, {
    __newindex = function(table, key, value)
        print("赋值操作:键 =", key, "值 =", value)
        -- 使用 rawset 绕过元方法,直接写入基础表,避免递归触发 __newindex
        rawset(table, key, value)
    end
})

t.name = "李四"   -- 正常执行(name 已存在,不触发 __newindex)
t.age = 20        -- 触发 __newindex,打印日志后通过 rawset 写入
print(t.name, t.age)  -- 输出:李四    20

💡 rawset 的必要性:若在 __newindex 中直接写 table[key] = value,会再次触发 __newindex,导致无限递归。rawset 可强制绕过元方法。


7.3 算术元方法 — __add / __sub / __mul / __div 等

通过元方法自定义表的算术运算,使两个表可以像数字一样进行加减乘除。

元方法 触发场景 说明
__add a + b 加法
__sub a - b 减法
__mul a * b 乘法
__div a / b 除法
__mod a % b 取模
__pow a ^ b 幂运算
__unm -a 取负
-- 示例:让 table 表示二维向量,支持加法
local vec1 = {x = 1, y = 2}
local vec2 = {x = 3, y = 4}

local vec_mt = {
    __add = function(a, b)
        return {x = a.x + b.x, y = a.y + b.y}
    end
}

setmetatable(vec1, vec_mt)
setmetatable(vec2, vec_mt)

local vec3 = vec1 + vec2
print(vec3.x, vec3.y)  -- 输出:4    6

7.4 关系元方法 — __eq / __lt / __le

控制表的比较行为。注意:只有当两个操作数共享同一个元表时,这些元方法才会被调用

元方法 触发场景 说明
__eq a == b 等于
__lt a < b 小于
__le a <= b 小于等于
local p1 = {name = "张三", age = 20}
local p2 = {name = "李四", age = 25}

local person_mt = {
    __eq = function(a, b)
        return a.age == b.age  -- 以 age 字段判断是否相等
    end,
    __lt = function(a, b)
        return a.age < b.age   -- 以 age 字段比较大小
    end
}

setmetatable(p1, person_mt)
setmetatable(p2, person_mt)

print(p1 == p2)  -- 输出:false(age 不同)
print(p1 < p2)   -- 输出:true(20 < 25)

7.5 __call — 把表当作函数调用

当基础表被当作函数执行(如 t())时触发,可实现"可调用对象"(Callable Object)。

local t = {name = "Lua"}

setmetatable(t, {
    __call = function(table, ...)
        print("调用了 table,参数:", ...)
        print("table 的 name:", table.name)
    end
})

t(10, 20)
-- 输出:
-- 调用了 table,参数:10    20
-- table 的 name:Lua

7.6 __tostring — 自定义打印格式

当用 print()tostring() 处理基础表时触发,替代默认的 table: 0xXXXXXXX 内存地址输出。

local person = {name = "张三", age = 20}

setmetatable(person, {
    __tostring = function(table)
        return "Person: " .. table.name .. "(" .. table.age .. "岁)"
    end
})

print(person)  -- 输出:Person: 张三(20岁)

7.7 元方法速查表

类别 元方法 触发时机 典型用途
索引查找 __index 访问不存在的键 继承、默认值、委托
索引赋值 __newindex 给不存在的键赋值 只读保护、赋值拦截、数据校验
算术运算 __add / __sub / __mul / __div / __mod / __pow / __unm 对应的算术操作符 自定义数据类型运算(向量、矩阵等)
关系比较 __eq / __lt / __le == / < / <= 自定义对象比较逻辑
调用 __call 表被当作函数调用 t() 工厂模式、函数对象
字符串化 __tostring print(t) / tostring(t) 自定义输出格式
长度 __len #t 自定义长度计算逻辑
迭代 __pairs / __ipairs pairs(t) / ipairs(t) 自定义遍历行为

🔑 设计哲学:元表机制让 Lua 在保持语言极简的同时,赋予开发者定义"新数据类型"的能力——表不再只是键值对容器,而可以成为向量、对象、函数对象等任意抽象。

8.模块(Module)与包(Package)

核心概念

概念 说明
模块 一个 .lua 文件即为一个模块。将相关的函数、变量、数据封装在文件中,对外暴露指定接口,未暴露的则为私有
多个功能相关的模块按目录结构组织的集合。Lua 中包的管理主要依靠目录结构 + package.path 配置
本质 模块的本质是把功能挂载到一个 table 上,最后 return 这个 table。外部通过 require 加载后,即可访问 table 中暴露的成员

8.1 定义模块

模块文件内部,用 local 声明的变量/函数为私有(模块内可见),挂载到返回的 table 上的为公开(外部可访问)。

-- mymath.lua(模块文件)

-- 私有函数:仅模块内部使用,不对外暴露
local function check_num(num)
    return type(num) == "number"
end

-- 创建模块表(公开接口载体)
local mymath = {}

-- 公开函数:加法
function mymath.add(a, b)
    if not (check_num(a) and check_num(b)) then
        error("参数必须是数字")
    end
    return a + b
end

-- 公开函数:乘法
function mymath.mul(a, b)
    if not (check_num(a) and check_num(b)) then
        error("参数必须是数字")
    end
    return a * b
end

-- 公开常量
mymath.PI = 3.1415926

-- 必须返回这个 table,否则外部 require 后无法访问
return mymath

8.2 加载模块

使用 require("模块名") 加载模块,这是 Lua 的核心模块加载机制。

-- main.lua(与 mymath.lua 放在同一目录下)

-- 加载模块(不带 .lua 后缀,不带路径)
local mm = require("mymath")

-- 调用模块的公开接口
print(mm.add(3, 5))    -- 输出:8
print(mm.mul(4, 6))    -- 输出:24
print(mm.PI)           -- 输出:3.1415926

-- 私有函数 check_num 无法访问
-- mm.check_num(1)     -- 报错:attempt to call a nil value

require 的关键特性:

特性 说明
缓存机制 同一模块只会加载一次,多次 require 返回同一 table(单例)。如需重新加载,需先 package.loaded["模块名"] = nil
路径查找 package.path 配置的路径依次查找,默认包含当前目录 ./?.lua
返回值 require 返回的是模块 return 的那个 table
命名规范 模块名避免与 Lua 标准库模块重名(如 stringtableio 等)

多级目录的模块加载:

若模块放在子目录中,使用 . 作为目录分隔符(而非 /\):

-- 目录结构:
-- project/
-- ├── main.lua
-- └── lib/
--     └── utils.lua

-- 加载 lib/utils.lua
local utils = require("lib.utils")

8.3 模块查找路径

require 根据 package.path 中的模板查找模块文件。默认路径通常包含当前目录。

-- 查看当前的模块查找路径
print(package.path)
-- 输出示例:./?.lua;/usr/local/share/lua/5.4/?.lua;/usr/local/share/lua/5.4/?/init.lua;...

-- 添加自定义路径(例如模块放在 lib 目录下)
package.path = package.path .. ";./lib/?.lua;./lib/?/init.lua"

-- 然后即可加载 lib 目录下的模块
local mymod = require("mymod")

💡 路径模板说明?.lua 中的 ? 会被替换为 require 传入的模块名。例如 require("foo") 会尝试查找 ./foo.lua


8.4 包的管理(目录级模块组织)

当模块数量增多时,应按功能组织成包。核心技巧是通过 init.lua 作为包的入口文件,使外部可以把整个目录当作一个模块加载。

目录结构示例:

project/
├── main.lua              # 主程序
└── utils/                # 包目录(包名为 utils)
    ├── init.lua          # 包的入口文件(关键!)
    ├── string.lua        # 子模块:字符串工具
    └── math.lua          # 子模块:数学工具

utils/init.lua(包入口):

-- utils/init.lua
local utils = {}

-- 加载子模块并挂载到 utils 上
utils.string = require("utils.string")
utils.math = require("utils.math")

-- 也可以将常用函数直接导出到包的顶层
function utils.trim(str)
    return utils.string.trim(str)
end

return utils

main.lua 中使用包:

-- 加载 utils 包(实际加载的是 utils/init.lua)
local utils = require("utils")

-- 调用子模块的函数
print(utils.string.trim("  hello lua  "))  -- 输出:hello lua
print(utils.math.square(5))                -- 输出:25

-- 调用包顶层导出的便捷函数
print(utils.trim("  test  "))              -- 输出:test

8.5 关键注意事项

注意点 说明
必须 return 模块表 忘记 return 会导致 require 返回 true(而非模块表),外部无法访问任何成员
私有性靠 local 实现 未用 local 声明的变量/函数会成为全局变量,污染全局命名空间
模块名与文件名一致 建议模块内部表名与文件名保持一致,增强可读性
缓存导致热重载困难 开发调试时若修改了模块文件,需先清除缓存再重新加载:package.loaded["模块名"] = nil
路径分隔符 require 中使用 . 分隔目录;package.path 中使用 ; 分隔多个路径模板

9. Lua 内置标准库

Lua 内置库无需额外安装,直接通过 require("库名") 或全局变量即可使用。


9.1 math — 数学库

-- 取整
print(math.floor(3.9))   -- 向下取整 → 3
print(math.ceil(3.1))    -- 向上取整 → 4
print(math.modf(3.14))   -- 拆分整数与小数 → 3    0.14

-- 绝对值
print(math.abs(-5))      -- → 5

-- 幂运算(两种写法等价)
print(2 ^ 3)             -- → 8
print(math.pow(2, 3))    -- → 8

-- 随机数(必须先设置种子)
math.randomseed(os.time())
print(math.random())         -- [0, 1) 之间的浮点数
print(math.random(100))      -- [1, 100] 之间的整数
print(math.random(10, 20))   -- [10, 20] 之间的整数

-- 生成 [0, 10) 之间的浮点数
local float_rand = math.random() * 10

-- 最值
print(math.max(1, 5, 3, 9))   -- → 9
print(math.min(-2, 0, 4, -5)) -- → -5

-- 取余(fmod 与 % 的符号规则不同)
print(math.fmod(7, 3))   -- → 1(结果符号与被除数一致)
print(7 % 3)              -- → 1(结果符号与除数一致)
print(math.fmod(-7, 3))  -- → -1

9.2 string — 字符串库

Lua 字符串操作支持两种调用风格:string.xxx(str, ...)str:xxx(...)(冒号语法自动将 str 作为第一个参数传入)。

local s = "  hello lua  "

-- 格式化(类似 C 语言 printf)
local str1 = string.format("名字:%s, 年龄:%d, 面积:%.2f", "fengfeng", 21, 3.1415)
print(str1)  -- → 名字:fengfeng, 年龄:21, 面积:3.14

-- 截取子串(索引从 1 开始,闭区间)
print(string.sub(s, 3, 7))   -- → "hello"

-- 查找子串(返回起始和结束位置,找不到返回 nil)
print(string.find(s, "lua"))  -- → 9    11

-- 替换(返回新字符串和替换次数)
print(string.gsub(s, "lua", "python"))  -- → "  hello python  "    1

-- 模式匹配
print(string.match(s, "%a+"))  -- → "hello"(匹配连续字母)

-- 冒号语法调用(更简洁)
print(s:upper())   -- → "  HELLO LUA  "(转大写)
print(s:len())     -- → 13(长度,含空格)
模式匹配(轻量正则)

Lua 没有独立的正则引擎,内置轻量模式匹配。与标准正则的核心差异:

功能 标准正则 Lua 模式 说明
字母 [a-zA-Z] %a % 代替 \ 转义
数字 \d %d
空白 \s %s
任意字符 . . 一致
重复 1+ 次 + + 一致
重复 0+ 次 * * 一致
开头/结尾 ^ / $ ^ / $ 一致
非贪婪匹配 +? / *? 不支持 Lua 仅支持贪婪匹配
分组捕获 (...) (...) 一致,但不支持非捕获组
反义(非字母) [^a-zA-Z] %A 大写表示反义
-- 示例1:提取第一个数字
local s1 = "价格:99元"
local num = string.match(s1, "%d+")
print(num)  -- → 99

-- 示例2:分组捕获键值对
local s2 = "name=张三,age=25"
local key, value = string.match(s2, "(%a+)=([^,]+)")
print(key, value)  -- → name    张三

-- 示例3:匹配前缀/后缀
local s3 = "hello.lua"
print(string.match(s3, "^%a+"))  -- → hello(开头字母)
print(string.match(s3, "%a+$"))  -- → lua(结尾字母)

9.3 table — 表操作库

Lua 原生表仅支持 # 取长度和索引访问,复杂操作需借助 table 库。

local t = {3, 1, 2}

-- 排序(原地排序,无返回值)
table.sort(t)
print(table.concat(t, ","))  -- → 1,2,3

-- 末尾插入
table.insert(t, 10)
print(table.concat(t, ","))  -- → 1,2,3,10

-- 指定位置插入
table.insert(t, 2, 99)
print(table.concat(t, ","))  -- → 1,99,2,3,10

-- 移除并返回最后一个元素
local last = table.remove(t)       -- → 10
print(table.concat(t, ","))      -- → 1,99,2,3

-- 移除指定位置元素
table.remove(t, 2)                -- 移除索引 2(即 99)
print(table.concat(t, ","))      -- → 1,2,3

自定义排序(降序):

local t = {1, 5, 3, 2, 4}
table.sort(t, function(a, b)
    return a > b  -- 降序:后一个比前一个小
end)
print(table.concat(t, ","))  -- → 5,4,3,2,1

⚠️ table.sort 是原地操作,不返回新表。上述示例中 t1 = table.sort(...) 的写法是错误的,t1 会得到 nil


9.4 os — 操作系统库

-- 时间格式化
print(os.date("%Y-%m-%d %H:%M:%S"))  -- → 2026-05-26 00:10:00

-- 时间戳(秒)
print(os.time())                     -- → 1767882504

-- 时间戳转字符串
print(os.date("%Y-%m-%d %H:%M:%S", 1767882504))

-- 环境变量
print(os.getenv("PATH"))

-- 执行系统命令
os.execute("chcp 65001")  -- Windows 设置 UTF-8 编码
os.execute("dir")         -- Windows 列出目录 / Linux 用 ls

-- 退出程序
os.exit(0)  -- 0 表示正常退出

9.5 io — 输入输出库

分为简单 IO(一次性操作)和文件句柄 IO(可控性强,推荐)。

-- 简单 IO:标准输入输出
io.write("请输入:")   -- 不换行输出(print 默认带换行)
io.flush()             -- 刷新缓冲区
local data = io.read() -- 读取一行(默认模式 "*l")
print("你输入的内容:", data)

-- 文件句柄 IO(推荐)
-- 写入文件
local f = io.open("test.txt", "w")  -- w=写入,r=读取,a=追加
if f then
    f:write("hello lua\n")
    f:close()
end

-- 读取文件
local f = io.open("test.txt", "r")
if f then
    local content = f:read("*a")  -- 读取全部内容
    print(content)
    f:close()
end

io.read() / f:read() 参数模式:

参数 含义 说明
"*l" 读取一行(不含换行符) 默认模式,文件尾返回 nil
"*L" 读取一行(含换行符) Lua 5.3+ 支持
"*a" 读取全部内容 适合小文件一次性读取
"*n" 读取一个数字 自动跳过前导空白,非数字返回 nil
n 读取 n 个字符 数字参数

9.6 第三方增强库

若使用 Lua 发行版(如 LuaRocks 安装环境或某些 IDE 内置环境),通常附带扩展库。纯官方 Lua 二进制文件仅包含上述标准库。

JSON 库
local json = require("json")

local user = {
    name = "fengfeng",
    age = 12,
    info = { addr = "长沙" }
}

-- 表 → JSON 字符串
local json_str = json.encode(user)
print(json_str)
-- → {"info":{"addr":"长沙"},"name":"fengfeng","age":12}

-- JSON 字符串 → 表
local tb = json.decode('{"info":{"addr":"长沙"},"name":"fengfeng","age":12}')
print(tb.name)       -- → fengfeng
print(tb.info.addr)  -- → 长沙
字符串增强库

部分发行版通过扩展原库的方式增强功能,加载后直接通过 string 调用:

require("string_ext")  -- 扩展 string 库

print(string.split("1,2,3,4,5", ","))     -- → {1, 2, 3, 4, 5}
print(string.trim("   前后有空格   "))    -- → "前后有空格"
表增强库
require("table_ext")

-- 注意:table.sort 仍是原地排序,以下写法修正了原笔记的错误
local t = {1, 5, 3, 2, 4}
table.sort(t)  -- 无返回值,直接修改 t
for _, v in ipairs(t) do
    io.write(v .. "\t")
end
print()  -- → 1    2    3    4    5

-- 降序
table.sort(t, function(a, b) return a > b end)
for _, v in ipairs(t) do
    io.write(v .. "\t")
end
print()  -- → 5    4    3    2    1

-- 获取表大小(含非数字索引)
print(table.size(t))  -- → 5

-- 获取所有键
local user = {name = "fengfeng", age = 12}
local keys = table.indices(user)
for _, v in pairs(keys) do
    print(v)  -- → name / age
end

-- 深拷贝
local u1 = table.clone(user)
u1.name = "zhangsan"
print(u1.name)  -- → zhangsan
print(user.name) -- → fengfeng(原表不受影响)

💡 增强库说明string_exttable_ext 等并非 Lua 官方标准库,而是特定发行版(如某些游戏引擎、IDE 插件或 LuaRocks 包)提供的扩展。若使用纯官方 Lua 环境,这些库需要自行安装。

10.OpenResty 连接 Redis 实战

OpenResty 通过 resty.redis 库(基于 cosocket 实现)提供非阻塞的 Redis 访问,连接可复用,性能远高于传统阻塞式 Redis 客户端。


1. Nginx 配置

nginx.confserver 块中添加 location:

location /test_redis {
    # 指定 Lua 脚本路径(相对 nginx 工作目录或绝对路径)
    content_by_lua_file lua/test_resty_redis.lua;
}

路径说明lua/test_resty_redis.lua 是相对 nginx 工作目录(prefix)的路径。若使用绝对路径,建议用 lua_code_cache on;(生产环境默认开启)避免每次请求重新加载。


2. Lua 脚本(lua/test_resty_redis.lua

-- 加载 OpenResty 内置 Redis 客户端
local redis = require("resty.redis")

-- 1. 创建客户端实例
local red = redis:new()

-- 设置超时(连接超时、发送超时、读取超时,单位:毫秒)
-- 注意:OpenResty 新版本推荐使用 set_timeouts 替代已弃用的 set_timeout
red:set_timeouts(1000, 1000, 1000)

-- 2. 建立 TCP 连接
local ok, err = red:connect("127.0.0.1", 6379)
if not ok then
    ngx.status = 500
    ngx.say("Redis 连接失败: ", err)
    return
end

-- 3. 认证(如 Redis 配置了密码,取消注释)
-- local ok, err = red:auth("your_password")
-- if not ok then
--     ngx.status = 403
--     ngx.say("Redis 认证失败: ", err)
--     return
-- end

-- 4. 执行 Redis 命令
red:set("name", "OpenResty")

-- 5. 读取数据(需区分 "键不存在" 和 "操作报错")
local res, err = red:get("name")
if err then
    ngx.status = 500
    ngx.say("Redis 读取失败: ", err)
    return
end

-- 处理 Redis 特殊返回值 ngx.null(表示键不存在,不等同于 Lua 的 nil)
if res == ngx.null then
    ngx.say("键 'name' 不存在")
else
    ngx.header.content_type = "text/plain; charset=utf-8"
    ngx.say("获取 name: ", res)
end

-- 6. 将连接放回连接池(核心优化点)
-- 参数:空闲超时时间(毫秒)、连接池最大容量
-- 调用后连接不会关闭,而是保持长连接供后续请求复用
local ok, err = red:set_keepalive(10000, 100)
if not ok then
    -- 连接池放入失败(如连接已断开),记录日志即可,不影响本次响应
    ngx.log(ngx.ERR, "连接池放入失败: ", err)
end

-- ⚠️ 切勿调用 red:close() 或 red:quit(),否则连接会被直接关闭,无法复用

3. 启动与验证

# 检查配置语法
nginx -t

# 启动或重载
nginx -s reload

# 测试访问
curl http://127.0.0.1/test_redis
# 预期输出:获取 name: OpenResty

4. 关键注意事项

要点 说明
连接池复用 set_keepalive 是性能核心。不调用则每次请求都新建/关闭 TCP 连接,开销巨大
不要 quit/close red:quit() 会向 Redis 发送 QUIT 命令并关闭连接;red:close() 直接关闭套接字。两者都会破坏连接池机制
ngx.null 判空 Redis 返回的 "nil" 在 OpenResty 中被封装为 ngx.null,不能用 if res == nil 判断键是否存在
超时设置 生产环境建议 set_timeouts(connect_timeout, send_timeout, read_timeout) 分别设置,避免慢查询拖垮 Worker
Select 数据库 若使用 red:select(1) 切换 DB,连接池中的连接会带有 DB 状态,复用时需注意上下文一致性

5. 进阶:封装为模块(推荐)

生产环境建议将 Redis 操作封装为模块,统一管理连接和错误:

-- lib/redis_util.lua
local redis = require("resty.redis")
local _M = {}

function _M.get(key)
    local red = redis:new()
    red:set_timeouts(1000, 1000, 1000)
    
    local ok, err = red:connect("127.0.0.1", 6379)
    if not ok then return nil, err end
    
    local res, err = red:get(key)
    red:set_keepalive(10000, 100)  -- 无论 get 成败,都尝试放回连接池
    
    if err then return nil, err end
    if res == ngx.null then return nil, "not_found" end
    
    return res, nil
end

return _M

使用时:

local redis_util = require("lib.redis_util")
local val, err = redis_util.get("name")
if err then
    ngx.say("错误: ", err)
else
    ngx.say("值: ", val)
end

6. 常见问题排查

现象 原因
访问返回 500 且日志显示 connection refused Redis 未启动,或绑定了 127.0.0.1 但 nginx 通过其他网卡访问
第一次请求慢,后续快 正常。第一次建立 TCP 连接,后续从连接池复用
set_keepalive 返回 connection in dubious state 连接已损坏(如 Redis 超时断开),OpenResty 会自动丢弃并新建连接
中文乱码 确保 ngx.header.content_type 包含 charset=utf-8,且 Redis 存储的是 UTF-8 编码数据

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐