-
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/rescue
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
usespanic/rescue
as exceptions internally. Thus proving that they are exceptions, and they're usable (arguably useful) as such.
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 indefer
:- 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.
1 2 3
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
rescue
, sopanic()
becomes a graceful assert failYou may not
rescue
, but your framework might. In fact, the standard Go library HTTP handler parent will catch... I'm sorry, "rescue
" 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). No way to run things at end of scope
This code is bad:
(and my GOD is it verbose)1 2 3 4 5 6 7 8 9 10 11
mutex.Lock() defer mutex.Unlock() doSomethingRandom() for _, foo := range foos { file, err := os.Open(foo) if err != nil { log.Fatal(err) } defer file.Close() doSomething(file) }
The reason is that no file is closed until the function returns. Now, how pretty is this code that fixes it?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
func() { mutex.Lock() defer mutex.Unlock() doSomethingRandom() }() for _, foo := range foos { func() { file, err := os.Open(foo) if err != nil { log.Fatal(err) } defer file.Close() doSomething(file) }() }
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 thatFile
objects actually have a finalizer that closes the file, but you don't know when or if that is called.if ... := ...; <cond> {
is crippledC 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:
1 2 3 4 5 6 7 8
foo := 0 if len(*someFlag) < 0 { if foo, err := doSomething(*someFlag); err != nil { log.Fatalf("so that failed: %v", err) } else { log.Printf("it succeeded: %d", foo) } }
Good code:
1 2 3 4 5 6 7 8 9 10 11
foo := 0 if len(*someFlag) < 0 { // Can't use := because then it'd create a new foo, // so need to define err manually. Oh, the verbosity! var err error if foo, err = doSomething(*someFlag); err != nil { log.Fatalf("so that failed: %v", err) } else { log.Printf("it succeeded: %d", foo) } }
Pointers satisfy interfaces
So for any "handle" you get back from a function you now have to check ifFooBar
is a struct or an interface. "func NewFooBar() FooBar {...}
". Should I passFooBar
s around, or pointers to FooBars? I don't know that unless I look in a second place to see ifFooBar
is a struct or an interface.Why do maps and channels need "
I don't mean "why?" on a technical level. I know whymake()
", when all others types have validnil
values?nil
slices are valid andnil
maps aren't. But people give C++ grief all the time because of "B follows from A" excuses.No interfaces or inheritance for variables
This is sometimes annoying. I don't feel strongly about it though.There are no compiler warnings, only errors! ... except there are warnings too
There are no warnings in the compiler. Except it's so bloody useful to be able to have warnings, that it's instead been made into an external tool to do it. Which is less useful because it's one extra step you have to manually take.
So Go is not my favourite language. It comes in second after C++. And for many if not most projects I prefer to use Go.