2013-11-14
One reason I like Go: it seems natural to avoid object churn
If you want to write well performing code in any language that features dynamic allocation and garbage collection, one of the important things is to avoid object churn. One of the things I like about Go is that I can easily write code where I'm pretty confident I'm not doing any unnecessary allocations. Even better, so far this code has generally been the natural way to do things.
To show you what I mean, here's typical code to copy from an input source to the network:
// 0-length buf is valid and does nothing
func writeall(dst io.Writer, buf[]byte) (err error) {
var nr int
for len(buf) > 0 && err == nil {
nr, err = dst.Write(buf)
buf = buf[nr:]
}
return
}
func fromto(src io.Reader, dst io.Writer) {
var rerr, werr error
var n int
buf := make([]byte, BUFSIZE)
for rerr == nil && werr == nil {
n, rerr = src.Read(buf[0:])
// we assume n is meaningful
// even if rerr is asserted.
werr = writeall(dst, buf[:n])
}
// real code would then handle rerr
// and werr and do something sensible.
}
If I'm correct I think there's exactly one dynamic memory allocation
in this entire chunk of code under normal circumstances, namely the
explicit allocation of buf in fromto(). In a language like Python,
very similar code would be creating and destroying temporary objects all
over the place and you would need to be very careful and write somewhat
unnatural code to minimize it.
(If and when there are IO errors, it's likely that the resulting
error values involve dynamic allocation behind the scenes. But
this is an exceptional case. Most of the time rerr and werr
will be nil.)
In this example a lot of the credit goes to Go's powerful concept of
slices. Slices explicitly make references to an underlying blob of
storage instead of making copies of it. Using slices we can (for
example) iterate our way through successive versions of the write buffer
in writeall() in a completely natural way without actually making any
copies; all we do is shuffle around a single static reference.
(Where this comes in really handy is the interface to .Write(), which
can take a simple slice instead of all of buffer, offset, and length
because slices basically encapsulate exactly that.)
Part of this is also the different semantics of names between Python and Go. Go uses what I've called storage semantics and storage semantics intrinsically does less allocation because you almost always have an assigned place to put fixed-size objects (especially if you give them static types, as Go does). In theory I think that a sufficiently clever compiler can handle all of the basic function arguments and return values here without any dynamic allocation, although I don't know if the normal Go compiler is quite that smart.
Sidebar: where I may be wrong
Storage semantics don't entirely save you in a garbage collected environment where you can take references to things and local variables don't have an explicit lifetime limitation. A clever compiler can do escape analysis to determine that local variables can be stack allocated instead of needing explicit heap allocations, which all of the variables here qualify for. I assume Go's compiler is reasonably clever here because it's a fairly important optimization if Go wants to be efficient.
A total failure of escape analysis here would result in the heap
allocation and then destruction of all of the local variables in
writeall() every time it was called. Variables in fromto()
would also be heap allocated but not reallocated during the for
loop.
(Inspecting the assembly from compiling this code suggests that both the standard Go compiler and gccgo are doing full escape analysis here and thus are heap allocating everything.)
2013-11-11
Go's getopt problem
The Go standard library is full of good stuff, people will commonly tell you (or tell the world when they write about Go). And then they will add a little qualifier: except for the flag package; you don't want to use that and you should get a sane getopt package. Sometimes the people writing this will name one or two such packages, which usually still leaves you with a problem.
(I saw such a Go-related slide recently that recommended something like 'getopt' or 'go-flags'. Do you know how many people have had the clever idea to write a Go command line argument parser that they call 'getopt'? The answer is 'more than one'. The Go wiki alone lists two, one of which links to four more. I have no idea which specific 'getopt' package the author of the slides thought was the one to use.)
The flags package is perhaps the most frustrating thing about Go for me. Uniquely out of the standard library it is clearly insufficient to fill a common need at even a basic level, yet faced with this common need the Go authors not so much punt as ignore you. If you want standard Unix command line parsing in your Go project (even a very small and otherwise self-contained one), your very first job is to try to sort through a great many options to pick a decent one. In the process you will add an external dependency and as we all know external dependencies are a great way to blow up your little project. All it takes is picking a getopt package that the author decides either not to support any more or to change the API on and whoops, fun times.
(Having your first external dependency also vastly complicates the build
instructions for a little Go program. If you have a self-contained
little program you can tell your coworkers to run 'go build <foo>.go'
or 'gccgo -o <foo> <foo>.go'. Add even one external dependency and
suddenly they need $GOPATH and everything that that implies.)
This is asinine. When the common advice is 'replace this standard library package', your standard library is flawed and you should fix this. The Go developers should pick or write one getopt package and put it in the standard library (and keep the flag package for backwards compatibility and for anyone who really needs it).
I don't expect this to happen, of course. If it was going to happen it would have happened already; it's not like this is a new problem.
(After all there's been enough time and demand for all sorts of people to write all sorts of argument parsing packages. Which is part of the problem.)
PS: yes, this is a grumpy rant. I do them sometimes.
(It is a close variant of why I want things to be in the Python standard library, especially for small projects.)