WTF-debugging: the case of the unfortunate design choices fooling perception

A few of my former colleagues at Google really love golang so I decided to start playing with it. I highly recommend the interactive tour https://tour.golang.org to get a quick sense of it. It's a fairly nice language, simple but with the parts you really need, feels nice and "javascripty" in object creation but still structured and typed strictly enough.

However, there is at least one case where the desire to avoid too prescriptive syntax results in an unfortunate combination of design choices leading to WTF-debugging.

Consider https://play.golang.org/p/yUlFI3aqYSS (run it, it prints 1,2,3,4). Now change line 12 (which could have been defined much further away, even in another file) by adding an asterisk in position 8 before Payload, to read:

func (p *Payload) UploadToS3() error {

Run it again and observe 4,4,4,4! Note that the loop where this happens is unchanged, but the loop variable "payload" has magically been changed from a value to a pointer. Spooky action at a distance now causes a different part of your program to be wrong. Keep staring at the loop and you will never figure it out.

    for _,payload := range payloads {
        go payload.UploadToS3()
    }

In java we would always know what is a value and what is a reference and we are of course also saved by the fact that variables used in closures have to be final (or effectively final). And in a functional language the variables would be immutable so this would never happen there either. In javascript, though, we deal with this all the time, so a javascript programmer might be more confused that the first version actually worked. One of the problems in go is that we can have either values or pointers, but we don't have to be explicit about it because the compiler is too helpful. Another problem is that it is unclear what code is executed now and what code is executed later. The word "go" is (at least not yet) a strong signal to my mind that the code after it is actually executed later. I have to go into deep analysis mode before I have a chance to perceive that. Consider the difference if we had been forced to write a little more, would it have been clearer?

    for _,payload := range payloads {
        go func() {
          payload.UploadToS3()
        }()
    }

I think that is slightly clearer, but I think it still indicates the problem with inline closures for asynchronous computing. In java I would tend to recommend to never use anonymous inner classes, but take the time to make it a named inner class, defined elsewhere, so you have a better chance of realizing that the code will execute at another time. I have seen otherwise excellent coders stumble on this temporal misperception.

Interestingly, the perceptual difficulty of when code executes can also go the other way. When using Mockito, the below code doesn't immediately signal your brain that the bar() method actually gets called.

    when(foo.bar()).thenReturn(5);

Comments

Popular Posts