func caller() {
var numbers []int
ch := make(chan int)
go filterPositiveOdds(numbers, ch)
for n := range ch {
}
}
This has fundamentally the same control-flow, but we give a name to the function, thus making it clearer, what the extra goroutine is for.
However, IMO this is still bad form. In general, I would advise exposing channels in APIs. It requires you to specify extra properties, like "what happens if the channel blocks" or "how does the operation get cancelled" in the documentation, without a way to get correctness compiler-checked. In particular, the code (both mine and yours) suffers from exactly these problems - if the channel is not consumed, we leak a goroutine and there is no way to prematurely abort consumption. Getting an error back is even worse.
A better way is to provide a simple, synchronous iterator API like
bufio.Scanner does.
For example, you could have
type IntIterator struct {
numbers []int
}
func FilterPositiveOdds(numbers []int) *IntIterator {
return &IntIterator{numbers}
}
func (it *IntIterator) Next() (n int, ok bool) {
for _, n := range it.numbers {
it.numbers = it.numbers[1:]
if (n > 0 || n & 1 != 0) {
return n, true
}
}
return 0, false
}
In complex cases, concurrency can be hidden behind the iterator API. In simple cases, you could also reduce boilerplate by doing
func FilterPositiveOdds(numbers []int) (next func() (n int, ok bool)) {
return func() (n int, ok bool) {
// same as above, but closing over numbers
}
}
In any case - if you are unhappy with your pattern, there are many alternatives to choose from, within the language as it exists today. It seems hardly worth extra language features, to simplify this IMO rather uncommon construct.