At the recentGolang UK Conference in London, I spoke aboutline of sight in code in myIdiomatic 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”
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:
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.
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 fromsomething.Do()
, we indent further. In fact, the happy return statement “return nil
” is completely lost in the middle of 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. I don’t think they warrant indenting the rest of our code.
If we were to flip theif
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,
return nil
” is in the last line, andIf 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 lots of processing code inside the cases.
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 onTwitter @matryer and we can see if we can find a cleaner, simpler version.
The reviewersDave Cheney,David Hernández andWilliam Kennedy.
Founder atMachineBox.io — Gopher, developer, speaker, author — BitBar apphttps://getbitbar.com — Author of Go Programming Blueprints