纯新手先看这一框
Lua 适合做什么?游戏脚本(魔兽/WoW 插件、Roblox、Love2D)、嵌入式与配置文件、Redis/Nginx 脚本、Neovim 插件、OpenResty 网关。它是"胶水 + 嵌入式"语言,常被嵌进 C/C++ 程序当配置和逻辑引擎。
为什么学?极简(核心语言不到 30 个关键字)、极易嵌入 C、支持热更新(改脚本不重启服务)、学习曲线平缓。会 Lua,你能给很多大型系统写"可插拔逻辑"。
怎么跑?本站每节"动手实验室"点 ▶ 直接出结果;本地装 Lua:官网 lua.org 下载,或终端 brew install lua / apt install lua5.4,然后 lua hello.lua。
新手最常踩的坑:① 数组默认从 1 开始,不是 0;② 没写 local 的变量全是全局,会悄悄污染;③ nil 索引会报错;④ 只有一种数据结构 table,数组/字典/对象/类全靠它。把这几条记牢,能少绕三小时。
1. Lua 是什么与怎么跑先建立全局认识
Lua(葡萄牙语"月亮")是 1993 年由巴西 PUC-Rio 推出的轻量级、可嵌入脚本语言。它的设计哲学是"用 C 做繁重的事,用 Lua 做灵活的事"——整个语言 + 虚拟机编译后只有几百 KB,却支持闭包、协程、动态类型、自动内存管理。
三种运行方式
- 交互式(REPL):终端输入
lua进入,逐行敲、回车即执行,适合试语法。 - 运行脚本文件:
lua 文件名.lua,生产环境主流方式。 - 嵌入到 C/C++:用
lua_State*API 在宿主程序里加载并执行 Lua 脚本,这是 Lua 的看家本领。
//、环境变量)略有差异。2. 第一个程序与注释从 print 开始
Lua 没有"主函数"概念,脚本从上往下顺序执行。print 是最常用的输出,自动换行。
-- 这是单行注释
print("Hello, Lua!") -- 直接输出字符串
--[[ 这是多行注释
可以写很多行
常用于临时屏蔽大段代码 ]]
-- 打印多个值,用逗号分隔,print 会用制表符连接
print("和是", 1 + 2, "类型是", type(3))
print 各参数之间默认用 制表符 \t 分隔(不是空格)。要精确控制格式,用 string.format(见第 10 节)。--[[ ]] 如果要"可嵌套取消",写成 --[=[ ]=] 这种不同层级的等号配对,避免内部再出现 ]] 提前截断。3. 变量与 8 种类型动态类型,但每个值都有明确类型
Lua 是动态类型:变量本身没有类型,值才有。用 type(v) 查看。一共 8 种基本类型:
| 类型 | 含义 | 例子 |
|---|---|---|
| nil | 空 / 不存在 | nil(未赋值的变量都是它) |
| boolean | 布尔 | true / false |
| number | 数字(5.3+ 区分浮点/整数) | 3, 3.14, 0x1F |
| string | 不可变字符串 | "hi", 'hi', [[多行]] |
| table | 唯一的复合结构 | {1,2,3}, {x=1} |
| function | 函数(一等公民) | function() end |
| userdata | C 侧数据(宿主对象) | 文件句柄、C 结构体 |
| thread | 协程 | coroutine.create(...) |
x = 10
print(type(x)) -- number
x = "hello"
print(type(x)) -- string(同一个变量换了类型)
print(type(nil)) -- nil
print(type(print)) -- function
-- 只有 false 和 nil 是"假",其余包括 0 和空串 "" 都是真!
if 0 then print("0 是真") end -- 会打印
0 和 "" 都是真值!和 C/Python 不一样。写 if x then 时,若 x 可能是 0 或空串,逻辑会和你想的不一样——要显式判断 if x ~= nil and x ~= 0 then。4. 字符串不可变、自动转换、长括号
字符串用双引号或单引号包裹,不可变(改字符串会生成新串)。连接用 ..(不是 +)。取长度用 #。
local s = "Lua"
print(s .. " 很轻量") -- 连接:Lua 很轻量
print(#s) -- 长度:3
print("a" .. 1) -- 数字自动转字符串:"a1"
-- 长字符串 / 多行:用双方括号 [[ ]],内部引号不用转义
local sql = [[
SELECT * FROM users
WHERE name = 'bob'
]]
print(sql)
-- 转义:\n \t \\ \" 等;单双引号等价
print("一行\n两行")
..:"值=" .. n。若写成 "值=" + n,Lua 会尝试把 "值=" 转数字失败 → 报错。另外 # 取字符串长度按字节算,UTF-8 中文占 3 字节,所以 #"中文" 是 6,不是 2;要数字符用 utf8.len。.. 拼接大字符串会生成很多中间串。大量拼接请用 table.concat(list) 一次性合并,速度快得多。5. table 表(核心中的核心)唯一的数据结构,撑起一切
Lua 只有一种复合类型 table,它是关联数组(哈希表):既可以用整数当键当"数组",也可以用字符串当键当"字典/对象"。数组默认从 1 开始。
-- 数组(序列):整数键从 1 开始
local a = {10, 20, 30}
print(a[1]) -- 10(不是 a[0]!)
print(#a) -- 3(长度)
-- 字典:字符串键
local person = { name = "Bob", age = 18 }
print(person.name) -- Bob(等价于 person["name"])
person.age = 19 -- 修改
person.city = "BJ" -- 新增字段
-- 混用:键可以是任意值(除 nil)
local t = { [1]="a", ["k"]="v", [true]=99 }
print(t[true]) -- 99
-- ipairs:只遍历"从 1 开始的连续整数键",遇到断档就停
-- pairs:遍历所有键值对(含字符串键),顺序不保证
for i, v in ipairs(a) do print(i, v) end
for k, v in pairs(person) do print(k, v) end
a[0] 是 nil;② #t 对有空洞的表不可靠(如 {1,2,nil,4} 长度可能算 2 或 4,取决于实现),要长度请保证是"紧凑序列"或用 table.maxn;③ 访问不存在的键返回 nil,不会报错,但拿 nil 做运算会炸。6. 控制流if / while / for
没有花括号,用 end 收尾;数值 for 的结束值是包含的(闭区间);比较用 < > <= >= == ~=(~= 是不等于)。
local n = 7
if n < 0 then
print("负")
elseif n == 0 then
print("零")
else
print("正")
end
-- 数值 for:i 从 1 到 5,步长默认 1
local s = 0
for i = 1, 5 do
s = s + i
end
print(s) -- 15
-- 带步长:从 10 到 1,每次 -2
for i = 10, 1, -2 do print(i) end -- 10 8 6 4 2
-- while
local i = 1
while i <= 3 do print(i); i = i + 1 end
-- 泛型 for(遍历表,见第 5 节)
for k, v in pairs({a=1,b=2}) do print(k,v) end
for i=1,5 的 5 是包含的(i 会取到 5);步长为负时,结束值必须比起始值小,否则循环一次都不执行。另外 for 的循环变量是局部且只读的,循环里改 i 不影响下一次迭代。7. 函数与多返回值一等公民、可变参数
函数是值,可以赋值给变量、当参数传、当返回值。函数可以返回多个值,这是 Lua 一大特色。
-- 定义与调用
local function add(a, b) return a + b end
print(add(2, 3)) -- 5
-- 多返回值
local function minmax(a, b)
if a < b then return a, b else return b, a end
end
local lo, hi = minmax(9, 4)
print(lo, hi) -- 4 9
-- 只取第一个
local first = minmax(9, 4) -- first = 4,多余返回值被丢弃
-- 可变参数 ...
local function sum(...)
local t = {...} -- 把参数收进表
local s = 0
for _, v in ipairs(t) do s = s + v end
return s
end
print(sum(1,2,3,4)) -- 10
print(select("#", 1,2,3)) -- 3(用 select 数出参数个数)
print(minmax(9,4)) 会打印两个;但 print("x", minmax(9,4)) 只拿到第一个(因为不在尾部)。需要强制保留时用括号包成单值:(minmax(9,4)) 只取第一个。8. 作用域与 local永远优先写 local
没写 local 的变量是全局,存进全局表 _G。全局变量随处可读写,极易互相污染、难排查。Lua 官方建议:默认全部用 local。
-- 全局(危险)
count = 0 -- 等价于 _G.count = 0,任何文件都能改
-- 局部(安全)
local count = 0 -- 只在当前块/文件可见
do
local x = 1 -- 块级作用域
print(x) -- 1
end
-- print(x) -- 这里 x 已不可见(nil)
-- 局部函数
local function helper() end
print(_G.count) -- 读全局
tmp,后加载的会覆盖前面的,且没有任何报错。大型 Lua 项目(如游戏 mod)80% 的诡异 bug 来自全局污染。铁律:能 local 就 local,模块接口才用 return 暴露。do ... end 或用 local _ENV = ...(第 6 进阶)可以彻底屏蔽意外的全局写入。9. 模块与 require用 return 暴露接口
Lua 没有 class/import 关键字,模块就是"一个返回 table 的 Lua 文件"。require 负责加载并缓存它。
-- mymath.lua 内容:
-- local mymath = {}
-- function mymath.add(a,b) return a+b end
-- return mymath
-- 主程序:
local mymath = require("mymath") -- 不带 .lua 后缀
print(mymath.add(2, 3)) -- 5
-- require 只会加载一次(缓存),第二次直接返回同一份
local again = require("mymath")
print(again == mymath) -- true
require 找文件的路径由 package.path 决定(类似其他语言的 PATH),找不到会报错;② 模块必须用 return 把接口交出去,不然 require 拿到的是 nil;③ 想强制重新加载,要从 package.loaded 里把缓存清掉。10. 常用标准库table / string / math / os
Lua 内核很小,功能靠标准库补。常用的几个:
-- table 库
local t = {3,1,2}
table.insert(t, 99) -- 末尾插入
table.sort(t) -- 排序 {1,2,3,99}
table.remove(t, 1) -- 删除第 1 个
print(table.concat({"a","b","c"}, "-")) -- a-b-c
-- string 库
print(string.upper("lua")) -- LUA
print(string.sub("hello", 1, 3)) -- hel
print(string.format("pi=%.2f", 3.14159)) -- pi=3.14
-- math 库
print(math.floor(3.9), math.max(1,9), math.pi)
-- 返回多个值时的解包
local nums = {10, 20, 30}
print(math.max(table.unpack(nums))) -- 30(unpack 把表拆成多参数)
table.unpack(5.2+;老版本 unpack)能把表"炸开"成多个参数传给函数,配合 math.max/print 很好用。注意 5.1 叫全局 unpack,5.2+ 挪到了 table.unpack。进阶 1 · 闭包函数捕获外部局部变量
当一个函数"活"在另一个函数内部,并且引用了外层函数的局部变量时,就形成了闭包。外层变量不会被回收,函数"记住"了它——这是实现私有状态和迭代器的关键。
-- 闭包实现计数器(外部无法直接改 n)
local function makeCounter()
local n = 0
return function()
n = n + 1
return n
end
end
local c = makeCounter()
print(c(), c(), c()) -- 1 2 3(n 被闭包持有,持续累加)
-- 闭包做迭代器:每次调用吐下一个
local function from(n)
return function()
n = n + 1
return n
end
end
local next = from(10)
print(next(), next()) -- 11 12
进阶 2 · 元表与元方法重载运算符、控制取值
每个 table 可以挂一个元表(metatable),里面放"元方法"来改写默认行为:找不到键时怎么办、相加时怎么办、打印时显示什么……
local t = {a = 1}
local mt = {
__index = function(self, k) return "缺省值" end, -- 取不到键时返回
__tostring = function(self) return "表内容:" .. tostring(self.a) end,
}
setmetatable(t, mt)
print(t.b) -- 缺省值(__index 兜底)
print(t) -- 表内容:1(__tostring 控制打印)
-- 用元表重载运算符(+ 等)
local Vec = {}
Vec.__add = function(a, b) return {x=a.x+b.x, y=a.y+b.y} end
setmetatable(Vec, Vec)
local v = setmetatable({x=1,y=2}, Vec)
local u = setmetatable({x=3,y=4}, Vec)
local w = v + u -- 触发 __add
print(w.x, w.y) -- 4 6
__index 可以是函数也可以是表(从那个表里找);② __newindex 控制"写入不存在的键",常用来做只读表/代理;③ __eq/__lt 等比较元方法只在两个表共用同一个元表时才生效,否则默认按引用比较。进阶 3 · 用 table 模拟面向对象没有 class,用元表造
Lua 没有 class 关键字,但用"原型 + __index"就能实现类和继承。这是写游戏逻辑、框架的标配手法。
-- 基类
local Animal = {}
Animal.__index = Animal
function Animal.new(name)
local o = {name = name}
setmetatable(o, Animal) -- o 找不到的方法去 Animal 里找
return o
end
function Animal:speak()
print(self.name .. " 叫了一声")
end
-- 子类(继承)
local Dog = setmetatable({}, Animal) -- Dog 的元表是 Animal
Dog.__index = Dog
function Dog.new(name)
local o = Animal.new(name)
setmetatable(o, Dog)
return o
end
function Dog:speak() -- 重写
print(self.name .. " 汪汪")
end
local d = Dog.new("阿黄")
d:speak() -- 阿黄 汪汪(调用了子类方法)
print(getmetatable(d).__index == Dog) -- true
function Animal:speak() 等价于 function Animal.speak(self),只是自动补了 self 参数。继承链就是一层层 __index 向上找方法。进阶 4 · 协程 coroutine协作式并发,比回调优雅
协程(coroutine)让函数能中途让出(yield)、之后恢复(resume),像"可暂停的函数"。它单线程、不并行,但能把异步/流式逻辑写得像同步一样直白。
local co = coroutine.create(function()
for i = 1, 3 do
print("产出", i)
coroutine.yield(i) -- 让出,把 i 传回去
end
return "结束"
end)
print(coroutine.status(co)) -- suspended
print(coroutine.resume(co)) -- true 产出1 1
print(coroutine.resume(co)) -- true 产出2 2
print(coroutine.resume(co)) -- true 产出3 3
print(coroutine.resume(co)) -- true 结束(函数跑完)
print(coroutine.resume(co)) -- false cannot resume dead coroutine
coroutine.wrap 返回一个可直接调用的函数,更省事。进阶 5 · 字符串模式匹配Lua 的"正则"(但不是正则)
Lua 的字符串"模式(pattern)"长得像正则,但更简单也更受限:没有 | 多选、没有贪婪/懒惰的复杂规则、捕获用 %1..%9。日常 90% 的文本处理它够用且更快。
local s = "订单 12345 金额 99.5"
-- %d 数字,+ 一个或多个
local code = s:match("订单 (%d+)")
print(code) -- 12345
-- gsub 替换,返回新串和替换次数
local new, n = s:gsub("%d+", "#")
print(new, n) -- 订单 # 金额 #.# 2
-- gmatch 迭代所有匹配
for num in string.gmatch("a1b2c3", "%d") do print(num) end -- 1 2 3
-- 常用魔法字符:%a字母 %d数字 %s空白 %w字母数字 %l小写 %u大写 %.点号 %%%转义
. 匹配任意字符(包括点),要匹配真正的点必须写成 %.。捕获组是 () 配合 %1,没有 \1 也没有命名捕获。复杂正则需求(如邮箱严格校验)Lua 模式力不从心,得上第三方 lpeg 库。进阶 6 · 环境与沙箱 _ENV控制全局、安全执行
Lua 5.2+ 用 _ENV 表示"当前全局环境"(本质就是一张表,默认是 _G)。把 _ENV 换成你自己的表,就能沙箱化一段不受信任的脚本——它只能访问你允许的少数函数。
-- 只暴露 print 和 math 的"白名单"环境
local safe_env = { print = print, math = math, ipairs = ipairs, pairs = pairs }
local code = [[
print("在沙箱里:", math.floor(3.9)) -- 允许
-- os.execute("rm -rf /") -- 若打开会报错:os 不在白名单
]]
local fn = load(code)
setfenv and setfenv(fn, safe_env) -- 5.1 用 setfenv
-- 5.2+:把 local _ENV = safe_env 写在代码顶部即可
fn and fn()
pcall 包住执行,既隔离又防崩。🧪 动手实验室(点 ▶ 直接看结果)
下面每个程序代码已预填,点「运行」即可看输出;想改就在框里改再运行。Lua 走云端编译(首次稍慢,相同代码会缓存秒回)。
① Hello 与多返回值
② table 遍历与求和
③ 函数多返回值 + 变长参数
④ 闭包计数器
⑤ 元表运算符重载(向量相加)
💡 连老师都不会教你的干货(工业级技巧)
- table 是 Lua 唯一的复合结构——数组、字典、对象、类、集合、队列,全靠它。理解了 table,Lua 就通了一半。
- 数组默认从 1 开始,且
#t对有空洞的表不可靠。要长度请保持 1..n 紧凑,或自己维护计数,别迷信#。 - 永远优先用
local。全局变量存进_G,跨文件互相覆盖还不报错,是 Lua 项目最隐蔽的 bug 来源。模块接口才用 return 暴露。 - 多返回值只在表达式尾部完整保留。想强制只取一个,用
(f())包成单值;想忽略多余值,直接少写几个接收变量。 - 闭包能实现私有状态与迭代器,但闭包持有变量不会被 GC。循环里大量 new 闭包要留意内存,必要时复用。
- 元表
__index实现继承与默认值;__newindex可做只读表/代理;__tostring控制打印、__eq/__lt控制比较(注意需共用元表才生效)。 - 协程比回调直观:用 resume/yield 把"异步流式"逻辑写成看似同步的代码,游戏 AI、状态机、OpenResty 高并发都靠它。但记住它不并行。
- 字符串模式用
%而非正则:没有|多选、没有回溯、捕获是%1..%9。性能稳、够用,但复杂校验得上 lpeg。 pcall包住错误而非裸assert:可恢复错误用pcall返回 true/false + 错误信息;xpcall还能带 traceback,定位线上错误更方便。_ENV重定义实现沙箱:把不受信任脚本的全局环境换成白名单表,再配合pcall,就能安全地让用户输入逻辑(插件系统、在线判题同理)。- 尾调用优化:写成
return f(...)的尾调用不新增栈帧,可无限递归不爆栈。写递归/状态机时优先用尾调用形式。 - 用 table 做缓存(memoization):把已算结果存
cache[key],斐波那契/动态规划类递归从指数级降到线性,几乎零成本。 - 大量字符串拼接用
table.concat,别在循环里反复..,否则生成大量中间串拖垮性能。 - 与 C 交互是 Lua 的杀手锏:宿主用
lua_State*、lua_push*/lua_to*注册 C 函数、读写 table,让 Lua 当逻辑层、C 当性能层——Redis 脚本、Nginx-Lua、游戏引擎皆此架构。
自测题(点击展开答案)
- Q1:Lua 数组第一个元素用哪个下标访问?A:
a[1]。Lua 数组默认从 1 开始,a[0]是 nil。 - Q2:没写
local的变量存在哪里,有什么风险?A:存在全局表_G里。风险是跨文件互相覆盖、且无报错,是最隐蔽的 bug 源;应默认全部 local。 - Q3:
if 0 then在 Lua 里是真还是假?A:真。Lua 中只有false和nil是假,0和空串""都是真,和 C/Python 不同。 - Q4:函数
return a, b在表达式print("x", f())里会拿到几个返回值?A:只有第一个。多返回值只在表达式尾部保留;f()不在尾部,多余返回值被丢弃。 - Q5:想让一个 table 相加(a + b)时触发自定义逻辑,该设哪个元方法?A:
__add。把它写进该 table 的元表,并重载加法行为;注意__eq/__lt等比较元方法需两个表共用同一元表才生效。