Zig vs Go: generics
Source: Dev.to
Introduction
Go introduced generics in version 1.18, allowing functions and structs to be parameterized by type. Zig has long supported compile‑time generics via the comptime keyword, enabling zero‑runtime overhead.
Function generics
Go
func doubleNumber[T constraints.Integer | constraints.Float](a T) T {
return a * T(2)
}
doubledFloat := doubleNumber[float32](5.3)
fmt.Println(doubledFloat)
// type parameter inferred
doubledInteger := doubleNumber(5)
fmt.Println(doubledInteger)
Zig
fn doubleNumber(comptime T: type, a: T) T {
return a * 2;
}
const doubledFloat = doubleNumber(f32, 5.3);
std.debug.print("{}\n", .{doubledFloat});
const doubledInteger = doubleNumber(i32, 5);
std.debug.print("{}\n", .{doubledInteger});
In Go, the generic is constrained by interfaces, which the compiler checks at compile time. Zig evaluates any errors during the compile‑time phase when it instantiates the function for the supplied type.
Struct generics
Go
type Stack[T any, Q any] struct {
Items []T
AlterItems []Q
Index int
}
intStack := Stack[int8, float32]{
Items: []int8{1, 2, 3, 4, 5},
AlterItems: []float32{1.1, 2.1, 3.1, 4.7, 5.3},
Index: 4,
}
fmt.Println(intStack.Items[intStack.Index], intStack.AlterItems[intStack.Index])
strStack := Stack[string, bool]{
Items: []string{"hello", "gophers"},
AlterItems: []bool{false, true},
Index: 0,
}
fmt.Println(strStack.Items[strStack.Index], strStack.AlterItems[strStack.Index])
Zig
fn Stack(comptime T: type, comptime Q: type) type {
return struct {
items: []const T,
alterItems: []const Q,
index: usize,
};
}
const intArr = [_]i32{ 1, 2, 3, 4, 5 };
const floatArr = [_]f32{ 1.1, 2.1, 3.1, 4.7, 5.3 };
const intStack = Stack(i32, f32){
.items = intArr[0..],
.alterItems = floatArr[0..],
.index = 4,
};
std.debug.print("{d} {d}\n", .{
intStack.items[intStack.index],
intStack.alterItems[intStack.index],
});
const strArr = [_][]const u8{ "hello", "ziguanas" };
const boolArr = [_]bool{ false, true };
const strStack = Stack([]const u8, bool){
.items = &strArr,
.alterItems = &boolArr,
.index = 1,
};
std.debug.print("{s} {}\n", .{
strStack.items[strStack.index],
strStack.alterItems[strStack.index],
});
Constraints vs. interfaces
In Go, constraints are expressed as interfaces. When a generic function accepts a struct, the struct must implement the methods used inside the function.
type IdTrackable interface {
GetID() string
}
type Article struct {
ID string
Name string
Category string
}
func (a Article) GetID() string { return a.ID }
type SKU struct {
ID string
ArticleID string
Available bool
}
func (s SKU) GetID() string { return s.ID }
func findById[T IdTrackable](items []T, id string) (*T, error) {
for _, item := range items {
if item.GetID() == id {
return &item, nil
}
}
return nil, errors.New("not found")
}
Example: Find by ID
Go usage
articles := []Article{
{ID: "a1", Name: "Laptop", Category: "Electronics"},
{ID: "a2", Name: "Book", Category: "Education"},
}
article, err := findById(articles, "a1")
if err == nil {
fmt.Printf("Found article: %+v\n", *article)
}
Zig implementation
const Article = struct {
id: []const u8,
name: []const u8,
category: []const u8,
};
const SKU = struct {
id: []const u8,
articleId: []const u8,
available: bool,
};
fn findById(comptime T: type, items: []const T, idx: []const u8) !T {
for (items) |item| {
if (std.mem.eql(u8, @field(item, "id"), idx)) {
return item;
}
}
return error.NotFound;
}
const articles = [_]Article{
.{ .id = "a1", .name = "Laptop", .category = "Electronics" },
.{ .id = "a2", .name = "Book", .category = "Education" },
};
const art = try findById(Article, &articles, "a2");
std.debug.print("Found article: {}\n", .{art});
In Zig, the generic function is instantiated at compile time for each distinct type, producing separate specialized versions without runtime cost.
Conclusion
Zig’s compile‑time metaprogramming offers a powerful, zero‑overhead approach to generics, while Go’s generics rely on runtime‑free type parameters constrained by interfaces. Both languages enable type‑safe, reusable code, but Zig provides broader metaprogramming capabilities that go beyond what Go currently supports.