After a bit more testing, I decided to go with the slice version. In
reality, all variations discussed in this thread would work fine.
Thanks to everyone for their suggestions.
As pointed out, the linked list could create a lot of extra garbage
and was a bit slower than all other alternatives in my testing. This
could be improved by storing multiple values in each node, but I don't
think that's worth it. Ring introduces extra logic and only wins over
the slice version on the total number of allocations and copies. The
overall performance is about the same or even slower, depending on the
actual workload.
Now I have a new problem to deal with. Suppose I want to drain the
entire pipe without indefinite blocking. The 'to' end of the pipe has
a channel buffer for just one value, the rest are stored in the
internal buffer (slice). The following code seems to work in some
cases, but not others:
for done := false; !done; {
select {
case v := <-to:
...
default:
done = true
}
}
Basically, I'm not sure how the scheduler works when 'v := <-to' is
evaluated and 'to' is found to be empty. If I do something like
'fmt.Println(v)' after v is received, then the loop receives all
buffered values. Removing that print statement causes the loop to exit
after the first value. How can I distinguish the case where 'to' is
empty because the pipe goroutine didn't get a chance to refill to's
buffer, from the case where there are no more values available?
- Max