Iterator patterns in Go
ewencp.org
ewencp.org
var v someType
for iter.Next(&v) {
// do something with v here
}
if err := iter.Err(); err != nil {
// the iterator had an error, handle that here.
}
http://godoc.org/labix.org/v2/mgo#Iter.NextModern languages should all support some form of foreach, e.g. Java:
for (Customer c : customers) { ... } // no iterator
C#, Scala and many others get this right as well. Go doesn't.And, this:
close(ch) // Remember to close or the loop never ends!
Seriously?I agree that having it hidden by a language feature is nice, but somewhere there is an iterator -- the class that supports that iteration must at least specify how it's accomplished. I would prefer if there were an easy way to integrate with the range keyword in Go.
EDIT: Adding to reply to some added comments. The close() one in particular really threw me. It makes sense in terms of how channels work, but is really unfortunate. When I first saw the channel-based approach, I thought it was a neat solution. But then I found all the problems with it (including the best example I could find of it online missing that close(), which is why I pointed it out!).
I think Go is close to getting it right if they would do what I mention at the end of the article: provide a way to hook into the range keyword.
Not really, they are really noise in a lot of situations. Right now, I can only think of one case where they are necessary: lazy traversal. Either the collection is huge or each element is expensive to materialize, so you want to do that one at a time. Iterators are perfect for this.
They are unnecessary in all other cases. For example, for your (key, value) example:
for (Entry<String, String> entry: map.entrySet()) { ... }
Go exposes too many implementation details to my taste.But the Java 'foreach' loop is just syntactic sugar around iterators, and your fragment will expand to:
for (Iterator<Entry<String, String>> iter = map.entrySet().iterator(); iter.hasNext(); ) {
Entry<String, String> entry = iter.next();
...
}
For this reason, the foreach-loop requires that the object that you loop over implements the Iterable interface.To be honest, every release of Go feels more and more like Java anyway. I won't be surprised when Go eventually ends up being a JVM-less Java in the same way a new OS will end up being unix. After all, "Those who don't understand Unix are condemned to reinvent it, poorly." – Henry Spencer
Maybe Java is the same way? Not to say unix (plan 9?) or java (scala?) are perfect, but there is a reason they are popular.
Sure, the JVM has long startup time (why should we care for most applications?) and gc/gcc-go produces nice binaries. But the Java ecosystem has many attractions as well: a common runtime for different programming languages, a wealth of high-quality libraries, a mature GC, great package management (via Maven or sbt), good IDEs, hotswap, instrumentation, etc.
Most of Go's advantages (lightweight threads, channels, good compile times) are also available in Java and the JVM.
IMHO it would've been more interesting if Google had pushed Java AOT compilation forward instead. But I guess the point of 20% time is that people can do what the heck they want. Perhaps RoboVM will push the envelope instead ;).
I wonder if they disapproval of the JVM is mostly shaped by slow and ugly AWT/Swing applications and huge frameworks that require lots of XML configuration. (Swing an be pretty, see IntelliJ. I try to avoid frameworks ;).)
And IMHO one technology is responsible for much of the hatred towards Java and that is Spring.
So you at least need to write them for your own data structures so that the code knows how to navigate them.
But yeah, Go still needs a few improvements to be usable as a language for generic programming.
for c := range customers { }
Of course, whether this is a real issue or not also depends on whether you're writing internal or external APIs. There are lots of other ways to generate uncollectible garbage too...
https://groups.google.com/forum/#!topic/golang-nuts/RmfHSHE9...
Note that there is no requirement that you close channels-- they aren't like file descriptors. They will be garbage collected after they're no longer needed.
goroutines, on the other hand, will stick around unless you clean them up. goroutines aren't iterators, and you shouldn't use them as iterators. They're more analogous to threads.
An example, scanning lines from standard input:
s := bufio.NewScanner(os.Stdin)
for s.Scan() {
fmt.Println(s.Text())
}
if err := s.Err(); err != nil {
fmt.Fprintln(os.Stderr, "reading standard input:", err)
} type Iterator interface {
// Next returns true if the iterator contains subsequent elements
// and advances its state to the next element if that is possible.
Next() (ok bool)
// Value returns the current value.
Value() interface{}
}
which you then use like this: iter := s.Iterator()
for iter.Next() {
fmt.Printf("%s\n", iter.Value())
}
I think this is a pretty good pattern: it's concise, looks less ugly than the callback and closure based iterators, and it's not using anything heavyweight like channels, so there should be no significant performance degradation.Out of curiosity, I added this pattern to OP's test code as StatefulIterator (https://github.com/ewencp/golang-iterators-benchmark/pull/1), in 2 versions: one in which the iterator is a struct, and one in which the iterator is an interface. The results look like this (the 4 last rows are my additions):
BenchmarkIntsCallbackIterator 500 3478962 ns/op
BenchmarkDataCallbackIterator 500 4437885 ns/op
BenchmarkIntsChannelIterator 10 186095017 ns/op
BenchmarkDataChannelIterator 10 188627451 ns/op
BenchmarkIntsBufferedChannelIterator 20 90941766 ns/op
BenchmarkDataBufferedChannelIterator 20 90175550 ns/op
BenchmarkIntsClosureIterator 500 5580257 ns/op
BenchmarkDataClosureIterator 500 6183472 ns/op
BenchmarkIntStatefulIterator 1000 2653265 ns/op
BenchmarkDataStatefulIterator 500 3582051 ns/op
BenchmarkIntStatefulIteratorInterface 200 7516778 ns/op
BenchmarkDataStatefulIteratorInterface 200 8250702 ns/op
ok _/Users/ryszard/Projects/golang-iterators-benchmark 33.717s
As you can see, the struct version is the fastest of the lot: 2653265 ns for ints and 3582051 ns structs (versus the callback version with 3478962 ns and 4437885 ns, respectively). The interface version is slower, but still in the same ballpark as the callback and closure based ones, while being much more readable (but that's only my personal opinion). The not-so-stellar performance of interfaces is a bit disappointing, but that is a known issue.Sure, if you're about to iterate over a terabyte of double-precision height map data or something like that, please use an array.
Also, I wouldn't use an array to iterate over a terabyte of double-precision height map -- have you tried that? I have, kind of. In a genome sequence visualization application. Try it sometime. It's fun.
I'm curious because that's what makes python the best language for me in a lot of cases.
Iterators that don't load 100,000,000 rows at a time for a result set are... useful. And that's what you get with the simple (iterator/generator func)/db cursor pattern in python.
Something similar in Go would go a long way in helping me get over the fixed braces position go enforces. :)
Can someone explain to me that last sentence? What's cache coherence?
The change made was to still iterate over a slice (i.e. an array), but the array now contains pointers to structs, which are allocated separately. This should scatter them across memory such that each step of the iteration needs to choice a different pointer to a different chunk of memory. This is a benchmarking issue which has bitten me in the past, so I made at least a feeble attempt to account for it.
However, I actually don't know enough about Go's memory allocation to really make sure I could force the conditions I wanted, so it's still possible all the data was lined up nicely, but just spread a bit farther apart. (In fact, this may even be likely since the allocation approach was very simple. Trying to do a lot of allocations and deallocations and filling in the data over a longer period would probably do a better job of addressing this concern.)
When you're writing performance critical code this is a good thing to have happen, and it can be worth a lot of effort to make sure that you benefit as often as possible. However general purpose code usually does not wind up with data that is optimally organized in memory. But in a benchmark the usage patterns are simple enough that it is trivial to get well-organized data in memory without even intending to. Whether this is likely to be predictive of real world performance depends on how carefully coded the real world program is.
When reading some value in RAM, values in the same "cache line" (that are right next to it) get cached. However, if another thread has to modify those values that just got cached, then the cache needs to be invalidated. This often happens in producer/consumer implementations.
If this is discovered to be a problem, it's often solved with what is called cache padding. I.e. if you've got variables A and B that are right next to each other and they are modified from different threads, then you add fake variables (padding) between them that's the size of a cache-line (which is between 32 bytes and 256 bytes, depending on architecture).
Cache coherence means that CPU cores with separate caches, but accessing the same RAM have a coherent view. Usually implemented on hardware level with cache coherence protocolls (MESI etc).
What the author wanted to say: The goals was to hopefully avoid cache misses. With []int the values are sequential in memory, so the probability is high that the next value is already loaded into the cache. With []*struct there is a sequential array of pointers which have to be dereferenced. This dereference means random access to memory locations all over the RAM. Probably more cache misses.
The author is correct to measure this, because cache miss patterns are hard to predict and this optimization might not be worth it.
The only downside is that you can't defer computation of values until they are asked for.
[0] http://www.informit.com/articles/printerfriendly.aspx?p=1407...
for ; iter.HasNext(); val = iter.Next() {
// do stuff
}
There's no reason to look down on the humble for loop. Complexity is a bad thing, usually.Channels and goroutines are also a very Go-like way to process data. But APIs that return channels assume that the caller wants concurrency, so be sure that that's true.
No reason? By that you mean that there are no downsides? a hasNext()/next() often means that calling next() when there is no next element (hasNext() == false) will result in a nullpointer exception. That's fine if you always do a hasNext() check before calling next(), but people do make mistakes like that. You might say that something like a loop with a hasNext() conditional is simple enough, but on the other hand I'm not too fond of even using indexing loops in Java (for (int = i = 0; i < array.length; i++) {} ) if you only need to traverse the array one item at a time; you don't need the expressive power of indexing the array rather than simply iterating over it, and this increased power makes you more likely to introduce bugs (wrong initial value of index variable; wrong loop conditional; wrong increment operation: mixing up the indexing variables in a nested loops (the two similar-looking characters 'i' and 'j' are often used in that case). Even ignoring this argument, you might have to make a more complicated loop where the hasNext() or next() is a bit scattered due to some conditionals. (EDIT: since you are talking specifically about for loops this point may not be very relevant.)
I would say that a construct that doesn't encapsulate enough so that you might get a runtime error is in a way worse than a construct that encapsulates enough to guarantee that that mistake can not happen at runtime. It might not be overall worse, but it does have a downside, IMO.
Y'all can't be just plain ignorant about what you can do in a language that has the features to support a proper collection library... So what's up with that? You like them for loops? Or does everything that's so awesome about go make it worth the apparently complete crap state of collections?
I suspect all this has to do with a choice of greater type safety over expressiveness, but I'm not sure. I know there's this big generics discussion in the go community. The thing about generics is you sprinkle butt-ugly and cross-eyed syntax on top of expressive and you get...expressive ugly cross eyed butt syntax collections.
Help me out here -- I read these rave go reviews and then I see this kind of thing and I think "well, it'd be better than Java -- maybe way better -- but it feels like Java redux all over again reheated."
That said, one of the language features that drew me in is structural typing. I'd yet to see a well supported statically typed language that used structural typing, and it seemed like it would make software maintenance a lot easier. Of course, tools are often a better solution than language features, so maybe that's a bad reason to use a language.