写代码如同写文章,每个人或多或少都有自己的编码风格;平时在开发项目的时候更多的是团队上的协作和交流而不是单打独斗。因此为了"提高开发效率","降低维护成本","促进团队合作",在开始编码工作之前应该有一套完成的代码规范来约束自己的编码风格,保证风格统一,提高代码整体的可读性,毕竟 Programming style is an art

善用调试器

编码

所有文件均采用:utf-8无BOM格式。(推荐)

缩进

OpenResty中使用 4个 空格做为缩进,即便Lua没有这样的要求。
这点开发者可以在使用的编辑器中将tab改为4个空格,来简化自己的操作。

# NO
if args then
  ngx.say(json.encode(args))
end

# YES
if args then
    ngx.say(json.encode(args))
end

空格

操作符两边都需要用一个空格来做分割,避免代码显得太过紧凑。

# NO
local age=12
local name      =         "小明"

# YES
local ags = 12
local name = "小明"

双目运算符,前后 都要有一个空格

# NO
(110+50* i,110+50 /(i+ 1), time+i -12)

# YES
(110 + 50 * i, 110 + 50 / (i + 1), time + i - 12)

逗号 前无空格后跟空格

# NO
function _M.max(a,b)
...
end

# YES
function _M.max(a, b)
...
end

# NO
for k,v in pairs(table) do
    ...
end

# YES
for k, v in pairs(table) do
    ...
end

做为 函数参数部分函数调用小括号 前后无空格。


lua解析语法时是采用空格等分割来解析的,某些情况下,不小心加空格会导致非预期的结果,
容易忘记相关空格,导致风格不统一,因此不如都不加。

# NO
function totable(val)
    local tab = { }
    if "table" == type(val) then
        return val
    end
    tab[1] = val
    return tab
end

> local clientTable = totable( serverTable )

# YES
function totable(val)
    local tab = {}
    if "table" == type(val) then
        return val
    end
    tab[1] = val
    return tab
end

> local clientTable = totable(serverTable)

空行

不少开发者会把其他语言的开发习惯带到 OpenResty 中来,比如在行尾增加一个分号。

--No
if a then
    ngx.say("hello");
end;

--Yes
if a then
    ngx.say("hello")
end

增加分号会让 Lua 代码显得非常丑陋,也是没有必要的。另外,不要为了节省代码的行数,后者为了显得“简洁”,而把多行代码变为一行。这样会在定位错误的时候不知道到底那一段代码出了问题:

--No
if a then ngx.say("hello") end

--yes
if a then
    ngx.say("hello")
end

函数之间需要用两个空行来做分隔:

-- No
local function test1()
    ...
end
local function test2()
    ...
end

-- Yes
local function test1()
    ...
end


local function test2()
    ...
end

如果有多个 if elseif 的分支,它们之间需要一个空行来做分隔:

-- No
if a == 1 then
    foo()
elseif a== 2 then
    bar()
elseif a == 3 then
    run()
else
    error()
end

-- Yes
if a == 1 then
    foo()

elseif a== 2 then
    bar()

elseif a == 3 then
    run()

else
    error()
end

函数注释和业务代码用一个空格分隔:

-- No
function _M.max(a, b)
--[[
    取最大值
    
    Params:
        a: 数值常量
        b: 数值常量
        
    Return:
        最大值
]]--
    return max(a, b)
end


-- Yes
function _M.max(a, b)
--[[
    取最大值
    
    Params:
        a: 数值常量
        b: 数值常量
        
    Return:
        最大值
    ]]

    return max(a, b)
end

命名

所有类的命名均采用大驼峰命名法(Pascal命名法)

local Route = {_VERSION=0.1}

函数

所有函数的命名均采用小驼峰命名法:

local Route = {_VERSION=0.1}

function Route.exampleFunction()
    ...
end

属性

所有类的成员均采用self. + 类型 + 名称 的方式,名称开头为大写(整体是小驼峰的写法)。

类型对照表:

类型缩写描述
inti整数
floatf浮点数(不分float or double)
booleanb布尔
strings字符串
enume枚举
ptrpuserdata
functionfunc函数
tablettable,后缀为Array代表数组,后缀为Map代表字典
self.iExampleAge = 12
self.sExampleName = "小明"
self.bExampleLocal = true

变量

变量采用 小驼峰命名法;使用 名词 或者 形容词 + 名词 的方式,例如:

local newestRoute = ""
local localPerson = ""

尽量避免出现仅靠部分字母大小写区分的相似变量,例如:

local posx, posX

常量

常量均采用大写字母下划线组合的方式。

function _M.new()
    local self = {
        SHARED_NAME = 'plugins',
        SHARED_SIZE = 100
    }
    
    return setmetatable(self, mt)
end

注释

单行注释

Lua单行注释使用 -- 开头,注意空格问题。

-- 这是单行注释

多行注释

Lua多行注释使用 --[[ 开头,以 ]] 结尾。
最后 ]] 前加一个缩进,是为了让某些编辑器(Sublime、VSCode)折叠时简洁一些。

--[[
    这是多行注释
    这是多行注释
    ]]

函数注释

函数注释格式,位于函数内部第一行开始,采用多行注释,三个字段:描述参数(可缺省)返回(可缺省).

function _M.exemple_function(parameter1, parameter2, ...)
--[[
    函数描述
    
    Params:
        parameter1: 参数类型
            参数描述
            
        parameter2: 参数类型
            参数描述
            
        ...
    
    Retrun:
        正常情况返回:返回描述
        异常情况返回:返回描述
        ...
    ]]
    
    ...
    
    return return1, return2
end

模块注释

模块注释一般从文件的第一行开始。

--[[
        DateTime: 创建时间
        Author:   作者
        Descripe: 模块描述信息
    ]]

变量

  1. 尽量使用local变量而非global变量。
  2. 同一行变量赋值不要超过 3 个,且无值时用nil显式赋值。
  3. 常量避免使用 magic number。
-- No
-- 9.8就是 magic number
local speed = time * 9.8

-- Yes
_G.ACCELERATION = 9.8
local speed = time * ACCELERATION

默认参数

function funcMethod(param)
    param = param or defalutValue
    ...
end

-- 默认值为true
param = param ~= false

-- 默认值为false
param = param or false

_ 做为想忽略的变量

# 循环中也要尽量避免使用 "i" "k" "v" 这类含义模糊不清的变量;
# 尽量使用一些具有精确含义的变量。

for _, v in pairs(args) do
    ...
end

字符串

不要在热代码路径上拼接字符串:

-- No
local s = ""
for i = 1, 100000 do
    s = s .. "a"
end

--Yes
local t = {}
for i = 1, 100000 do
    t[i] = "a"
end
local s = table.concat(t, "")

table

请使用 table.new(narr, nrec) 创建新的table。

local route = table.new(0, 10)

请使用 table.isempty(tb) 判断table是否为空。

local is_empty = table.isempty(tab1)
if is_emptry then
    print("table is empty")
else
    print(table isn't empty)
end

请使用 table.isarray() 判断一个table是否是一个数组。

local isarray = require "table.isarray"

print(isarray{"a", true, 3.14})  -- true
print(isarray{"dog" = 3})  -- false
print(isarray{})  -- true

请使用 table.nkeys(tab) 取出一个table的元素个数。

local nkeys = require "table.nkeys"

print(nkeys({}))  -- 0
print(nkeys({ "a", nil, "b" }))  -- 2
print(nkeys({ dog = 3, cat = 4, bird = nil }))  -- 2
print(nkeys({ "a", dog = 3, cat = 4 }))  -- 3

请使用 table.clone(tab) 来复制一个table。

local clone = require "table.clone"

local x = {x=12, y={5, 6, 7}}
local y = clone(x)
... use y ...

函数


1.明确函数的功能,精确的实现函数的设计,避免函数在功能上出现歧义。
2.接口参数的合法性检查,明确由调用者负责,而不是函数内部做参数的合法性检测。

-- No
local function sum(a, b)
    if not a or not b then
        return 
    end
    
    if type(a) ~= 'number' or type(b) ~= 'number' then
        return 
    end
    
    return a + b
end

print(sum(1, 2))

-- Yes
local function sum(a, b)
    return a + b
end

print(sum(1, 2))

为简单功能编写函数

function max(a, b)
    return (a > b) and a or b    
end


1.不要设计多用途而面面俱到的函数
2.避免设计一些多个功能集于一身的函数,因为这很有可能使函数的含义难以理解,且让测试和维护变得困难。
3.让函数的功能内聚

对于一些只有在一个上级函数中才能用到的函数应该考虑将此函数合并到上级函数中,而不必单独存在。

-- No
local function example_function(p1, p2)
    ...
    return r1
end

function _M.parent_function(p1, p2)
    local res = example_funciton(p1, p2)
    ...
    return r1
end

-- Yes
function _M.parent_function(p1, p2)
    # 或者将函数省掉只留下功能逻辑
    local function example_function(p1, p2)
        ...
        return r1
    end

    local res = example_funciton(p1, p2)
    ...
    return r1
end

设计高扇入,合理扇出的函数。

说明:扇入指有多少上级函数调用它;扇出是指一个函数直接调用其他函数的数目。

扇入过大,表明使用此函数的上级函数越多,这样的函数使用效率高,但不能违背函数间的独立性而单纯地追求高扇入。
扇出过大,表明函数过分复杂,需要控制和协调过多的下级函数;而扇出过小,表明函数的调用层次可能过多,不利于程序的阅读和函数结构的分析,并且程序运行时会对系统资源(如堆栈空间等)造成压力。
良好的软件结构通常是顶层函数的扇出较高,中层函数的扇出较少,而底层函数则扇入到公共模块中。

错误处理

对于有错误信息返回的函数,必须对错误信息进行判断和处理:

--No
local sock = ngx.socket.tcp()
local ok = sock:connect("www.google.com", 80)
ngx.say("successfully connected to google!")

--Yes
local sock = ngx.socket.tcp()
local ok, err = sock:connect("www.google.com", 80)
if not ok then
    ngx.say("failed to connect to google: ", err)
    return
end
ngx.say("successfully connected to google!")

自己编写的函数,错误信息要作为第二个参数,用字符串的格式返回:

--No
local function foo()
    local ok, err = func()
    if not ok then
        return false
    end
    return true
end

--No
local function foo()
    local ok, err = func()
    if not ok then
        return false, {msg = err}
    end
    return true
end

-- Yes
local function foo()
    local ok, err = func()
    if not ok then
        return false, "failed to call func(): " .. err
    end
    return true
end

建议

多次使用的全局接口,可以使用局部变量保存后再使用:

-- example1
for i = 1, 10000000 do
    local x = math.sin(i)    
end

-- example2
local sin = math.sin
for i = 1, 10000000 do
    local x = sin(i) 
end

-- example2 效率会高于 example1

创建 非常多 的小size表时,应预先填充好表的大小

-- example1
for i = 1, 10000000 do
    local tab = {}
    tab[1] = 1
    tab[2] = 2
    tab[3] = 3
end

-- example2
for i = 1, 10000000 do
    local tab = {1, 1, 1}
    tab[1] = 1
    tab[2] = 2
    tab[3] = 3
end

-- example2 效率高于 example1

很多个 字符串连接,使用 table.concat 而非 ..

-- example1
local str1 = ''
for i = 1, 100000 do
    str1 = str1 .. 'a' 
end

-- example2
local str2 = ''
local tab = {}
for i = 1, 100000 do
    tab[#tab + 1] = 'a'
end
str2 = table.concat(tab, '')

-- example2 效率高于 example1,效果十分显著

用 do .. end 可以明确限定局部变量的作用域

local v
do
    local x = 1
end -- x作用域结束,被系统清理
最后修改:2020 年 10 月 15 日 11 : 47 PM
如果觉得我的文章对你有用,请随意赞赏