Lua 是一种轻量级、可嵌入的脚本语言,解释器核心仅约 2 万行 C 代码,设计目标是"用最小的体积提供最大的扩展能力"。它语法简洁、易于学习,被广泛用于游戏开发(魔兽世界、Roblox)、Web 网关(OpenResty)、嵌入式系统等场景。本篇教程带你在 30 分钟内完成环境搭建并写出第一个 Lua 脚本。
1. 环境搭建
macOS(推荐 Homebrew):
brew install lua
Linux(Debian/Ubuntu):
sudo apt-get install lua5.4
Windows:从 LuaBinaries 下载预编译二进制,解压后将 lua54.exe 所在目录加入 PATH 即可。
安装完成后验证:
lua -v
# Lua 5.4.x Copyright (C) 1994-2024 Lua.org, PUC-Rio
用 REPL 交互式学习
直接输入 lua 即可进入交互式解释器(REPL),这是学习语法最快的方式——每写一行立刻看到结果,不用保存文件再运行:
$ lua
Lua 5.4.6 Copyright (C) 1994-2023 Lua.org, PUC-Rio
> print("hello")
hello
> = 1 + 2 -- REPL 中可以用 = 快速打印表达式
3
> name = "lua" -- 定义变量
> = name:upper() -- 立刻验证结果
LUA
几个 REPL 小技巧:
- 用
= 表达式代替print(表达式),更省事; - 多行代码(如函数定义)直接逐行输入,REPL 会等待
end再执行; - 输入
os.exit()或按Ctrl+D退出。
编辑器配置:VSCode + Lua 插件
学习阶段推荐 VSCode,安装 sumneko.lua(Lua Language Server)插件,可以获得:
- 语法高亮与自动补全;
- 实时的类型检查与未定义变量警告(能帮你提前发现把局部变量写成全局的笔误);
- 悬停查看标准库函数的文档。
配合 Code Runner 类插件,按一个快捷键就能运行当前 .lua 文件,学习体验会顺畅很多。
关于 Lua 版本:5.1 / 5.3 / 5.4 学哪个?
Lua 的版本差异比 Python 2/3 温和得多,但仍有几个关键点需要注意:
| 版本 | 现状 | 典型用户 |
|---|---|---|
| Lua 5.1 | 老但应用极广 | LuaJIT、OpenResty、魔兽世界插件 |
| Lua 5.3 | 引入整数/浮点区分、位运算 | 部分嵌入式项目 |
| Lua 5.4 | 当前最新稳定版 | 官方推荐的新项目 |
建议:自学直接用 Lua 5.4;如果目标明确是 OpenResty 或游戏热更新(基于 LuaJIT),再补 5.1 的差异即可。本教程代码同时兼容 5.3 和 5.4。各版本的详细差异与选型分析见 Lua 版本对比。
2. 运行第一个脚本
新建文件 hello.lua:
print("Hello, Lua!")
运行:
lua hello.lua
3. 基本语法
- 注释:单行用
--,多行用--[[ ... ]] - 语句:不需要分号结尾(写了也合法)
- 变量:默认是全局变量,用
local声明局部变量(推荐,避免污染全局环境)
-- 单行注释
--[[
多行注释
]]
local name = "Lua" -- 局部变量
version = 5.4 -- 全局变量(不推荐)
新手常见坑:Lua 变量不加
local就是全局的。函数内部忘了写local,变量会泄漏到全局环境,和其他模块的同名变量互相覆盖,是非常隐蔽的 bug 来源。养成"默认都写local“的习惯。
4. 数据类型
Lua 只有 8 种基本类型,常用的有:
| 类型 | 示例 | 说明 |
|---|---|---|
| nil | nil | 表示"无值”,未定义的变量即为 nil |
| boolean | true / false | 条件判断中,只有 nil 和 false 为假 |
| number | 42、3.14 | Lua 5.3+ 区分整数与浮点 |
| string | "hello" | 单双引号均可,.. 进行拼接 |
| table | {1, 2, 3} | 唯一的复合数据结构 |
| function | function() end | 函数是一等公民 |
local a = nil
local b = true
local c = 42
local d = "hello"
print(type(d)) -- string
新手常见坑:在 Lua 的条件判断里,只有
nil和false为假,0和空字符串""都是真!这和 Python、JavaScript 都不一样:if 0 then print("0 是真值") end -- 会打印! if "" then print("空串也是真") end -- 会打印!
5. 控制结构
条件判断:
local score = 85
if score >= 90 then
print("优秀")
elseif score >= 60 then
print("及格")
else
print("不及格")
end
循环:
-- while
local i = 1
while i <= 3 do
print(i)
i = i + 1 -- 注意:Lua 没有 ++ 运算符
end
-- 数值 for
for i = 1, 5 do
print(i) -- 1 2 3 4 5
end
for i = 10, 1, -2 do
print(i) -- 10 8 6 4 2(第三个参数是步长)
end
-- repeat-until(至少执行一次)
local n = 0
repeat
n = n + 1
until n >= 3
新手常见坑:Lua 的
for i = 1, 5是闭区间,会执行到 5,这点和 C/Java 的for (i=1; i<5; i++)直觉相反。另外 Lua 没有++、--、+=这些运算符,只能写i = i + 1。
6. 函数
函数用 function 定义,支持多返回值:
-- 定义与调用
local function greet(name)
return "Hello, " .. name .. "!"
end
print(greet("World")) -- Hello, World!
-- 多返回值
local function divmod(a, b)
return a // b, a % b
end
local q, r = divmod(10, 3)
print(q, r) -- 3 1
新手常见坑:注意
/和//的区别——/是浮点除法,//是整数除法(向下取整):print(7 / 2) -- 3.5 print(7 // 2) -- 3 print(10 / 2) -- 5.0(注意是浮点数 5.0)从 Python 2 转来的读者尤其要注意:Lua 的
/永远是浮点除法。
7. 表(table):Lua 的核心
table 是 Lua 中唯一的复合数据结构,可以同时当数组和字典用:
-- 作为数组(注意:下标从 1 开始!)
local fruits = {"apple", "banana", "cherry"}
print(fruits[1]) -- apple
print(#fruits) -- 3(# 取长度)
-- 作为字典
local person = {name = "John", age = 30}
print(person.name) -- John
print(person["age"]) -- 30
-- 遍历
for i, fruit in ipairs(fruits) do
print(i, fruit)
end
for key, value in pairs(person) do
print(key, value)
end
新手常见坑 1:table 下标从 1 开始,不是 0!
fruits[0]是nil,这是所有从 C/Java/Python 转来的人必踩的第一个坑。新手常见坑 2:
pairs遍历的顺序不保证是插入顺序;要按顺序遍历数组部分请用ipairs。另外ipairs遇到nil空洞就会停止,所以数组中间不能有nil。新手常见坑 3:字符串比较直接用
==即可,Lua 的字符串是不可变且被内部驻留(interned)的,==比较的就是内容,不需要像 Java 那样用equals:local a = "hello" local b = "he" .. "llo" print(a == b) -- true
想深入 table 与泛型 for 的细节(比如自定义迭代器),可以继续读 Lua 数组与迭代器。
8. 字符串操作
local s = "hello"
print(s .. " world") -- 拼接:hello world
print(string.upper(s)) -- HELLO
print(string.len(s)) -- 5
print(string.format("%d-%s", 42, "lua")) -- 42-lua
Lua 还有一套自己的模式匹配语法(类似但不同于正则表达式),用于查找、替换、提取字符串,是文本处理的利器。进阶用法见 Lua 字符串模式匹配。
9. 模块与错误处理
用 require 加载模块,用 pcall 捕获错误:
-- mymodule.lua
local M = {}
function M.say_hi()
return "hi"
end
return M
-- main.lua
local m = require("mymodule")
print(m.say_hi())
-- 错误处理
local ok, err = pcall(function()
error("出错了")
end)
print(ok, err) -- false 出错了
模块系统(require 的搜索路径、package.path、LuaRocks 包管理)和错误处理(pcall/xpcall 的区别)都有更多细节,分别参考 Lua 模块与包管理 和 Lua 错误处理最佳实践。
10. Lua 与其他语言的直观对比
如果你已经会 Python 或 JavaScript,下面这张速查表能帮你快速建立直觉:
| 主题 | Lua | Python / JavaScript |
|---|---|---|
| 数组下标 | 从 1 开始 | 从 0 开始 |
| 假值 | 只有 nil 和 false | 0、""、null 等都是假 |
| 代码块 | then / do / end 包裹 | 缩进(Python)或 {}(JS) |
| 字符串拼接 | .. | + |
| 不等于 | ~= | != |
| 局部变量 | local x | let x / 直接赋值 |
| 哈希表 | table(数组字典二合一) | dict / Object |
| 多返回值 | 原生支持 return a, b | Python 靠元组,JS 靠数组解构 |
几个值得单独记住的点:
- 一切皆 table:Python 的 list、dict,JS 的数组、对象,在 Lua 里统一用 table 表达,甚至连面向对象也是用 table + 元表模拟出来的(见 Lua 面向对象编程)。
- 函数是一等公民:和 JavaScript 一样,函数可以赋值给变量、作为参数传递。Lua 的闭包机制非常干净,详见 Lua 闭包与 Upvalue。
- 没有类和异常:Lua 没有
class关键字,也没有try/except,错误处理用pcall把函数包起来"试调用"。 - 协程是语言内置的:不需要 async/await 语法,协程是标准库的一部分,进阶内容见 Lua 协程深入解析。
如果你正在权衡要不要学 Lua,也可以看看 Lua 与 Python 的对比,了解两者各自适合的场景。
11. 综合示例:命令行记账本
把上面的知识组合起来,写一个比"成绩统计"更有意思的小项目——一个简易记账本,支持记一笔、看账单、算结余:
-- ledger.lua:简易命令行记账本
local ledger = {
records = {}, -- 每条记录:{item=项目, amount=金额(正为收入,负为支出)}
}
-- 记一笔账
function ledger.add(item, amount)
table.insert(ledger.records, {item = item, amount = amount})
end
-- 计算总结余
function ledger.balance()
local sum = 0
for _, r in ipairs(ledger.records) do
sum = sum + r.amount
end
return sum
end
-- 打印账单
function ledger.report()
print(string.format("%-4s %-12s %10s", "编号", "项目", "金额"))
print(string.rep("-", 30))
for i, r in ipairs(ledger.records) do
print(string.format("%-4d %-12s %+10.2f", i, r.item, r.amount))
end
print(string.rep("-", 30))
print(string.format("总结余: %.2f 元", ledger.balance()))
end
-- 录入几笔账
ledger.add("工资", 8000)
ledger.add("房租", -2500)
ledger.add("午餐", -35)
ledger.add("卖旧书", 120)
ledger.report()
运行结果:
编号 项目 金额
------------------------------
1 工资 +8000.00
2 房租 -2500.00
3 午餐 -35.00
4 卖旧书 +120.00
------------------------------
总结余: 5585.00 元
这个例子用到了本教程的大部分知识点:
- table 嵌套:
records是一个数组,每个元素又是一个字典; - 函数作为 table 的字段:
ledger.add这种写法是 Lua 组织代码的常见风格,再进一步就演变成面向对象; - 泛型 for + ipairs:按顺序遍历数组;
- string.format:
%-12s左对齐、%+10.2f强制带符号并保留两位小数; - string.rep:重复字符串画出分隔线。
可以试着给它加功能:比如按类别统计支出、把账单保存到文件(用 io.open),作为练习。
下一步学什么
完成本教程后,你已经掌握了 Lua 的基础语法。建议按下面的顺序继续深入:
- Lua 的数组与迭代器 —— 深入理解 table 与泛型 for
- Lua 元表与元方法 —— Lua 最强大的机制
- Lua 模块与包管理 —— 组织大型项目
- Lua 错误处理最佳实践 —— 写出健壮的代码
完整的学习路径可以参考 Lua 学习路线图,或回到 Lua 专题导航 查看全部文章。
常见问题(FAQ)
Lua 适合零基础作为第一门编程语言吗?
适合。Lua 的语法规则极少(关键字只有 20 来个),没有类和异常等复杂概念,8 种数据类型一天就能过完。很多游戏引擎(如 Roblox)正是用它教青少年编程。不过要注意 Lua 的入门教程和社区规模比 Python 小,遇到冷门问题时查资料会稍费劲。
学 Lua 还是学 Python?
取决于目标:想做游戏脚本、嵌入式、OpenResty 网关,选 Lua;想做数据分析、AI、自动化办公,选 Python。Lua 的优势是解释器极小、嵌入 C/C++ 项目非常容易;Python 的优势是生态庞大。详细的取舍分析见 Lua 与 Python 的对比。
Lua 5.1 和 5.4 学哪个版本?
自学直接用 5.4(当前最新稳定版),本教程代码均兼容 5.3/5.4。只有当你的目标平台锁定旧版本时才需要补差异:比如 OpenResty 和 LuaJIT 基于 5.1,主要区别是 5.1 没有位运算、没有整数除法 //,且 setfenv 等旧机制已被 _ENV 取代。
为什么我的 Lua 数组第一个元素取不到?
大概率是你用了下标 0。Lua 的数组约定下标从 1 开始,arr[0] 是 nil(不会报错,更容易迷惑)。遍历时用 ipairs 会自动从 1 开始,能避开这个问题。
学完这篇教程后,多久能上手写游戏脚本?
如果每天投入 1-2 小时,掌握本教程内容后再花一到两周学习 元表 和 面向对象,就可以开始读游戏项目里的 Lua 脚本了。建议按 Lua 学习路线图 的八周计划推进。
相关阅读
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。