Основы языка Zig
Основы языка Zig
Что такое Zig?
Zig — это язык программирования со следующими особенностями:
- Типизация — статическая, сильная; вывод типов есть (
const x = 42); явные аннотации при необходимости; неявные небезопасные преобразования запрещены. - Парадигма — императивный, процедурный; метапрограммирование через
comptime; структуры, union и enum вместо классического ООП (нет наследования, исключений, макросов препроцессора). - Уровень — низкоуровневый, системный: прямой контроль над памятью, указателями и layout данных; близок к C, но с современными проверками компилятора.
- Выполнение — компилируемый AOT (
zig build,zig run); LLVM-бэкенд; не интерпретируемый (comptime — вычисления на этапе сборки, не REPL). - Память — ручное управление через аллокаторы; без сборщика мусора;
deferдля детерминированного освобождения; правило "кто выделил — тот освобождает". - Платформа — кроссплатформенный (Linux, Windows, macOS, BSD, embedded, WebAssembly); нативный машинный код; не управляемый runtime; кросс-компиляция из коробки; умеет компилировать и вызывать C-код.
- Формат разработки — требует структуры проекта (
build.zig,build.zig.zon); одиночный файл можно собрать черезzig run file.zig, но идиоматичный цикл —zig build. - Направление — системное программирование (ОС, драйверы, embedded, компиляторы), инфраструктура, CLI, игры, высокопроизводительные сервисы; позиционируется как современная альтернатива C.
- REPL — встроенного REPL нет; для быстрых экспериментов —
zig run,zig test; интерактивной оболочки в toolchain нет. - Поколение — современный (с 2015–2016, pre-1.0, ветка 0.x); активная разработка, до стабильного 1.0 возможны breaking changes.
- Параллелизм и асинхронность — нативные потоки ОС (
std.Thread.spawn); стабильногоasync/awaitнет (снят с 0.11, новая модель в разработке); блокирующий I/O, неблокирующие примитивы платформы или сторонние event loop. - Безопасность — не memory-safe — указатели, ручная память, ответственность на разработчике; компилятор устраняет часть неопределённого поведения C (неявные приведения, необработанные ошибки); без скрытой магии и runtime-сюрпризов.
Если какой-то пункт из списка непонятен — подробные определения и примеры в Язык программирования.
Если вы пришли из C/C++ или только начинаете системное программирование, Zig удобно читать как "язык с явными правилами". Вы точно видите, где выделяется память, где может возникнуть ошибка и почему компилятор отклоняет сомнительные преобразования типов.
В практической работе это ощущается быстро: небольшой CLI-инструмент на Zig можно собрать за минуты, а модель ошибок и аллокаторов остаётся такой же и в больших сервисах.
Zig — это системный язык программирования, разработанный с целью стать простым, надёжным и эффективным инструментом для написания низкоуровневого кода. Он сочетает в себе контроль над ресурсами, присущий таким языкам, как C, с современными возможностями, направленными на повышение безопасности и читаемости. Zig не требует внешнего препроцессора, не использует макросы в традиционном смысле и не включает автоматическое управление памятью. Вместо этого он предоставляет разработчику явные механизмы управления временем жизни данных, выделения памяти и обработки ошибок.
Одна из ключевых целей Zig — устранение неопределённого поведения, характерного для C, за счёт строгой проверки на этапе компиляции и выполнения. Язык поддерживает кросс-компиляцию "из коробки", что делает его удобным для разработки под различные архитектуры без необходимости настройки сложных toolchain’ов. Zig также может использоваться как замена компилятору C, поскольку он включает в себя собственный LLVM-бэкенд и способен компилировать C-код напрямую.
Синтаксис и структура программы
Программа на Zig начинается с точки входа — функции pub fn main. На практике используют несколько допустимых сигнатур: main() void, main() !void (с обработкой ошибок через try) или чтение аргументов через std.process внутри main. Аргументы командной строки не передаются параметрами main, как в C — их получают из стандартной библиотеки.
Простейшая программа на Zig выглядит так:
const std = @import("std");
pub fn main() void {
std.debug.print("Привет, Zig!\n", .{});
}
Разбор:
@import("std")подключает модуль стандартной библиотеки, чтобы использовать базовые API языка.pub fn main() voidобъявляет точку входа;voidуказывает, что функция не возвращает полезное значение.std.debug.printвыполняет форматируемый вывод;\nпереносит строку в консоли.- Пустой tuple
.{}передаётся как набор аргументов форматтера, когда в шаблоне нет плейсхолдеров.
Здесь директива @import("std") загружает стандартную библиотеку Zig. Ключевое слово const объявляет иммутабельную переменную std, через которую доступны модули стандартной библиотеки. Функция main объявлена как pub, что делает её видимой извне, и возвращает тип void, то есть ничего не возвращает. Вызов std.debug.print выводит строку в стандартный поток вывода. Второй аргумент — это кортеж (tuple), содержащий значения для подстановки в строку формата. В данном случае кортеж пуст, так как строка не содержит плейсхолдеров.
Типы данных
Zig является статически типизированным языком с выводом типов. Это означает, что тип каждой переменной известен на этапе компиляции, но разработчик не обязан указывать его явно, если компилятор может определить его по контексту.
Целочисленные типы
Zig предоставляет явные целочисленные типы с фиксированным размером. Например:
i8,i16,i32,i64,i128— знаковые целые;u8,u16,u32,u64,u128— беззнаковые целые.
Также существуют специальные типы:
isizeиusize— целые числа, размер которых соответствует разрядности системы (32 или 64 бита);comptime_int— целое число, известное только на этапе компиляции.
Пример:
const x: i32 = 42;
const y = u8(255); // явное приведение типа
Разбор:
const x: i32 = 42;явно фиксирует тип переменной как знаковое 32-битное целое.const y = u8(255);демонстрирует явное приведение вu8(диапазон0..255), что делает намерение разработчика прозрачным.- Такой стиль помогает компилятору раньше поймать ошибки диапазона и смешения знаковых/беззнаковых типов.
Во втором случае используется синтаксис вызова типа как функции — это безопасное приведение, которое проверяется на этапе компиляции или выполнения в зависимости от контекста.
Числа с плавающей точкой
Zig поддерживает два основных типа для чисел с плавающей точкой:
f32— одинарная точность;f64— двойная точность.
Пример:
const pi: f64 = 3.141592653589793;
const e = 2.71828; // тип выводится как f64
Разбор:
f64задаёт число двойной точности по IEEE 754, подходящее для большинства инженерных расчётов.- Во второй строке тип выводится автоматически, но остаётся строго статическим и проверяется на этапе компиляции.
- Пример показывает сочетание явной аннотации (
pi) и вывода типа (e) без потери типобезопасности.
Логический тип
Логический тип в Zig называется bool и принимает два значения: true и false.
const is_ready: bool = true;
const can_start = is_ready and not false;
Разбор:
boolпринимает толькоtrueилиfalse; числа в условие подставить нельзя.and— логический оператор с коротким замыканием: правая часть не вычисляется, если результат уже известен.not falseэквивалентноtrue, поэтомуcan_startсовпадает сis_ready.
Символы и строки
Символ в Zig представлен типом u8, так как язык использует UTF-8 для представления текста. Строка — это указатель на последовательность байтов с нулевым завершением ([*:0]const u8) или срез ([]const u8).
Пример:
const message = "Hello, Zig!";
Разбор:
- Литерал в двойных кавычках — неизменяемая последовательность байтов UTF-8 с завершающим
\0. - Тип выводится автоматически и включает длину в типе массива (для ASCII здесь 11 символов + терминатор).
- Такой литерал удобен для констант и C-совместимых API, но для функций чаще передают срез
[]const u8.
Литерал в кавычках — это массив байт UTF-8 с нулевым терминатором (sentinel 0). Тип выводится автоматически, например *const [11:0]u8 для этой ASCII-строки (11 символов + \0). Для кириллицы размер в байтах больше числа символов — удобно смотреть через message.len или @sizeOf(@TypeOf(message)) на этапе компиляции.
Чаще в API передают срез без привязки к литералу:
const greeting: []const u8 = "Привет";
Разбор:
- Аннотация
[]const u8явно задаёт срез байтов только для чтения. - Строковый литерал неявно приводится к срезу нужной длины без копирования в кучу.
- Такой тип удобно передавать в функции парсинга, сравнения и логирования.
Строки в Zig иммутабельны по умолчанию. Для изменения содержимого требуется выделение памяти и работа с изменяемыми срезами ([]u8).
Срез как "окно" на массив без копирования:
const data = [_]u8{ 'H', 'e', 'l', 'l', 'o' };
const hello = data[0..5];
// hello — []const u8 длиной 5, указывает на те же байты, что и data
Разбор:
data[0..5]формирует срез по полуинтервалу — индексы0,1,2,3,4.- Срез хранит указатель и длину, но не владеет памятью исходного массива.
- Такой приём часто используют для парсинга и передачи подстрок в функции.
Управление памятью
Zig не включает сборщик мусора. Вместо этого он предоставляет разработчику полный контроль над выделением и освобождением памяти через аллокаторы. Аллокатор — это структура, реализующая интерфейс выделения и освобождения памяти. Это позволяет легко подставлять разные стратегии: от простого arena-аллокатора до пулов памяти.
Пример выделения динамической строки:
Код ITЗагрузка примера кода…
Разбор:
- Сигнатура
!voidсообщает, что функция может вернуть ошибку;tryавтоматически пробрасывает её наверх. GeneralPurposeAllocatorсоздаёт универсальный аллокатор, аdefer _ = gpa.deinit();корректно завершает его работу.allocator.alloc(u8, source.len)выделяет буфер нужной длины;defer allocator.free(buffer);симметрично освобождает память.@memcpy(buffer, source)копирует байты строки в выделенный буфер.- Формат
{s}печатает срез байтов как строку, если данные корректны как UTF-8/байтовая строка.
Здесь используется GeneralPurposeAllocator — универсальный аллокатор из стандартной библиотеки. Ключевое слово defer гарантирует, что память будет освобождена при выходе из области видимости. Конструкция try обрабатывает возможные ошибки выделения памяти.
Часто используемые аллокаторы:
| Аллокатор | Назначение |
|---|---|
GeneralPurposeAllocator | Универсальный, с leak detection в debug |
ArenaAllocator | Массовое освобождение одним deinit |
FixedBufferAllocator | Буфер на стеке или в статике |
page_allocator | Системные страницы, без free по элементам |
Правило: кто выделил — тот освобождает; аллокатор передают параметром в функции, которые аллоцируют.
Для временных объектов в одной функции удобен arena — всё освобождается одним deinit:
const std = @import("std");
pub fn buildMessage(allocator: std.mem.Allocator) ![]u8 {
var arena = std.heap.ArenaAllocator.init(allocator);
defer arena.deinit();
const arena_alloc = arena.allocator();
const part1 = try arena_alloc.dupe(u8, "Zig");
const part2 = try arena_alloc.dupe(u8, " — системный язык");
return try std.mem.concat(arena_alloc, u8, &.{ part1, part2 });
}
Разбор:
ArenaAllocatorвыделяет память "пакетом" и освобождает её целиком приarena.deinit().dupeкопирует строку в память arena; отдельныйfreeдля каждого куска не нужен.std.mem.concatсклеивает срезы в один новый буфер через тот же аллокатор arena.- После выхода из функции arena уничтожается, а итоговый
[]u8уже принадлежит вызывающему коду (если его скопировали в долгоживущую память).
Обработка ошибок
Интерактивное демо — в Zig нет исключений, ошибки — значения
errorи!T; смотрите сценарий "код ошибки" и стек. Подробнее: ошибки и исключения.
Play ITЗагрузка интерактивного демо…
Zig использует явную обработку ошибок через тип error. Ошибки — это перечислимые значения, которые могут быть объединены с другими типами с помощью оператора !.
Пример:
Код ITЗагрузка примера кода…
Разбор:
const FileError = error{...};создаёт собственное множество ошибок домена работы с файлами.fn openFile(...) FileError!voidвозвращает либо успех (void), либо одну из ошибокFileError.- В
mainиспользуетсяcatch |err|иswitchдля раздельной обработки каждого сценария. - Первый вызов показывает компактный
catchкак выражение, второй — блочный вариант с дополнительной логикой иreturn. - Такой подход делает поток ошибок полностью явным без исключений и скрытых переходов.
Тип FileError!void означает: "функция возвращает void или одну из ошибок из FileError". Обработка ошибок осуществляется через switch или с помощью try, catch.
Короткий вариант с try — ошибка сразу уходит на уровень выше:
pub fn main() !void {
try openFile("/data.txt");
std.debug.print("Файл открыт\n", .{});
}
Разбор:
try openFile(...)эквивалентенopenFile(...) catch |err| return err;.- Если
openFileвернёт ошибку,mainзавершится немедленно и передаст её вызывающему коду. - При успехе выполнение продолжается, и печатается сообщение об открытии файла.
Функции
Функции в Zig объявляются с помощью ключевого слова fn. Они могут быть generic’ами, если используют параметры времени компиляции.
Пример обобщённой функции:
fn max(a: anytype, b: anytype) @TypeOf(a, b) {
return if (a > b) a else b;
}
Разбор:
anytypeпозволяет принять аргументы разных конкретных типов, если операции внутри валидны для них.@TypeOf(a, b)вычисляет результирующий тип на этапе компиляции и закрепляет его в сигнатуре.if (...) a else bиспользуется как выражение, поэтому функция получается короткой и без промежуточных переменных.- Компилятор специализирует такую функцию под фактические типы вызова, что сохраняет производительность.
Здесь anytype означает, что аргумент может быть любого типа, совместимого с оператором сравнения. @TypeOf(a, b) возвращает общий тип аргументов, который становится возвращаемым типом функции.
Вызов:
const x = max(10, 20); // x: comptime_int
const y = max(3.14, 2.71); // y: f64
Разбор:
- Первый вызов специализирует
maxдля целых литералов; результат —20. - Второй вызов создаёт отдельную версию для
f64; сравнение идёт в вещественной арифметике. - Один и тот же исходный код функции порождает разные машинные реализации без ручного дублирования.
Структуры и составные типы
Структуры в Zig объявляются с помощью ключевого слова struct. Они могут содержать поля и методы; полиморфизм достигается через comptime, anytype и явные vtable, а не через отдельный механизм "трейтов", как в Rust.
Пример:
Код ITЗагрузка примера кода…
Разбор:
const Point = struct { ... }объявляет пользовательский тип с полямиxиy.fn distanceFromOrigin(self: Point) f64— метод структуры;selfпередаётся первым параметром.@sqrt(self.x * self.x + self.y * self.y)реализует вычисление длины вектора по теореме Пифагора.- Инициализация
Point{ .x = 3.0, .y = 4.0 }использует именованные поля, что упрощает чтение и снижает риск ошибок порядка аргументов.
Метод distanceFromOrigin принимает self как первый аргумент — это соглашение Zig для методов экземпляра.
Компиляция и инструментарий
Zig поставляется со встроенным компилятором, линковщиком и менеджером зависимостей. Компиляция программы выполняется одной командой:
zig build-exe hello.zig
Проекты организуются через файл build.zig, который описывает сборку с помощью Zig-кода, а не внешних DSL. Это позволяет использовать всю выразительность языка для настройки сборки.
Пример минимального build.zig:
Код ITЗагрузка примера кода…
Разбор:
pub fn build(b: *std.Build) void— точка входа системы сборки Zig для проекта.standardTargetOptionsиstandardOptimizeOptionчитают флаги--targetи-Doptimizeиз командной строки.addExecutableописывает исполняемый файл: имя, корневой.zigи параметры оптимизации.installArtifactдобавляет бинарник в шаг установки, доступный черезzig build install.
Команда zig build запускает этот скрипт и собирает проект.
Компиляция во время компиляции (comptime)
Одной из самых выразительных возможностей Zig является поддержка вычислений на этапе компиляции через ключевое слово comptime. Оно указывает, что выражение или блок кода должен быть выполнен не во время запуска программы, а во время её сборки. Это позволяет генерировать структуры данных, проверять условия и даже создавать новые типы без потерь производительности в рантайме.
Пример простого использования:
const std = @import("std");
fn factorial(comptime n: u32) u32 {
if (n <= 1) return 1;
return n * factorial(n - 1);
}
pub fn main() void {
const compile_time_value = comptime factorial(5);
std.debug.print("Факториал 5 равен {d}\n", .{compile_time_value});
}
Разбор:
- Параметр
comptime n: u32требует, чтобы значение было известно уже во время компиляции. - Рекурсивная
factorialвычисляется целиком при сборке, когда вызов обёрнут вcomptime. - Полученный результат встраивается как константа в итоговый бинарник, уменьшая runtime-работу.
- Такой паттерн полезен для вычисляемых констант, размеров массивов и compile-time проверок.
Здесь вызов factorial(5) происходит на этапе компиляции, и результат становится константой. Если передать в factorial значение, недоступное на этапе компиляции (например, введённое пользователем), компилятор выдаст ошибку.
Более сложный пример — создание массива с известным размером на этапе компиляции:
Код ITЗагрузка примера кода…
Разбор:
fn createArray(comptime size — usize, value: u8) [size]u8возвращает массив, размер которого является частью типа.var result: [size]u8 = undefined;резервирует память под массив без начальной инициализации всех элементов.- Цикл
for (0..size)заполняет каждый индекс одинаковым значениемvalue. comptime createArray(...)фиксирует содержимое массива на этапе компиляции, что исключает динамическую аллокацию.
Функция createArray принимает параметр size как comptime, что означает: "этот аргумент обязан быть известен при компиляции". В результате создаётся статический массив фиксированного размера, не требующий динамического выделения памяти.
Компиляция во времени компиляции также используется для реализации обобщённого программирования, метапрограммирования и генерации кода без макросов.
Работа с C-библиотеками
Zig предоставляет встроенную совместимость с C. Он может напрямую компилировать C-код, импортировать заголовочные файлы и вызывать функции из системных библиотек без необходимости внешних инструментов.
Для импорта C-заголовка используется директива @cImport:
const c = @cImport({
@cInclude("stdio.h");
});
pub fn main() void {
_ = c.printf("Привет из C!\n");
}
Разбор:
@cImportзапускает C-парсер Zig и генерирует Zig-представления функций/типов из заголовков.@cInclude("stdio.h")добавляет объявления стандартного C API, включаяprintf.c.printf(...)демонстрирует прямой вызов функции из C без ручного написания биндингов.- Префикс
c.показывает, что символ импортирован из C-окружения, а не объявлен в Zig-коде.
Компилятор Zig автоматически генерирует привязки к функциям, переменным и типам из заголовка. Это позволяет использовать существующие C-библиотеки так же естественно, как если бы они были написаны на Zig.
Если требуется собрать проект с внешней C-библиотекой, например, libcurl, достаточно указать её в build.zig:
const exe = b.addExecutable(.{
.name = "fetcher",
.root_source_file = b.path("src/main.zig"),
});
exe.linkLibC();
exe.linkSystemLibrary("curl");
После этого можно вызывать функции из curl напрямую:
Код ITЗагрузка примера кода…
Разбор:
- Блок инициализации/очистки
curl_global_init+defer curl_global_cleanupоформляет жизненный цикл библиотеки явно и безопасно. curl_easy_init()возвращает nullable-указатель; конструкцияif (curl) |handle|распаковывает его только при успехе.curl_easy_setopt(..., CURLOPT_URL, ...)настраивает URL запроса, затемcurl_easy_performвыполняет HTTP-операцию.curl_easy_cleanup(handle)освобождает ресурсы конкретного easy-handle и завершает локальный цикл работы.
Такой подход делает Zig мощным инструментом для постепенной миграции C-проектов или для написания высокопроизводительных обёрток над системными API.
Безопасность и проверки на этапе выполнения
Zig стремится устранить неопределённое поведение, характерное для C. Для этого он вводит набор проверок, активных по умолчанию в режиме отладки (Debug). Валидация внешнего ввода на границе программы — Проверка и валидация. Эти проверки включают:
- Контроль выхода за границы массива;
- Проверку целочисленного переполнения;
- Обнаружение использования неинициализированных переменных;
- Валидацию указателей.
Пример:
pub fn main() void {
var arr = [_]u8{ 1, 2, 3 };
const index = 10;
_ = arr[index]; // Ошибка: индекс за пределами массива
}
Разбор:
[_]u8{ 1, 2, 3 }создаёт массив из трёх байтов, где размер выводится автоматически.- Доступ
arr[index]проверяется рантаймом в безопасных режимах (Debug,ReleaseSafe). - Значение
index = 10выходит за границы массива, поэтому срабатывает диагностируемая ошибка доступа. - Пример иллюстрирует ключевое отличие Zig от C: выход за пределы ловится, а не остаётся тихим UB.
В режиме -O Debug программа завершится с понятным сообщением об ошибке. В -O ReleaseFast часть проверок отключается ради производительности; для критичных мест используют явные @intCast, @truncate или оставляют -O ReleaseSafe.
Zig также не допускает неявных приведений между целочисленными типами разного размера или знаковости. Любое преобразование должно быть явным:
const a: u8 = 200;
// const b: i8 = a; // Ошибка компиляции
const b: i16 = @intCast(a); // Явное и безопасное преобразование
Разбор:
- Строка
// const b: i8 = a;не скомпилируется: типыu8иi8несовместимы без явного приведения. @intCast(a)расширяетu8доi16с проверкой диапазона в безопасных режимах.- Явное приведение делает намерение разработчика видимым в коде и в ревью.
Функция @intCast проверяет, помещается ли значение в целевой тип. Если нет — возникает ошибка на этапе выполнения (в отладочном режиме) или неопределённое поведение (в релизе без проверок).
Практический пример — CLI-утилита для чтения файла
Рассмотрим небольшую утилиту командной строки, которая читает содержимое текстового файла и выводит его в терминал.
Код ITЗагрузка примера кода…
Разбор:
readFileпринимаетallocatorпараметром, чтобы вызывающая сторона контролировала стратегию памяти.file.stat()даёт размер файла, после чего выделяется буфер точной длины и читается весь контент.- В
mainаргументы командной строки выделяются черезargsAllocи освобождаютсяargsFreeдля симметрии владения. tryв каждом I/O шаге упрощает проброс ошибок без скрытой обработки.stdout.writeAll(content)гарантирует запись всего буфера, а не частичного куска.
Эта программа:
- Использует
GeneralPurposeAllocatorдля управления памятью; - Читает аргументы командной строки;
- Открывает файл, выделяет буфер нужного размера и читает всё содержимое;
- Выводит данные в стандартный поток.
Обратите внимание на отсутствие глобального состояния, явное управление ресурсами и чёткую обработку ошибок. Каждый шаг контролируется разработчиком, а компилятор помогает избежать распространённых ошибок.
Система модулей и организация кода
Zig использует иерархическую систему модулей, основанную на файловой структуре проекта. Каждый файл .zig представляет собой модуль, который может экспортировать функции, типы, константы и другие сущности через ключевое слово pub. Импорт модуля осуществляется с помощью встроенной функции @import, которая принимает путь к файлу относительно корня проекта или стандартной библиотеки.
Рассмотрим типичную структуру проекта:
my_project/
├── build.zig
└── src/
├── main.zig
└── utils.zig
Файл src/utils.zig может содержать вспомогательные функции:
// src/utils.zig
const std = @import("std");
pub fn greet(name: []const u8) void {
std.debug.print("Привет, {s}!\n", .{name});
}
pub const version = "1.0.0";
В main.zig этот модуль импортируется так:
// src/main.zig
const std = @import("std");
const utils = @import("utils.zig");
pub fn main() void {
utils.greet("Zig");
std.debug.print("Версия: {s}\n", .{utils.version});
}
Разбор:
@import("utils.zig")подключает соседний модуль как значение типаtype/namespace.utils.greet(...)вызывает публичную функцию из импортированного файла.utils.versionчитает публичную константу модуля без дублирования кода.
Обратите внимание: @import("std") — это особый случай, указывающий на встроенную стандартную библиотеку. Для пользовательских модулей указывается относительный путь в виде строки.
Внешние зависимости описывают в build.zig.zon, а в build.zig подключают через b.dependency:
const some_lib_dep = b.dependency("some_lib", .{
.target = target,
.optimize = optimize,
});
exe.root_module.addImport("some_lib", some_lib_dep.module("some_lib"));
После этого в коде:
const some_lib = @import("some_lib");
Такой подход даёт воспроизводимую сборку без отдельного менеджера пакетов вне экосистемы Zig.
Тестирование и документирование внутри кода
Zig интегрирует тестирование непосредственно в язык. Любой блок кода может содержать тесты, помеченные ключевым словом test. Тесты компилируются только при запуске в режиме тестирования и не влияют на производственный код.
Пример:
const std = @import("std");
fn add(a: i32, b: i32) i32 {
return a + b;
}
test "сложение положительных чисел" {
try std.testing.expectEqual(@as(i32, 5), add(2, 3));
}
test "сложение с нулём" {
try std.testing.expectEqual(@as(i32, 42), add(0, 42));
}
Разбор:
- Блоки
test "имя" { ... }компилируются только приzig test, а не в обычномzig build. expectEqualсравнивает ожидаемое и фактическое значение и падает при расхождении.@as(i32, 5)явно задаёт тип литерала, чтобы сравнение было однозначным для компилятора.
Запуск тестов — для одного файла:
zig test src/main.zig
В проекте с build.zig обычно используют zig build test. Компилятор находит все блоки test в модуле. std.testing.expectEqual и std.testing.expect проверяют условия и завершают тест с ошибкой при несовпадении.
Документация встраивается в комментарии /// перед объявлениями. HTML-документация генерируется через сборку проекта (Autodoc), например:
zig build doc
/// Складывает два целых числа.
pub fn add(a: i32, b: i32) i32 {
return a + b;
}
Точная цель (doc) настраивается в build.zig проекта. Комментарии остаются рядом с кодом, что снижает рассинхронизацию с реализацией.
Асинхронность и конкурентность
Статус языка: экспериментальные ключевые слова
async,await,suspendиresumeбыли в ранних сборках Zig, но сняты с поддержки начиная с 0.11. Команда разрабатывает новую модель асинхронного I/O; в прикладном коде сегодня опираются на блокирующий I/O, потоки ОС,std.Thread, неблокирующие примитивы платформы или сторонние event loop.
Пока языкового async/await нет, типичный паттерн — явная пауза и последовательная логика:
const std = @import("std");
fn runTask() !void {
std.debug.print("Начало задачи\n", .{});
std.time.sleep(100 * std.time.ns_per_ms);
std.debug.print("Задача завершена\n", .{});
}
pub fn main() !void {
try runTask();
}
Разбор:
std.time.sleepблокирует текущий поток на указанное число наносекунд.100 * std.time.ns_per_msпереводит 100 миллисекунд в наносекунды через именованную константу.try runTask()пробрасывает возможную ошибку изrunTaskнаверх по стеку.
Для параллелизма создают потоки (std.Thread.spawn) или выносят I/O в отдельные модули. При появлении стабильного async API в релизе Zig этот раздел стоит обновить по официальным release notes.
Написание кросс-платформенных приложений
Одна из сильных сторон Zig — встроенная поддержка кросс-компиляции. Компилятор содержит toolchain’и для всех основных платформ — Windows, Linux, macOS, FreeBSD, а также для встраиваемых систем (ARM, RISC-V и другие). Чтобы собрать программу под другую архитектуру, достаточно указать цель:
zig build-exe main.zig --target x86_64-windows-gnu
zig build-exe main.zig --target aarch64-linux-musl
Никаких дополнительных установок не требуется. Zig поставляется со всеми необходимыми заголовками и библиотеками.
Условная компиляция достигается через параметры времени компиляции:
const std = @import("std");
pub fn main() void {
if (@import("builtin").target.os.tag == .windows) {
std.debug.print("Запущено на Windows\n", .{});
} else if (@import("builtin").target.os.tag == .linux) {
std.debug.print("Запущено на Linux\n", .{});
}
}
Разбор:
@import("builtin")даёт метаданные текущей цели компиляции (ОС, CPU, режим оптимизации).target.os.tag— перечисление ОС; сравнение с.windows/.linuxвыбирает ветку на этапе сборки.- Неиспользуемые ветки для других ОС компилятор может отбросить, если условие известно статически.
Модуль builtin предоставляет информацию о текущей цели компиляции — операционной системе, архитектуре, ABI и других параметрах. Это позволяет писать платформенно-зависимый код без внешних макросов или препроцессоров.
Сравнение с другими системными языками
Zig занимает уникальное положение среди системных языков. По сравнению с C, он устраняет источники неопределённого поведения, добавляет безопасные приведения типов, встроенную обработку ошибок и современный инструментарий. При этом он сохраняет ту же модель памяти и производительность.
По сравнению с Rust, Zig отказывается от сложной системы владения и заимствования. Вместо этого он даёт разработчику полный контроль над временем жизни объектов, полагаясь на дисциплину и явное управление. Это делает Zig проще для освоения тем, кто знаком с C, но требует большей ответственности от программиста.
В отличие от C++, Zig не включает шаблоны, исключения, виртуальные таблицы или множественное наследование. Он заменяет эти механизмы более простыми и предсказуемыми конструкциями: comptime-вычислениями, явной передачей интерфейсов и композицией.
Zig — это стремление сделать системное программирование более надёжным, понятным и доступным без жертв в производительности или контроле.
Чек-лист после изучения основ
- Вы запускаете проект через
zig buildи понимаете назначениеbuild.zig. - Умеете читать сигнатуры
!Tи обрабатывать ошибки черезtryиcatch. - Различаете фиксированные массивы
[N]Tи срезы[]T. - Передаёте аллокатор в функции, которые выделяют память.
- Понимаете, когда использовать
Debug,ReleaseSafeиReleaseFast.
Связанные статьи
- Типы данных и управление памятью — углубление в типовую систему и владение ресурсами.
- Управляющие конструкции и операторы Zig — flow-контроль и арифметика без сюрпризов.
- Функции и время компиляции — переход к
comptimeи reusable API.