OpenResty Coding Style Guide
API7.ai
December 15, 2022
Many development languages have their coding specifications to tell developers some conventions in the field, to keep the style of code written consistent, and to avoid some common pitfalls. Python's PEP 8 is an excellent example of this, and almost all Python developers have read this coding specification written by Python's authors.
OpenResty doesn't have its coding specification yet, and some developers are repeatedly reviewed and asked to change their code style after submitting PRs, which consumes a lot of time and effort that could be avoided.
There are two Lint tools in OpenResty that can help you detect code style automatically: luacheck
and lj-releng
. The former is a common Lint tool in Lua and OpenResty world, and the latter is a Lint tool written in Perl
by OpenResty itself.
For myself, I install the luacheck
plugin in the VS Code editor so that I have a tool to auto-suggest when I write code; and in the CI of a project, I run both tools, e.g.
luacheck -q lua
./utils/lj-releng lua/*.lua lua/apisix/*.lua
After all, one more tool for testing is never a bad thing.
However, these two tools are more about detecting global variables, length per line, and other most basic code styles, which are still far from the detailed level of Python PEP 8, and there is no documentation for you to refer to.
So today, based on my experience in OpenResty-related open-source projects, I've summarized the OpenResty coding style documentation. This specification is also consistent with the code style of some API gateways like APISIX and Kong.
Indentation
In OpenResty, we use 4 spaces as indentation markers, although Lua does not require such syntax. Here are two examples of incorrect and correct codes.
--No
if a then
ngx.say("hello")
end
--yes
if a then
ngx.say("hello")
end
For convenience, we can simplify the operation by changing the tab to 4 spaces in the editor you are using.
Space
On both sides of the operator, a space is needed to separate them. The following are two examples of incorrect and correct codes.
--No
local i=1
local s = "apisix"
--Yes
local i = 1
local s = "apisix"
Blank line
Many developers bring development conventions from other languages to OpenResty, such as adding a semicolon to the end of a line:
--No
if a then
ngx.say("hello");
end;
But in fact, adding semicolons makes Lua code look very ugly, which is unnecessary. Also, you should not turn multiple lines of code into a single line to save lines to be concise. Doing so will leave you with no idea which section of code is at fault when you locate the error.
--No
if a then ngx.say("hello") end
--yes
if a then
ngx.say("hello")
end
In addition, the functions need to be separated by two blank lines.
--No
local function foo()
end
local function bar()
end
--Yes
local function foo()
end
local function bar()
end
If there are multiple if elseif
branches, they also need to be separated by a blank line.
--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
Maximum length per line
Each line should not exceed 80 characters; if it exceeds that, we need to line break and align. And when aligning line breaks, we need to reflect the correspondence between the top and bottom lines. For the example below, the function's argument on the second line should be to the right of the left bracket on the first line.
--No
return limit_conn_new("plugin-limit-conn", conf.conn, conf.burst, conf.default_conn_delay)
--Yes
return limit_conn_new("plugin-limit-conn", conf.conn, conf.burst,
conf.default_conn_delay)
If it is the alignment of splicing strings, we need to put ..
in the next line.
--No
return limit_conn_new("plugin-limit-conn" .. "plugin-limit-conn" ..
"plugin-limit-conn")
--Yes
return limit_conn_new("plugin-limit-conn" .. "plugin-limit-conn"
.. "plugin-limit-conn")
Variable
This point was also emphasized several times in previous articles: we should always use local variables rather than global variables.
--No
i = 1
s = "apisix"
--Yes
local i = 1
local s = "apisix"
As for the naming of variables, the snake_case
style should be used.
--No
local IndexArr = 1
local str_Name = "apisix"
--Yes
local index_arr = 1
local str_name = "apisix"
For constants, on the other hand, the all-caps
style is to be used.
--No
local max_int = 65535
local server_name = "apisix"
--Yes
local MAX_INT = 65535
local SERVER_NAME = "apisix"
Table
In OpenResty, we use table.new
to pre-allocate the table.
--No
local t = {}
for i = 1, 100 do
t[i] = i
end
--Yes
local new_tab = require "table.new"
local t = new_tab(100, 0)
for i = 1, 100 do
t[i] = i
end
Also, note that you must not use nil
in the array, and if you must use null, use ngx.null
to indicate that.
--No
local t = {1, 2, nil, 3}
--Yes
local t = {1, 2, ngx.null, 3}
String
Never splice strings on a hot code path.
--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, "")
Function
The naming of functions also follows snake_case
.
--No
local function testNginx()
end
--Yes
local function test_nginx()
end
And the function should return as early as possible.
--No
local function check(age, name)
local ret = true
if age < 20 then
ret = false
end
if name == "a" then
ret = false
end
-- do something else
return ret
--Yes
local function check(age, name)
if age < 20 then
return false
end
if name == "a" then
return false
end
-- do something else
return true
Module
All require
d libraries must be local
ized:
--No
local function foo()
local ok, err = ngx.timer.at(delay, handler)
end
--Yes
local timer_at = ngx.timer.at
local function foo()
local ok, err = timer_at(delay, handler)
end
For consistency of style, require
and ngx
also need to be local
ized:
--No
local core = require("apisix.core")
local timer_at = ngx.timer.at
local function foo()
local ok, err = timer_at(delay, handler)
end
--Yes
local ngx = ngx
local require = require
local core = require("apisix.core")
local timer_at = ngx.timer.at
local function foo()
local ok, err = timer_at(delay, handler)
end
Error handling
For functions that return with error information, the error information must be judged and processed:
--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!")
And in the case of functions written by yourself, the error information is to be returned as a second parameter in string format:
--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
Summary
This is an initial version of the coding style guide, and we'll be making it available on GitHub for ongoing updates and maintenance. You are welcome to share this specification so that more OpenResty users can get involved.