Now we get to the annoying aspects of Go
I’ve spent another ten hours or so writing extensive functional tests for my code, and fixing bugs, so I have some further thoughts about Go.
POLA (Principle of Least Astonishment) #
gotest
continues to be easy to use and is versatile, even if you do have to implement a bunch
of basic functionality yourself (there are no convenient “assert”
functions, there’s no setup or teardown functionality, and nothing like
TestNG’s DataProvider
mechanism). Even functional testing of a server process is easier than in other
languages, although I think most of that is again due to Go helping me
separate my code into modules. I also found my first non-contrived
reason to use goroutines for the first time. I
really like goroutines, but that’s probably because I really like
Erlang’s process paradigm.
What really bugs me are the inconsistencies. Go is “self-consistent”, which seems to be the Go-team’s answer to any criticism of consistency in the language, which is itself annoying; but it isn’t consistent in the Principle of Least Astonishment way. In fact, Go is really pretty bad when it comes to POLA. The thing that kept biting me this weekend – and, admittedly, it was my fault resulting from a lack of experience with Go – was the following case:
package main
type Foo map[string]string
type Bar struct {
thingOne string
thingTwo int
}
func main() {
x := new(Foo)
y := new(Bar)
(*y).thingOne = "hello"
(*y).thingTwo = 1
(*x)["x"] = "goodbye"
(*x)["y"] = "world"
}
Go requires that you know details about the implementation of the types
defined above. x
is not really a new object; it’s a pointer to something that doesn’t exist, while
y
is an actually allocated object. If you try to use x
, you’ll get a
runtime fault and your program will crash. Instead, you have to do
this:
x = make(Foo)
However, if you try to do the same thing with Bar:
y = make(Bar)
you get a compiler error. This bothers me, because it’s one of those
submarine bugs – if you make a mistake in how you allocate an
instance, you won’t find out unless your tests actually hit that code.
I had hoped we’d have gotten away from that by now. I’d rather Go
just made make()
work on
structures, and then you could eschew new()
entirely
(unless you really needed it for a specific purpose), and this type of
error would become exceedingly rare. It’s a gotcha; it’s an
inconsistency in the conceptual syntax.
I’ve mentioned before that I like catching errors as early in the development process as possible, and this is the reason why I prefer strongly typed languages. The sort of problem that I’ve just mentioned weakens Go a bit in this area; for comparison, Haskell eliminates this sort of submarine bug – if you can get your program to compile, it will run, and if it fails, it’ll be entirely due to a logical, algorithmic error, not a type-usage one. There’s no compiler yet that will check your logic for you, so Haskell is pretty close to perfect, in that respect.
Before anybody comments on my syntax use above, I’ll point out that the
dereferencing of the y
pointer is
unnecessary: Go does that for you. I did it for illustration purposes.
Pointers #
It’s been a long time since I’ve had to use pointers, and during my development and testing I was floundering around trying to determine when and when not to use them. At one point, I was getting annoyed and was thinking that this was another case of POLA in Go, but it turns out I didn’t have to worry about it. All you have to do is to ensure that any function that needs to change a structure (or variable) gets passed a pointer, and Go happily figures out the rest. Another example:
func (b *Bar) foo() { b.thing = 1 }
func (b Bar) write() string { return fmt.Sprint(b) }
func tralaTrala() {
var b Bar
b.foo()
fmt.Println(b.write())
a := new(Bar)
a.foo()
fmt.Println(a.write())
}
tralaTrala()
does not need to know whether a
and b
are pointers or not; Go will “do the right thing” here, and the syntax is the same despite the fact that the types of the two variables are different (one’s a pointer, the other isn’t). The only real issue is that, when writing a function, the programmer does need to know whether foo()
and write()
modify the state of the Bar they belong to, because s/he needs to know whether to request a pointer or not:
func doSomething( b Bar ) {
b.foo()
}
will probably end up being a runtime logic error, and Go does nothing to
help you with these. This makes me think that the safest way to use Go
is to pretend that it’s entirely functional, and write all of your code
that way, until you need to optimize it. This would mean that the
signature for foo()
would be foo() Bar
and it’d be used like this:
func doSomething( b Bar ) Bar {
rv := b.foo()
return rv
}
This is another thing that I’m not entirely happy about, but it may be a reasonable trade-off to provide a powerful optimization path.