A Beginner's Experience with Go

I've spent the last few weeks messing around with Go. It's a new(ish) open-source language designed for systems-level programming with modern hardware in mind. Its development is primarily driven from within Google (that's where it was born), but it is fully open-source and has a very large community of users and developers outside of Google.

A good way to get started is to check out the Tour. It's an interactive demo and tutorial in one. There is a very helpful, smart, active group on the mailing list as well.

While Go is still very fresh for me, I thought I'd write up my initial impressions from the perspective of an experienced developer who is new to the language. For the record, my experience has been with Visual Basic, ASP, IBM's Net.Data, JavaScript, PHP, Perl, VB.NET, and Python. I'm not claiming to be an expert in any of these languages, although I'd have to say I've learned a lot over 15 years and most of that experience has gone into making me a pretty good Python guy. The point of this is just to point out that I have no background in lower-level stuff like C or C++ worth speaking of.

Culture shock

Formatting

As a Pythonista, I have grown accustomed to a lot of community best practices. Four-whitespace indentation, using_underscores in my variable names, etc. In Go, the norm is tabs for indentation, and camelCase. The Go Playground indents tabs with eight spaces, leading me to (incorrectly) assume that eight spaces was the norm. In fact, the "norm" is to use a tab and set tabs to whatever width you like in your editor. (Thanks to Dan and Sebastian for the correction.)

Statically-typed

The shift to statically-typed variables actually wasn't an issue. The biggest affect this has had on my programming is learning that the signatures of functions must contain the types of the variables they accept and return. This has not been a problem -- this has been great, and I find myself missing it when I read Python code.

Pointers

Go's use of pointers is pretty simple, and I already understood how pointers work from reading K&R. However, Go's pointers are simpler because there's no pointer arithmetic. The only confusing thing is that, despite functions having explicit types in their signatures, it's not necessary to pass the "correct" thing -- a variable or a pointer to it -- in all circumstances, because Go does it for you sometimes. It's not difficult at all to get working, but it doesn't help you understand the rules.

Compiled

Go is my first compiled langage. This has only been an upside, though. As a Python programmer, I'm used to a lot of work ensuring the correct Python version is installed on a target server, which sometimes means manually compiling it and making sure certain libraries exist on the system first to ensure Python can import required modules. And then there's setting up the virtualenv, and then maintaining the packages in that virtualenv as the application grows. Dropping a single executable is a pleasure in comparison.

Why Go is as convenient as Python despite compilation:

  • Compiling code takes practically no time.
  • It's trivial to run using go run (which compiles and runs). This can easily replace my usual jump to an iPython session to try something.
  • The Go Playground is always there when I want to experiment with syntax or something.

With both Go and Python, I frequently launch a tmux session with side-by-side panes, develop in vim on the left, and have my code running in a loop on the right, for real-time feedback whenever I save my code. For any practical purpose, it makes no difference whether that loop is compiling my Go code or interpreting my Python code.

Runtime errors?

For my first week or two with Go, I didn't get a single runtime error except one I caused intentionally (as part of the Tour). This is because all my stupid mistakes were caught at compile time. I can't count the number of times I've seen attribute and type errors in Python production code. How many times has a variable you tried to treat like a list or dict turned out to be None?

As I started doing slightly larger projects I did manage to achieve some runtime errors, but these were due to me not being familiar with the language, and were almost always easily understood and solved right away, and happened immediately the first time I ran the compiled code. So no broken code could make it to production (disclaimer: I have not put any of my Go binaries into a production environment yet).

Concurrency

Go has concurrency (not parallelism) built in. It's trivial to execute a function in the background, and the built in chan (channel) type makes it dead simple to communicate among processes regardless of how they are run. This has changed the way I write code. Specifically, I think more about what functionality really has to happen in the same code block before writing, resulting in smaller functions and cleaner code. This design is not only easier to read, but allows concurrency and (depending on a couple of things) parallelism to be added with barely any changes -- sometimes no changes at all.

Unused stuff

The Go compiler is a strict nanny. You can't import modules and then not use them. You can't assign variables and then not use them. This makes my habit of running my code often during all stages of development difficult. I end up having to either comment out things in various places, or put in stubs by printing a variable I'm not otherwise using just so it doesn't cause a compiler error. Overall I appreciate the cleanliness that enforces, so I'm dealing with it and may find that a change in my development approach (for the better) could resolve it.

Error handling

Error handling is very different in Go than in Python. In Go, practically every time you do anything you have to stop and see if you have an error. Here's an incomplete sample showing what I mean:

val, err := someFunction()
if err != nil {
    // do something here, probably involving
    // exiting the current function and returning an error,
    // or terminating the whole program
}
// do what you really wanted to do (if you got this far)

So you've called a function that returns a value that you want. However, that function also returns an error. If all is right with the world, that will be nil and you can go about your business. But you have to check. A lot.

There are various ways (both acceptable and frowned upon) to reduce the number of times you have to type in that if block pattern. However, it still needs to be dealt with consciously at all times.

Community

I've been on a lot of mailing lists and forums over the years, for various programming languages, frameworks, and other tech stuff. I have to say that the average helpfulness and sheer understanding of programming on this list far exceeds that of any other I've spent time on. I don't know why this is, and I'm sure there are many reasons. But I think a big part of it is that like all mailing lists it's a self-selected group, and this group has chosen to use a low(ish) level language that will probably scare off the non-programmers who haunt so many other lists. You know -- the kind who scrapes together enough JavaScript, Python, PHP, or whatever to kind of make something (kind of) run and then constantly asks for help because they actually don't know what they're doing. The kind that just doesn't want to learn and squeaks by on handouts and copy/paste. I'm not picking on inexperienced coders -- I'm picking on the ones that don't even try.

Low level?

Go aims to replace C and C++ more than Python and Ruby. Despite this, in my experience it's often comparable to Python in ease-of-programming. And that's coming from someone who couldn't write enough C to, well, do anything beyond "Hello world" and some simple math.

In fact, I've written multiple small programs in both Python and Go to compare them, and most of them are practically identical, line-for-line (ignoring minor syntax differences, of course). However, as soon as you do anything non-trivial, you find yourself having to do things like this:

x = append(x, y)

in Go when in Python it's just:

x.append(y)

The same goes for joining a slice (list) into a string, or splitting a string into a slice (list) -- you have to import a module in the standard library to do either one. This isn't really a problem, but it was annoying at first before I was in the habit of not trying to write Python in my Go code.

Speed

Being statically-typed and compiled (probably more due to the static-typing than the compilation), Go is a lot faster. How much faster depends on what the code is doing (and probably even moreso on how well I wrote it). However, the comparisons I've done have my Go code running 3 to 15 times faster than my Python code. Usually closer to 10x and that's with me barely knowing what I'm doing in Go.

Deployment

I've already mentioned this above but really want to belabor the point. Deploying Go code is amazing. Copy a binary. Done. I don't think I can convey how awesome this is.

Cross-compiling is laughably simple. Set an environment variable or two and build your binary. Done and done. Oh, did I mention compiling is as easy as typing go build? Amazing. I lost track of how many times I tried to learn C and got distracted, confused, and ultimately put off by attempting to get my code to compile when I tried to use anything other than stdio.h.

What next?

If you want to learn more about Go, especially if you're a Python programmer, check out this interview with one of the developers of Go, who has a lot more years of Python experience than Go, and this talk.

As for me, Python is still my go-to language for multiple reasons:

  • It's the language I know best.
  • It's the language that "fits my brain" better than any other -- the reason I chose it over Ruby, Perl, etc.
  • It's the language which I use to make a living.

However, I'm very excited about Go, and am planning to use it for all my recreational projects that aren't particularly time-sensitive. I wouldn't be surprised to be writing more Go than Python a year from now.

Comments !

blogroll

social