1. 49
  1. 7

    Really good one. I got interested in Go immediately in 2009, contributed pre-1.0, been using it professionally for 6 years now, and still learned two things here: about the iota counting behavior, and that reading from nil maps is ok.

    As to something that could possibly be added to the article, the best pitfall of Go that comes to my mind is the value-receiver data race, as described by Dave Cheney. Took me a long while to grasp what’s going on, and is really subtle.

    1. 1

      Is it because the channel is getting copied?

      1. 3

        I think it’s that compute() ‘owns’ rpc and might be writing to it at the same time the following line is (implicitly) copying rpc onto the stack to serve as a value receiver. It’d still be a race if the sync stuff were located outside the struct; the only important thing is something might be messing with the struct in another thread while you call one of its methods that has a value receiver.

        It’s not a race when version() has a pointer receiver only because version() doesn’t access anything that compute() is messing with (since it doesn’t access anything at all!).

        1. 2

          Yep, the sneaky thing is that calling a value-receiver method is implicitly a “read” of the receiver object due to the need to copy it (even if the receiver value is ostensibly ignored, as in this case, a copy is apparently still being made). I spoiled it somewhat by how I titled the link; if I saw such code in a code review before reading this article, I would not connect the dots and realize that calling a value receiver triggers an implicit read from data race point of view.

        2. 2

          I found this explanation on the golang-nuts list: https://groups.google.com/g/golang-nuts/c/gJtXQhpoA4U/m/Atlm2IHVCAAJ

      2. 4

        Pretty solid article!

        • I did not know about “.” imports
        • difference between a nil slice and an empty slice: I’d mention how they’re encoded differently to JSON (arguably a bug)
        • maps as sets: a second disadvantage to map[x]bool is that it’s a redundant encoding, leading to plenty of chances of confusing things (been there)
        • would have liked to see named return values labelled a trap, but suppose there’s not really anything particularly surprising about them – they just make the code harder to read
        1. 3
          • would have liked to see named return values labelled a trap, but suppose there’s not really anything particularly surprising about them – they just make the code harder to read

          Oh, there is something surprising about named return values namely that they’re the only way to return an error from a deferred function.

          Deferred functions may read and assign to the returning function’s named return values. [source]

          Here’s an example that demonstrates this.

          1. 1

            Right, that’s their reason for being even as far as I understand, since it’s the only way to return errors from defers.

        2. 2

          Good article! Misleading title though, those are some of the brightest corners I’ve ever seen ;)

          1. 1

            Looks like the website is down. It’s on the web archive though: https://web.archive.org/web/20210322071128/https://rytisbiel.com/2021/03/06/darker-corners-of-go/

            1. 1

              There’s a darker corner of _ imports that the Go core devs failed to account for. My take: if a package that’s intended to be _ imported for side-effects has any public functions in it, you’ve made a mistake.

              Case in point: net/http/pprof, which is mentioned in this article. It’s a nice debugging convenience, that adds some routes to net/http.DefaultServeMux (itself a convenience global) providing access to profiler traces. The methods for serving those traces over http are also in the package, and there’s a nice public Handler function that gives you the handler for a given trace in case you need to use it somewhere else. All seems good.

              What if you’re not using DefaultServeMux? Well, there’s a note in the docs that says “If you are not using DefaultServeMux, you will have to register handlers with the mux you are using”, which explains the usefulness of that Handler function.

              What if you are using DefaultServeMux, but you want to expose those profiling endpoints on a different HTTP server, on a different port, maybe for security reasons? Well, that’s where you’re kind of screwed. You can’t import the package to get Handler because as soon as you import it, it will register its handlers on the default mux, and there’s no way to tell it not to, and no way to unregister handlers once registered. Your only choices are A) rewrite the app so that it doesn’t use DefaultServeMux (and therefore anything registered on it won’t be exposed) or B) reimplement what net/http/pprof does in your own package, without the offending init function.

              To avoid all of this trouble, they could have had one package that contained the implementation, and Handler, and even a RegisterOnMux function that would install all of the handlers on a given mux. And then another package that contained nothing but func init() { pprof.RegisterOnMux(http.DefaultServeMux) } so that people could still have the one-line convenience of the _ import. They didn’t, and it’s too late to fix it now (for Go 1, at least), but I hope other people can learn from the mistake.