I’ve always liked C and its simplicity, but it’s kinda tedious to write bigger programs in it, which is why I’ve always refrained from doing so. It’s not even the memory management, it’s simple things, like working with strings, or the overhead you have for calling “methods”, animal_do_something(animal)
. In comes Go, the programming language by Google.
Go is a modern C, and that’s the way you have to approach it. You don’t have classes, polymorphism, enumerators, or exceptions, you get what you’d expect from a language which’s design began with “we’ll start from C”. This is good for everybody who likes C, but might be bad for people who are too used to higher level languages. You can’t use Go like you use C# or Java for the most parts. Take classes for example.
I always thought of classes as glorified structs with some functions, but I gotta admit that coding without any kind of polymorphism is hard in the beginning. It takes a moment to get used to the idea that you don’t create a base class Animal
, from which you inherit in Dog
, from which you inherit in GermanShepherd
, overwriting methods and Properties along the way. If all you have are structs, how do you solve that?
In Go you can embed structs in other structs. Doing so allows you to directly use anything that struct has access to. This way you can “inherit” fields and methods. Methods work similar to extension methods in C#, allowing you to specify functions that take any type as their first parameter, making it “this”.
type Animal struct {
Name string
}
type Dog struct {
Animal
}
func (animal Animal) MakeSound() {
fmt.Println("Moo")
}
func (dog Dog) MakeSound() {
fmt.Println("Woof")
}
func main() {
dog := Dog{Animal{"James"}}
fmt.Println(dog.Name) // James
dog.MakeSound() // Woof
dog.Animal.MakeSound() // Moo
}
While it’s not possible to cast a Dog
to an Animal
, you can pass Dog.Animal
around to functions. However, by doing that so, you lose your “overridden” methods, because you then have a type Animal
, not Dog
. This problem is solved with interfaces.
A struct satisfies an interface automatically if it provides the correct methods. Aside from that, they work pretty much like you’d expect. This is a nice addition to the way C handled it.
type SoundMaker interface {
MakeSound()
}
func MakeSound(sm SoundMaker) {
sm.MakeSound()
}
...
MakeSound(dog) // Woof
While this looks weird at first, you can really do anything with this design, you don’t need classes and all their features. While there are cases where classes might’ve been a little more straight forward, generally you just have to think a bit different in order to reach your goal.
One thing that’s still strange to me are the packages. In Go you’re supposed to create a “workspace” in which you work, where you put all your Go projects. This is to allow the go tool to automatically download and compile dependencies only once. And it can even do that automatically, based on the imports in your project.
> go get github.com/someuser/somerepo
import "github.com/someuser/somerepo"
This is a cool feature, the versioning problems aside, that you can’t really solve if your goal is to allow downloading packages from anywhere.
But if you have a big project, that might have “sub-packages”, and you store your project in your workspace at “GOPATH/github.com/exectails/project”, the packages going down to “project/subfolder/anotherfolder”, you have to import them using the path “github.com/exectails/project/subfolder/anotherfolder” anywhere you need that package, Go looking for the files from the root, your GOPATH. While Go does support relative paths, it’s not idiomatic to use them apparently.
That’s probably just another thing you have to unlearn though. Packages aren’t namespaces and I suspect you’re supposed to design your projects much more modular, without too many sub-packages. The long imports would still be a little annoying, but at least they’d make sense.
That’s the two things I struggled with, looking into Go the past few days.
All in all I really like it. It’s very clean, it’s “non-magic” (it doesn’t hide much), and it’s fun to work with. It extends ideas from C very cleverly, changing just the right amount. For example, you don’t have enums, but you have consts!
const (
First = iota // 0
Second // 1
Third // 2
)
iota
is a counter for that const scope, incrementing by one for each value. You only have to define the first one, the others automatically use the same. So far so normal, but let’s say you wanted to make a bitmask, where you don’t increment each value by 1, but shift it by X bit.
const (
First = 1 << iota // 1
Second // 2
Third // 4
)
This really blew my mind when I discovered it. It doesn’t change the world or anything, but it’s just so clever. That’s Go to me.