If you choose to select indefinitely, yes, you will need another channel to signal when you should return, or the goroutine will continue to run, and continue to hold a reference to that first channel.
If you choose to select indefinitely, yes, you will need another channel to signal when you should return, or the goroutine will continue to run, and continue to hold a reference to that first channel.
A concrete example, slide 34 [1] contains this code:
func main() {
c := boring("Joe")
for {
select {
case s := <-c:
fmt.Println(s)
case <-time.After(1 * time.Second):
fmt.Println("You're too slow.")
return
}
}
}
This is code that looks really simple, but it's only correct because the process is known to terminate. You couldn't, for example, have the body of this function as a subroutine somewhere: every time you execute it a boring() goroutine is going to be left hanging, which is a classic memory leak. What's worse, "c" looks like an average variable I don't need to worry about (yay garbage collection!) but it's not because it escapes to a running goroutine.Correct me if I'm wrong but even if boring() was a much simpler function that returned after generating one value, if the timeout happened first it would be left hanging. In that case the only thing that would make the program correct is that of two racing goroutines, one is expected to return first. That's the kind of thing that keeps me up at night, especially when Go makes it so easy to treat goroutines and channels like every other GC-managed data structure when in fact they are every bit as leaky and difficult as threads.
Such is the case of the time.After() function, which easily solves the problem by returning a buffered channel. That would not work for an infinite generator of course.
The power and orthogonality of Go's concurrency primitives is actually encouraging. I'm not picking holes in it because I think Go is a bad language. On the contrary, I just think that it's not the endgame of concurrent programming. Using channels is not a safe operation in the same way that passing around memory references in a GC-managed language is safe, or passing values in a type-safe language is safe. You can write a correct, concurrent program with pretty much any behavior you want in Go, but it's still a minefield of deadlocks and memory leaks. I think there's room for improvement there.
(By open mind, I mean that I acknowledge that I have years of being steeped in an asynchronous message passing environment and that I may need to unlearn some things and learn others. I'm a polyglot programmer, so this isn't my first time for that, either. There are some some useful things that Go's channel style is easier to work with than Erlang's style. Still, right now I'm cautiously negative on the net utility of that, even so.)
I've deadlocked Erlang twice in many years. It didn't make it to production either time. I deadlocked Go twice in the first two days, and several times since. (And Go terminates the entire process upon deadlock.) And what really concerns me in terms of recommending it for work is that while I instantly understood how I deadlocked Go, it was only because of the fact that I'm fairly experienced now in reasoning with these sorts of advanced concurrency constructs. Many of my coworkers would have been stuck for hours.
It's not out of the running yet, considering many of its positive qualities and the competition it is up against, but the Unambiguous Win I was hoping for is not emerging. (Many of these issues may be avoidable with style; for instance, one can write goroutines that manage resources in a fairly Erlang-y style and which are services that have a continuous receive-reply loop, and those are safe enough. But I'd really rather have more compiler support here rather than depending on convention; that's how we got to the realization that conventional threads are a bad idea in the first place.)
My start with go was just like yours, but it quickly started to "click", and more robust patterns began emerging more naturally for me. I don't think I can verbalize yet what has made the difference for me, but it may something like "structure your code around channels" vs "use channels to synchronize your code".
When I first started with golang I too was flailing around with deadlocks, but once you have spent a bit of time with it, it becomes obvious when you have messed up.
I think that this is more of a lack of familiarity with the language rather than an intrinsic problem with golang.
That is exactly what I can't get over. I don't understand why this is the default. Most processes in the real world are not synchronous. Sending an asynchronous message makes more sense as a primitive (synchronous can just be modeled on top as a mini protocol, sender sends his own address). The other way is not as clear. (Spawn a separate concurrency primitive and send to the channel from there?).
The other thing that bothers me is the focus on channels rather than on processes. The two can be seen as equivalent but for some reason process-centric design seems more natural to me.
There is of course Scala with Akka but I don't see JVM a benefit, I see it as a nuisance (it is more of an irrational political thing probably come to think of it).