.daniel's musings - Object Oriented Go


Object Oriented Go

September 7, 2024 3:08 PM

Go is quickly becoming one of my favourite languages to work with. It's simple, concise and has a lot of power behind it. I've been using it for a while now and I've been learning a lot about the language and its features. However, one thing that I've been struggling with is how to write object-oriented code in Go. Go is a statically typed language and it doesn't have classes like other object-oriented languages such as Ruby, which for me as a Ruby developer, has been a bit of a challenge. But after some time and a lot of reading, I've come to understand how to write object-oriented code in Go. In this post, I'll share some of the things I've learned about writing object-oriented Go.

Structs

In Go, you can define a struct to represent a data structure. A struct is a collection of fields that can be of different types. You can define a struct like this:

type Person struct {
    Name string
    Age  int
}

Here we define a struct Person with two fields Name and Age. You can create an instance of this struct like this:

p := Person{Name: "John", Age: 30}

You can access the fields of the struct like this:

fmt.Println(p.Name) // John
fmt.Println(p.Age)  // 30

Methods

In Go, you can also define methods on a struct. You can think of a method as a function that belongs to a struct. You can define a method on a struct like this:

func (p Person) SayHello() {
    fmt.Println("Hello, my name is", p.Name)
}

Here we define a method SayHello on the Person struct. You can call the method like this:

p.SayHello() // Hello, my name is John

Behind the scenes, Go is passing the instance of the struct as the first argument to the method. This is similar to how Ruby passes self as the first argument to an instance method. What this means is that you can access the fields of the struct inside the method using the instance of the struct. You can also modify the fields of the struct inside the method using a pointer to the struct. Here's an example:

func (p *Person) SetName(name string) {
    p.Name = name
}

You can call the method like this:

p.SetName("Jane")
fmt.Println(p.Name) // Jane

But what about inheritance?

Go doesn't have inheritance like in Ruby. Instead, Go has composition. You can embed a struct within another struct to achieve composition. Here's an example:

type Employee struct {
    Person
    Salary int
}

Here we define a struct Employee that embeds the Person struct. This means that an Employee has all the fields and methods of a Person. You can create an instance of an Employee like this:

e := Employee{Person: Person{Name: "Jane", Age: 25}, Salary: 50000}

You can also create an instance of an Employee like this:

p := Person{Name: "Jane", Age: 25}
e := Employee{Person: p, Salary: 50000}

You can then access the fields and methods of the embedded struct like this:

fmt.Println(e.Name) // Jane
fmt.Println(e.Age)  // 25
e.SayHello()        // Hello, my name is Jane

I long for the Ruby way

One of my favourite things about Ruby is the ability to call methods on objects and pass messages between objects. This is one of the things that I miss the most when working with Go. There are multiple ways of achieving this in Go, one of them is using the actor pattern. I've written a post about it here (if it's not published yet, it will be soon).

Lets say you want to do some arithmetic on some values. Directly you could do something like this:

func add(x, y int) int {
    return x + y
}

func main() {
    res := add(2, 5)
    fmt.Println(res) // 7
}

This works fine but what if you want to do it the Ruby way? The ruby way being to send a message to an object and have it do the work for you. In Ruby you could do something like this:

class Adder
  def add(x, y)
    x + y
  end
end

adder = Adder.new
res = adder.add(2, 5)
puts res # 7

In Go, you can achieve something similar. Here's an example:

package arithmetic

type Calculator struct {
	a, b float64
}

func New(a, b float64) *Calculator {
	return &Calculator{a: a, b: b, vals: vals}
}

func (c *Calculator) Add() float64 {
	return c.a + c.b
}

Here we define a struct Calculator with two fields a and b. We also define a method on the Calculator struct to do some addition. You can create an instance of a Calculator like this:

c := arithmetic.New(2, 5)

You can call the method on the Calculator like this:

fmt.Println(c.Add())      // 7

You can also call the method on the Calculator like this:

fmt.Println(arithmetic.New(2, 5).Add())      // 7

This approach is pretty rigid but it's a good starting point and allows you to do some basic object-oriented programming in Go. You can also use interfaces to define a contract that a struct must implement, this way you can define a set of methods that a struct must have.

Conclusion

Having been spoilt by Ruby's object-oriented features, I have a deep and fond appreciation for it and I'm always looking for ways to replicate that in other languages. Learning how to do so in Go has been a fun and interesting journey. I hope this post has been helpful to you and has given you some insight into how to write object-oriented code in Go. Please note that I am still learning and there may be better ways of doing things and I'm always open to learning new things, at this time this is how I've been doing things and it's been working for me. If you have any suggestions or feedback, please feel free to reach out and let me know. I'm always looking to learn and improve. Thanks for reading!

References