1. 16
  1.  

    1. 11

      That’s a lot of testing and wording when the spec is quite clear:

      A nil channel is never ready for communication.

      With clarifications in the spec for sending

      A send on a nil channel blocks forever.

      and receiving

      Receiving from a nil channel blocks forever.

      in case the original statement was not clear enough.

      1. 4

        I thought the main benefit of the article was towards the end, and what @carlana summarized well here:

        Nil channels are useful because essentially any practical use of channels should have a select with multiple branches and nil lets you turn off one branch of the select while keeping the others working. Even the most basic thing you can do, send work to some worker goroutines and receive answered data back, will have two branches for send data and receive data and at a certain point you will have sent everything and want to turn the sending branch off while still receiving.

        I’ve been writing Go since 2012 and I didn’t know this. 🤷‍♂️

      2. 11

        Nil channels are useful because essentially any practical use of channels should have a select with multiple branches and nil lets you turn off one branch of the select while keeping the others working. Even the most basic thing you can do, send work to some worker goroutines and receive answered data back, will have two branches for send data and receive data and at a certain point you will have sent everything and want to turn the sending branch off while still receiving.

        1. 5

          You don’t really need nil for that, even if it’s the easiest ATM. The same thing can be done with an empty channel for select arms that receive, and a channel with a full buffer for send arms. The zero value for a channel could’ve instead been a valid channel with no buffer, which would satisfy both of the above.

          I’m personally not a fan of having nil be a valid value for some blessed types, it’s pretty error prone IMO (as nil is in general).

          1. 3

            Some of these things are historical patterns that have trickled down. Go really should be approached in the context of C and non-object oriented approaches to compound types. They were not trying to make an entirely new language with no baggage: they were comfortable with C and they just wanted something a bit better. If that’s a good thing or not is a different matter. I’d say for some things the defaults are not clear; some these things could be compile or runtime errors.

            But much like in C a variable declaration does not initialize it (disregard zero values here); it’s also not a compile error to use an unitialized variable. Just like in C to both create and initialize things you resort to function patterns like makeXmake(X) in Go – or newXNewX in Go. Just like in C passing an unitialized pointer to a struct as an argument to a function is allowed and the function decides what to do with it (e.g. passing a nil slice to append).

            In the Plan9 source code you find these exact same patterns in C which makes the decisions in Go much clearer. For example the function that appends to a string automatically initializes it if the string pointer given to it is NULL. In the absense of a constructor-like approach this kind of late-initialization helps with the ergonomics when writing C. That’s why you also see the pattern if x == nil { x.init() } in some Go methods where x is a pointer receiver.

            Having that frame of reference also help make some “weird” things clearer, like why you can append to a nil slice but not assign to a nil map – append() is a function and initializes the nil slice on the spot; somemap["key"] = "value" is a dereferencing operation and dereferencing a NULL pointer (the nil map) makes no sense. A whole bunch of other things also start to make more sense in that headspace.

            1. 3

              I know why Go is how it is, and understand their decisions. Doesn’t mean I can’t disagree with some of them! “It is what it is” is not a good reason for status quo when designing, what at the time was, a new thing; especially when trying to make it fool/Googler proof ;)

              Here specifically, the chan struct could’ve been designed so the zero value is an unbuffered empty channel, which makes sense for a zero value and avoids needing to add nil to the mix.

              1. 3

                Yeah I wasn’t necessarily writing to you in particular but more in general for people that might pass this by and maybe get a better way to look at these issues. There’s plenty to disagree!

                Digressing into the second topic, I think they were well justified in not straying from the status quo when creating Go. I believe many of the decisions they made were actually very good and showed foresight, even if the language does not cover all the bases. But that’s a longer conversation.

            2. 2

              I never thought about this. Thanks! make(chan T, 0) indeed seems to work as nil with my (simple) tests.

              https://go.dev/play/p/bnVOQnQMW-c

            3. [Comment removed by author]

            4. 4

              I think the language would be a lot nicer without the make() init. In the last 8 years using Go, every time I took a break away from Go and came back, the nil map, nil channel, and zero maps init tripped me up quite consistently. With generics available now, I think there are plenty of ways to clean up these older APIs and make the language friendlier to new users.

              1. 3

                I was going to write a comment about why you really want nil channels, but carlana already did so I’ll just bring it to your attention in case you’re only watching replies.

                Nil maps aren’t so directly useful, but the difference between an empty map and no map at all is 48 bytes, which is non-negligible if you have some kind of data structure with millions of maps that might or might not exist.

                1. 4

                  I was going to write a comment about why you really want nil channels, but carlana already did so I’ll just bring it to your attention in case you’re only watching replies.

                  The issue is not the functionality, it’s the implicitness. It’s that if you forget to make() your channel you get a behaviour which is very likely to screw you over when you don’t expect it.

                  1. 2

                    If it gave you a channel by default it would be impliclty open or closed, buffered (with some capacity) or unbuffered. But it declines to do that, and has you construct what you want, which makes the code more explicit.

                    At the same time, there’s a need for one more thing, a (non)channel that is never available for communication. Go types must always have a zero value, and “a channel that’s never ready” is a lot more zero-ish than anything else you could come up with.

                    Yes, there’s a guideline that zero values should be useful when possible (a nil slice is ready for append, a zero bytes.Buffer is empty, a zero sync.Mutex is unlocked), but that takes a backseat to the requirement for a zero value to be uniquely zero. Complaining about how

                    var ch chan int
                    ch <- 42
                    

                    fails feels the same as complaining about how

                    var ptr *int
                    *ptr = 42
                    

                    fails.

                    1. 4

                      If it gave you a channel by default

                      How about it doesn’t do that either.

                      But it declines to do that

                      Would that it did. It does give me a channel by default, one that is pretty much never what I want.

                      which makes the code more explicit.

                      Ah yes, the explicitness of implicitly doing something stupid.

                      Go types must always have a zero value

                      Have you considered that that’s a mistake?

                      Complaining about how […] fails.

                      It does does it not? In both cases the language does something which is at best useless and at worst harmful, and which, with a little more effort put into its design, it could just not do.

                      1. 1

                        one that is pretty much never what I want.

                        Like I said elsewhere, for pretty much every non-trivial use of channels, you will want a nil channel at some point, so you can deactivate one branch of a select. It’s pretty much always one of the things I want.

                        1. 1

                          I think I disagree? Or at least I’ve never made good use of a nil channel. Maybe now that I’ve learned about its uses for select{...} I’ll have a different opinion, but there have been plenty of times I don’t want a nil channel. And this problem isn’t limited to channels either–Go also gives nil pointers and nil maps by default, even though a nil pointer or map is frequently a bug. Defaulting to a zero value is certainly an improvement on C’s default (“whatever was in that particular memory region”), but I think it would be a lot better if it just forced us to initialize the memory.

                          1. 3

                            I do wish that map was a value type by default and you would need to write *m to actually use it. That would be much more convenient. The Go team said they did that in early versions of Go, but they got sick of the pointer, so they made it internal to the map, but I think that was a mistake.

                            1. 3

                              Defaulting to a zero value is certainly an improvement on C’s default (“whatever was in that particular memory region”)

                              Technically it’s UB, which is even worse. You may get whatever was at that location, or you might get the compiler deleting the entire thing and / or going off the rails completely.

                              1. 2

                                Good point. Even keeping track of what is/isn’t UB is a big headache.

                              2. 1

                                I think it would be a lot better if it just forced us to initialize the memory.

                                That would have implications across the whole language design that wouldn’t, in my opinion, be overall good. Zero-initialization is quite fundamental.

                                This is also my answer to masklinn’s “how about it doesn’t do that either” in a comment I couldn’t bring myself to respond directly to.

                                1. 2

                                  Yeah, it’s not going to happen, but I’m convinced that would have been the choice to make in 2012 (or earlier). I can live with it, and Go is still the most productive tool in my bucket, but that particular decision is pretty disappointing, especially because we can’t back out of it the way we could have done if we had mandatory initialization (you can relax that requirement without breaking compatibility).

                        2. 1

                          To add to that point, there’s another issue which is that many channels oughtn’t be nil, but Go doesn’t give us a very good way to express that. In fact, it goes even further and makes nil a default even when a nil channel would be a bug. I really, really wish Go had (reasonably implemented) sum types.

                      2. 3

                        I haven’t done Go seriously in a while, but when I did, I was continually annoyed at this sort of thing because there’s no way to encapsulate these patterns to make it easy to get them right. I remember reading a Go team post about how to use goroutines properly and ranting about how Go’s only solution for reusing high-level code patterns is blog posts.

                        But now that it has generics, is it possible to solve this? Has someone made a package of things like worker pools and goroutine combinators (e.g., split/merge) that get this stuff right so you don’t have to rediscover the mistakes?

                        1. 2

                          As an example of what annoyed me, it was things like this blog post on pipelines and cancellation. Which should have just been a library, not a blog post.

                            1. 1

                              Yes! Thank you, I will check this out, as it looks like I may have a Go project coming up in the near future.

                              1. 3

                                Conc has a truly awful API. It really shows the power of writing a good blog post to make your package popular. I made my own concurrency package, and there were no ideas in conc worth copying. Honestly though, my recommendation for most people is to just use https://pkg.go.dev/golang.org/x/sync/errgroup since it’s semi-standard.