一、为什么需要代码规范

很多人在写 Lua 的时候,都经历过这样的时刻:脚本越改越长,变量名天马行空,一个函数几百行,最后自己都不想再打开这个文件。Lua 这门语言以小巧灵活著称,但灵活过头,就容易变成"自由搏击"。无论是游戏里的 AI、配置脚本,还是 OpenResty 网关里的业务代码,只要规模稍微变大,缺乏统一规则的代码就会变得很难读、很难改。

1.1 没有规范的后果

你可以想象一个没定规矩的厨房:刀具乱放、调料不贴标签、有人喜欢用碗也有人喜欢用盆。厨师换了一个又一个,最后谁做饭都像探险。代码也一样,没有统一的命名和排版,每次排查问题都要先花大把时间理解之前的人想干什么。

对比一下两端代码。第一段是典型的"临时写写"风格:

-- 技术栈:Lua 5.4
-- 糟糕示例:变量名没有含义,逻辑缩成一团
local a=10
local b=2
local c={}
for i=1,a do c[i]=i*b end
function x()
    local s=0
    for k,v in pairs(c) do
        s=s+v
    end
    return s
end
print(x())

第二段来自一个稍微讲究点的项目:

-- 技术栈:Lua 5.4
-- 清晰示例:用名字表达意图
local total_numbers = 10
local multiplier = 2
local numbers = {}

-- 生成待计算的数列
for index = 1, total_numbers do
    numbers[index] = index * multiplier
end

-- 计算所有数字的总和
local function sum(list)
    local total = 0
    for _, value in ipairs(list) do
        total = total + value
    end
    return total
end

print(sum(numbers))

同样的事情,第二种写法让阅读的人几乎不用思考。这就是代码规范的价值:它让代码的后一半工作量,也就是被别人读懂的成本,变得非常低。

二、规范制定的核心原则

制定 Lua 代码规范之前,请先记住三句话:简单、一致、可检查。

简单是指规范的数量要克制。如果随手一列就是八十条"必须",团队成员记不住,也容易产生抵触。宁可先选二十条最影响阅读的规则,把基础打牢。

一致的意思是同样的事情只能有一个写法。比如循环变量用 i 还是 index,函数名用动词开头还是名词开头,都不要搞出两套。哪怕某套写法不是最优,只要所有人统一,维护成本也会低于每次都纠结。

可检查则是说规范要靠工具落地。人眼总会疲劳,写代码时也总有疏忽。用 luacheck 之类的静态检查工具可以把一部分规则变成自动判断,让规范执行得更加稳定。

三、具体规范示例与解读

下面我结合实际经验,讲一讲我认为最值得先制定起来的几个方面。

3.1 命名规范

Lua 里最常见的命名风格是蛇形命名法,也就是用下划线分隔单词。局部变量和函数用小写开头,常量用大写。布尔变量问自己"是不是",用 is、has 这类前缀。

-- 技术栈:Lua 5.4
local player_name = "Alice"     -- 变量:名词 + 下划线
local is_online = false         -- 布尔值:is 开头
local MAX_RETRY = 3             -- 常量:全大写

local function send_message(receiver, content)
    local channel = get_channel(receiver)  -- 中间变量也要有含义
    if channel then
        channel:send(content)
    end
end

为什么要在意这些?因为代码是给人读的,其次才是给机器执行。一个叫 a 的变量和叫 total_score 的变量,在后续修改中带来的认知负担完全不同。

3.2 缩进与排版

缩进统一用空格,别混用 Tab。到底缩进两个还是四个空格,团队内部决定就好,但选定之后就不许改。行宽最好限制在 100 到 120 个字符之内,超出就换行。

-- 技术栈:Lua 5.4
-- 推荐:参数过多时,每个参数占一行
local function calculate_damage(
    attacker,
    defender,
    skill_ratio,
    is_critical
)
    local base = attacker.atk - defender.def
    if is_critical then
        base = base * 2
    end
    return math.max(0, base) * skill_ratio
end

排版规范看起来表面,实际上作用很大。整齐的代码让人一眼就能看出逻辑结构,函数从哪开始、到哪结束,循环里包含哪些语句,都会清晰很多。

3.3 注释规范

注释最忌讳记流水账。-- 这里加一 这种话,对读代码的人毫无帮助。应该写的是"为什么这么做"和"需要注意什么"。Lua 常用 -- 写单行注释,长说明可以用 --[[ ]] 块注释。

-- 技术栈:Lua 5.4
-- 连接服务器,失败后按指数退避重试
-- 原因:网络抖动时瞬时失败很常见,直接放弃会误伤业务
local function connect_with_retry(host, port)
    local attempt = 0
    repeat
        local ok = try_connect(host, port)
        if ok then
            return true
        end
        attempt = attempt + 1
        local wait_ms = attempt * attempt * 100  -- 退避时间递增
        sleep(wait_ms)
    until attempt >= MAX_RETRY
    return false
end

请记住,好的注释不重复代码已经告诉你的内容,而是补全代码里没有的上下文。比如上面例子里的"网络抖动"就是一个关键的背景信息。

3.4 错误处理

Lua 没有异常机制,常用的错误处理手段是返回两个值:一个正常结果,一个错误信息。调用的时候一定要接收并判断返回值。如果使用 pcall,也要防止把所有错误都吞掉,连个日志都不留。

-- 技术栈:Lua 5.4
local function read_config(path)
    local file, open_err = io.open(path, "r")
    if not file then
        return nil, string.format("无法打开配置:%s", open_err)
    end

    local data = file:read("*a")
    file:close()

    -- 这里用 pcall 捕获所有运行时错误
    local ok, chunk = pcall(function()
        return assert(load("return " .. data))()
    end)
    if not ok then
        return nil, string.format("配置解析失败:%s", chunk)
    end
    return chunk
end

-- 调用方必须检查错误
local cfg, err = read_config("app.conf")
if not cfg then
    print("读取配置出错:", err)
    return
end

统一错误处理方式是很有价值的。如果一个函数返回 nil 表示失败,另一个返回 false 表示失败,调用方就会经常犯糊涂。把"nil + 错误信息"当作默认约定,整个项目都会清爽很多。

3.5 模块与函数设计

函数长度最好控制在二十到三十行以内。如果它自己都把自己绕晕了,就必须拆成小组件。独立的小函数更容易测试,也更容易复用。Lua 里的模块,本质上就是一个 return 出去的 table,把公共函数放进去,把不参与外部交互的细节留在文件内部。

-- 技术栈:Lua 5.4
-- utils.lua 文件内容
local utils = {}

-- 计算表格里数字的平均值,忽略非数字元素
function utils.average(list)
    local sum = 0
    local count = 0
    for _, value in ipairs(list) do
        if type(value) == "number" then
            sum = sum + value
            count = count + 1
        end
    end
    if count == 0 then
        return nil, "list 里没有数字"
    end
    return sum / count
end

return utils

在另一个文件里可以这样使用:

-- 技术栈:Lua 5.4
local utils = require("utils")

local scores = { 90, 80, 70 }
local avg, err = utils.average(scores)
if avg then
    print("平均分是", avg)
else
    print("计算失败:", err)
end

四、维护方法:工具与流程

规范不是拿笔写出来就完事的。它需要被使用、被检查、被迭代,才会真正活下来。

4.1 静态检查:luacheck

在 Lua 生态里,luacheck 是最常见的静态检查工具。它能发现未使用的变量、意外的全局变量、重复定义等问题。最好把你的规则写进一个 .luacheckrc 文件,这个文件本身就是一段 Lua 代码,可以放进项目仓库,让所有人都共享同一套设置。

-- 技术栈:Lua 5.4(.luacheckrc 配置示例)
-- 忽略行尾多余空格的提示
ignore = { "631" }

-- 允许访问这些全局变量
globals = {
    "wibble",
    "widget"
}

-- 不检查第三方目录
exclude_files = {
    "vendor/",
    "deps/"
}

-- 最大行长度设为 120
max_line_length = 120

有了这样的配置文件,每个人在本地跑一遍 luacheck,就能在提交代码之前先清除大部分低级问题。而且这个工具的配置本身就是 Lua 代码,懂 Lua 的开发者几乎没有学习成本。

4.2 代码评审与持续集成

代码评审是维护规范的另一道防线。人不如机器那么细致,但人能发现工具发现不了的问题,比如一个函数是否设计得合理,一段逻辑是否绕了远路。我们可以在开发流程中增加一个检查环节,让每次改动都通过 lint 工具和同事的 review。如果项目已经用了 GitHub、GitLab 这类平台,还可以在合并请求之前强制跑一遍静态检查。

当然,要把这一步做成自动化,就需要把检查命令接到 CI 里。CI 是不休息的把关员,不管谁提交代码,它都会去执行检查。只要检查没有通过,就拒绝合并。这样规范就真正嵌到了流程里,而不是挂在墙上。

4.3 让规范保持更新

代码规范不是一锤子买卖。Lua 版本在升级,团队的写法偏好也在变。当大家发现某个规则不合理,或者某个新增的写法需要被约定时,就要及时把规范文档更新掉。我建议把规范文档放在仓库的 docs 目录下,并在文档里注明每次变更的原因和日期。这样后面的人就能理解,为什么会有这条规范,而不是把它当成没来由的命令。

五、应用场景、技术优缺点

5.1 典型应用场景

Lua 代码规范最常被需要的场景有这些:游戏服务器与客户端逻辑,尤其是使用 Lua 做热更新的项目;OpenResty 网关的处理脚本,因为通常要多人维护;嵌入式设备上的控制程序,调试麻烦,提前规范能减少事故;还有 Neovim 插件、Redis 脚本、各类配置生成脚本。只要代码不止一个人看,规范就值得制定。

5.2 技术优缺点

优点非常明显。第一,阅读成本低,新成员很快就能融入。第二,bug 更容易暴露,因为异常写法会显得特别突兀。第三,配合静态检查工具,很多低级错误在编译器之前就被拦住。第四,代码结构统一之后,重构和调优也会更加从容。

缺点也要看到。规范多了会限制灵活性,Lua 的"少即是多"特色可能被层层规则磨掉。每一条规范都需要团队投入精力去学习和维护。如果团队只有一个人,或者项目只是临时的工具脚本,那套繁琐流程反而会成为负担。所以规范并不是越多越好,而是恰到好处。

六、注意事项

在制定和维护 Lua 代码规范时,有几个坑非常值得留意。

第一,别想一口吃成胖子。一次推 30 条规则,不如一个月只推 5 条,让团队真正适应了再加入下一批。

第二,强规范要明确"必须"和"建议"。命名、缩进、错误处理这些必须统一;注释风格则可以列出推荐写法,但不强制。这样既保证了底线,又留了变通空间。

第三,所有规则都应该写清楚原因。没有原因的规则会引发激烈争论,写了原因后,大家才知道这条规则解决的是什么问题。

第四,注意 Lua 的版本差异。Lua 5.1、5.3、5.4 在标准库和语法上有所不同。比如 # 取长度运算符对 table 的行为,在不同版本里可能有微妙差别。规范里最好标注它适用的版本,免得有人拿旧习惯在新环境里踩坑。

第五,别把规范变成碎片。规范文档应该只有一个权威入口,所有补充说明都从入口链接过去。分散在十几个群聊天记录里的规范,等于没有规范。

第六,要允许例外。对于一些一次性脚本、代码生成器输出、外部代码,明确地豁免是合理的。你可以用文件头注释的方式说明"此文件不受规范约束",避免工具报错让人心烦。

七、文章总结

Lua 是一门非常自由的动态语言,自由度越高,越需要一套靠得住的代码规则。今天我们聊了为什么要有规范,也详细介绍了命名、排版、注释、错误处理、模块设计这些最基础的规范点,还聊了利用 luacheck 和代码评审来维护规范的方法。最后我给了几个贴近实战的注意事项,希望你能少走弯路。

代码规范本身不是目的,它的最终目的,是让团队更高效地写出质量稳定的代码。每个项目都可以从自己最痛的地方开始,选几条规则跑起来,然后用工具和流程帮着落地。等你坚持一段时间,再回头看那些改造过的项目,就会真切感受到:原来代码曾经那么乱,而现在舒坦多了。