lua学习笔记
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 环境搭建
-
选择对应系统的版本(Windows 建议下载
lua-5.4.x_Win64_bin.zip) -
解压后将目录添加到系统 PATH 环境变量
-
打开终端(CMD / PowerShell),输入以下命令验证
lua -v # 或带版本号的命令,如:lua54 -v -
进入 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 关键注意事项
-
数字键与字符串键严格区分
t[1]与t["1"]是两个完全不同的键,访问时切勿混淆。 -
#运算符的适用范围
仅对连续数字索引的数组有效。字典型表或稀疏数组不要使用#求长度,结果不可预期。
-
nil对遍历的影响-
ipairs()遇到nil会立即终止,后续元素不再遍历。 -
如需保留"空位",建议用
0或空字符串""占位,而非nil。
-
-
表的引用特性(非值拷贝)
local t1 = {a = 1} local t2 = t1 -- t2 只是 t1 的引用,指向同一块内存 t2.a = 2 print(t1.a) -- 输出:2(修改 t2 会影响 t1) -- 如需独立拷贝,需手动深拷贝或使用第三方库 -
索引起点
Lua 数组索引默认从 1 开始。虽然可以手动指定t[0],但标准库函数(如table.insert、ipairs)均按 1 处理,建议遵循惯例。
5. Lua 流程控制
5.1 条件判断(if 语句)
Lua 的 if 语句与其他语言逻辑相似,但有两个特殊点:
-
没有
elif或else 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(遍历集合)
配合迭代器函数使用,最常用的是 ipairs 和 pairs:
-- 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
每个闭包都是独立的。
counter1和counter2虽然来自同一个工厂函数,但各自维护独立的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 标准库模块重名(如 string、table、io 等) |
多级目录的模块加载:
若模块放在子目录中,使用 . 作为目录分隔符(而非 / 或 \):
-- 目录结构:
-- 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_ext、table_ext等并非 Lua 官方标准库,而是特定发行版(如某些游戏引擎、IDE 插件或 LuaRocks 包)提供的扩展。若使用纯官方 Lua 环境,这些库需要自行安装。
10.OpenResty 连接 Redis 实战
OpenResty 通过 resty.redis 库(基于 cosocket 实现)提供非阻塞的 Redis 访问,连接可复用,性能远高于传统阻塞式 Redis 客户端。
1. Nginx 配置
在 nginx.conf 的 server 块中添加 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 编码数据 |
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐




所有评论(0)