Nim — компилируемый язык со статической типизацией, который читается почти как Python: отступы вместо скобок, короткие объявления. Программа собирается в нативный бинарник через C, C++ или JavaScript, работает без виртуальной машины и часто обходится без сборщика мусора. Для администратора это один файл без зависимостей.
Что даёт Nim и где он уместен
Язык появился как замена скриптам, которым не хватает скорости, и C-программам, которым не хватает безопасности. В Nim 2.x по умолчанию используется управление памятью ORC: подсчёт ссылок с отдельным проходом по циклам. Это компромисс между простотой Python и предсказуемостью C.
Практические ниши:
- консольные утилиты и парсеры с быстрым стартом;
- сетевые демоны и небольшие HTTP-сервисы;
- замена Python-скриптов в cron, где важны память и отсутствие интерпретатора;
- генерация кода и DSL через макросы.
Слабое место — экосистема: библиотек меньше, чем у Python или Go. Если проект живёт за счёт готовых SDK, выгода может оказаться нулевой.
Установка и первый проект
В репозиториях Debian и Ubuntu пакет nim часто отстаёт от актуальной ветки. Надёжнее поставить компилятор через choosenim в домашний каталог.
curl https://nim-lang.org/choosenim/init.sh -sSf | sh
choosenim stable
nim --version
mkdir app && cd app
nimble init app
nimble build -d:release
nimble — штатный менеджер пакетов и сборщик. Команда init создаёт в текущем каталоге src/, файл app.nimble, заготовку теста и .gitignore. Запуск без установки — nim r src/app.nim.
Синтаксис: типы, процедуры, модули
Блоки выделяются отступами, типы выводятся из значений. let создаёт неизменяемую привязку, var — изменяемую, const вычисляется при компиляции. Процедуры объявляются через proc, чистые функции — через func, а звёздочка после имени экспортирует символ.
import std/[strformat, strutils]
type
Level* = enum lvlInfo, lvlWarn, lvlError
Event* = object
ts*: int64
level*: Level
message*: string
proc parseLevel*(raw: string): Level =
case raw.toLowerAscii
of "info": lvlInfo
of "warn": lvlWarn
of "error": lvlError
else: raise newException(ValueError, "unknown level: " & raw)
let e = Event(ts: 1, level: parseLevel("WARN"), message: "slow query")
echo &"[{e.level}] {e.ts}: {e.message}"
case по перечислению требует полноты: если добавить уровень и забыть ветку, сборка остановится. Модуль — обычный файл, а import std/... подключает стандартную библиотеку.
Память: ARC, ORC и предсказуемое освобождение
В Nim нет обязательного глобального сборщика мусора. Историческая стратегия refc сочетала подсчёт ссылок с периодическим сбором циклов, но в Nim 2.x основной режим — ORC. Он освобождает объект сразу, когда исчезает последняя ссылка, а отдельный проход разбирает циклы.
type Buffer = object
data: ptr UncheckedArray[byte]
len: int
proc `=destroy`(b: var Buffer) =
if b.data != nil:
dealloc(b.data)
b.data = nil
b.len = 0
var b = Buffer(data: cast[ptr UncheckedArray[byte]](alloc(1024)), len: 1024)
# память освободится при выходе из области видимости
Ручной alloc нужен редко: seq, string и Table управляются автоматически. Для внешнего C-кода добавьте =copy и =sink, иначе компилятор сгенерирует поверхностное копирование и двойное освобождение. Режим меняется флагом --mm:arc или --mm:refc, но смешивать их в проекте не стоит.
Ошибки, исключения и контракты
Nim использует исключения, но позволяет проверять их при компиляции. Прагма raises фиксирует список допустимых исключений, а defer гарантирует закрытие файла при любом выходе.
import std/[os, strutils]
proc readConfig(path: string): string {.raises: [IOError].} =
let f = open(path)
defer: f.close()
result = f.readAll()
try:
let text = readConfig("/etc/myapp.conf")
echo text.len
except IOError as e:
stderr.writeLine "config error: " & e.msg
quit 1
Для библиотек удобно возвращать свой тип-результат, особенно если API вызывается из C. Не ловите CatchableError целиком: так в отчёт попадают ошибки программиста, которые лучше падать сразу. Флаг --panics:on делает нарушение границ массива неотлавливаемым.
Сборка, оптимизация и кросс-компиляция
Флаги удобно держать в nim.cfg рядом с проектом. Для релиза включайте оптимизацию, но не danger по умолчанию: этот режим убирает проверки границ и подходит только для доверенных данных.
# nim.cfg
--path:"src"
--opt:speed
--define:release
--stackTrace:on
--lineTrace:on
nim c -d:release --opt:speed src/app.nim
nim c -d:danger --passC:-flto --passL:-flto src/app.nim
nim c --os:linux --cpu:arm64 -d:release src/app.nim
ldd src/app
Кросс-компиляция требует C-тулчейна: для arm64 на x86 нужен aarch64-linux-gnu-gcc и флаг --gcc.exe:aarch64-linux-gnu-gcc. Полностью статический бинарник собирают с musl; ldd должен показать «not a dynamic executable».
Тесты, форматирование и CI
Стандартный unittest не требует внешних зависимостей, а nimble test собирает и запускает тесты из tests/. В конвейере достаточно проверки типов, тестов и форматирования.
nim check src/app.nim
nimble test
nimpretty src/app.nim
nim check быстрее полной сборки и ловит несовпадение типов. nimpretty приводит файлы к единому стилю; запускайте его в pre-commit. Версию компилятора фиксируйте в CI через choosenim.
Отладка и профилирование
Без отладочной информации стек вызовов бесполезен. Для падений собирайте бинарник с --debugger:native, а для узких мест используйте встроенный профилировщик: отчёт появится в profile_results.txt.
nim c --debugger:native --stackTrace:on --lineTrace:on src/app.nim
gdb ./src/app
nim c --profiler:on --stackTrace:on -d:release src/app.nim
./src/app
cat profile_results.txt
Для службы проверяйте коды возврата через systemctl status. Сетевые проблемы разбирайте с strace -f -p PID и ss -lntp, но только после воспроизведения в тесте.
Чек-лист перед продакшеном
- сборка идёт с
-d:release, аdangerне используется без необходимости; - все внешние входные данные проверяются до парсинга;
- зависимости зафиксированы, версия Nim указана в CI;
- бинарник проверен на целевой архитектуре, а не только на машине сборки;
- логи не содержат токенов и путей с персональными данными.
Итог
Nim даёт редкое сочетание: привычный синтаксис, нативная скорость и один исполняемый файл без зависимостей. Он подходит для утилит, сетевых служб и мест, где Python уже медленный, а C слишком опасен. Начните с небольшой утилиты, измерьте размер бинарника и потребление памяти, и только потом переносите на Nim что-то критичное.
Главное — не включать danger «на всякий случай» и не тащить ручное управление памятью туда, где хватает seq и string.