Optimizing function implementations for readability

During my early university days, my CS professors would be adamant about keeping function blocks short and concise. Their advice went along the lines of the following:

"If a function implementation does not fit on a single screen, then it must be split up into smaller functions."

Keep in mind that these guidelines have their roots in an era when by screen, people were referring to the amount of code that could fit in an 80×25 character terminal! Fast forward to today where things have changed: software engineers have access to high-resolution monitors, editors, and bespoke IDEs that come preloaded a wide gamut of sophisticated analysis and refactoring tools. Still, the same bit of advice is just as important for writing code that is easy for others to review, extend, and maintain.

In the Single responsibility section, we discussed the merits of the SRP. Unsurprisingly, the same principle also applies to function blocks and is something to keep at the back of your mind when coding.

By decomposing a complex function into smaller functions, the code becomes easier to read and reason about. This may not seem important at first, but think about a case where you don't touch the code for a couple of months and then need to dive back into it while trying to track down a bug. As a bonus, the isolated bits of logic also become easier to test, especially if you are following the practice of writing table-driven tests.

Naturally, it follows that the same approach can be applied to existing code. If you find yourself navigating through a lengthy function that contains deeply nested if/else blocks, repeated blocks of code, or its implementation tackles several seemingly unrelated concerns, it would be a great opportunity to apply some drive-by refactoring and extract any potential self-contained blocks of logic into separate functions.

Additionally, when creating new functions or splitting existing functions into smaller ones, a good idea is to arrange the functions so that they appear in call order within the file they are defined in, that is, if A() calls B() and C(), then both B() and C() must appear below, but not necessarily immediately after, A(). This makes it much easier for other engineers (or people just curious to understand how something works) to skim through existing code.

Each rule comes with exceptions and this rule is no different. Unless the compiler is very good at inlining functions, splitting the business logic across functions sometimes takes a toll on performance. Although the performance hit is, in many cases, insignificant, when the end goal is to produce high-performance code that contains tight inner loops or code that is expected to be called with high frequency, it may be a good idea to keep the implementation neatly tucked within a single function to avoid the extra Go runtime overhead that's incurred when making function calls (for example, pushing arguments to the stack, checking that the stack is large enough for the callee, and popping things off the stack when the function call returns).

There is always a trade-off between code readability and performance. When dealing with complex systems, readability is oftentimes preferred, but at the end of the day, it's up to you and your team to figure out which mix of readability versus performance works best for your particular use case.