2025-02-24
Go's behavior for zero value channels and maps is partly a choice
How Go behaves if you have a zero value channel or map (a 'nil' channel or map) is somewhat confusing (cf, via). When we talk about it, it's worth remembering that this behavior is a somewhat arbitrary choice on Go's part, not a fundamental set of requirements that stems from, for example, other language semantics. Go has reasons to have channels and maps behave as they do, but some those reasons have to do with how channel and map values are implemented and some are about what's convenient for programming.
As hinted at by how their zero value is called a 'nil' value, channel
and map values are both implemented as pointers to runtime data
structures. A nil channel or map has no such runtime data structure
allocated for it (and the pointer value is nil); these structures
are allocated by make()
. However,
this doesn't entirely allow us to predict what happens when you use
nil values of either type. It's not unreasonable for an attempt to
assign an element to a nil map to panic, since the nil map has no
runtime data structure allocated to hold anything we try to put in
it. But you don't have to say that a nil map is empty and looking
up elements in it gives you a zero value; I think you could have
this panic instead, just as assigning an element does. However,
this would probably result in less safe code that paniced more
(and probably had more checks for nil maps, too).
Then there's nil channels, which don't behave like nil maps. It would make sense for receiving from a nil channel to yield the zero value, much like looking up an element in a nil map, and for sending to a nil channel to panic, again like assigning to an element in a nil map (although in the channel case it would be because there's no runtime data structure where your goroutine could metaphorically hang its hat waiting for a receiver). Instead Go chooses to make both operations (permanently) block your goroutine, with panicing on send reserved for sending to a non-nil but closed channel.
The current semantics of sending on a closed channel combined with select statements (and to a lesser extent receiving from a closed channel) means that Go needs a channel zero value that is never ready to send or receive. However, I believe that Go could readily make actual sends or receives on nil channels panic without any language problems. As a practical matter, sending or receiving on a nil channel is a bug that will leak your goroutine even if your program doesn't deadlock.
Similarly, Go could choose to allocate an empty map runtime data
structure for zero value maps, and then let you assign to elements
in the resulting map rather than panicing. If desired, I think you
could preserve a distinction between empty maps and nil maps. There
would be some drawbacks to this that cut against Go's general
philosophy of being relatively explicit about (heap) allocations
and you'd want a clever compiler that didn't bother creating those
zero value runtime map data structures when they'd just be overwritten
by 'make()
' or a return value from a function call or the like.
(I can certainly imagine a quite Go like language where maps don't
have to be explicitly set up any more than slices do, although you
might still use 'make()
' if you wanted to provide size hints to
the runtime.)
Sidebar: why you need something like nil channels
We all know that sometimes you want to stop sending or receiving
on a channel in a select statement. On first impression
it looks like closing a channel (instead of setting the channel to
nil) could be made to work for this (it doesn't currently). The
problem is that closing a channel is a global thing, while you may
only want a local effect; you want to remove the channel from your
select
, but not close down other uses of it by other goroutines.
This need for a local effect pretty much requires a special, distinct channel value that is never ready for sending or receiving, so you can overwrite the old channel value with this special value, which we might as well call a 'nil channel'. Without a channel value that serves this purpose you'd have to complicate select statements with some other way to disable specific channels.
(I had to work this out in my head as part of writing this entry so I might as well write it down for my future self.)