InquirerUrdaneta gets new leaders after dismissal of mayor, vice mayorRTP Desporto12h30 Samu muito elogiado e os pequenos que foram grandes na TaçaESPNBears CB Kyler Gordon returns with vengeance, answers criticsPunchUNILORIN graduates 16,152 students at 41st convocation ceremonyESPN DeportesDestacan audiencia y asistencia de WNBAThe Jerusalem PostHamas planned to detonate bombs at Gaza refugee camps to cover terrorists' escape, IDF revealsBollywood HungamaTiger Shroff Firing Incident: Three accused arrested by Kota police while travelling on Pune-Jaipur expressMintFSSAI proposes ban on added sugars in infant foods: Why it wants to act and how sucrose, fructose can harm babiesLa TerceraUcrania afirma que una reunión de Putin con Xi y Trump solo servirá para continuar la guerraRTL BoulevardVan Douzen Kroes tot Imaan Hammam: deze 5 Nederlanders schitteren bij Victoria's Secret Fashion ShowZDF heuteEntdecken Sie das ZDF-NachrichtenstudioThe South AfricanGovernment pays tribute to media on ‘Black Wednesday’
The Daily Newsstand · Free, Always
Monday, October 19, 2026

Как одна забытая local ломает Lua‑код и почему эту ошибку так сложно найти

Translate

Меня зовут Андрей Бирюков. Я — независимый эксперт в области ИТ и ИБ, преподаю в учебных центрах, пишу статьи и книги. 

Скриптовый язык 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. «Швейцарский нож РБПО: карта инструментов безопасной разработки на практике». Записаться.

Другие темы для разработчиков и не только собрали в дайджесте вебинаров октября.

View the original on Хабр →

KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.