Lua 是什么:一门装进别人程序里的语言

Lua 诞生于 1993 年,在葡萄牙语里是“月亮”的意思。它很少当主角,更多时候是被嵌进用 C 或 C++ 写成的宿主程序里,负责那些需要频繁改动、又不想重新编译整个软件的逻辑。三十多年过去,《魔兽世界》的插件、Redis 的脚本、Nginx 网关的扩展里,依然跑着它。

名字、出身与设计哲学

Lua 出自巴西里约热内卢天主教大学(PUC-Rio)的 Tecgraf 实验室,作者是 Roberto Ierusalimschy、Waldemar Celes 和 Luiz Henrique de Figueiredo 三个人。它最初为巴西石油公司的数据描述与配置需求而写,从诞生那天起就没打算做大型独立应用,而是准备被“嵌”进别人的程序。

它的设计原则通常被概括成“机制而非策略”。语言只给一套足够小的底层零件,怎么组合由你自己定。所以 Lua 里没有 class,没有继承关键字,也没有 try/catch,但用表、元表和协程,你照样能把这些拼出来。整个官方实现是两万多行标准 C,跨平台能力接近一句老话:能跑 C 的地方就能跑 Lua。

它凭什么被反复选择

  • 体积小。解释器核心只有几百 KB,编译出的可执行文件常在 200 KB 上下,塞进路由器和单片机都没什么负担。
  • 跑得快。虚拟机按寄存器模型设计,纯计算任务通常比 Python 快,换成 LuaJIT 之后差距还会拉开。
  • 好嵌入。官方只提供一套精简的 C API,宿主程序愿意开放哪些能力,完全由自己决定。
  • 语言小。关键字二十来个,语法几天就能上手,但表和元表足以撑起复杂系统。
  • 活得久。从 1993 年延续至今,语法改动一直很克制,老代码的迁移成本相对可控。

Lua 5.5:2025 年底的这次大更新

先看一张现状表,把版本和生态的坐标定下来。

项目现状
标准 Lua5.5.0,2025 年 12 月 22 日发布
当前补丁5.5.1,2026 年 8 月 3 日发布
上一代稳定版5.4 系列,补丁已更新到 5.4.9
高性能实现LuaJIT,走 2.1 滚动更新,兼容 Lua 5.1
包管理LuaRocks
语言服务器lua-language-server

从 2020 年 6 月的 5.4.0 到 5.5.0,中间隔了大约五年半,这也是 Lua 少见的一次大版本跨越。它没有往语言里堆新概念,改动集中在内存、延迟和安全性这三处长期被抱怨的地方。

数组更紧凑,内存省得多

Lua 只有表这一种容器,数组也是用表模拟出来的。过去纯整数下标的表走通用结构,元素一多内存就堆得厉害。5.5 换了一套更紧凑的数组布局,官方在变更说明里给出的数字是:大型数组的内存占用最多可以减少约 60%。对嵌入式设备和手游这类内存吃紧的场景,升级后不用改一行代码就能拿到这份收益。

垃圾回收改成增量式

5.4 主打的是分代回收,思路是“大多数对象死得早”,先频繁清理年轻对象。5.5 把主回收周期改成了增量执行:一次大的回收不再一口气暂停几十毫秒,而是拆成小步,插在程序运行中间完成。对帧循环、实时音视频这类对延迟敏感的负载,这项改动的意义比省内存还大。

全局变量要显式声明

这条最可能影响老代码。5.5 把 global 收作保留字,变量默认不再自动变成全局,想用全局就得明确声明。好处是拼错变量名、漏写 local 这类低级错误能在早期就被抓出来;代价是升级前得把散落各处的隐式全局梳理一遍。

-- Lua 5.5 新特性一:显式声明全局变量
global AppName = "demo"       -- 声明一个全局变量
global Port <const> = 8080    -- 全局常量

-- 只要一个块里出现过 global 声明,这个块里所有变量都必须先声明
-- EnableDebug = true    -- 这里会直接报错,而不是静默创建全局变量

global 还有通配形式 global *,表示“其余未声明的名字都当全局”,专门用来给老代码过渡。

其它调整与升级注意

还有一批零碎但实用的改动:for 循环里的控制变量变成只读,想改就得在循环体内另声明一个同名局部变量;浮点数默认以足够的十进制精度输出,能被原样读回;新增 table.create 用于高效建表;utf8.offset 现在还会返回字符的结束位置;支持由外部内存管理的字符串,方便和 C 侧做零拷贝交互。

不兼容的地方集中在几处:global 成了保留字;__call 元方法链最多只能串 15 个对象;错误处理里 nil 作为错误对象会被替换成字符串;C API 侧 lua_resetthread、lua_setcstacklimit 等函数被弃用,垃圾回收参数改用统一的 param 选项传递。完整的变更清单以官方文档为准:Lua 5.5 参考手册。

十分钟把 Lua 跑起来

安装

Linux 上能直接用包管理器装:

sudo apt install lua5.4        # Debian 或 Ubuntu,装 5.4
brew install lua               # macOS

想用最新的 5.5,从官方源码编译最省心:

curl -R -O https://www.lua.org/ftp/lua-5.5.0.tar.gz
tar zxf lua-5.5.0.tar.gz
cd lua-5.5.0
make linux                     # 其他平台把 linux 换成对应目标
make test                      # 跑一遍自带测试
sudo make install              # 安装到系统目录

编译完成后,src 目录下会得到三个东西:解释器 lua、编译器 luac,以及静态库 liblua.a。

第一个脚本

新建一个 hello.lua,写入下面的内容:

-- 字符串用两个点号拼接
local name = "世界"
print("你好," .. name)

-- 表既能当数组也能当字典
local person = { name = "阿宝", age = 30, skills = { "Lua", "C" } }
print(person.name, person.skills[1])    -- 索引从 1 开始

-- 遍历序列用 ipairs,遍历键值对用 pairs
for i, skill in ipairs(person.skills) do
  print(i, skill)
end

存盘后执行 lua hello.lua 就能看到输出。

交互式解释器

直接敲 lua 会进入 REPL,写一句执行一句,用来试语法特别顺手。

lua
> print(1 + 2)
3
> =math.pi
3.1415926535898

5.5 里 REPL 还支持动态加载 readline,补全和历史记录比过去顺手了一些。

核心语法速通

八种基本类型

Lua 的类型很少,一共八种:nil、boolean、number、string、function、thread、table、userdata。数字在 5.3 之后区分整数和浮点两个子类型,math.type(3) 返回 integer,math.type(3.0) 返回 float。

变量不需要声明类型,赋什么就是什么。默认作用域是全局,所以写代码时要养成加 local 的习惯,性能和变量污染两方面都受益。

表:唯一却万能

表是 Lua 里唯一的内置容器,数组、字典、对象、模块全靠它。索引默认从 1 开始,没赋值的空位是 nil,# 用来取长度。

local arr = { "a", "b", "c" }
print(#arr)              -- 3

local dict = { x = 1, y = 2 }
dict.z = 3               -- 字段随时能加
print(dict.z)            -- 3

控制结构与函数

-- if / elseif / else
local score = 85
if score >= 90 then
  print("优秀")
elseif score >= 60 then
  print("及格")
else
  print("不及格")
end

-- 数值 for
for i = 1, 5 do print(i) end

-- while
local n = 3
while n > 0 do
  print(n)
  n = n - 1
end

-- 函数是一等公民,可以赋值、传参、被返回
local function add(a, b) return a + b end
print(add(2, 3))

闭包是 Lua 用得最多的一招,函数会把定义时所在的局部变量一起带走。

local function make_counter()
  local count = 0
  return function()
    count = count + 1
    return count
  end
end

local next_id = make_counter()
print(next_id())     -- 1
print(next_id())     -- 2

元表与面向对象

Lua 没有 class,靠元表的 __index 元方法实现继承,这是 Lua 写面向对象的标准套路。

local Animal = {}
Animal.__index = Animal

function Animal.new(name)
  local self = setmetatable({}, Animal)
  self.name = name
  return self
end

function Animal:speak()
  print(self.name .. " 叫了一声")
end

-- 让 Dog 继承 Animal
local Dog = setmetatable({}, { __index = Animal })
Dog.__index = Dog

function Dog.new(name)
  local self = Animal.new(name)
  return setmetatable(self, Dog)
end

function Dog:speak()
  print(self.name .. " 汪了一声")
end

Dog.new("旺财"):speak()     -- 旺财 汪了一声

同样的机制还有一圈兄弟姐妹:__newindex 拦截对不存在的键的赋值(只读表、代理表都靠它)、__add 与 __eq 等运算符元方法让表能参与加减比较、__len 接管长度操作、__call 让表能像函数一样被调用。这些机制拼起来,Lua 就有了面向对象、面向切面乃至各种花式玩法——代价是完全没有编译期检查,字段写错只能等运行时发现。

协程与错误处理

协程是“能随时暂停和恢复的函数”,是 Lua 里做生成器、状态机和协作式并发的原语。它不是线程,没有抢占,切换点完全由代码里的 yield 决定:

-- 协程:每次 resume 推进一步,yield 交出控制权
local producer = coroutine.create(function()
  for i = 1, 3 do
    coroutine.yield(i * i)
  end
end)

print(coroutine.resume(producer))    -- true 和 1
print(coroutine.resume(producer))    -- true 和 4
print(coroutine.resume(producer))    -- true 和 9
print(coroutine.resume(producer))    -- true,协程已跑完,没有值了

resume 的第一个返回值永远是“是否成功”,出错时第二个返回值是错误对象。这也引出了 Lua 的错误处理约定:库函数失败返回 nil 加错误消息,业务代码用 error 抛错、pcall 兜底:

-- 错误处理:pcall 兜底,xpcall 能带出完整调用栈
local function charge(amount)
  if amount <= 0 then
    error("充值金额必须大于 0", 0)    -- 第二个参数 0 表示错误消息不带位置前缀
  end
  return amount
end

local ok, result = pcall(charge, -5)
print(ok, result)    -- false 和 充值金额必须大于 0

-- 想拿到出错时的调用栈,把 debug.traceback 作为处理器传给 xpcall
local ok2, err2 = xpcall(charge, debug.traceback, -5)

版本进化史:从 5.0 到 5.5 每代加了什么

把主版本按时间铺开,能看清 Lua 的升级节奏——稳,但每一步都动真格:

版本发布年份里程碑
Lua 1.01993首个版本,诞生于 PUC-Rio
Lua 5.02003元表机制与协程成型
Lua 5.12006module 与 require 模块体系
Lua 5.22011_ENV 环境机制、goto 语句
Lua 5.32015独立整数类型、位运算、utf8 库
Lua 5.42020分代垃圾回收、const 与 close 属性
Lua 5.52025全局变量声明、命名可变参数、只读循环变量

5.3:整数从浮点里分家

5.3 之前 Lua 只有一种“数”,全是双精度浮点。5.3 把 64 位整数请回语言,除法和整除从此是两回事,位运算也进了语言:

-- Lua 5.3:整数除法与位运算
print(7 / 2)      -- 3.5,普通除法结果是浮点数
print(7 // 2)     -- 3,整除
print(1 << 10)    -- 1024,位移
print(3 & 5)      -- 1,按位与

5.4:常量、自动关闭与分代回收

5.4 给局部变量加了两个属性:const 属性声明常量,赋值直接在编译期报错;close 属性声明“离开作用域自动关闭”的变量,配合元方法 __close,文件句柄、锁这类资源再也不用手动清理:

-- Lua 5.4:to-be-closed 变量,函数怎么退出都会自动关门
local function read_config(path)
  local f <close> = assert(io.open(path, "r"))
  return f:read("a")    -- 无论正常返回还是中途出错,f 都会被自动关闭
end

local rate <const> = 0.5
-- rate = 0.6    -- 编译期直接报错

同版本把垃圾回收升级为分代模式,并新增 warn 函数和整型 for 循环的默认步长修正。

LuaJIT 与 Luau:绕不开的两条岔路

标准 Lua 之外有两套实现必须认识。LuaJIT 是 Mike Pall 自 2005 年起维护的即时编译实现,官网明确以 Lua 5.1 语义为基础,附带 JIT 编译、BitOp 位运算和 FFI 外部函数接口——FFI 让你在 Lua 里直接声明 C 函数和结构体来调用,不再需要写胶水绑定,Neovim 和 OpenResty 都跑在它上面。它的 2.1 分支以滚动发布方式长期演进,不追随官方 5.4 与 5.5 的语言特性。

Luau 则是 Roblox 为自家游戏平台从 Lua 5.1 出发做的开源方言,加入了渐进类型注解、严格模式等工程化特性。也就是说,2026 年同时存在“官方 Lua(5.4 与 5.5)”“LuaJIT(5.1 语义加 JIT)”“Luau(5.1 语义加类型系统)”三条线。给宿主写代码时认准宿主用哪套,别把 5.5 的 global 声明写进 Neovim 配置里——LuaJIT 根本不认识它。

从入门到精通的学习路线

“精通”这个词有点大,落到实处是一层一层往上垒。

阶段目标建议投入
入门装好解释器,跑通值、表、控制流、函数一到两周
核心进阶吃透元表、协程、错误处理,能读懂别人的插件三到四周
工程化LuaRocks 装库、luacheck 查错、LuaLS 补全、打包自己的库一到两个月
精通用 C API 把 Lua 嵌进自己的项目,精读官方源码,跟进 lua-l长期

第一阶段:把语法和标准库用熟

目标是不用查手册也能把语言写顺。重点是表的构造与遍历、多返回值、可变长参数、尾调用、字符串模式匹配(不是正则,是 Lua 自己的一套),以及和 io、os、math、string、table 这几个标准库打交道。这一阶段解决的是“能写出来”。

第二阶段:学会工程化

语法会了,还得知道怎么组织代码。用 require 拆模块,用 LuaRocks 管依赖,让每个模块把要暴露的东西 return 出去,再给项目配一个 .luarc.json,让语言服务器认得你的目录结构。这一阶段解决的是“能维护”。

第三阶段:碰 C 与性能

真正拉开差距的是这几块:元表的高级用法、协程在业务里的调度、C API 与宿主程序的双向调用,以及定位性能瓶颈。写游戏或网关的人,最后基本都会被推到这一步。用 collectgarbage("count") 看内存,用 LuaJIT 顶住热点路径,把重活下放到 C 侧,都是常见套路。这一阶段解决的是“能扛住”。

Lua 现在都在哪干活

宿主全景

学 Lua 的第一课是认清定位:Lua 几乎从不单独作为主语言去开发完整应用,它的主战场是当宿主程序里的嵌入脚本层。下面这张表能把生态定位一眼看清。

宿主用 Lua 做什么用的哪套实现
Neovim配置文件与全部插件生态LuaJIT(Lua 5.1 语义)
OpenRestyNginx 上的动态网关与业务逻辑LuaJIT
RedisEVAL 服务器端脚本官方 Lua 5.1
Roblox全部游戏脚本Luau 方言
魔兽世界插件开发官方 Lua 5.1
LÖVE(Love2D)2D 独立游戏开发官方 Lua
Defold2D 与 3D 游戏脚本官方 Lua
Nmap 与 Wireshark扩展脚本官方 Lua

2026 年学 Lua 最现实的三个理由,恰好对应三个方向:给 Neovim 写配置和插件(编辑器方向)、用 LÖVE 或 Defold 做独立游戏(游戏方向)、在 OpenResty 网关层写业务(服务端方向)。

游戏与插件

游戏是 Lua 最大的地盘。《魔兽世界》的界面插件、《愤怒的小鸟》的游戏逻辑、Roblox 平台上的创作脚本,用的都是 Lua 或它的方言 Luau。引擎把渲染、物理这些重活留在 C++ 里,把玩法、任务、UI 交给 Lua,改内容不用重编客户端,热更新一下就行。

网关与 Web

OpenResty 把 Nginx 和 LuaJIT 拼在一起,用 Lua 写网关逻辑、限流、鉴权、动态路由,是国内后端很常见的一套组合。对熟悉 Nginx 配置、又想给请求处理加一点编程能力的人来说,这条路的上手成本很低。

嵌入式与物联网

解释器小、能裁剪、能嵌 C,这些特点让 Lua 在嵌入式里站得很稳。路由器固件 OpenWRT 用 Lua 做配置,NodeMCU 一类物联网固件直接用 Lua 控制硬件,一些工业控制器也让用户用 Lua 写简单的联动规则。

数据库与配置

Redis 支持执行 Lua 脚本,可以把你的一串命令打包成一次原子操作,避免来回的网络开销。也有不少软件干脆拿 Lua 当配置文件格式,因为它既可读又能执行,比 JSON、XML 灵活得多。

工具链与学习资料

运行时与包管理

标准 Lua 从官方网站获取,源码和手册都在那里。需要更高性能时用 LuaJIT,它兼容 Lua 5.1 语法,还附带 FFI 可以直接调 C 函数。装第三方库用 LuaRocks,用法和 pip、npm 差不多。

编辑器与语言服务器

写 Lua 建议配上 lua-language-server。它本身也是用 Lua 写成的,支持从 5.1 到 5.5 以及 LuaJIT,能补全、能查错、能跨版本做兼容性提示,接进 VSCode 或 Neovim 都行。再把 luacheck 挂到编辑器上,未定义变量、全局污染这类问题当场就能标出来。

书与官方文档

最权威的入门书是 Lua 作者亲自写的《Lua 程序设计》第 4 版,中文版由电子工业出版社引进,英文原版 Programming in Lua 在官网可查。这本书的内容基于 Lua 5.3,语言思想部分完全通用,但讲到垃圾回收、整数除法这些细节时,要记得对照新版。真正需要精确答案时,以官方手册为准:Lua 5.5 参考手册。

一个实用的建议:别在版本选择上纠结太久。先拿 5.5 把基础语法敲一遍,等真正写游戏插件或网关扩展时,再按宿主环境锁定的版本回退。

新手最容易踩的坑

  • 下标从 1 开始。 从 Python、C 转 Lua 的人写 t[0] 不报错,拿到的是 nil,然后整个循环空转。数组、字符串下标全部从 1 数起。
  • 漏写 local 就是无声的全局。 函数里一个变量忘了 local,它就成了全局,两个函数互相污染还查不出原因。用 luacheck 事先拦截,上 5.5 就用 global 声明把这类错误变成显式报错。
  • 井号长度只对“无洞数组”有意义。 表里有 nil 空洞时,#t 返回哪一侧边界是未定义行为,可能每次不同。需要准确长度就自己维护一个计数,或用 table.pack 的 n 字段。
  • ipairs 见 nil 就停。 数组中段混进 nil,ipairs 会提前结束遍历。要完整遍历又可能含 nil 时,用数字 for 配合已知长度。
  • pairs 不保证顺序。 想按顺序输出就先 table.sort 一份键列表。
  • 循环里大量字符串拼接性能很差。 字符串在 Lua 里不可变,.. 在循环里每轮都生成新串,用 table.concat 把片段攒进表再一次性连接。
  • 整数溢出会回绕。 5.3 起的 64 位整数运算溢出时不报错,直接环绕。别用 Lua 算天文数字与密码学场景的大数。
  • 给宿主写脚本先确认版本。 Neovim 是 LuaJIT(5.1 语义),Redis 是 5.1,LÖVE 用官方 Lua。把 5.3 之后才有的 //、位运算、goto 写进 LuaJIT 环境,直接语法报错。
  • 元方法里小心无限递归。 __index 里再用普通索引会再次触发元方法,必要时改用 rawget 与 rawset 绕开元表。

和 Python 比,什么时候该选 Lua

这两个常被放在一起问,答案其实不复杂:看你想让它当主角还是当配角。

使用场景更合适的选择
数据处理、爬虫、自动化Python
数据分析与 AIPython
Web 后端与业务系统Python
游戏脚本、插件、热更新Lua
嵌进 C 或 C++ 宿主程序Lua
资源受限的嵌入式环境Lua

一句话总结:不确定的时候先学 Python;明确是游戏脚本,或者要给一个已有的宿主程序加扩展逻辑,Lua 更顺手。

写在最后

Lua 不是那种能拿去面试里吹一小时的热门语言。它的价值在于位置感很强:清楚自己该待在哪一层,然后把那一层做到足够轻、足够快。2025 年底的 5.5 没有给语言堆新概念,反而在内存、延迟、安全性这些地方补了短板,对一门三十多岁的语言来说,这种克制挺难得。

Lua 的“精通”从来不是背下多少语言特性——它的语言特性一只手数得完——而是三件事:把表和元表的组合玩熟,把协程的调度想透,把宿主环境摸清。2026 年这一版 5.5 把最常见的运行时哑弹(隐式全局)变成了显式错误,学它的时机比任何时候都好。选一个宿主动手,比读十篇教程都管用。

入门与进阶资源清单

发表评论

验证码图片,点击可更换