Zig: The Modern Alternative to C
infoworld.com
infoworld.com
- Arrays and slices. A slice is a pointer and a length. There's subscript checking on slices. You'd expect that the preferred operation would be to take a slice from a slice, but the documentation does not mention that option. This may be a documentation error. There is a ".." operator for specifying bounds within a slice, but the examples given only show it being used to extract a fixed-length array. Slices are important. They let you do most of the things done with pointer arithmetic in C, usually badly.
- No objects, just structs. Structs are available in "packed" mode (no filler bits), something rarely seen since Pascal. There are discriminated variant types (Rust calls those enums) and undiscriminated variants (like C unions.)
- Strings are unchecked UTF-8, which is probably OK.
- "Sentinel terminated arrays" are a generalization of null-terminated strings. Strange, but perhaps harmless. The "char *argv[]" array in C main programs is a null-terminated array of null-terminated strings. You don't really need "argc". That's one of the last remnants of that idea, and it doesn't seem worth reviving.
It's reasonable enough, but it's maybe the fifth try at a better C.
C doesn't have 'objects' either, just structs.
In Zig you can add functions to structs though, and you have UFCS syntax sugar, both together is pretty close to class methods ;)
Those sentinel terminated arrays are a killer feature for C API interop, since you don't need to allocate separate C strings on the stack or heap just for passing string literals into the C API (Zig string literals are such sentinel-terminated arrays, so they are both slices and zero-terminated for C string compatibility). I really wish Rust would use the same idea, it would simplify creating C API wrappers a lot.
One of my favourite features of Zig is actually the arbitrary width integers, C23 is getting those too though.
...and in general Zig is fixing all the 'sloppy parts' of C, for instance it's very rigorous about not allowing implicit conversions which would lose data (e.g. trying to assign an u8 to an i8 is an error, u7 to i8 is allowed though, because no data would be lost).
PS: also how could I forget about the comptime features, this is for instance an area which is ignored by many other attempts of creating a better C, but one of the core features of Zig.
> There is no malloc keyword like in C/C++.
https://en.cppreference.com/w/c/memory/malloc
and a standard library function in C++
https://en.cppreference.com/w/cpp/memory/c/malloc
?
(It is conjuring things from "nowhere" in the higher level language, obviously normal functions aren't supposed to just make objects, only transform them - but this can, so that's magic)
You can point linker to a different implementation for all it cares.
Just being pedantic, nothing to see here.
If you don't want to do any of this, you can't achieve the performance from high level software (where "high level" here means C rather than machine code) that people have come to expect and demand.
Is not a problem of interpretation. It is just that the C standard uses the term object for just a bunch of memory (with a type) and the C++ standard does so too. Nothing to do with objects in the OOP sense.
By the way, the document you linked to is not the standard. The published standards are not freely available, but here is the relevant part of current draft: https://eel.is/c++draft/intro.object
Isn't that just "a variable"?
int * int_ptr = malloc(sizeof(int));
int_ptr is a variable (and also an object), the pointed to memory is an object but not a variable.Zero terminated strings suck, you need to do allocation everywhere to work with them, or else you must in-place mutate the strings in hard to reason about ways and they can't fully represent arbitrary text. So I definitely wouldn't want Rust's &str string references to have either behaviour.
If what you do is interact closely with C wrappers, Rust of course provides a type for this: std::ffi::CString and that's fine, but it is a miserable structure to use for actual string work.
A programming language which is supposed to interact with those APIs directly really shouldn't make this harder than it needs to be, and Zig's sentinel-terminated arrays are a pretty clever solution to the problem (even though they still need a temporary allocation to convert a regular slice to a zero-terminated slice for "dynamically built strings", but one can get surprisingly far with just string literals).
C libraries are going to mostly be like that because it's how C works. If that's our high bar we're never getting anywhere.
Operating systems don't use many strings -- strings are awful because they're variable size and need parsing. Filenames are the most obvious place the kernel needs strings, and here the 0 terminating string is pretty horrible actually, the kernel can't assume the sentinel is present as that's obviously a massive security hole, so we end up not benefiting from the use of the sentinel, it's there because of C compatibility, as that wanes in significance there's no reason it needs to remain.
On most operating systems the kernel system call ABI is not stable, which means you aren't supposed to rely on this stuff anyway, and once you're not talking directly to the kernel ABI you're free to choose abstractions which better suit both sides.
Apart from that, zero-terminated strings aren't strictly bad. They offer an in-band signal that is sometimes very practical. Slices are more generally flexible, especially w.r.t substrings, but when you look at a memory dump for example it's great to have those terminators.
> once you're not talking directly to the kernel ABI you're free to choose abstractions which better suit both sides.
Requiring string copies just to reshape arguments according to idealistic beliefs is a huge yak shave. It means creating all those wrappers, handling all those extra failure modes, and coming up with extra versions of records (often on-disk structures) that have fixed size zero-terminated string buffers embedded.
The on-disk fixed size structure stuff is actually nicer in a language that favours slices, because with C-strings we're incurring a special case, what happens when the sub-structure is full? With slices that's just fine, but with zero termination that's not a valid string (because the sentinel is missing) and so you have to decide what to do about that.
No, you need to do what needs to be done according to the on-disk format. The disk doesn't care what you think is the ideal string representation. If the available space is used up, it is used up. That's just the semantics that come with the physical reality of existing systems where you can't just malloc an extra space on the heap. Having an internal representation that matches what's on the disk is the right call, everything else is only layering complexity.
Also, you're making it look like it was hard to use, but those semantics are very easy to program against, using something like snprintf(buffer, sizeof buffer, "%s", ...), or just do it manually.
// Assuming buffer is a mut [u8] and text is the text value we'd like to write into it
// if we've got some string, just let text = string.as_bytes();
if text.len() > buffer.len() {
// As with snprintf we need to decide what to do if it won't all fit
// ... for example let's just truncate it
let truncated = &text[..buffer.len()];
buffer.copy_from_slice(truncated);
} else {
let (left, _) = buffer.split_at_mut(text.len());
left.copy_from_slice(&text);
}
As I thought I'd explained, the yak shaving problems for your C-style strings are on the other side, reading this structure. With pointer + length strings this is no trouble but with a 0-terminated string you have to conjure that sentinel from somewhere, maybe allocating in the process. Slower and uglier.In addition, Zig, while about as simple as C, has the same expressiveness as C++, not to mention its exceptional build capabilities.
I don't know if all that would prove sufficient to one day replace C and/or C++ -- replacing incumbents is always an uphill battle -- but Zig is the most revolutionary (rather than evolutionary) low-level language we've seen in a very, very long time. It offers a completely different approach to how low-level programs are to be written and built.
Because Zig is so simple, how revolutionary it is can be missed when you look at one feature at a time, and can only be appreciated once you consider the whole: The mix of simplicity and power, and the eye toward tooling.
That's the real problem. It's a mixture of safe and unsafe features, with no clear boundary. C++ is like that. The trouble is, people keep using raw pointers and mess up. Read CERT advisories. Most of them come from that class of error.
This is the same problem all the attempts to fix up C have hit.
Just say no to unsafe code.
The problem with eliminating unsafe code altogether is that doing so has a cost that, in some situations, is not worth it. Sometimes it's a runtime code in memory footprint or CPU resources; sometimes it's a cost in code complexity or development speed, which may have an adverse impact on correctness, which is the real thing we're interested in.
Finding the sweet spots among the various options can only be done empirically, and because Zig is so different (it's as different from C as it is from C++ and as different from either as it is from Rust) there's nothing we can extrapolate from. Because it is its very particular combination of features (and lack thereof) that is its message, those who look at a particular aspect and say, oh, this feature is like language X, therefore Zig will have the characteristics of X, completely miss what Zig does. It tries to strike a particular balance between the compiler, the human reader, and the tooling and development experience that can only be considered as a whole.
I have no idea if Zig is "good" or if it will be successful -- only time will tell -- but I find the language so refreshing and fascinating because it is a radical departure from everything we've seen in that space in quite a long time. There is no language with a similar mix of features (and lack of features).
An interesting point, especially at API boundaries.
C and Zig not having information hiding is to some extent a feature. C++ is notorious for having information hiding without safety. Almost no other language has that combination.
Although I only have a beginner knowledge of the language, it seems to me that Zig is just on the edge of being able to provide a nice unification of the C opaque-pointer idiom with other opaque handles (e.g. POSIX file descriptors, GL handles).
BTW, how did you reach the conclusion there isn't "all that much new C being written"?
I fully agree that what matters is data but I stumbled over your remark that control flow and syntax mightn't be an issue. In fact I'd go so far as to say that above a certain level of abstraction it's all about control flow and syntax. But then Zig doesn't seem to be addressing such levels so perhaps you're right.
> A distinctive feature of Zig is that it does not deal with memory allocation directly in the language. There is no malloc keyword like in C/C++. Instead, access to the heap is handled explicitly in the standard library.
I guess it is all you need to know about the article quality.
It's actually a rather interesting idea, if I had the time for a C-focused side project that sounds really fun! :)
I agree that the culture is different and that is significant, but I do not see it in the notion of the language itself (without its stdlib).
Also what is idiomatic for Zig does change as it is not a mature language. Though the allocator passing is expected to remain. Odin did an interesting thing and the allocator is put in a 'context' which is an actual part of the language.
Yes, but it doesn't argue that it's not the same in C. Just that it's not the same as languages who do the memory allocation themselves.
>You could create a replacement library for C stdlib and it could also expect an allocator in the parameters if a given function could allocate.
You could do anyting in C, even write Zig in it. And vice versa, you co do malloc in Zig. But allocators are how Zig is designed/used, whereas C opts for malloc.
In any case it's not nearly as limiting as the 'red-blue' async-await color split in other languages, and I can't think of a single downside of explicitly passing allocators into stdlib modules instead of having a hidden global allocator.
The approach to always pass allocators isn't all that Zig specific either, it's also a good library design practice in pretty much all other languages which allow explicit memory management.
"I can't believe it's not C!"
> C-style for loops can almost always be replaced with iterators in a more concise manner.
Not if you want to optimise for performance.A simple example: I search through a list of n items to find item m, iterators typically don't let you break out of the iteration.
Zig is the only contender I've seen where the for loop looks iterator-like, but also supports break/continue.
That'd be a pretty crappy iterator implementation, short-circuiting is basically what iterators are all about (how else would you handle infinite iterators?). For example, in Rust, it's simply Iterator::find(): https://doc.rust-lang.org/std/iter/trait.Iterator.html#metho...
for loops in Rust are basically just syntax sugar for a while loop taking an element of the iterator each iteration, you can use break/continue as normal.
I'm curious as to which language you worked with that didn't allow this.
edit: plus has a GC if you don't want to do manual memory management, already has a package manager, good at producing native/static binaries, can do automatic type inference, user defined types, cross compiling is super easy...
> without loops
This is a false dichotomy. You can use iterators to replace three-clause loops without changing the body:
for row in 0..rows {
for col in 0..cols {
arr[row][col]
}
}
This is far easier to read than three-clause loops which repeat redundant information repeatedly and redundantly.Is this (note the enclosing braces)
{
var thing = thinger();
while(cond(thing)) : (mutate(thing)){
// do whatever
}
}
really better than this? for(var thing = thinger(); cond(thing); mutate(thing)){
// do whatever
}I’d like to love rust but it’s so big I just don’t have time to learn it all. It’s a pity rust is so damn big. Anyhow zig looks like a great alternative even if it lacks the safety of rust.
Zig has a build system, but it's integrated into the compiler and stdlib (e.g. there's usually a 'build.zig' file in the project root which is regular Zig code using 'build system' modules from stdlib, this build.zig is then transparently compiled and run to 'perform the build').
(it's interesting that nothing of this is so special to Zig that other compilers - even C compilers - couldn't use the same approach)
It's trying to make a new systems programming ecosystem, sort of, but also trying to interop well with C source code but not at the tooling level... not exactly a coherent adoption curve.
That said, using Zig as the build tool seems to bring trivial cross-compilation support and portability, without having to deal with linkers [2]. Cross-compilation has always been and remains a PITA, so that's pretty compelling.
[1] https://andrewkelley.me/post/zig-cc-powerful-drop-in-replace...
[2] https://zig.news/kristoff/compile-a-c-c-project-with-zig-368...
https://github.com/floooh/sokol-tools/blob/master/build.zig
The basic idea is the same as in all other build systems: you describe your build as number of build steps and their dependencies. There's really no reason why this build description should happen in a separate build tool and language.
Another approach would be to integrate your zig code as a static library, and have your existing build system call out to zig’s build system just for that part of the build. Linking it to the rest of your code could then be performed by your existing build system, just as it would link any other library.
In practice, Zig is quite flexible.
_ = unused_variable;
...statements (although I'm starting to prefer Typescript's convention to mark unused variables and function parameters with a leading underscore to suppress unused errors).In any case, once I started to write more TS code where the TS compiler and linters are usually configured to also catch any unused variables as "errors" I no longer see Zig's stance on the same thing as critical as before, it's more or less "just" a tooling issue.
And it is not even hard to solve, just add a “production” profile where it is an error for all I care, and a “debug” one where it really shouldn’t be linting my code unnecessarily.
Rust's linter, clippy, provides a rich seam of stuff that definitely shouldn't be fatal but some / many people would value knowing about. For example yesterday I built a Range which intentionally is the wrong way up, isize::MAX..isize::MIN - and so clippy says well, that's probably not what you wanted. This is a good lint. If I ever do the same thing by mistake I definitely want to know about it, but this time was not a mistake, so I wrote an allow attribute on that Range to silence the linter and also flag to readers, "No, this is on purpose".
There can be a benefit to a singular coherent vision behind something - committees can produce languages which don't do anything well by compromising everywhere - but one of the negatives of visionaries is that they can stubbornly stick to their preferences overriding any other consideration. Andrew is quite sure he's right, nothing will overcome that.
It's OK, C++ manages to suffer from afflictions of a single coherent vision (Bjarne Stroustrup's) and having an unwieldy committee structure (WG21 the "C++ Standards Committee") and despite the resulting flaws it was enormously successful.
I honestly can't think of one.
IDEs just gray out the unused variables and that is an 1000x better way of handling this issue.
Extremely common bug 1: a function returns an error/future, but you don’t check/poll it “foo();”. May happen because you copy some tutorial code that’s not rigid about error checking, or you don’t realize that this language doesn’t have futures that run without being polled.
Quite common bug 2: you store the error/future, but don’t check/poll it “let a = foo();”. May happen when moving code, certain branches, copy paste error, not enough coffee.
Opt-in: it doesn’t matter if the call succeeded or the future is polled, leave me alone “let _ = foo()”. May happen in test code for example. This is not the common case, so the opt-in annoyance is justified to improve the common case.
Without recursively commenting out any further variable that have also become unused by my action (which I hope we can all agree is extreme tedious and error prone), I am left with no choice but assigning it to _, which as you mentioned already have a normal usage, making it later very hard to discern which is just a temporarily unused variable, or a deliberately ignored one. The very “feature” can cause bugs, besides being annoying as hell.
With the usual warnings approach, you can rely on compiler/IDE/pre-commit tooling to find unused variables - they'll all nag you until it's fixed. With Zig's autofix approach, those problems are immediately silenced and you don't have any help from the compiler or tooling to find unused variables. It's quite an ironic outcome if you think about it.
An unused variable means you weren't passing it on anywhere so there is no code which depends on its value, so how can it be a bug?
future = x.do_async(); return;
should not error out because of 'unused variable', it should give a error message concerning the lifetime of the future object.
value1 <- f()
value2 <- f()
g(value1)
g(value1)
or other similar cases
public void processTransaction() {
string state = "start";
getCardDetails();
state = "serialize";
serializeCardData();
state = "send data";
sendData();
state = "check return code";
bool success = checkReturnCode();
if (!success) {
state = "failed";
doFailThing();
return;
}
state = "store transaction record";
storeTheThing();
state = "complete";
}
First, this code is a simple example, in reality, the code has bunch of branching and doesn't actually call out to functions to do things like "getCardDetails," so just replace that function in the example above with some parsing logic of a string to parse Track1 and Track2 data. The equivalent method we actually have in our code base to do that is ~1500 lines of code. But that string is doing nothing that a comment couldn't accomplish. Or more importantly, what the code could describe itself if it actually adhered to good design principles. Ideally, I would refactor this, but the owner of the company is adament on keeping this 1500 line abomination untouched.For me, I often unused variables in production code are usually filling the void a comment or good design would have filled.
And yes, lots of people who don't like Zig's choice agree unused variables are bad and shouldn't survive into your release code. They just don't agree with Andrew that it's a fatal error and the program shouldn't build.
Zig regards forgetting to assign a variable to a return value as a compile time error because there's an entire class of bugs that stem from it, e.g. resource leaks.
I also ran into an issue where I wanted to initialize an array with 30k 0 values and I ended up having to write a loop to do it, and ran into issues with the loop and assigning values. It felt like if the language was going to be militant about certain things to prevent you from making mistakes, that it should just go the whole way. At least with Rust after fighting with the borrow checker I would know I had a safe program. With Zig I have to fight with the langauge, but then it's not even memory safe once I get everything working
var arr: [30_000]u8 = undefined;
for (&arr) |*x| x.* = 0; var arr = std.mem.zeroes([30_000]u8);
P.S: Not a loop. The implementation of std.mem.zeroes for an non-sentineled array is: return [_]info.child{zeroes(info.child)} ** info.len;
Which in this case will be equivalent to: return [_]u8{0} ** 30_000;
Which uses the comptime-only `*` operator to expand the single-item u8 array with value 0 into an array of 30k 0s.Your video card driver on the other hand doesn’t have too much “business logic”, but really cares about all those pesky details.
You then use those primitives to implement parallelism however you want. Just like you'd do in C. Or, ykno, someone makes a library that does green threads the way you want using those primitives.
1. Coroutines have dynamic stacks, meaning that you can start a lot of threads without exhausting your memory: https://medium.com/a-journey-with-go/go-how-does-the-gorouti...
2. Go has a lightweight threading model that doesn't map one thread to one OS/kernel level thread. Enabling you to have thousands of threads operating at once. limited by GOMAXPROCS.
I was just wondering because the article mentioned Zig having a lot of the benefits of Rust and Go, and this is one thing where Rust doesn't shine, so I was wondering if they had an answer for this.
There is one event loop in the stdlib, but it has some limitations and there is some discussion to change it or remove it https://github.com/ziglang/zig/issues/8224
Some Zig libraries implementing an event loop:
- libxev by Mitchell Hashimoto https://github.com/mitchellh/libxev
- one from the Tigerbeetle DB https://github.com/tigerbeetledb/tigerbeetle/tree/main/src/i...
They both use kernel concurrency features (io_uring/epool on Linux, kqueue on MacOS). They are quite low level and do not implement a lightweight threading model comparable to Go concurrency.
IMO the key point of Go's concurrency are channels, and those have already been implemented in Zig, with comparable semantics (eg blocking when the channel is full).
Here is how I use a channel (defined a few lines above) to implement the main loop of an interactive program I wrote:
https://github.com/kristoff-it/bork/blob/master/src/main.zig...
Long answer: Zig does a lot of stuff to make the easy way of dealing with memory the correct way. The typical pattern is to use defer/errdefer statements to clean up on end of scope or in case of error. One of the included allocators checks for leaks and use after free, and it is trivial to change which allocator is used based on build mode. It is also very strict about pointers and has a whole lot of different kinds to represent different use cases and include appropriate safety checks (in safe and debug modes) when dealing with them.
Otherwise why don’t build it for C instead?
For example, for pointers, assume allocator interface create, alloc, destroy, and free functions work as advertised, and trust them. Anything that uses the pointers is checked. For file descriptors, assume open and close are trusted, everything in between is checked.
You can't really build it so easily for c because c language is not generally compiled to an IR, and the syntax is rather ill-defined. It's not context-free and it has lexical macros.
Anyways statically checked annotated c exists, SEL4 is a good example, but iirc it actually checks arm machine code, and yes it is memory safe C.
This is about what we can claim about runtime properties. What exactly do you mean by a checked pointer? A pointer has temporal and spatial boundaries — will you add a huge amount of metadata to each pointer to runtime validate whether it’s correctly used? That’s pretty much what valgrind and sanitizers do, but that has quite an overhead. In general you can’t check it for any kind of pointer usage. Rust as I mentioned can get away with it by heavily restricting how pointers can be used. Unchecked is an escape hatch, you can’t just put the “checked” boundary at any place you wish, it has to be placed in a way that makes analysis of its assumed properties sound. In case of Rust this is true, but you would have to restrict Zig’s semantics to something like Rust’s to make your idea workable.
That's exactly the point. You would be restricting zig's semantics within code that you've fenced. You claim it's not possible. Trivially it should be possible, because this is simply the equivalent of abstracting rusts compiler logic (and in the worst case scenario even it's type system, via annotations) to a sidecar step that is run at a different phase of building your code. Or, more generically, formalizing logic that a code reviewer is doing in their head to understand data lifecycles. Really, to support your impossibility claim: It's on you to give an example of zig code that wouldn't be analyzable.
Also surely you can write some zig to rust compiler (which mind you, will not compile most of your programs as the equivalent rust is not semantically correct), but there is not much point.
Re code:
let ptr = allocate();
if (undecideableProperty()) {
destroy ptr;
}
print(*ptr);
Insert any number of undecidable property there, one elegant example would be the Goldbach conjecture.I don't understand why would anybody say that, it's seems such a simple concept to me that nothing ever will replace C. Think about just the Linux kernel, nothing else. Nobody will and can rewrite every part of it that a C compiler will not be needed.
(I think there's less than a ten percent chance of this happening)
const std = @import("std");
pub fn main() !void {
const stdout = std.io.getStdOut().writer();
try stdout.print("Hello, {s}!\n", .{"world"});
}
To me, this looks worse than anything I have seen before. const print = @import("std").debug.print;
pub fn main() void {
print("Hello {s}!", .{"World"});
}
Or using the logging API: const info = @import("std").log.info;
pub fn main() void {
info("Hello {s}!", .{"World"});
}
(I'm actually not sure why Zig doesn't have a simple way to print to stdout, but I guess there are 'reasons')Two concerns to remain for me:
* version 1.0 release
* bus factor, the major contributor seems still mainly a single person
I hope it can take off soon.Sure it allows you to move fast but at what cost
Whether it's going to become a good alternative for Go will depend mainly on the stdlib I think. What's clear is that the Zig stdlib will be much richer than C's or C++'s, but whether it will be a good match for Go's stdlib for backend work remains to be seen.
Go lets you write C in your Go files, and reference it with a psuedo-package, "C". So you can import C headers, but Go itself doesn't come with a C compiler toolchain, and ends up relying on your system's C compiler, and thus you lose Go's cross-compilation, unless you have the headers for your target platform. I've seen some have been using "zig cc" as their C compiler with Go, given it makes cross-compilation easy.
Zig has manual memory management, doesn't have much of a runtime (no CSP out of the box) and above all is not mature yet (breaking changes do happen). As of now it has one of the best stories for interfacing with C as it can directly use C headers.
TCL is an elegant scripting language, very LISP-y, where most data structures are lists of strings.
Go on the other hand is more like a modern mix of C and Pascal with GC and lots of convenient libraries, and which builds statically.
The performance of TCL and Go are also a world apart.
- gaming
- web apps
- networking
- CLIs
- distributed systems
- crypto
- systems programming (like for example databases)
- language tooling (for example JS ecosystem has a lot of Rust based tools being developed right now)
and probably much more.
Go is used for every single of these categories too. Databases? Check. Networking? Check. CLIs? Check. And so on, and so on. The truth is that it's often a matter of taste, timing and various constraints that are often non technical at all.
Taste is the most subjective reason, nevertheless it's often a driving choice. If I start a new personal project I usually default to Rust these days, unless it's a bad fit, but I've successfully used Rust in many different domains, so it's safe to say Rust covers most of my programming needs (other than maybe a quick throwaway script). Even if Go was a sligtly better fit, I would choose Rust anyway, cause I just enjoy it more.
When it comes to constraints, I think that is most often relevant in a professional setting. I've worked in many different companies and very rarely, especially when a company is on the bigger side, you can just choose whatever tools you want just based on technical requirements. If a company is a Ruby shop, they will have to have a very strong reason to start a new project in a different language, even if it is clearly a better choice. But then a good question is: what makes a tool a better choice? It will very often be really hard to tell until you actually do the project. Have you ever been in a discussion about choosing a new language for a project? I have been, lots of times. One time was especially interesting, cause we were discussing what to use for a real time application keeping a long running websocket connections. We were considering Ruby (it was a Ruby shop), Go, Elixir and Rust. We chose Rust in the end, but I'm sure that we could have been successful with Elixir or Go too. The most interesting part? There was an experienced developer that was pushing for Ruby hard. He was working as a "Principal Engineer" and if it was up to him I'm sure he would have chosen Ruby, which for me was the least feasible choice. Would it be a good choice? I don't think so, but I can't say for certain and we will probably never know. So as you can see, even with very clear requirements people will disagree on "the best tool for the job" (which is also why I hate this term). But even if everyone agrees on a technical aspect, there might be also other constraints. If a company is growing very fast they might be worried about hiring for example. And they may choose a language that is technically a bit worse than some alternatives, but has a bigger talent pool.
Timing is a tricky one, cause at the moment both languages are quite mature, but if you think about it, a lot of well known big projects in Go (k8s, docker, terraform etc) were started when Rust was honestly quite rough. So while I'm not saying if k8s was started today, it would be definitely started in Rust, I think that at least some of those projects might consider Rust. The same goes for companies that are invested in Go already. Even if Rust made sense for some of the new projects they would probably lean heavily towards Go, cause of a collective company experience.
So, yes, Rust is an alternative to Go in many (most?) domains. In the same way Zig can be in this place too when it gets more tooling and maturity.
As you said it all depends on the circumstances. But I don't really see Zig competing with Go. They both can do mostly the same things, but they both approach them from quite a different sides.
For example bash is being used in:
- gaming (https://github.com/JosefZIla/bash2048)
- web apps (https://github.com/avleen/bashttpd)
- networking
- CLIs
- distributed systems (https://github.com/frameable/aviary.sh)
- crypto (https://armedia.com/blog/blockchain-program-written-bash/ https://github.com/grondilu/bitcoin-bash-tools)
- systems programming (https://github.com/damphat/kv-bash)
- language tooling
Some of those make more sense than others. However we all talk about a mythical general case. For every language there are niches that are covered by it more significantly. For Go it would probably be web backend. It doesn't mean it is only suited to this one niche, it is used in everything. In general it is used there more. I don't believe that Rust sees the most use in the same niche to the same order that Go sees it.
Is Rust or Zig an alternative to php, awk or Lisp or vice versa? In practice I don't really think so.
I guess it all depends on one's definition of "alternative". I don't think that a statistical Go programmer would see Zig as a real alternative. Statistical C programmer might see it as a Go alternative, but that probably would not be a question he would ask.
Of course you have more to handle by yourself in Zig. For example a simple string concatenation will need more work as there is no implicit heap allocation.
For interoperability with Go it's quite the same than C<>Go through Cgo: doable but not ideal.
However the Zig toolchain can be used to improve the build system of Go projects with Cgo dependancies. That's what Uber is doing https://jakstys.lt/2022/how-uber-uses-zig/
This is really the meat of it. They should be using a different language but struggle with two better choices.
That said, I do all my projects in it these days if I can get away with it because I like it so much and it is reliable enough for me... well, except for the fact that the current master doesn't actually implement suspend/resume yet and I use that for coroutines.
# Writing it like this is bad:
print (length (drop_failed (get_students (school_system))));
# But like this is okay?
school_system.get_students().drop_failed().length().print();Weird.
Additionally, in Zig temporaries (which you would be implicitly constructing by chaining) are immutable, meaning that a chain of calls would only compile as long as it doesn't try to mutate any intermediate value (which is a nice protection against footguns).
But really, this is a coding style that's considered fine in Rust, not in Zig.
And still compare it to C?