DEV Community

Cover image for Zig vs Go: structs
Paolo Carraro
Paolo Carraro

Posted on

Zig vs Go: structs

(you can find previous post about same topic here)

The organization of data models through structs is not very different between Zig and Go, as both don't follow a primarily OOP approach with classes, constructors, declared interface implementations, and methods. However, both provide ways to define functions that become methods available for all entities typed by the struct.

Defining a struct

Let's look at an example of defining a model:

// Go
// Exported due to capitalized first letter
type Synth struct {
    manufacturer string // private
    name         string // private
    IsAnalog     bool // public
    ControlsNum  int8 // public
}
Enter fullscreen mode Exit fullscreen mode
// Zig
// Exported due to pub keyword
pub const Synth = struct {
    // all field are public
    manufacturer: []const u8,
    name: []const u8,
    is_analog: bool,
    controls_num: u8,
}
Enter fullscreen mode Exit fullscreen mode

The first difference lies in the definition of what is public and what is not: in Zig, if the struct is in a different module, it must be marked as pub, while in Go it must have a capital letter as its first letter. As for struct fields, in Zig they are always all public.

Instantiating a struct

To instantiate a struct, in both languages, it is idiomatic to have a function that returns the struct, which can be initialized with any values passed to the function itself. Normally in Go a pointer is returned since the entity's lifecycle is managed by the garbage collector, while in Zig the value is returned since memory allocation will be handled by the caller.

// Go idiomatic constructor
func NewSynth(manufacturer string, name string, isAnalog bool, controlsNum int8) *Synth {
    return &Synth{
        manufacturer: manufacturer,
        name:         name,
        IsAnalog:     isAnalog,
        ControlsNum:  controlsNum,
    }

Enter fullscreen mode Exit fullscreen mode
// Zig idiomatic constructor
pub const Synth = struct {
// ... omissis field definitions see above
    pub fn init(manufacturer: []const u8, name: []const u8, is_analog: bool, controls_num: u8) Synth {
        return .{
            .manufacturer = manufacturer,
            .name = name,
            .is_analog = is_analog,
            .controls_num = controls_num,
        };
    }
}
Enter fullscreen mode Exit fullscreen mode

The use of dot notation .{} to instantiate the struct indicates that the type will be inferred from context, in this case from the return type specified by the function. Already in this second example, we can notice that in Zig, functions belonging to the struct type are defined inside it, and unlike struct fields, functions that you want to expose must also be preceded by the pub keyword.

Defining methods

Let's now define three methods with different access types and returns.

// Go struct methods 

// Private method returning a single value
func (s *Synth) getCompleteName() string {
    return fmt.Sprintf("%s %s", s.manufacturer, s.name)
}

// Public method with no return value
func (s *Synth) Play() {
    fmt.Printf("%s is playing!\n", s.getCompleteName())
}

// Public method returning multiple values
func (s *Synth) Info() (int8, bool) {
    return s.ControlsNum, s.IsAnalog
}
Enter fullscreen mode Exit fullscreen mode
// Zig struct methods 
pub const Synth = struct {
// ... omissis fields and init definitions 
    // Private method returning either an error or a value
    fn getCompleteName(self: Synth, destination_buffer: []u8) ![]u8 {
        return try fmt.bufPrint(destination_buffer, "{s} {s}", .{ self.manufacturer, self.name });
    }

    // Public method that may return an error
    pub fn play(self: Synth) !void {
        var db: [128]u8 = undefined;
        const cn = try self.getCompleteName(&db);
        debug.print("{s} is playing!\n", .{cn});
    }

    // Public method returning multiple values
    pub fn info(self: Synth) struct { u8, bool } {
        return .{ self.controls_num, self.is_analog };
    }
}
Enter fullscreen mode Exit fullscreen mode

We notice that in Zig, the first parameter is a reference to the struct instance itself: this is how the receiver is defined. In Go, instead, it is placed in parentheses before the function. In some cases, the return type includes the ! which in Zig is called an error union type: it declares that the caller must handle a possible error value in addition to the expected value. In case of error, this will be returned directly by the try instruction, captured from the return of fmt.bufPrint.

Constant parameters and pointer receivers

In Go, the entity's pointer is passed as the receiver so that, if modifications need to be made to the struct's values, they act on the struct itself and not on a copy. In Zig, parameters passed to a function are always constants, therefore if we want to modify a value we must pass the pointer as a constant and then treat it as such inside the function.

// Go
func (s *Synth) ChangeName(name string) {
    s.name = name
}
Enter fullscreen mode Exit fullscreen mode
// Zig
pub fn changeName(self: *Synth, name: []const u8) void {
    self.name = name;
}
Enter fullscreen mode Exit fullscreen mode

If we wanted to pass a parameter as a pointer for optimization reasons while also wanting to make it immutable, we can annotate it as *const Synth.

Top comments (0)