Me talking about Line of sight in code at Golang UK Conference, London 2016

Line of sight in code

At the recent Golang UK Conference in London, I spoke about line of sight in code in my Idiomatic Go Tricks talk (slides are online) and I wanted to explain it a little further here.

Line of sight is “a straight line along which an observer has unobstructed vision”
Line of sight in code: Left edge is happy path, indented is error handling and edge cases

A good line of sight makes no difference to what your function does, but it does help other humans who might need to read your code. The idea is that another programmer (including your future self) can glance down a single column and understand the expected flow of the code. If they have to jump around parsing if conditions in their brains moving in and out of code blocks, it makes that task much more difficult.

Most people focus on the cost of writing code (ever heard “how long will this take to finish?”) But the far greater cost is in maintaining code — especially in successful projects. Making functions obvious, clear, simple and easy to understand is vital to this cause.

Tips for a good line of sight:

  • Align the happy path to the left; you should quickly be able to scan down one column to see the expected execution flow
  • Don’t hide happy path logic inside a nest of indented braces
  • Exit early from your function
  • Avoid else returns; consider flipping the if statement
  • Put the happy return statement as the very last line
  • Extract functions and methods to keep bodies small and readable
  • If you need big indented bodies, consider giving them their own function

Of course, there will be plenty of great reasons to break all of these rules — but adopting this style as a default, we have found that our code becomes much more readable.

Avoid else returns

A key to writing code with a good line of sight is to keep the else bodies small, or avoid them altogether if you can. Consider this code:

if something.OK() {
something.Lock()
defer something.Unlock()
err := something.Do()
if err == nil {
stop := StartTimer()
defer stop()
log.Println("working...")
doWork(something)
<-something.Done() // wait for it
log.Println("finished")
return nil
} else {
return err
}
} else {
return errors.New("something not ok")
}

This represents how we might initially think about what our function is doing (“if something is OK, then do this, if there are no errors, then do this” etc.) but it becomes quite difficult to follow.

The ‘happy path’ (the route that execution will take if all goes well) is difficult to follow in the above code. It indents on the second line and continues from there. When we check the error return from something.Do(), we indent further. In fact, the happy return statement “return nil” is completely lost in the code.

It is very common for the else bodies to be a single returning line — in Go as well as other languages, as they deal with aborting or exiting the function so they don’t warrant indenting the rest of our code.

Flip the if statement

If we were to flip the if statements (! bang them, if you like), you can see that the code becomes much more readable:

if !something.OK() {  // flipped
return errors.New("something not ok")
}
something.Lock()
defer something.Unlock()
err := something.Do()
if err != nil { // flipped
return err
}
stop := StartTimer()
defer stop()
log.Println("working...")
doWork(something)
<-something.Done() // wait for it
log.Println("finished")
return nil

In this code, we are exiting early and our exit code stands apart from our normal code. Also,

  • the happy path flows down the left hand edge,
  • we indent only to deal with errors and edge cases,
  • our happy return statement “return nil” is in the last line, and
  • we have fewer indented code blocks.

Promote big conditional blocks to their own functions

If you cannot avoid a chunky else body or bloated switch/select cases (I get it, sometimes you can’t), then consider breaking each body into its own function:

func processValue(v interface{}) error {
switch val := v.(type) {
case string:
return processString(val)
case int:
return processInt(val)
case bool:
return processBool(val)
default:
return fmt.Errorf("unsupported type %T", v)
}
}

This is much easier to read than having all the processing code inside the cases.

Share your experience

If you agree with me, please consider sharing this post — as the more people who sign-up to this, the better (and more consistent) Go code will become.

Do you have some code that’s tricky to read? Why not share it on Twitter @matryer and we can see if we can find a cleaner, simpler version.

Thanks to…

The reviewers Dave Cheney, David Hernández and William Kennedy.