Kotan Code 枯淡コード

In search of simple, elegant code

Menu Close

Object-Oriented Design in Go

One of the first things I tend to look for in exploring a new language is OOD. What does a class look like? How does inheritance work? And, because I’m such a huge fan of Scala, I now ask questions like “Does it support mixins (traits)?”. The trick with Go is that there are no classes. That’s right, it’s a language that can be called object-oriented but you’ll never write a single class.

Let’s take a look at a little bit of sample code, where I’ve created a struct (remember those? Ah, the good old days…) called Mob and I’ve also created one called Coordinate:

type Coordinate struct {
	X	int
	Y	int
}

type Mob struct {
	Hitpoints	int
	Name		string
	AggroRadius	int
	Coordinate
}

Something worth noting here is that it might look like Mob inherits from Coordinate, but that’s not what’s happening. Instead, the Mob struct is embedding Coordinate struct. To really be object-oriented, I need to be able to write methods on my “objects”. Go supports this by allowing you to define functions that have something called an explicit receiver. In other words, when you invoke a method on a struct, that instance of that struct becomes the explicit receiver for that function.

Let’s create a method that moves our mob around a game board. In the process, you’ll see another one of Go’s differences from traditional C in that it can return multiple values. What happens if we invoke a method called MoveTo in a traditional OOP language and it fails? How do we know where the mob stopped en route to its original location? There are other classes we traditionally create like MoveResult or some junk like that, but that’s all ceremony slapped on top of the real problem. Here’s how to do it in Go:

func (mob *Mob) MoveTo(x, y int) (success bool, newLoc Coordinate) {
	success = true
	newLoc = Coordinate{x,y}

	mob.X = x
	mob.Coordinate.Y = y // can choose to be explicit about embed or not

	return success, newLoc
}

In the preceding code, the mob *Mob is declaring an explicit receiver of type Mob and it will be named mob (think of it as being able to provide a name for the traditional this in other OOP languages). X and Y are integer parameters to the method, and success and newLoc are the return values from the method.

Now let’s take a look at how to create a Mob and use it:

func main() {
	var brutus = Mob{
		Hitpoints: 500,
		Name: "Brutus",
		AggroRadius: 20,
		Coordinate: Coordinate{X: 100, Y: 200},
	}

	madeIt, newLoc := brutus.MoveTo(60, 50)
	fmt.Printf("Did I make it? %s\n", madeIt)
	fmt.Printf("Brutus is now at %d,%d\n", newLoc.X, newLoc.Y)

	// Can also shortcut through the embed:
	fmt.Printf("Brutus's X location - %d (or %d)\n", brutus.X, brutus.Coordinate.X)
}

I strongly encourage you, if you’re just learning Go, to copy this stuff in (remember each file needs a package) and run it with something like go run mob.go. Also remember that there’s absolutely no restriction on filename and it doesn’t need to correlate to the package it contains or the structs used. One subtle take-away from this is that you can add explicit receiver functions (methods) to any struct from anywhere in your code.

Here’s something subtle but important: Did you notice that it looked like I used a pointer in the MoveTo method? If you’ve got the code running, go ahead and delete the asterisk and run it again. Note that you don’t get any memory violations or null pointer exceptions or any of the usual crap you would expect from downgrading a pointer to a stack reference. BUT, what does happen is that the modification of the mob’s current location is ignored. Well, it’s not really ignored, but the modification is being made to a stack copy of the mob which is different than the mob reference sitting inside the main() function. To ensure that changes made inside the method actually happen to the shared instance of the mob between main() and the method, you need to use a pointer. This is because the method isn’t some vtable dispatched reflection-discovered shenanigan, it’s a legitimate C-style function that just happens to have been passed a value called mob. This is one of those dichotomies I mentioned in my previous blog post, about how it seems that Go is straddling the line between low-level, C-like functionality and modern awesomesauce like goroutines and asynchronous processing via communication over channels.

In my next post, I’ll show you some stuff about type inference and another cool feature: implicit interfaces.