Skip to content
Back to Interview Guides
Interview Guide

Top 30 Zig Coding Challenges for Developers

· 3 min read

Zig Challenges for Robust and Performant Code

Zig is a systems programming language that prioritizes simplicity, performance, and safety. For developers, mastering Zig’s unique approach to error handling, comptime, and memory management opens up opportunities in game development, embedded systems, and more. For employers, finding engineers who can write robust, efficient, and maintainable code is a top priority.

That’s why we’ve put together this collection of 30 hands-on Zig challenges. We’ve organized them into three levels—10 for Junior, 10 for Mid-Level, and 10 for Senior developers—to help you build your skills, prepare for your next interview, or find the perfect candidate for your team.

Jump to Your Level

⚡ Junior Developer ️ Mid-Level Developer Senior Developer

Junior Developer Challenges

1. Hello, World!

The traditional first program to verify a working setup.

const std = @import("std");

pub fn main() !void {
    const stdout = std.io.getStdOut().writer();
    try stdout.print("Hello, {s}!\n", .{"world"});
}

2. Sum of a Slice of Integers

A basic test of loops and slice manipulation.

fn sum(numbers: []const i32) i32 {
    var total: i32 = 0;
    for (numbers) |num| {
        total += num;
    }
    return total;
}

3. Reverse a String

A simple test of string manipulation.

const std = @import("std");

fn reverse(allocator: std.mem.Allocator, s: []const u8) ![]u8 {
    const reversed = try allocator.alloc(u8, s.len);
    for (s, 0..) |char, i| {
        reversed[s.len - 1 - i] = char;
    }
    return reversed;
}

4. Find the Largest Number in a Slice

A fundamental algorithm using a simple loop.

fn largest(slice: []const i32) i32 {
    var largest = slice[0];
    for (slice) |item| {
        if (item > largest) {
            largest = item;
        }
    }
    return largest;
}

5. Check if a String is a Palindrome

Tests string manipulation and comparison logic.

const std = @import("std");

fn is_palindrome(s: []const u8) bool {
    var i: usize = 0;
    var j: usize = s.len - 1;
    while (i < j) {
        if (s[i] != s[j]) {
            return false;
        }
        i += 1;
        j -= 1;
    }
    return true;
}

6. FizzBuzz

The classic test of basic conditional logic and loops.

const std = @import("std");

fn fizzbuzz(n: u32) void {
    var i: u32 = 1;
    while (i <= n) : (i += 1) {
        if (i % 15 == 0) {
            std.debug.print("FizzBuzz\n", .{});
        } else if (i % 3 == 0) {
            std.debug.print("Fizz\n", .{});
        } else if (i % 5 == 0) {
            std.debug.print("Buzz\n", .{});
        } else {
            std.debug.print("{d}\n", .{i});
        }
    }
}

7. Simple `struct` for a `Person`

Introduces custom types and data organization.

const Person = struct {
    name: []const u8,
    age: u32,
};

8. Function that Returns an Error

A simple function to demonstrate error handling.

const MyError = error{
    SomethingWentWrong,
};

fn do_something(fail: bool) MyError!void {
    if (fail) {
        return MyError.SomethingWentWrong;
    }
}

9. Simple Loop to Print Numbers 1-10

A basic loop to demonstrate syntax.

const std = @import("std");

fn print_numbers() void {
    var i: u32 = 1;
    while (i <= 10) : (i += 1) {
        std.debug.print("{d}\n", .{i});
    }
}

10. Check if a Number is Positive, Negative, or Zero

Tests basic conditional logic.

const std = @import("std");

fn check_number(n: i32) void {
    if (n > 0) {
        std.debug.print("Positive\n", .{});
    } else if (n < 0) {
        std.debug.print("Negative\n", .{});
    } else {
        std.debug.print("Zero\n", .{});
    }
}

Mid-Level Developer Challenges

1. Word Count in a String using a `HashMap`

Use a `HashMap` to count the frequency of words in a text.

const std = @import("std");

fn word_count(allocator: std.mem.Allocator, text: []const u8) !std.StringHashMap {
    var map = std.StringHashMap.init(allocator);
    var it = std.mem.split(u8, text, " ");
    while (it.next()) |word| {
        const count = map.get(word) orelse 0;
        try map.put(word, count + 1);
    }
    return map;
}

2. Implement a Simple Interface

A simple interface for a `Shape` with an `area` method.

const Shape = struct {
    area: fn (*const anyopaque) f64,
};

const Circle = struct {
    radius: f64,

    fn area(self: *const Circle) f64 {
        return std.math.pi * self.radius * self.radius;
    }
};

3. JSON Serialization and Deserialization with `std.json`

Convert Zig structs to and from JSON using `std.json`.

const std = @import("std");

const User = struct {
    name: []const u8,
    age: u32,
};

// Serialization: std.json.stringify(user, .{}, writer)
// Deserialization: std.json.parse(User, &reader, .{})

4. Find the First Non-Repeating Character in a String

A common interview question that can be solved with a `HashMap`.

const std = @import("std");

fn first_non_repeating_character(allocator: std.mem.Allocator, s: []const u8) !?u8 {
    var counts = std.AutoHashMap(u8, u32).init(allocator);
    defer counts.deinit();

    for (s) |char| {
        const count = counts.get(char) orelse 0;
        try counts.put(char, count + 1);
    }

    for (s) |char| {
        if (counts.get(char).? == 1) {
            return char;
        }
    }

    return null;
}

5. Implement a Simple Generic Function

A simple generic function that takes a slice of any type and prints its elements.

const std = @import("std");

fn print_slice(comptime T: type, slice: []const T) void {
    for (slice) |item| {
        std.debug.print("{any}\n", .{item});
    }
}

6. Error Handling with `try` and `catch`

A simple example of error handling with `try` and `catch`.

const std = @import("std");

const MyError = error{
    SomethingWentWrong,
};

fn do_something(fail: bool) MyError!void {
    if (fail) {
        return MyError.SomethingWentWrong;
    }
}

pub fn main() !void {
    try do_something(true) catch |err| {
        std.debug.print("Caught error: {any}\n", .{err});
    };
}

7. Use of Anonymous Functions

A simple example of an anonymous function.

const std = @import("std");

pub fn main() !void {
    const add_one = struct {
        fn run(x: i32) i32 {
            return x + 1;
        }
    }.run;

    std.debug.print("{d}\n", .{add_one(1)});
}

8. Implement a Simple Command-Line Argument Parser

A simple command-line argument parser.

const std = @import("std");

pub fn main() !void {
    var args = try std.process.argsAlloc(std.heap.page_allocator);
    defer std.process.argsFree(std.heap.page_allocator, args);

    for (args) |arg| {
        std.debug.print("{s}\n", .{arg});
    }
}

9. Read and Write to a File

A simple example of reading and writing to a file.

const std = @import("std");

pub fn main() !void {
    const file = try std.fs.cwd().createFile("foo.txt", .{});
    defer file.close();

    try file.writer().print("Hello, {s}!\n", .{"world"});

    const contents = try std.fs.cwd().readFileAlloc(std.heap.page_allocator, "foo.txt", 1024);
    defer std.heap.page_allocator.free(contents);

    std.debug.print("{s}\n", .{contents});
}

10. Implement a Simple Linked List

A simple implementation of a linked list.

const Node = struct {
    data: i32,
    next: ?*Node,
};

const LinkedList = struct {
    head: ?*Node,
};

Senior Developer Challenges

1. Implement a Thread-Safe Counter using `std.Thread.Mutex`

A thread-safe counter using `std.Thread.Mutex`.

const std = @import("std");

var counter: u32 = 0;
var mutex = std.Thread.Mutex{};

fn increment() void {
    mutex.lock();
    defer mutex.unlock();
    counter += 1;
}

2. Create a Simple Web Server with `std.net`

A simple web server with `std.net`.

const std = @import("std");

pub fn main() !void {
    var listener = try std.net.tcpConnectToAddress("127.0.0.1:8080");
    defer listener.close();

    while (true) {
        var conn = try listener.accept();
        defer conn.close();

        // ...
    }
}

3. Implement a Simple Memory Allocator

A simple memory allocator.

const std = @import("std");

const MyAllocator = struct {
    allocator: std.mem.Allocator,

    fn alloc(self: *MyAllocator, len: usize, ptr_align: u29, len_align: u29, ret_addr: usize) ?[*]u8 {
        return self.allocator.rawAlloc(len, ptr_align, len_align, ret_addr);
    }

    fn free(self: *MyAllocator, buf: []u8, buf_align: u29, ret_addr: usize) void {
        self.allocator.rawFree(buf, buf_align, ret_addr);
    }
};

4. Write a Comptime Function

A simple comptime function.

const std = @import("std");

fn comptime_add(comptime a: i32, comptime b: i32) i32 {
    return a + b;
}

pub fn main() !void {
    const result = comptime_add(1, 2);
    std.debug.print("{d}\n", .{result});
}

5. Implement a Simple Async Runtime

A simple async runtime.

// This is a complex task that would require a lot of code.
// A full implementation would be too long for this format.

6. Use of `extern` to Interact with C Code

A simple example of using `extern` to interact with C code.

const c = @cImport({
    @cInclude("stdio.h");
});

pub fn main() !void {
    c.printf("Hello, %s!\n", "world");
}

7. Implement a Simple Concurrent Queue

A simple concurrent queue.

const std = @import("std");

const ConcurrentQueue = struct {
    inner: std.ArrayList(i32),
    mutex: std.Thread.Mutex,
};

8. Write a Simple Key-Value Store

A simple key-value store.

const std = @import("std");

const KvStore = struct {
    map: std.StringHashMap(i32),
};

9. Implement a Simple State Machine

A simple state machine.

const State = enum {
    Start,
    Running,
    Stop,
};

const StateMachine = struct {
    state: State,
};

10. Create a Simple Build Script for a Project

A simple build script for a project.

// In build.zig
const std = @import("std");

pub fn build(b: *std.Build) void {
    const target = b.standardTargetOptions(.{});
    const optimize = b.standardOptimizeOption(.{});

    const exe = b.addExecutable(.{
        .name = "my-app",
        .root_source_file = .{ .path = "src/main.zig" },
        .target = target,
        .optimize = optimize,
    });

    b.installArtifact(exe);
}

Tips to Prepare for Zig Coding Challenges

  • Master Comptime: Zig's compile-time execution is a powerful feature. Understand how to use it to write more efficient and flexible code.
  • Embrace Explicit Error Handling: Zig's error handling is built around error unions. Practice using `try` and `catch` to handle errors gracefully.
  • Understand Memory Management: Zig gives you control over memory management. Practice using allocators and `defer` to manage memory safely.
  • Know the Standard Library: Zig's standard library is still growing, but it's full of useful tools. Get familiar with `std.mem`, `std.fs`, and `std.json`.
  • Use the Zig Build System: Zig's build system is a powerful tool for managing complex projects. Practice writing build scripts to compile and test your code.

Conclusion

Zig's focus on simplicity, performance, and safety makes it a powerful tool for building robust software. By practicing with these challenges, you'll be well-prepared to tackle any interview and build amazing applications. Happy coding!

Skip the interview marathon.

We pre-vet senior engineers across Asia using these exact questions and more. Get matched in 24 hours, $0 upfront.

Get Pre-Vetted Talent
WhatsApp