Russian | English
Это руководство по стилю программирования на LuaJit, которому я стараюсь придерживаться в своих проектах. Основная цель прийти к единой согласованности между проектами. А так же подобрать наиболее комфортный стиль, при котором будет совмещено: удобство чтения, четкое разделение абстракций, отсутствие запутанной логики.
В качестве ориентира были взяты лучшие практики(субъективно) из следующих руководств:
- https://github.com/luarocks/lua-style-guide
- https://www.tarantool.io/en/doc/latest/contributing/lua_style_guide/
- https://github.com/Olivine-Labs/lua-style-guide
- Типы
- Таблицы
- Строки
- Функции
- Циклы
- Свойства
- Переменные
- Условные выражения
- Блоки
- Комментарии
- Пробелы
- Запятые
- Разделитель
- Преобразование типов
- Именования
- Конструкторы
- Модули
- Метаметоды
- Обработка ошибок
- Структура файлов
- Статический анализ
- Тестирование
- Производительность
- В Lua есть следующие основные типы:
nil,boolean,number,string,table,function.
-- Примитивные типы передаются по значению
local foo = 1
local bar = foo -- bar = 1, копия
-- Составные типы передаются по ссылке
local foo = { 1, 2, 3 }
local bar = foo -- bar ссылается на ту же таблицу- Для явной проверки типа используйте
type().
-- Хорошо
if type(value) == 'function' then
value(data)
end
if type(value) == 'table' then
merge(result, value)
end- Для преобразования используйте
tonumber()иtostring()явно (см. Преобразование типов).
- При создании модели заполняйте поля последовательно, а не через литерал.
Обоснование: Последовательное заполнение удобнее для моделей с валидацией — между присваиванием полей можно вставить проверку и обработку ошибки.
-- Плохо
local model = {
id = tonumber(data.id),
name = tostring(data.name),
locked = data.locked or false,
}
-- Хорошо
local model = {}
model.id = tonumber(data.id)
model.name = tostring(data.name)
model.locked = data.locked or false- Для словарей и коротких конфигурационных таблиц допустим литерал.
-- Хорошо
local index = {
name = 'primary',
options = { parts = { 'id' }, unique = true },
}- Используйте
setmetatableдля задания__serialize = 'map', когда пустая таблица должна сериализоваться как объект, а не как массив.
setmetatable(data.social_url, { __serialize = 'map' })- Для копирования таблиц используйте
table.copy(поверхностное) иtable.deepcopy(глубокое), не пишите свою реализацию.
- Используйте одинарные кавычки
''для строк.
-- Плохо
local name = "John"
-- Хорошо
local name = 'John'- При конкатенации переносите оператор на новую строку.
-- Плохо
local infoStr = 'Info: ' .. 'Host: ' .. '0.0.0.0' .. ' ' .. 'Port: ' .. 8080
-- Хорошо
local infoStr = 'Info: '
.. 'Host: ' .. '0.0.0.0' .. ' '
.. 'Port: ' .. 8080- Не используйте
\zдля переноса текста на новую строку.
-- Ужасно
-- Это редкий случай, когда внутри функций необходима генерация -
-- текста с учетом переноса строки, лучше избегать использования \z.
local text = '\z
First Name: Alex\z
\nLast Name: Wazowsky\z
\nAge: 27\z
'
-- Вместо этого лучше вынести текст в двойные квадратные кавычки
local text = [[
First Name: Alex
Last Name: Wazowsky
Age: 27]]- Всегда используйте круглые скобки при вызове функций.
-- Плохо
local m = require 'module'
print 'hello'
-- Хорошо
local m = require('module')
print('hello')- Предпочитайте следующий стиль функций.
-- Плохо
local foo = function()
-- ...
end
-- Хорошо
local function foo()
-- ...
end- Для методов сервисов используйте запись через точку, без
self.
local service = {}
function service.create(data)
-- ...
end
function service.read(id)
-- ...
end
return service- Для методов, которым нужен доступ к
self, используйте запись через двоеточие.
function Errors:field_missing(field)
self.count = self.count + 1
-- ...
end- Функции, возвращающие результат с возможной ошибкой, используют паттерн
return value, error.
--- Получить пользователя по id
-- @param id (number) id пользователя
-- @return[1] user (table)
-- @return[2] err (table|nil)
function service.read(id)
local user, err = sql(query, { id = id })
if err then
return nil, err
end
return user, nil
end- Избегайте длинных списков аргументов. Если параметров больше трёх — передавайте таблицу.
-- Плохо
local function createUser(name, email, role, locked, avatarUrl)
-- ...
end
-- Хорошо
local function createUser(data)
-- data.name, data.email, data.role, ...
end- Для массивов (числовой индекс) предпочитайте
for i = 1, #tbl doвместоipairs.
for i = 1, #routes do
local route = routes[i]
httpd:route(route.options, route.handler)
end- Для итерации по парам ключ-значение используйте
pairs.
for key, val in pairs(tbl) do
-- ...
end- Если значение индекса не используется, именуйте его
_.
for _, val in pairs(tbl) do
process(val)
end- Используйте точечную нотацию для доступа к известным свойствам.
-- Хорошо
local name = user.first_name
local id = user.id- Используйте скобочную нотацию
[]для доступа по динамическому ключу.
local field = 'first_name'
local value = user[field]- Всегда используйте
localдля объявления переменных. Глобальные переменные не допускаются (за исключением Tarantool API:box,_TARANTOOL).
-- Плохо
name = 'John'
-- Хорошо
local name = 'John'- Объявляйте каждую переменную на отдельной строке.
-- Плохо
local a, b = 1, 2
-- Хорошо
local a = 1
local b = 2-
Объявляйте переменные как можно ближе к месту использования.
-
Используйте
box.NULLвместоnilпри работе с Tarantool-данными, где нужно различать "нет значения" и "значение не задано".
if data.username ~= box.NULL then
model.username = tostring(data.username)
end- Используйте ранний выход (guard clause) вместо глубокой вложенности.
-- Плохо
local function process(data)
if data then
if data.id then
-- ...
end
end
end
-- Хорошо
local function process(data)
if not data then
return nil
end
if not data.id then
return nil
end
-- ...
end- Для значений по умолчанию используйте
or.
local locked = data.locked or false
local name = data.name or 'Unknown'- Для проверки наличия опционального параметра используйте
and.
local init = opts and opts.init- При работе с данными из Tarantool используйте явные проверки на
nilиbox.NULL.
Обоснование:
box.NULLявляется truthy в условных выражениях, но семантически означает "нет значения".if x thenне отловитbox.NULL, что приведёт к неожиданному поведению.
-- Опасно при работе с Tarantool
if data.field then
-- box.NULL пройдёт эту проверку
end
-- Хорошо
if data.field ~= nil and data.field ~= box.NULL then
-- ...
end- Предпочитайте позитивные проверки негативным, если есть обе ветки.
-- Плохо
if not thing then
handle_missing()
else
handle_present()
end
-- Хорошо
if thing then
handle_present()
else
handle_missing()
end- Допускается использование
not (x == y)вместоx ~= y, когда это делает код читабельнее.
- Блоки
if,for,while,functionвсегда завершаютсяendна отдельной строке.
if condition then
-- ...
end
for i = 1, 10 do
-- ...
end- Не используйте однострочные блоки.
-- Плохо
if condition then return end
-- Хорошо
if condition then
return
end-
Язык комментариев — русский.
-
Пробел после
--.
--плохо
-- хорошо- Документация функций оформляется в стиле LuaDoc: первая строка через
---, остальные через--.
--- Получить пользователя по id
-- @param id (number) id пользователя
-- @return[1] user (table)
-- @return[2] err (table|nil)
function service.read(id)- Доступные LuaDoc-теги:
| Тег | Формат | Описание |
|---|---|---|
@module |
@module path.to.module |
Идентификатор модуля |
@param |
@param name (type) описание |
Параметр функции |
@param[optchain] |
@param[optchain] name (type) описание |
Опциональный параметр |
@return[1] |
@return[1] name (type) описание |
Первое возвращаемое значение |
@return[2] |
@return[2] name (type) описание |
Второе возвращаемое значение (ошибка) |
@usage |
блок с примером | Пример использования |
- Пример с
@usage:
--- Выполнить SQL-запрос
-- @param sql_query (string) SQL-строка
-- @param values (table) таблица значений
-- @return[1] result
-- @return[2] error
-- @usage
-- local rows = sql.execute("SELECT * FROM users WHERE name = ${name}", { name = 'Alex' })
-- if rows == nil then
-- log.error('No rows')
-- end- Для визуального разделения секций используйте блоки комментариев.
-- ----------------------------
-- Точка входа
-- ----------------------------- Для примеров данных (JSON-запросы/ответы) используйте блочные комментарии
--[[ ... --]].
--[[ Данные для регистрации
{
"phone": "+71234567890",
"email": "foo@bar.baz", -- Опционально
"password": "qwerty",
"first_name": "John",
"last_name": "Doe"
}
--]]- Используйте
TODOиFIXMEтеги.TODO— для запланированной доработки,FIXME— для известной проблемы.
-- TODO: реализовать пагинацию
-- FIXME: неэффективный запрос, нужна оптимизация- Для ссылок на внешние ресурсы используйте
See:.
-- See: https://www.tarantool.io/en/doc/latest/reference/...- Директивы
luacheckоформляются как комментарии.
-- luacheck: ignore req
-- luacheck: push ignore 631
-- ... код ...
-- luacheck: pop- Пустая строка-комментарий
--используется как визуальный разделитель внутри блока кода.
-- Проверка что пользователя с таким телефоном нет
--
local existing, _ = authService.findByPhone(data.phone)
--- Не комментируйте синтаксис — читатель должен знать Lua. Комментируйте намерение и поведение.
-- Плохо
-- Присваиваем переменной x значение 1
local x = 1
-- Хорошо
-- Начальное смещение для пагинации
local x = 1- Используйте 2 пробела для отступов. Не используйте табуляцию.
-- Хорошо
local function foo()
if condition then
bar()
end
end- Ставьте пробелы вокруг операторов.
-- Плохо
local x=1+2
-- Хорошо
local x = 1 + 2- Ставьте пробел после запятой.
-- Плохо
local tbl = {1,2,3}
-- Хорошо
local tbl = { 1, 2, 3 }- Не ставьте пробелы внутри скобок вызова функции.
-- Плохо
print( 'hello' )
-- Хорошо
print('hello')-
Оставляйте одну пустую строку между функциями.
-
Максимальная длина строки — 130 символов.
- Ставьте запятую после каждого элемента таблицы, включая последний (trailing comma).
-- Хорошо
local tbl = {
name = 'John',
age = 27,
}- В однострочных таблицах trailing comma не нужна.
local point = { x = 1, y = 2 }- Не используйте точку с запятой
;для разделения выражений.
-- Плохо
local a = 1; local b = 2;
-- Хорошо
local a = 1
local b = 2- Используйте
tonumber()для явного приведения к числу.
model.id = tonumber(data.id)- Используйте
tostring()для явного приведения к строке.
model.first_name = tostring(data.first_name)- Не полагайтесь на неявное приведение типов.
-- Плохо
local result = '5' + 3
-- Хорошо
local result = tonumber('5') + 3- camelCase — для переменных и функций.
local userId = 1
local function getUserById(id) end-
В отдельных случаях допустимо использование snake_case для методов (например,
Errors:field_missing), когда это обусловлено стилем API объекта. -
PascalCase — для конструкторов и классов.
local function User(data) end
local function Errors() end- UPPER_CASE — для констант и значений перечислений.
local roles = {
USER = 1,
GUIDE = 2,
ADMIN = 3,
}- _snake_case — для приватных методов и внутренних переменных модуля.
function MyClass._internal_method(self)
-- ...
end- Для булевых функций используйте префиксы
is/has.
-- Плохо
local function valid(data)
return data.id ~= nil
end
-- Хорошо
local function isValid(data)
return data.id ~= nil
end
local function hasErrors(self)
return self.count > 0
end- Аргументы функций допустимо писать в snake_case, но предпочтительнее camelCase.
-- Допустимо
local function createUser(avatar_url)
-- ...
end
-- Лучше
local function createUser(avatarUrl)
-- ...
end- Поля объектов хранилища (Tarantool spaces) пишутся в snake_case.
-- Поля из БД
model.first_name = tostring(data.first_name)
model.avatar_url = data.avatar_url-
Имена файлов: PascalCase для моделей и сервисов (
User.lua,Errors.lua,UserService.lua), snake_case для утилит (find.lua,merge.lua). -
Неиспользуемые переменные именуйте
_.
for _, val in pairs(tbl) do- Избегайте однобуквенных имён, кроме итераторов (
i,k,v,_).
- Конструкторы именуются в PascalCase и возвращают
model, errors.
--- Конструктор модели User
-- @param data (table) данные пользователя
-- @param opts (table) опции
-- @return[1] model (table)
-- @return[2] errors (table|nil)
local function User(data, opts)
local init = opts and opts.init
local errors = Errors:new({ space = 'users' })
local model = {}
-- Валидация и заполнение полей
if tonumber(data.id) then
model.id = tonumber(data.id)
else
errors:field_missing('id')
end
if errors:has_errors() then
return nil, errors:get_compact()
end
return model, nil
end
return User- Для OOP-конструкторов используйте
setmetatableи методnew.
local MyClass = {}
MyClass.__index = MyClass
function MyClass:new(params)
local obj = {}
obj.field = params.field
setmetatable(obj, self)
return obj
end- Каждый файл — один модуль. Модуль заканчивается одним
return.
-- Утилита — возвращает функцию
local function find(tbl, value)
-- ...
end
return find-- Сервис — возвращает таблицу с методами
local service = {}
function service.create(data) end
function service.read(id) end
function service.update(fields, where) end
function service.delete(id) end
return service-- Перечисление — возвращает таблицу констант
return {
USER = 1,
GUIDE = 2,
ADMIN = 3,
}requireвызовы группируйте в начале файла.- Сортируйте порядок
require-ов от системных/встроенных модулей к модулям приложения.
local log = require('log')
local datetime = require('datetime')
local sql = require('src.libs.sql')
local User = require('src.models.User')
local Errors = require('src.models.Errors')
local roles = require('src.enums.roles')- Именуйте переменную
requireпо имени модуля. Не переименовывайте произвольно.
Обоснование: Код читается проще, если не нужно возвращаться к началу файла, чтобы проверить, как именно назван модуль.
-- Плохо
local skt = require('socket')
local j = require('json')
-- Хорошо
local socket = require('socket')
local json = require('json')-
Подключение модуля через
requireне должно вызывать побочных эффектов (запись в БД, вывод в консоль, изменение глобального состояния). Единственное допустимое действие — загрузка зависимостей и возврат таблицы модуля. -
Не используйте
module(). Всегда используйте паттерн сreturn.
- Используйте
__indexдля наследования.
local Errors = {}
Errors.__index = Errors- Используйте
__serializeдля контроля сериализации (Tarantool).
setmetatable(result, { __serialize = 'map' })-
Используйте
__tostringдля отладочного вывода объектов. -
Не злоупотребляйте метаметодами. Если задачу можно решить без них — решайте без них.
- Файл организуется в следующем порядке:
-- 1. Подключение модулей (require)
local log = require('log')
local sql = require('src.libs.sql')
local User = require('src.models.User')
-- 2. Локальные переменные и константы
local MAX_RETRIES = 3
-- 3. Локальные функции (вспомогательные)
local function helper()
-- ...
end
-- 4. Основная логика / определение модуля
local service = {}
function service.create(data)
-- ...
end
-- 5. Возврат модуля
return service- Правила оформления комментариев и документации описаны в секции Комментарии.
- Функции, которые могут завершиться с ожидаемой ошибкой (I/O, валидация, запросы к БД), возвращают
nil, err.
local user, err = service.read(user_id)
if err then
return nil, err
end- При ошибке API (неправильное использование функции) используйте
error()илиassert().
function service.read(id)
assert(type(id) == 'number', 'id must be a number')
-- ...
end- Не возвращайте больше двух значений. Если нужно вернуть дополнительные данные — оберните их в таблицу.
-- Плохо
return result, err, details, code
-- Хорошо
return result, { message = err, details = details, code = code }- При проверке ошибки сначала проверяйте
err/nil, затем продолжайте работу.
-- Плохо
local user, err = service.read(id)
if user then
-- ...
end
-- Хорошо
local user, err = service.read(id)
if err then
log.error(err)
return nil, err
end
-- работа с user- Для накопления ошибок валидации используйте объект ошибок.
local errors = Errors:new({ space = 'users' })
if not data.id then
errors:field_missing('id')
end
if not data.name then
errors:field_missing('name')
end
if errors:has_errors() then
return nil, errors:get_compact()
end-
Код должен проходить проверку luacheck.
-
Настройки проекта описаны в
.luacheckrc. -
Основные параметры:
| Параметр | Значение |
|---|---|
| Стандарт | min |
| Макс. длина строки | 130 символов |
| Допустимые глобальные | box, _TARANTOOL, p |
- Предупреждения luacheck, которые допустимо игнорировать (настроены в
.luacheckrc):
| Код | Описание |
|---|---|
| 411 | Переопределение локальной переменной |
| 412 | Переопределение аргумента |
| 413 | Переопределение переменной цикла |
| 421–423 | Затенение (shadowing) локальных переменных |
| 431–433 | Затенение upvalue |
| 581 | Использование not (x == y) |
- Если luacheck выдаёт ложное срабатывание — используйте директивы в комментариях (см. Комментарии).
-
Тестовые файлы размещаются в директории
tests/. -
Именование тестовых файлов:
*_test.lua. -
Используйте
luatestв качестве фреймворка для тестирования.
local t = require('luatest')
local g = t.group('group_name')
g.before_all(function()
-- подготовка
end)
g.test_example = function()
local result = service.read(1)
t.assert_is_not(result, nil)
t.assert_equals(result.id, 1)
end-
Каждый тест должен быть независимым от других.
-
Тестируйте граничные случаи и ошибки, а не только "happy path".
- Избегайте создания лишних замыканий и таблиц в циклах.
-- Плохо: новая функция на каждой итерации
for i = 1, #items do
items[i]:on('event', function() handle(i) end)
end
-- Хорошо: функция создаётся один раз
local function handler(i)
return function() handle(i) end
end
for i = 1, #items do
items[i]:on('event', handler(i))
end- Используйте
table.new(narr, nrec)для предаллокации таблиц, если известен примерный размер (LuaJIT / Tarantool).