Привет, Хабр!
Меня зовут Андрей Бирюков. Я — независимый эксперт в области ИТ и ИБ, преподаю в учебных центрах, пишу статьи и книги.
Скриптовый язык Lua, (Луна по‑португальски), получил широкую популярность среди разработчиков, благодаря своей легкости, гибкости и простому синтаксису.
Его можно встретить не только на различных устройствах, но даже в скриптах для Wireshark (это будет тема отдельной статьи).

Однако даже в таком простом языке легко столкнуться с проблемами, особенно когда дело касается архитектуры кода и взаимодействия модулей.
Сегодня поговорим об одной из таких проблем — глобальных переменных в Lua — и разберёмся, как их надо и не надо использовать.
Когда одна буква решает всё
Наш разбор проблем мы начнём с самой простой и при этом наиболее часто встречающейся.
В Lua переменная становится глобальной, если вы не написали local. Вроде бы звучит банально, и кажется, что каждый джун это знает, но посмотрите на этот код:
local function calculate_bonus(score) multiplier = 1.5 -- забыли local return score * multiplier end calculate_bonus(100) print(multiplier) -- 1.5, глобальная переменная создана
С точки зрения выполнения функция работает, тесты проходят. Но теперь представьте, что рядом есть другая функция:
local function apply_penalty(score) multiplier = 0.5 -- та же опечатка return score * multiplier end
И в итоге мы получаем, что multiplier стала общей глобальной переменной. Вызовите calculate_bonus, потом apply_penalty, затем снова calculate_bonus — и в итоге вы получите разные результаты.
Это классическая ошибка, которая проявляется только при определённой последовательности вызовов, и её не всегда можно поймать в тестах.
Здесь в качестве решения необходимо приучить себя всегда писать local, даже если кажется, что переменная нужна «везде».
Если действительно нужна глобальная, то создавайте её явно через модуль или таблицу‑конфиг.

Например, так:
local Config = {} Config.multiplier = 1.5 local function calculate_bonus(score) return score * Config.multiplier end
При таком написании кода вы всегда видите, откуда приходят данные, и разночтения практически исключены.
_G или швейцарский нож, который режет пальцы
В Lua есть специальная таблица _G, в которой лежат все глобальные переменные, и через неё можно читать и писать что угодно:
G.myvariable = 42 print(my_variable) -- напишет 42 _G.print = function() end print("hello") -- не напишет ничего
И второй пример не является шуткой, хотя кажется, что никто в здравом уме не будет переопределять print.
Но если вы случайно перезапишете print, pairs, ipairs или require через _G, то весь код после этого начнёт вести себя непредсказуемо. Особенно опасно это делать в средах, где несколько модулей или плагинов работают в одном пространстве имён.
Например, вот реальный кейс из игрового движка.
Плагин A для отладки переопределил
G.pairsсвоей версией с логированием. А плагин B использовалpairsв горячем цикле, из‑за чего производительность упала в десять раз, и никто не понимал почему, пока не нашли строчкуG.pairs = my_pairsв чужом коде.
Здесь правилом хорошего тона является отказ от использования _G напрямую (если, конечно, вы не разрабатываете какую‑нибудь песочницу или другую среду эмуляции). Читать оттуда тоже стоит осторожно, так как полученное значение тоже может измениться в любой момент.
Если вам нужно изолировать модуль, используйте локальную таблицу как пространство имён:
local M = {} function M.do_something() -- здесь только M и стандартные функции end return M
Ломаем замыкания
В Lua 5.1 для изоляции окружения использовался setfenv, но начиная с версии 5.2+ его заменили на _ENV.
Обе конструкции действительно мощные, и обе легко ломают код, если не понимать, как работают замыкания.
Взгляните на классическую ловушку с setfenv:
-- Lua 5.1 local function make_counter() local count = 0 return function() count = count + 1 return count end end local counter = make_counter() setfenv(counter, {}) -- изолировали окружение print(counter()) -- ошибка: attempt to perform arithmetic on upvalue 'count'
Здесь проблема заключается в том, что setfenv меняет окружение функции, но count на самом деле — это локальная переменная, определённая вне функции, но используемая внутри неё. Однако если внутри замыкания была глобальная переменная, она внезапно исчезнет:
С ENV в Lua 5.2+ ситуация стала несколько тоньше, так как ENV — это обычная локальная переменная, которая определяет, где искать глобальные имена. Но если вы захватите _ENV в замыкании, а потом измените его снаружи, замыкание продолжит использовать старое окружение:
Для этой проблемы лучшим решением будет отказ от использования ENV вообще, так как это инструмент для фреймворков, а не для повседневной разработки.
Но если вы пишете песочницу, используйте load с явным окружением вместо манипуляций с ENV в рантайме.
Как глобальные переменные загрязняют пространство имён между модулями
А теперь давайте представим два модуля, которые «немного» используют глобальные переменные. Например, вот так:
-- module_a.lua cache = {} function get_cached(key) return cache[key] end return { get_cached = get_cached } -- module_b.lua cache = {} -- перезаписали чужой cache function set_cached(key, value) cache[key] = value end return { set_cached = set_cached }
И вот модуль module_a.get_cached начинает читать из того же cache, что и module_b.set_cached. Если модули загружаются в разном порядке или один из них сбрасывает cache, данные теряются.
При этом, когда мы тестируем каждый модуль отдельно, у нас всё зелёное, но в интеграции начинается хаос.
Здесь каждый модуль должен возвращать таблицу и хранить состояние внутри неё, и никаких глобальных cache, config, state.
Например:
-- module_a.lua local M = {} local cache = {} function M.get_cached(key) return cache[key] end return M
Практический итог
Для того чтобы выявить ошибки в своём коде, прежде всего включите проверку на глобальные переменные.
В Lua есть
debug.getinfoиdebug.sethook, но проще использовать статический анализатор вродеluacheck. Он найдётx = 10безlocalбыстрее, чем вы напишете следующий баг.Также, если вам нужна конфигурация, доступная везде, передавайте её явно. Например, через аргументы функции, замыкание или через модуль.
В средах с плагинами (OpenResty, Redis, игровые движки) всегда проверяйте, не пишет ли чужой код в
G. Одна строкаG.something = ...может обрушить всю систему.И наконец, не используйте
setfenvи_ENVдля «быстрого» решения задач изоляции. Это низкоуровневые инструменты с неочевидными побочными эффектами на замыкания. Если вам нужна песочница, то лучше воспользоватьсяload(code, name, "t", env).
Важно понимать, что глобальные переменные в Lua сами по себе не являются злом. Это инструмент, который слишком легко можно использовать неправильно.
Одна забытая local — и вам придётся отлаживать поведение, которое зависит от порядка загрузки модулей. Не давайте вашему коду такого шанса вас обмануть.

Даже небольшая ошибка в коде может привести к неожиданным последствиям, особенно если её не обнаружили до релиза. О том, как выявлять подобные проблемы и какие инструменты помогают повысить качество и безопасность разработки, поговорим на бесплатных открытых уроках:
11 ноября, 20:00. «Ошибки до релиза: автоматическая проверка кода 1 С». Записаться.
16 ноября, 20:00. «Швейцарский нож РБПО: карта инструментов безопасной разработки на практике». Записаться.
Другие темы для разработчиков и не только собрали в дайджесте вебинаров октября.