Why would you want to learn Zig?
Personally, learning Zig is just for fun. I don't see a lot of jobs requesting zig and the ones that do list it with other languages that are nice to haves. So if you are reading this and looking for employment, this is probably not the language to learn first though it does have a lot of commonalities with other languages. In my humble opinion, the best language to learn first is Bash but honestly any language that you are interested in is going to get you further than one you aren't.
With all that being said, Learning is fun and thats why I'm on this journey!
What is Zig?
Zig is considered a spiritual successor to C. It aims to offer low-level control over the processor and memory but with a more modern and safer syntax. It emphasizes type safety at compile time and avoids hidden control flow and allocations, making it highly suitable for game development, system programming, and other performance-critical applications. Zig also provides numerous metaprogramming capabilities with compile-time function execution and reflection, allowing developers to write efficient, error-free, and clean code.
How to Install Zig
You can install Zig using Homebrew with the command:
brew install zig
Verify the installation by running:
zig version
Add an extention to your Visual Studio Code editor called Zig Language
so that your editor will do syntax highlighting for you.
Hello World
After installing Zig, initialize a new project by running:
zig init
This will create a src directory and the files build.zig and build.zig.zon.
You can run the default build using:
zig build run
It will output something like:
All your codebase are belong to us.
Run zig build test to run the tests.Note: This is a humorous reference, possibly a play on the meme "All your base are belong to us."
To create a "Hello World" program, create a new file named hello-world.zig:
touch hello-world.zig
Add the following code to hello-world.zig:
const std = @import("std");
pub fn main() void {
std.debug.print("Hello world!\n", .{});
}
Explanation of the code:
const std = @import("std");
imports the standard library.
pub fn main() void
declares the main function. The void indicates that it does not return any value.
std.debug.print("Hello world!\n", .{});
prints the string "Hello world!\n"
. It takes two arguments: the string to print and an empty tuple .{}
.
The \n
adds a new line at the end of the output.
All statements in Zig end with a semicolon ;
.
Now, run your program using:
zig run hello-world.zig
Variables, Constansts, and Data Types
Variables
Variables hold data and can change value throughout the live of a program.
Constants
Like variables but they can be changed after they are initiated.
Data types
Defines the type of data that the variables hold
Primitive Types
- bool
- i8, i16, i32, i64 for signed integers
- u8, u16, u32, u64 for unsigned integers
- f32, f64, for 32 and 64 bit floating point values. Akin to doubles
Other data types
- The tuple (we saw this in the hello world example)
- Its a data structure (collection of elements)
- A compound data types
- Allows us to group values
Exporing Constants and Variables
We aren't going to disect all of this code because we are getting in too deep here so the goal is just to look at the maxIterations
and counter
in the example:
touch counter.zig
Inside counter.zig, paste the following code:
const std = @import("std");
pub fn main() void {
const maxIterations : i32 = 10;
var counter: i32 = 0;
while(counter < maxIterations) {
std.debug.print("Iteration {d}\n", .{counter});
counter += 1; // increments the counter because there is no ++ in zig
}
}
In this example, maxIterations
will not change so it's a immutable constant or (const
) but counter
will increment by 1 so its a mutable variable (var
).
When possible, use const
because it allows for morre efficient programs.
We use variables (var
) anytime where the initial value is temporary and expect the program to modify it. This is just like javascript and other languages we've learned together.
Run the code using:
zig run counter.zig
> zig run counter.zig
Iteration 0
Iteration 1
Iteration 2
Iteration 3
Iteration 4
Iteration 5
Iteration 6
Iteration 7
Iteration 8
Iteration 9
Signed Vs Unsigned Integers
What does an unsigned integer represent? Well it cannot represent negative numbers but it can represent a larger whole positive number. While it goes outside of the scope of this guide, when you store values in memory, it uses one of the bits if its assigned values to determine whether the value is negative or positive. So, if we declare it explicitly as unsigned, meaning we don't need to store a signed symbol, then we save a bit of memory and can double the amount of values we can store.
touch datatypes.zig
const std = @import("std");
pub fn main() void {
var myInt: i32 = 123;
const myUnsignedInt: u64 = 12345678912345;
std.debug.print("Value of myInt: {}\n", .{myInt});
std.debug.print("Value of myUnsignedInt: {}\n", .{myUnsignedInt});
myInt = 100; // reassigned
std.debug.print("Value of myInt now: {}\n", .{myInt});
}
Something you notice here is the lack of a {d}
in the print statements, this is because zig can infer at compile time the type, unironically called compile time type inference.
zig run datatypes.zig
> zig run datatypes.zig
Value of myInt: 123
Value of myUnsignedInt: 12345678912345
Value of myInt now: 100
Tuples
touch tuples.zig
const std = @import("std");
pub fn main() void {
const myTuple = .{ 42, "hello", 3.14 };
const intValue: i32 = myTuple[0];
const stringValue: []const u8 = myTuple[1]; // What is this thing?!
const floatValue: f64 = myTuple[2];
std.debug.print("My intValue: {d}\n", .{intValue});
std.debug.print("myStringValue: {s}\n", .{stringValue});
std.debug.print("floatValue: {d:.2}\n", .{floatValue});
// alternatively we can write it out like this:
std.debug.print("Integer: {d}, String: {s}, Float: {d:.2}\n", .{ intValue, stringValue, floatValue });
}
What you can see here - const stringValue: []const u8 = myTuple[1];
is that we are saying the output of the string is going to be an array of characters so the syntax for that is []const u8
.
The format specifiers {d}
, {s}
, {d:.2}
are used to specify a format for the interpolation of the tuple into the spot for the {}
. {d}
is for "decimal value", {s}
is for string, and the {d:.2}
specifies this is a decimal value with 2 decimal places.
> zig run tuples.zig
My intValue: 42
myStringValue: hello
floatValue: 3.14
Integer: 42, String: hello, Float: 3.14
Conclusion:
While we haven't talked about all the data types or what a loop is, we have discussed a hello world application and developed a counter that iterates from 0-9. We also learned about tuples and signed vs unsigned integers.
In my next article, we will discuss more on data types in Zig and control of flow statements.
Drew is a seasoned DevOps Engineer with a rich background that spans multiple industries and technologies. With foundational training as a Nuclear Engineer in the US Navy, Drew brings a meticulous approach to operational efficiency and reliability. His expertise lies in cloud migration strategies, CI/CD automation, and Kubernetes orchestration. Known for a keen focus on facts and correctness, Drew is proficient in a range of programming languages including Bash and JavaScript. His diverse experiences, from serving in the military to working in the corporate world, have equipped him with a comprehensive worldview and a knack for creative problem-solving. Drew advocates for streamlined, fact-based approaches in both code and business, making him a reliable authority in the tech industry.