Why Go is not my favourite language

Oct 24, 2013,
Categories: coding

1. Go has exceptions and return values for error

Yes it does. Yes, it really really does.

We can discuss
this for hours but in the end it boils down to four points:

In Go some errors cause stack unrolling, with the possibility to for
each step in the call stack register code that runs before the stack
is unrolled further, or for the unrolling to stop. In programming
languages, that’s called “exceptions”. Idiomatic use in a language
of the feature doesn’t affect what the feature is. Saying that
Go doesn’t have exceptions is like saying Go doesn’t have NULL
pointers (it has nil pointers).

There is no non-tautology definition of exceptions that includes the
ones in Python, C++ and Java, but does not include panic/recover
in Go. Go on, try to language lawyer your way into a definition. In
spoken/written languages we have words, and those words have
meaning. And the word for this meaning is “exceptions”.

The programmer must write exception-safe code, or it will be broken
code.

encoding/json uses panic/recover as exceptions internally. Thus
proving that they are exceptions, and they’re usable (arguably
useful) as such.

These four points feel like four stages of grief. Denial (“error codes
only!”), bargaining (“What if we redefine the word ‘exception?`”) and
eventually acceptance (“sigh, ok… what does this mean for my
code?”).

defer isn’t just a good idea. It’s the law. If you want to do something while holding a lock
then you have to lock, defer unlock, and then do whatever it is that needs to be done.
Here’s an incomplete list of things that can cause stack unroll, thus requiring cleanup to be done
in defer:

Array or slice lookups

Acquiring a lock

Expected system files (like /dev/random) missing

Bugs in your code

Bugs in library code

Operating on channels

This code is not safe! It can leave your program in a bad state.

mutex.Lock()fmt.Println(foo[0])mutex.Unlock()

Fine, it has exceptions and you have to write exception-safe code. So what?

Do you know anybody who thinks mixing exceptions and error return
values is a good idea? The reason Go gets away with it is that Go
programmers stick fingers in their ears and scream “la la la, Go
doesn’t have exceptions” and thus avoid defending that choice.

Fine, but I don’t ever use recover, so panic() becomes a graceful assert fail

You may not recover, but your framework might. In fact, the standard
Go library HTTP handler parent will catch… I’m sorry, “recover” a
panic and prevent your program from crashing gracefully. All code in
your HTTP handlers, and any library (yours or third party) now has to
be exception safe. Idiomatic Go must be exception safe. It’s not like
idiomatic Erlang where you can assume your process will
actually die (One can write bad code in any language, I’m specifically
referring to idiomatic code here).

C++ runs destructors at end of scope, enabling the lovely
std::lock_guard. Python has the with statement, and Java (the
weakest in the bunch in this one aspect, aside from Go) has finally.

The defer is needless typing. What do I want to do when
my file object is unreachable? Close the file, obviously! Why do I
have to tell Go that? My intent is obvious. C++ and CPython will do it
automatically. It’s so obvious that File objects actually
have a finalizer that closes the file, but you don’t know when or if
that is called.

3. if ... := ...; ...cond... { is crippled

C syntax for assigning and checking for error in one line doesn’t
really work with multiple return values (well, it does, but it would
use the comma operator. Not optimal). So when Go supplied this
return-and-check syntax it seemed pretty nice. Until it collided with
scoping rules. If the function returns more than one value then you
either have to manually “var” all values and use “=”, or do all work
in an else handler.

Bad code

Code code

foo:=0iflen(*someFlag)<0{// Can't use := because then it'd create a new foo,// so need to define err manually. Oh, the verbosity!varerrerroriffoo,err=doSomething(*someFlag);err!=nil{log.Fatalf("so that failed: %v",err)}else{log.Printf("it succeeded: %d",foo)}}

4. Pointers satisfy interfaces

So for any “handle” you get back from a function you now have to check
if FooBar is a struct or an interface. func NewFooBar() FooBar
{...}. Should I pass FooBars around, or pointers to FooBars? I
don’t know that unless I look in a second place to see if FooBar is
a struct or an interface.

5. Why do maps and channels need “make()”, when all others types have valid nil values?

I don’t mean “why?” on a technical level. I know why nil slices are
valid and nil maps aren’t. But people give C++ grief all the time
because of “B follows from A” excuses.

6. No interfaces or inheritance for variables

This is sometimes annoying. I don’t feel strongly about it though.

7. There are no compiler warnings, only errors! … except there are warnings too