Zig vs Go: generics

Published: (February 9, 2026 at 06:07 PM EST)
4 min read
Source: Dev.to

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.

0 views
Back to Blog

Related posts

Read more »

Lessons from Zig

The Zig programming language maintains an intentionally small standard library. Components that do not meet strict inclusion criteria are removed and relocated...

Go 1.26 is released

Today the Go team is pleased to release Go 1.26. You can find its binary archives and installers on the download pagehttps://go.dev/dl/. Language changes Go 1.2...

O que são generics?

Generics são uma funcionalidade introduzida no Java 5 que permite criar classes, interfaces e métodos que trabalham com diferentes tipos de dados. Eles eliminam...