Proposal Details

I'm not sure if this is a bug, or an intended behavior (couldn't find the answer anywhere; looked through package docs, release notes, and GitHub Issues), but as someone who's been using Go for more than a decade, I was really surprised to realize that ranging over nil iterators panics.

E.g. the following code panics:

// This panics:
var it iter.Seq[int]
for n := range it {
    fmt.Println(n)
}

While ranging over nil slices and maps is allowed:

// Doesn't panic:
var it []int
for n := range it {
    fmt.Println(n)
}
// Doesn't panic
var it map[int]int
for n := range it {
    fmt.Println(n)
}

The map example is the thing that brought me to raise this problem. Normally uninitialized maps are not useful anywhere, and accessing them causes panic, but ranging over them is allowed. Seems inconsistent (and frankly a bit unfair) that doing the same with iterators is not allowed.

So, I'd like to propose to allow ranging over nil iterators in the similar way as ranging over nil slices and maps is allowed. I'd assume this is a backwards-compatible change.

Comment From: gabyhelp

Related Issues and Documentation

(Emoji vote if this was helpful or unhelpful; more detailed feedback welcome in this discussion.)

Comment From: thepudds

See https://github.com/golang/go/issues/65629#issuecomment-1973523788.

Comment From: burdiyan

Thanks @gabyhelp, @thepudds! Not sure how did I miss the other thread, because I did run a search for various forms of "nil iterator".

Now at least I understand the behavior, although I disagree quite a bit with the decision.

Maybe it would be useful to document this behavior somewhere in the iter package. Even though Russ' argument of "iterators are functions, and calling a nil function panics" makes sense, it's a bit confusing if you think about iterators as some special kind of function, which I assume would be pretty common even for seasoned gophers, at least in the beginning until you realize that there's nothing special about them.

Comment From: thepudds

Hi @burdiyan, maybe it could be more explicit, but it's maybe at least arguably implied by the spec?

From the "For statements with range clause" section (emphasis added):

  1. For a function f, the iteration proceeds by calling f with a new, synthesized yield function as its argument.

and from the "Calls" section:

Calling a nil function value causes a run-time panic.

Comment From: earthboundkid

It's too late to revisit #65629, but package iter could have a None function that yields nothing and a Some or One function that just yields one thing. The alternative is to either DIY, or do slices.Values([]T{}), but using slices.Values is less efficient than a custom type, and DIY is too much effort for a simple, common task.

Comment From: burdiyan

Thanks @thepudds, the revisited spec is what I missed to read!

Similar to what @earthboundkid is suggesting, I propose adding a few convenience functions to the iter package (similar to the ones bellow), which could help users construct empty iterators that are safe to range over. I ended up doing exactly this in my own iterx package where I have a few other utilities for working with iterators.

// NopSeq is a convenience function
// which returns an empty no-op Seq
// that is safe to range over.
func NopSeq[T any]() Seq[T] {
    return func(func(T) bool) {}
}

// NopSeq2 is the same as NopSeq, but for Seq2.
func NopSeq2[K, V any]() Seq2[K, V] {
    return func(func(K, V) bool) {}
}

The presence of these functions would presumably indicate that ranging over a nil iterator is not safe.

Comment From: Merovius

The map example is the thing that brought me to raise this problem. Normally uninitialized maps are not useful anywhere, and accessing them causes panic. Seems inconsistent (and frankly a bit unfair) that doing the same with iterators is not allowed.

No, only writes cause a panic, all read-operations are fine (and return the zero value). ~Arguably, the same is true for nil slices~ (that's just confusing the matter here, TBH)

To be clear: My point is that you are making an argument-from-consistency. But I disagree with that. Maps and functions and channels and pointers might all be able to be nil, but they all behave very differently and in ways that are very specific to their relative kind of type. For example, range over a nil channel blocks forever, instead of just doing nothing, which is also "inconsistent" with other types. range over a nil pointer panics, which is consistent with func. It's all rather incomparable.

Comment From: seankhliao

Duplicate of #65629

Comment From: earthboundkid

Made an issue for Nop and friends: #68947.