DEV Community

Cover image for Learning Go as a Ruby Developer #6: Living Without Classes
Shrouk Abozeid
Shrouk Abozeid

Posted on AI-assisted

Learning Go as a Ruby Developer #6: Living Without Classes

One of the questions I had while learning Go was:

If Go doesn't have classes, where do I put my data?

In Ruby, I'm used to creating a class and defining its attributes and behavior there:

class User
  attr_accessor :name, :age

  def initialize(name, age)
    @name = name
    @age = age
  end
end

user = User.new("Alice", 30)
Enter fullscreen mode Exit fullscreen mode

Go doesn't have classes.

Instead, one of the building blocks it gives us is the struct.

A struct is a collection of fields that allows us to group related data together.


Structs in Go

A struct is defined using the struct keyword:

type Person struct {
    Name string
    Age  int
}
Enter fullscreen mode Exit fullscreen mode

Here we're defining a type called Person with two fields:

  • Name, which is a string
  • Age, which is an int

We can then create a value of that type.

person := Person{
    Name: "Alice",
    Age:  30,
}
Enter fullscreen mode Exit fullscreen mode

And access its fields using .:

fmt.Println(person.Name)
fmt.Println(person.Age)
Enter fullscreen mode Exit fullscreen mode

This feels quite similar to accessing attributes on a Ruby object:

person.name
person.age
Enter fullscreen mode Exit fullscreen mode

But the underlying idea is different.


Creating Struct Values

There are several ways to initialize a struct.

The first is to provide the values in the same order as the fields:

person := Person{"Alice", 30}
Enter fullscreen mode Exit fullscreen mode

We can also explicitly specify the field names:

person := Person{
    Name: "Alice",
    Age:  30,
}
Enter fullscreen mode Exit fullscreen mode

This second form is easier to understand, especially when a struct has many fields.

We can also initialize only some fields:

person := Person{
    Name: "Alice",
}
Enter fullscreen mode Exit fullscreen mode

The fields we don't specify receive their zero value.

So Age would be:

0
Enter fullscreen mode Exit fullscreen mode

We can even create an empty struct:

person := Person{}
Enter fullscreen mode Exit fullscreen mode

In that case, all fields get their respective zero values.

This connects nicely with what we saw in Part 2 when talking about zero values.


Structs and Pointers

Structs can also be used with pointers.

For example:

person := &Person{
    Name: "Alice",
    Age:  30,
}
Enter fullscreen mode Exit fullscreen mode

Now person is a pointer to a Person.

We can still access the fields directly:

fmt.Println(person.Name)
Enter fullscreen mode Exit fullscreen mode

Go allows this as a convenient shorthand for dereferencing the pointer first.

Conceptually, this:

person.Name
Enter fullscreen mode Exit fullscreen mode

is an easier way of working with:

(*person).Name
Enter fullscreen mode Exit fullscreen mode

This was another place where Go reminded me of C++.

As a Ruby developer, pointers aren't something I normally think about when working with objects.

In Go, understanding whether you're working with a value or a pointer becomes much more important.


Creating Your Own Types

Go also allows us to create a new named type based on another type.

The syntax is:

type <typename> <datatype>
Enter fullscreen mode Exit fullscreen mode

For example:

type Banana int
Enter fullscreen mode Exit fullscreen mode

Now Banana is its own type based on int.

We can declare a variable using it:

var myBanana Banana = 50
Enter fullscreen mode Exit fullscreen mode

This might look unnecessary at first.

Why create Banana if it's basically an int?

The important idea is that we're giving a type a specific meaning.

Instead of having an application full of anonymous int values, we can create named types that communicate what those values represent.


Ruby's Approach

Ruby is dynamically typed, so we normally don't create separate primitive-based types in the same way.

For example:

age = 30
Enter fullscreen mode Exit fullscreen mode

The variable simply references an Integer.

If we want to represent something like a person, we'd typically create a class:

class Person
  attr_accessor :name, :age
end
Enter fullscreen mode Exit fullscreen mode

Go separates these concepts.

A struct gives us a way to group fields:

type Person struct {
    Name string
    Age  int
}
Enter fullscreen mode Exit fullscreen mode

And a custom type gives us a way to define a new named type:

type Banana int
Enter fullscreen mode Exit fullscreen mode

There doesn't need to be a class involved.


Struct vs Ruby Class

This is where I initially had to change my mental model.

A Ruby class usually combines data and behavior.

class Person
  def initialize(name)
    @name = name
  end

  def say_hello
    puts "Hello #{@name}"
  end
end
Enter fullscreen mode Exit fullscreen mode

A Go struct, on the other hand, primarily describes the data:

type Person struct {
    Name string
}
Enter fullscreen mode Exit fullscreen mode

And later, behavior can be associated with the type using methods.

For example:

func (p Person) SayHello() {
    fmt.Println("Hello", p.Name)
}
Enter fullscreen mode Exit fullscreen mode

Then:

person := Person{Name: "Alice"}

person.SayHello()
Enter fullscreen mode Exit fullscreen mode

We'll look at methods more closely when we continue exploring how Go handles behavior without classes.


Go vs Ruby

Concept Go Ruby
Group related data struct class, Struct, etc.
Define a struct type Person struct { ... } class Person
Access fields person.Name person.name
Initialize Person{Name: "Alice"} Person.new("Alice")
Missing fields Get zero value Depends on initialization
Empty value Person{} Person.new
Pointer to value &Person{...} No explicit pointer syntax
Custom type type Banana int No direct equivalent in the same type-system sense

Main idea

Go doesn't really ask you to think in terms of classes first.

Instead, you can start with the data:

type Person struct {
    Name string
    Age  int
}
Enter fullscreen mode Exit fullscreen mode

And that's it.

No constructor is required just to create a value.

No inheritance is involved.

No class hierarchy is necessary.

You simply define the type and create values from it.


But TBH I still like Ruby's object model.

When working on a Rails application, classes provide a natural place for models, services, and domain behavior.

Top comments (0)