OpenResty コーディングスタイルガイド

API7.ai

December 15, 2022

OpenResty (NGINX + Lua)

多くの開発言語には、開発者にその分野での慣習を伝え、コードのスタイルを一貫させ、一般的な落とし穴を避けるためのコーディング規約があります。PythonのPEP 8はその良い例で、ほぼすべてのPython開発者がこのPythonの作者によって書かれたコーディング規約を読んでいます。

OpenRestyにはまだ独自のコーディング規約がなく、一部の開発者はPRを提出した後、コードスタイルを変更するよう繰り返しレビューされ、多くの時間と労力を消費していますが、これは避けられるものです。

OpenRestyには、コードスタイルを自動的に検出するための2つのLintツールがあります: luachecklj-releng です。前者はLuaおよびOpenRestyの世界で一般的なLintツールであり、後者はOpenResty自身がPerlで書いたLintツールです。

私自身は、VS Codeエディタにluacheckプラグインをインストールして、コードを書く際に自動提案を行うツールを利用しています。また、プロジェクトのCIでは両方のツールを実行しています。例えば、

luacheck -q lua

./utils/lj-releng lua/*.lua lua/apisix/*.lua

結局のところ、テスト用のツールがもう一つあっても悪いことはありません。

しかし、これらの2つのツールは、グローバル変数の検出、1行あたりの長さ、その他の最も基本的なコードスタイルの検出に重点を置いており、PythonのPEP 8のような詳細なレベルにはまだ遠く、参照するためのドキュメントもありません。

そこで今日、OpenResty関連のオープンソースプロジェクトでの経験に基づいて、OpenRestyのコーディングスタイルドキュメントをまとめました。この規約は、APISIXやKongなどのAPIゲートウェイのコードスタイルとも一致しています。

インデント

OpenRestyでは、4つのスペースをインデントマーカーとして使用します。Luaにはそのような構文は必要ありませんが、以下に間違ったコードと正しいコードの2つの例を示します。

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

便利のために、使用しているエディタでタブを4つのスペースに変更することで操作を簡略化できます。

スペース

演算子の両側には、スペースを入れて区切る必要があります。以下に間違ったコードと正しいコードの2つの例を示します。

--No
local i=1
local s    =    "apisix"
--Yes
local i = 1
local s = "apisix"

空白行

多くの開発者は、他の言語からの開発慣習をOpenRestyに持ち込みます。例えば、行末にセミコロンを追加するなどです。

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

しかし、実際にはセミコロンを追加するとLuaのコードが非常に見苦しくなり、これは不要です。また、複数行のコードを1行にまとめて簡潔にしようとするべきではありません。これを行うと、エラーを特定する際にどのセクションのコードが問題なのかわからなくなります。

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

さらに、関数は2つの空白行で区切る必要があります。

--No
local function foo()
end
local function bar()
end
--Yes
local function foo()
end


local function bar()
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

1行あたりの最大長

各行は80文字を超えてはいけません。それを超える場合は、改行して揃える必要があります。また、改行を揃える際には、上下の行の対応関係を反映させる必要があります。以下の例では、2行目の関数の引数は、1行目の左括弧の右側に配置する必要があります。

--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)

文字列の連結を揃える場合は、..を次の行に配置する必要があります。

--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")

変数

この点は以前の記事でも何度か強調されましたが、常にグローバル変数ではなくローカル変数を使用するべきです。

--No
i = 1
s = "apisix"
--Yes
local i = 1
local s = "apisix"

変数の命名に関しては、snake_caseスタイルを使用するべきです。

--No
local IndexArr = 1
local str_Name = "apisix"
--Yes
local index_arr = 1
local str_name = "apisix"

一方、定数の場合は、all-capsスタイルを使用するべきです。

--No
local max_int = 65535
local server_name = "apisix"

--Yes
local MAX_INT = 65535
local SERVER_NAME = "apisix"

テーブル

OpenRestyでは、table.newを使用してテーブルを事前に割り当てます。

--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

また、配列でnilを使用してはいけません。nullを使用する必要がある場合は、ngx.nullを使用して示します。

--No
local t = {1, 2, nil, 3}

--Yes
local t = {1, 2, ngx.null, 3}

文字列

ホットコードパスで文字列を連結してはいけません。

--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, "")

関数

関数の命名もsnake_caseに従います。

--No
local function testNginx()
end
--Yes
local function test_nginx()
end

また、関数はできるだけ早く返すべきです。

--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

モジュール

すべてのrequireされたライブラリはlocal化する必要があります。

--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

スタイルの一貫性のために、requirengxlocal化する必要があります。

--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

エラーハンドリング

エラー情報を返す関数の場合、エラー情報を判断して処理する必要があります。

--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!")

また、自分で書いた関数の場合、エラー情報は文字列形式で2番目のパラメータとして返すべきです。

--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

まとめ

これはコーディングスタイルガイドの初期バージョンであり、GitHubで公開して継続的に更新とメンテナンスを行います。この規約を共有して、より多くのOpenRestyユーザーが参加できるようにしてください。