Efficient Go

Data-Driven Performance Optimization

With Early Release ebooks, you get books in their earliest form—the author’s raw and unedited content as they write—so you can take advantage of these technologies long before the official release of these titles.

Bartłomiej Płotka with Frederic Branczyk

Efficient Go

by Bartłomiej Płotka with Frederic Branczyk

Copyright © 2022 Alloc Limited. All rights reserved.

Printed in the United States of America.

Published by O’Reilly Media, Inc., 1005 Gravenstein Highway North, Sebastopol, CA 95472.

O’Reilly books may be purchased for educational, business, or sales promotional use. Online editions are also available for most titles (http://oreilly.com). For more information, contact our corporate/institutional sales department: 800-998-9938 or corporate@oreilly.com.

- Acquisitions Editor: Suzanne McQuade

- Development Editor: Melissa Potter

- Production Editor:

- Copyeditor:

- Proofreader:

- Indexer:

- Interior Designer: David Futato

- Cover Designer: Karen Montgomery

- Illustrator:

- May 2022: First Edition

Revision History for the Early Release

- 2021-09-16: First Release

See http://oreilly.com/catalog/errata.csp?isbn=9781098105716 for release details.

The O’Reilly logo is a registered trademark of O’Reilly Media, Inc. Efficient Go, the cover image, and related trade dress are trademarks of O’Reilly Media, Inc.

The views expressed in this work are those of the authors, and do not represent the publisher’s views. While the publisher and the authors have used good faith efforts to ensure that the information and instructions contained in this work are accurate, the publisher and the authors disclaim all responsibility for errors or omissions, including without limitation responsibility for damages resulting from the use of or reliance on this work. Use of the information and instructions contained in this work is at your own risk. If any code samples or other technology this work contains or describes is subject to open source licenses or the intellectual property rights of others, it is your responsibility to ensure that your use thereof complies with such licenses and/or rights.

978-1-0981-0571-6

[LSI]

Preface

Welcome to the “Efficient Go” book! Get comfortable, take your time and let’s start with a short section that will help you read this book efficiently! In the following sections, you will learn the main goals behind this book, and why I found it necessary to focus my time on the efficiency subject. You will also learn how to get the most of this book and effectively use it in your software development tasks.

Why I Wrote This Book?

The real problem is that programmers have spent far too much time worrying about efficiency in the wrong places and at the wrong times; premature optimization is the root of all evil (or at least most of it) in programming.

Donald E. Knuth, Computer Programming as an Art (1974)

“Premature optimization is the root of all evil” has long been the rallying cry by software engineers to avoid any thought of application performance until the very end of the software development cycle (at which point the optimization phase is typically ignored for economic/time-to-market reasons).

Randall Hyde, The Fallacy of Premature Optimization (2009)

What do you think about efficiency optimizations? Are you in the #OptimizationIsTheEvil team or #HighPerformance team? Or a little bit of both?

On the one hand, developing software is challenging. It always was. Creating functionalities, features, ensuring compatibility, maintainability, reliability, and other qualities in fast pacing environment is expensive and takes time. It would be convenient if we could skip performance and efficiency aspects which usually takes a lot of time. Optimizations can hurt the project development created in any programming language, mostly if done unnecessarily. On the other hand, I genuinely believe that even in the 20s of the twenty-first century, we cannot completely forget about software efficiency. And yes, I am saying this boldly while having a hand-sized computer called a smartphone with an 8-core CPU, 5G network, 12GB of memory, and GPU in my pocket.

I would like to invite you to a third group through this book: the #PragmaticEfficency team. Why? Let me explain to you why efficiency still matters in daily programming and system building job.

I learned this hard way through various stages of my career. In the beginning, in my CS university in Gdańsk, I was learning C++, algorithms, and data structures on those particular coding platforms, novel at that time. Those platforms allow to define the problem, input, and output and submit a solution as an isolated function. Our grade was based purely on the platform evaluated score, running automatic unit tests, and measuring execution time. Test cases were brutal and had stringent latency limits. In a way, exercises like those were harmful to our young and fragile programming mentality. Because no human was reading our code, readability and robustness did not matter. We learned to see every line of code as a source of stress causing milliseconds. We joined our first dev jobs knowing that cin/cout are few nanoseconds faster than scanf/printf, but missing experience on how to name variables or write robust unit tests.

As you can imagine, professional software development drastically shifted my coding priorities. After all, code had to fulfill business needs, so the most important mission was to deliver as many features in as little time possible. Most of the performance-related optimizations were not rewarded and summarized as premature or micro-optimizations. We were encouraged to produce value quickly under short deadlines. The fact that my first major job was at Intel in the R&D1 unit was only magnifying this effect. In the end, this was an amazing few years of a learning experience, but most of our work was hitting the garbage bin sooner or later. After a time, we switched to using Go, which allowed us to produce features in our infrastructure division even faster. Did efficiency and other software qualities like code readability matter at that time, even for a research-related job? When looking at this now, I would be positive. We worked in bigger teams, sometimes contributing to big projects like OpenStack2 or Mesos3. However, at that time, my perception was quite the opposite: Optimizations were, in most cases, evil.

In 2016, I moved to the United Kingdom and joined a startup in London called “Improbable.” When you grow out of the junior development level, you feel like you know almost everything about coding. Of course, I was never so wrong! I joined Infrastructure Team, which was following SRE4 pattern. We were running large microservices clusters in dozens of data centers worldwide that had to run 24h, seven days a week. We were developing Go services, running them on the scale on Kubernetes, and performing strict on-call duties. I learned that efficiency and performance of your software matters and it isn’t easy to make it so. It did not matter if your API call returns a valid result if it takes minutes to complete. It does not matter if your service is stable and can handle incoming traffic if it uses too much computing resources, and we are running out of the cloud provider quota or startup funds.

Fast-forward few years, I was maintaining a few popular open source projects written in Go, like Prometheus5, Thanos6 and gRPC middlewares7, building large observability systems at Red Hat and being active in the CNCF8 community. Open source software demands different development priorities, too, flipping constraints and requirements, usually bringing maintainability and operational simplicity over high performance.

If that is true, how can we explain that for highly adopted software like Prometheus or Thanos one third9 of all reported issues were mentioning “too high memory usage”, “request too slow”, “too much disk used” or “service saturating my network”? This highlight existence of the elephant in the room: software efficiency. We could try to argue that maybe those are extremes because both of those are more or less time-series databases, or we are just simply bad at coding. Maybe. Even if this is the case, it only proves the value in elaborating on the efficiency topic even more!

The fact that there is strong demand for efficiency optimizations on important, heavily used software functionalities is only one part of the book’s motivation. Through all those years, I noticed hygiene, etc. that there are not many resources about pragmatic efficiency (There is the reason why this book is called Efficiency Go, not Ultra High Performance Go!, Go performance is magic TBD

This book is about giving you tools and knowledge to answer when and how to apply performance efficiency optimization, efficiently, without wasting your time.

This book will equip you with the knowledge and toolset, allowing you to find an outstanding balance that strongly depends on your organization’s goals.

Who This Book is For?

I designed this book for developers who:

- Want to make their next step in Go

- Who doesn’t know when to optimize their program for efficiency and how much is enough?

- Who wants to know how to make their system be faster and use fewer resources while maximizing chances of quick wins

TBD

How This Book is Organized?

This book is organized into three distinct parts. The first one goes through essential knowledge and efficiency awareness. This part is mostly language-agnostic too!

Chapter 1 will allow you to navigate different software qualities and decide what to focus on, when efficiency can help you and when to prioritize other aspects of the software. In chapter 2, we will dive into Go specific. You will learn how Go treats efficiency and the main places where you should be careful. In chapter 3, we will also go through essential computational resources and their characteristics. You will learn pragmatic ways of thinking about those that can allow you to focus on what’s matters when optimizing for efficiency. Chapter 4 will walk you through a practical optimization process. Efficiency can take enormous amounts of your time, but systematic approaches help you save a lot of time and effort.

In part 2, you will learn how to gather data and insights about your program’s efficiency. You will learn how to evaluate progress and perform data-driven decisions continuously.

Chapter 5 will explain different observability signals and how to set up and obtain them using open source tools quickly. Chapter 6 goes through Go specific internal runtime and compiler functionalities, how they can affect efficiency, and practically observe and debug them. Chapter 7, contributed by observability expert Frederic Branczyk, will teach you about the powerful profiling world, one of the quickest ways to find the efficiency bottleneck. In this part, the last chapter will explain the healthy benchmarking process through A to Z.

Finally, the third part will give you an exhaustive look at known efficiency tricks and tips you can use to optimize your applications. Ready to use and apply, it can work as a swiss-army tool: a references guide that’s worth keeping handy when creating or reviewing Go code.

Chapter 9 goes through basic efficiency tricks with low compromise on readability or maintenance. Chapter 10 dives into more advanced solutions, helping you when resource constants are much stricter. Chapter 11 goes through a few real cases of extensive Go optimizations I have seen in the open source. We will go through them to strengthen and use in practice what you learned through the whole book.

This book was designed to be read in order of chapters. However, if you feel a little bit more experienced and looking to improve your Go code today or look to solve particular efficiency issues like a memory leak, I would recommend starting with Chapter 9 and using it as an initial checklist. There are quick wins and known bottlenecks you might find useful before diving deeper.

Conventions Used in This Book

The following typographical conventions are used in this book:

Italic

Indicates new terms, URLs, email addresses, filenames, and file extensions.

Constant width

Used for program listings, as well as within paragraphs to refer to program elements such as variable or function names, databases, data types, environment variables, statements, and keywords.

Constant width bold

Shows commands or other text that should be typed literally by the user.

Constant width italic

Shows text that should be replaced with user-supplied values or by values determined by context.

TIP

This element signifies a tip or suggestion.

NOTE

This element signifies a general note.

WARNING

This element indicates a warning or caution.

Using Code Examples

Supplemental material (code examples, exercises, etc.) is available for download at https://github.com/oreillymedia/title_title.

If you have a technical question or a problem using the code examples, please send email to bookquestions@oreilly.com.

This book is here to help you get your job done. In general, if example code is offered with this book, you may use it in your programs and documentation. You do not need to contact us for permission unless you’re reproducing a significant portion of the code. For example, writing a program that uses several chunks of code from this book does not require permission. Selling or distributing examples from O’Reilly books does require permission. Answering a question by citing this book and quoting example code does not require permission. Incorporating a significant amount of example code from this book into your product’s documentation does require permission.

We appreciate, but generally do not require, attribution. An attribution usually includes the title, author, publisher, and ISBN. For example: “Efficient Go by Bartłomiej Płotka with Frederic Branczyk (O’Reilly). Copyright 2022 Alloc Limited, 978-1-0981-0571-6.”

If you feel your use of code examples falls outside fair use or the permission given above, feel free to contact us at permissions@oreilly.com.

O’Reilly Online Learning

NOTE

For more than 40 years, O’Reilly Media has provided technology and business training, knowledge, and insight to help companies succeed.

Our unique network of experts and innovators share their knowledge and expertise through books, articles, and our online learning platform. O’Reilly’s online learning platform gives you on-demand access to live training courses, in-depth learning paths, interactive coding environments, and a vast collection of text and video from O’Reilly and 200+ other publishers. For more information, visit http://oreilly.com.

How to Contact Us

Please address comments and questions concerning this book to the publisher:

- O’Reilly Media, Inc.

- 1005 Gravenstein Highway North

- Sebastopol, CA 95472

- 800-998-9938 (in the United States or Canada)

- 707-829-0515 (international or local)

- 707-829-0104 (fax)

We have a web page for this book, where we list errata, examples, and any additional information. You can access this page at https://safaribooksonline.com/library/view/~/9781098105709.

Email bookquestions@oreilly.com to comment or ask technical questions about this book.

For news and information about our books and courses, visit http://oreilly.com.

Find us on Facebook: http://facebook.com/oreilly

Follow us on Twitter: http://twitter.com/oreillymedia

Watch us on YouTube: http://www.youtube.com/oreillymedia

Acknowledgments

O’Reilly Interactive Katacoda Scenarios

Interactive Katacoda scenarios mimic real-world production environments and let you write and run code as you learn, right in your browser. The author has developed a collection of Katacoda scenarios to give you hands-on practice with the tools and practices outlined in this book. Visit http://oreilly.com for more information about our interactive content, to view the ebook format for this title, and also to learn about all our learning platform has to offer.

1   Research and Development (R&D) is a common naming of an organizational unit focused on innovations and tools that might help the business in the future.

2   Orchestrator system for virtual machines written in Python

3   Orechstrating system for processes written in C++ and framework called libprocess. Popular from novel dual scheduling system

4   Site Reliability Engineering (SRE) function was first introduced by Google in Site Reliability Engineering book

5   Prometheus is metrics monitoring solution written in Go. https://prometheus.io

6   Thanos is a project that helps to scale Prometheus. I co-created this project with Fabian Reinartz in November 2017. https://thanos.io

7   gRPC is a popular RPC framework created by Google, leveraging HTTP/2: https://grpc.io/ The project, I maintain is an official middlewares allowing to instrument and customize gRPC client or server handlers: https://github.com/grpc-ecosystem/go-grpc-middleware/

8   The Cloud Native Computing Foundation (CNCF). See website for details.

9   TBD:check if 1/3 or more, provide link?

Chapter 1. Software Efficiency Matters

The primary task of software engineers is the cost-effective development of maintainable and useful software.

Jon Louis Bentley, Writing Efficient Programs (1982)

A NOTE FOR EARLY RELEASE READERS

With Early Release ebooks, you get books in their earliest form—the author’s raw and unedited content as they write—so you can take advantage of these technologies long before the official release of these titles.

This will be the 1st chapter of the final book. Please note that the GitHub repo will be made active later on.

If you have comments about how we might improve the content and/or examples in this book, or if you notice missing material within this chapter, please reach out to the editor at mpotter@oreilly.com.

Even after 40 years, Jon’s definition of development is fairly accurate. The ultimate goal for any engineer is to create a useful product that can sustain user needs for the product lifetime. Unfortunately, nowadays, not every developer realizes the significance of the software cost. The truth can be brutal; stating that the development process can be expensive might be an underestimation. For instance, it took five years and 250 engineers for Rockstar to develop a popular Grand Theft Auto 5 video game, which was estimated to cost $137.5 million. On the other hand, to create a usable, commercialized operating system, Apple had to spend way over $500 million before the first release of macOS in 2001.

Because of the high cost, when producing software, it’s crucial to focus our efforts on things that matter the most. Ideally, we don’t want to waste engineering time and energy on unnecessary actions, like spending weeks on subjective code refactoring that does not reduce code complexity or deep micro-optimizations of the function executed rarely. The industry is continually inventing new patterns to pursue an efficient development process. Agile, Kanban methods allowing to adapt to ever-changing requirements, specialized programming languages for mobile platforms like Kotlin, or frameworks for building websites like React are only some examples. Engineers innovate in those fields because every inefficiency increases the cost.

What makes it even more difficult is that when developing software now, we should also be aware of the future costs. Running and maintenance cost is typically higher than the initial development. Code changes to stay competitive, bug fixing, incidents, installations, or even electricity consumed are only a few examples of Total software Cost of Ownership (TOC) we, developers, tend to forget. Agile methodologies help to reveal this cost early by releasing software often and getting feedback sooner. Unfortunately, because of development complexity, it’s a prevalent mistake to focus on the short-term goal of publishing the software quicker. Many dangerous shortcuts naturally come to our minds. Skipping testing parts, not investing enough in security, documentation, clean code, or code performance might indicate smart development efficiency and pragmatism. The problem is that if taken too far, they can cause an enormous cost, ranging from your product not being useful on the market to extremes like taking airplanes down1.

At this point, based on the title “Efficient Go”, you might be asking how this book will motivate you to spend more of your precious time on software execution performance characteristics like speed or efficiency. If we, software creators, should care, as Jon wrote, about development cost-effectiveness, why not focus purely on the bare minimum needed for the software to work? Waiting a few seconds more for application execution never killed anyone. On top of that, the hardware is getting cheaper and faster every month. In 2021, it’s not difficult to buy a smartphone with a dozen GBs of RAM. Finger-sized, 2TB SSD disks capable of 7 GB/s read and write throughput are available. Even home PC workstations are hitting never before seen performance scores. With0 8 CPUs or more that can perform billions of cycles per second each and with 2TB of RAM we can compute things fast. After all, improving performance on the software level alone is a complicated topic. Especially when you are new, it is common to lose time optimizing without significant program speedups. And even if we start caring about the latency introduced by our code, things like Java Virtual Machine or Go compiler will apply their optimizations anyway. Overall, spending more time on something tricky like performance that can also sacrifice our code’s reliability and maintainability may sound like a bad idea. These are only a few of the numerous reasons why typically engineers put performance optimizations on the lowest position of the development priority list, far in the outskirts of the mentioned software bare minimum.

Unfortunately, as with everything extreme, there is a risk in such performance deprioritization. In essence, there is a difference between consciously postponing optimizations and making silly mistakes causing inefficiencies and slowdowns. However, don’t be worried! I will not try to convince you in this book that all of this is wrong, and you should now measure the number of nanoseconds each code line introduces or how many bits it allocates in memory before putting it in your software. You should not. I am far from trying to motivate you to put a performance on the top of your development priority list. Instead, I would like to propose a subtle but essential change to how we, software engineers, should think about application performance. It will allow you to bring small but effective habits to your programming and development management cycle. Based on data and as early as possible in the development cycle, you will learn how to tell when you can safely ignore or postpone program inefficiencies. And when you can’t afford to skip performance optimizations, where and how to apply them effectively, and when to stop.

Machines have become increasingly cheap compared to people; any discussion of computer efficiency that fails to take this into account is short-sighted. “Efficiency” involves the reduction of overall cost - not just machine time over the life of the program, but also time spent by the programmer and by the users of the program.

Brian W. Kernighan and P. J. Plauger, The Elements of Programming Style (1978)

In “Motivation For This Book” you will learn what made me decide not to treat performance optimizations as an unnecessary evil. In “Behind Performance” we will unpack the word performance and learn how it is related to efficiency in this book’s title. Through “Common Performance Misconceptions” to “Be Vigilant to Simplifications”, we will challenge five serious misconceptions around efficiency and performance, often descoping such work from developer minds. You will learn that thinking about efficiency is not reserved only for “high performance” software. Finally, in “Efficiency: The Key to Pragmatic Code Performance” I will teach you why particularly efficiency will allow us to think about performance optimizations effectively without sacrificing time and other software qualities. This chapter might feel theoretical, but trust me. Those insights will train your essential programming judgment on how and if to adopt particular efficiency optimizations, algorithms, and code improvements presented in Part 3 of this book. This chapter is also fully language agnostic, so it should be practical for non-Go developers too!

Motivation For This Book

When writing this book, I was 29 years old. That might not feel like much experience, but I started full-time, professional programming when I was 19. I did full-time Computer Science studies in parallel to work at Intel around infrastructure. Initially coding in Python and C++, then jumping into Go (due to Kubernetes hype) for most of those years. I wrote or reviewed tens of thousands of code lines for various software that had to run on production, be reliable, and scale. From around 2017, in London, UK, I was lucky enough to develop primarily open-source software in various projects written in Go. This includes a popular time-series database for monitoring purposes called Prometheus. I also had an opportunity to co-create a large distributed system project called Thanos. I would not be surprised if my code is running somewhere in your company infrastructure too!

I am grateful for those open-source opportunities. If not those, most likely, I would not decide to write this book. The reason is that nothing taught me as much about software development as doing this in the open. You interact with diverse people, from different places worldwide, with different backgrounds, goals, and needs. It’s sometimes challenging. It would probably be easier to stick to working only with ex-Google, ex-Facebook, ex-Amazon like I did before. However, I was always motivated to look around more and see the bigger picture of everyday software development problems and challenges. In my opinion, that picture does not look perfect. With more people programming overall, often without a computer science background, there are plenty of mistakes and misconceptions, especially related to software performance.

Overall, with the fantastic people I had a chance to work with, I believe we achieved amazing things. I was lucky to work in environments where high-quality code was more important than decreasing code review iterations or reducing time spent addressing style issues. We thrived for good system design, code maintainability, and readability. We tried to bring those values to open source too, and I think we did a good job there. However, there is one important thing I would improve if I had a chance to write, for instance, the Thanos project again. You probably would not guess what. I would try to focus more on the pragmatic efficiency of my code and the algorithms we chose. I would focus on learning how to quickly gather data about my code module’s performance, benchmark, profile, and understand many performance tools. Go, and other languages provide. I would avoid many hours I spent making mistakes and understanding the performance tooling and Go runtime behaviour. And don’t get me wrong, the Thanos system nowadays is faster and uses much fewer resources than some competitors, but it took a lot of time, and there is still a massive amount of hardware resources we could use less. If I would apply the knowledge, tips, and suggestions that you will learn in this book, I believe we could cut the development cost in half, if not more, to have Thanos in the state we have today. (I hope my ex-boss who paid for this work won’t read that thesis!).

Don’t you believe me? Well, hear this: Thanos was almost fully functional in February 2018, after three months of development we did with Fabian Reinartz. However, it took six more months to improve performance to the state where queries no longer crashed the process due to OOMs2, and latencies were enough for initial production use.

What blocked me from caring about efficiency a little bit more from the start? First of all, a lack of skills. There was not much literature that would give me the practical answer to our performance or scaling questions, especially for Go. And even when I found a way to improve our Go code’s efficiency, almost no one could review it and verify it properly. In both open-source and organization, not many around me had practical awareness of what to do around Go code performance. Fortunately, today we live in a better world, with consolidated literature about pragmatic efficiency. You are reading such literature right now!

The second reason is even harder to improve upon. It is about the misleading perception of performance optimizations. The impression that “premature optimization is the root of all evil” as Donald E. Knuth wrote3, spread the world, giving everyone an excellent excuse to do less optimization work. Taken to the extreme, it demotivated people to even learn about efficiency practices and think about it, which had a strong impact on the software industry we know now. In this chapter, in “Common Performance Misconceptions”, I have collected five major ones that we will unpack. Those and similar misconceptions are the main reasons why basic programs like Microsoft Excel are so slow, why the battery in your smartphone only lasts a few hours, and why your cloud provider bill is so large.

Let’s start with unpacking some of those misconceptions by first exploring what performance actually means.

Behind Performance

Before discussing why software efficiency or any form of optimizations matters and when to apply them, we must first demystify the overused word performance. In engineering, this word is used in too many contexts and can mean different things, so let’s unpack it to avoid confusion.

DID YOU KNOW?

Word performant does not exist in English vocabulary. Sadly our code cannot be performant, indicating that there is always room to improve things4. The question is, at what point we should say stop.

When people say “this application is performing poorly”, they usually mean that this particular program is executing slowly5. However, if the same people would say, “Bartek is not performing well at work”, they probably do not mean that Bartek is walking too slowly from the computer to the meeting room. In my experience, a significant number of people in software development consider the word performance a synonym of speed. For others, it means the overall quality of execution, which is the original definition of this word6. This phenomenon is sometimes called a “Semantic Diffusion”. It occurs when a word starts to be used by larger groups with different meaning it originally had.].

The word performance in computer performance means the same thing that performance means in other contexts, that is, it means “How well is the computer doing the work it is supposed to do?

Computer Performance Analysis with Mathematica by Arnold O. Allen, Academic Press

I think Arnold’s definition describes the performance world as accurately as possible, so it’s might be the first actionable item you can take from this book. Be specific.

CLARIFY WHEN SOMEONE USES THE WORD “PERFORMANCE”.

When reading the documentation, code, bug trackers, or attending conference talks, be careful when you hear that word. Ask follow-up questions and ensure what the author means.

In practice, performance, as the quality of overall execution, might contain much more than we typically think. It might feel picky, but if we want to improve software development’s cost-effectiveness, we must communicate clearly, efficiently and effectively!

I would suggest we avoid this word unless we can specify what we mean. Imagine you are reporting a bug in a bug tracker like GitHub Issues. Especially there, don’t just mention “low performance”, but instead specify what exactly was the unexpected behaviour of the application you were describing. Similarly, when describing improvements for a software release in the changelog7, don’t just mention “improving performance”. Describe what exactly was enhanced. Maybe part of the system is now less prone to user input errors, use less RAM (if yes, how much less, in what circumstances?) or execute something faster (how many seconds faster, for what kind of workloads?). Being explicit will save time for you and your users.

When you see the word performance in my book, in the context of application computation, you can refer to it as visualized in Figure 1-1.

Figure 1-1. Performance Definition

In principle, software performance means “how well software runs” and consists of three core execution elements you can improve (or sacrifice):

Accuracy

The number of errors you do make while doing the work to accomplish the task. It can be measured for software by the number of wrong results your application produces. For example, how many requests finished with non 200 HTTP status codes in a web system

Speed

How fast you do the work needed to accomplish the task, the timeliness of execution. It can be observed by operation latency or throughput. For example, compressing 1GB of data in memory typically takes around 10s (latency), allowing approximately 100MB per second throughput

Efficiency

The ratio of the useful energy delivered by a dynamic system to the energy supplied to it. In simpler words, it is the indicator of how many extra resources, energy, or work was used to accomplish the task. A waste. It is sometimes an easily measurable concept, quantified by the ratio of sound output to total helpful input. For instance, if our operation of fetching 64 bytes of valuable data from disk allocates 420 bytes on RAM, our memory efficiency would equal . Note that this does not mean our operation is 15.23% efficient in total. We did not calculate energy, CPU time, heat and other efficiencies. For practical purposes, we tend to specify what efficiency we have in mind. Alternatively, when talking about overall program efficiency, we mean we don’t waste significant effort.

Improving any of those enhances the performance of the running application or system. It can help with reliability, availability, resiliency, overall latency, and more. Similarly, ignoring any of those can make our software less useful. Those three elements might also feel disjointed, but in fact, they are connected. For instance, notice that we can still achieve better reliability and availability without changing accuracy (not reducing the number of bugs). For example, with efficiency, reducing memory consumption decreases the chances of running out of memory and crashing the application or host operating system. This book focuses on knowledge, technics and methods, allowing you to increase the efficiency and speed of your running code without degrading accuracy.

IT’S NO MISTAKE THAT THE TITLE OF MY BOOK IS CALLED “EFFICIENT GO”

My goal is to teach you pragmatic skills allowing you to produce high quality, accurate, efficient and fast code with minimum effort. For this purposes, when I mention the overall efficiency of the code in my book (without saying a particular resource), I mean both speed and efficiency as shown in Figure 1-1. Trust me, this will help us to get through the subject effectively. You will learn more about why in “Efficiency: The Key to Pragmatic Code Performance”.

Misleading use of the performance word might be the tip of the misconceptions iceberg in this area. We will now walk through many more serious stereotypes and tendencies that are causing the development and our software to worsen. In the best case, it results in more expensive to run or less useful programs. In the worse case, causing severe social and financial organizational problems.

Common Performance Misconceptions

The number of times when I was asked, on my code reviews or sprint planning, to ignore performance “for now” is staggering. And you have probably heard that too! I also rejected someone else’s change-set for the same reasons numerous times. Perhaps our changes were dismissed at that time for good reasons, especially if they were micro-optimizations that added unnecessary complexity.

On the other hand, there were also cases where the reasons for rejection were based on common, factual performance misconceptions. Let’s try to unpack five of the most damaging misunderstandings. Be cautious when you hear some of those generalized statements. Demystifying them might help you save enormous development costs long term.

Optimized Code is Not Readable

Undoubtedly, one of the most critical qualities of software code is its readability.

(…) it is more important to make the purpose of the code unmistakable than to display virtuosity. (…) The problem with obscure code is that debugging and modification become much more difficult, and these are already the hardest aspects of computer programming. Besides, there is the added danger that a too clever program may not say what you thought it said.

Brian W. Kernighan and P. J. Plauger, The Elements of Programming Style (1978)

When we think about ultra-fast code, the first thing that sometimes comes to our minds is those clever, low-level implementations with a bunch of byte shifts, magic byte paddings, and unrolled loops. Or worse, pure assembly code linked to your application. Yes, low-level optimizations in this direction can make our code significantly less readable, but let’s be honest, those are extreme and rare cases and should be applied ultra carefully.

Code optimizations might produce some extra complexity, increase cognitive load, and make our code harder to maintain. But such risk exists if we add any other functionality or change the code for different reasons. The problem is that engineers tend to connect optimization to complexity to the extreme and avoid performance optimization as fire. In their minds, it translates to an immediate negative readability impact. The point of this section is to show you that there are ways to make performance-optimized code clear. Efficiency and readability can coexist. Similarly, you can add a feature to your program that does not impact your ability to understand code. Refusing to write more efficient code because of fear of losing readability is like refusing to add vital functionality to avoid complexity. Of course, we can consider descoping it, but we should evaluate the consequences first.

For example, when you want to add extra validation to the input, you can naively paste a complex 50 code line’s waterfall of if statements directly into the handling function, making the next reader of your code cry (Or yourself when you will revisit this code month later). Or, you can encapsulate everything to a single func validate(input string) error function, which will add only slight complexity. Alternatively, to avoid modifying the handling block of code, you can design the code to validate it on the caller side or in middleware. We can also rethink our system design and move validation complexity to another system or component, thus not implementing this feature at all.

How are performance improvements in our code different from extra features? I would argue they are not. You can design efficiency optimizations with readability in mind in the same manner as you do with features. Both can impact readability in a variety of ways—from trashing your code to adding little complexity. Both can be entirely transparent to the readers if hidden under abstractions8.

Yet, we tend to mark optimizations as the primary source of readability problems. The foremost damaging consequence of this and other misconceptions in this chapter is that it’s often used as an excuse to ignore performance improvements completely. This often leads to something called “premature pessimization”, the act of making the program less efficient, the opposite of optimization.

Easy on yourself, easy on the code: All other things being equal, notably code complexity and readability, certain efficient design patterns and coding idioms should just flow naturally from your fingertips and are no harder to write than the pessimized alternatives. This is not premature optimization; it is avoiding gratuitous [author: unnecesary] pessimization.

H. Sutter and A. Alexandrescu, C++ Coding Standards: 101 Rules

Readability is essential. I would even argue that unreadable code is rarely efficient in the long haul. When software evolves, it’s easy to break previously made too clever optimization because we misinterpret or misunderstand it. Similar to bugs and mistakes, it’s easier to cause performance issues in tricky code. In XREF HERE, you will see examples of pragmatic efficiency, where the code stays maintainable and easy to read despite having better performance.

TIP

It’s easier to optimize readable code than make heavily optimized code readable. If you can’t achieve both, in most cases, default to readability.

Optimization often results in less readable code because we don’t design good efficiency into our software from the beginning. If you refuse to think about efficiency now, it might be too late to optimize the code later without impacting readability. It’s much easier to find a way to introduce a simpler and more efficient way of doing things in the fresh modules where we just started to design APIs and abstractions. As you will learn in XREF HERE, we can do performance optimizations on many different levels, not only via nit-picking and code tuning. Perhaps we can choose a more efficient algorithm, faster data structure, or chose a different system tradeoff. These will likely result in much cleaner, maintainable code and better performance than improving efficiency after releasing the software. Under many constraints, like backward compatibility, integrations, or strict interfaces, our only way to improve performance would be to introduce additional, often significant, complexity to the code or system.

So, if you add new code, don’t sacrifice readability. Surprisingly, code after optimization can be more readable! Let’s look at a few Go code examples. In Example 1-1, you will see a sub-optimal code, potentially a “pessimization”, that I have personally seen hundreds of times when reviewing student or junior developer Go code:

Example 1-1. Simple calculation for the ratio of reported errors.

type ReportGetter interface {

Get() []Report

}

func FailureRatio(reports ReportGetter) float64 {

if len(reports.Get()) == 0 {

return 0

}

var sum float64

for _, report := range reports.Get() {

if report.Error() != nil {

sum++

}

}

return sum / float64(len(reports.Get()))

}

This is a simplified example, but there is quite a popular pattern of passing a function or interface to get elements needed for operation instead of passing them directly. It is useful when elements are dynamically added, cached, or fetched from remote databases.

I think you would agree that code from Example 1-1 would work for most cases, is simple and readable. Still, I would most likely not accept such code mainly because of potential efficiency issues. I would suggest simple modification as in Example 1-2 instead:

Example 1-2. Simple, more efficient calculation for the ratio of reported errors.

func FailureRatio(reports ReportGetter) float64 {

got := reports.Get()

if len(got) == 0 {

return 0

}

var sum float64

for _, report := range got {

if report.Error() != nil {

sum++

}

}

return sum / float64(len(got))

}

Notice that in comparison with Example 1-1 instead of calling method Get in three places, I do it once and reuse the result via the got variable.

Some developers could argue that the FailureRatio function is potentially used very rarely, it’s not on a critical path, and the current ReportGetter implementation is very cheap and fast. They could argue that Example 1-1 is more readable and only a slower few nanoseconds due to three times fewer function calls (Get). They could call my suggestion a “premature optimization”.

However, I deem it is a very popular case of premature pessimization. It is a silly case of rejecting more efficient code that does not speed up things a lot right now but does not harm either. On the contrary, I would argue that in our example, Example 1-2 is superior in many aspects:

Example 1-2 code is more efficient

Interfaces allow us to replace the implementation. They represent a certain contract between users and implementations. From the point of view of the FailureRatio function, we cannot assume anything beyond that contract. Most likely, we cannot assume that the ReportGetter ’s Get code will always be fast and cheap9. Tomorrow, someone might swap the Get code with the expensive I/O operation against filesystem, implementation with mutexes or call to the remote database10. Users will most likely forget to optimize this function from Example 1-1 when this will happen.

Example 1-2 code is safer

It is potentially not visible in plain sight, but the code from Example 1-1 has a considerable risk of introducing race conditions. We may hit a problem if the ReportGetter implementation is synchronized with other threads that dynamically change the Get() result over time. It’s better to avoid races and ensure consistency within a function body. Race errors are the hardest to debug and detect, so it’s better to be safe than sorry.

Example 1-2 code is more readable

We might be adding one more line and extra variable, but at the end, the code Example 1-2 is explicitly telling us that we want to use the same result across three usages. By replacing three instances of the Get() call with a simple variable, we also minimize the potential side effects, making our FailureRatio purely functional (except the first line). By all means Example 1-2 is thus more readable then Example 1-1.

“PREMATURE” OPTIMIZATION IS THE ROOT OF (READABILITY) EVIL

Such a statement might be accurate, but evil is in the “premature” part. Not every performance optimization is premature. Furthermore, such a rule is not a license for rejecting or forgetting about more efficient solutions with comparable complexity.

Another example of optimized code yielding clarity is visualized by code Example 1-3 and Example 1-4:

Example 1-3. Simple loop without optimization

func createSlice(n int) (slice []string) {

for i := 0; i < n; i++ {

slice = append(slice, "I", "am", "going", "to", "take", "some", "space")

}

return slice

}

Return named parameter called “slice” will create a variable holding an empty string slice at the start of the function call.

We append seven string items to the slice and repeat that n times.

Example 1-3 shows how we usually fill slices in Go, and one would say nothing is wrong here. It just works. However, I would argue that this is not how we should append in the loop if we know exactly how many elements we will append to the slice upfront. Instead, in my opinion, we should always write it as in Example 1-4.

Example 1-4. Simple loop with pre-allocation optimization. Is this less readable?

func createSlice(n int) []string {

slice := make([]string, 0, n*7)

for i := 0; i < n; i++ {

slice = append(slice, "I", "am", "going", "to", "take", "some", "space")

}

return slice

}

We are creating a variable holding the string slice. We are also allocating space (capacity) for n * 7 strings for this slice.

We append seven string items to the slice and repeat that n times.

We will talk about the efficiency optimizations like in Example 1-2 and Example 1-4 in XREF HERE with the more profound Go runtime knowledge from XREF HERE. In principle, both allow our program to do less work. In Example 1-4 thanks to initial pre-allocation, internal append implementation does not need to extend slice size in memory progressively. We do it once at the start. Now, I would like you to focus on the following question: Is this code more or less readable?

Readability can often be objective, but I would argue the more efficient code from Example 1-4 is more understandable. It adds one more line, so we could say the code is a bit more complex, but at the same time, it is explicit and clear in the message. Not only does it help Go runtime to perform less work, but it also hints to the reader about the purpose of this loop and how many iterations we exactly expect.

If you have never seen raw usage of the built-in make function in Go, you probably would say that this code is less readable. That is fair. However, once you realize the benefit and start using this pattern consistently across the code, it becomes a good habit. Even more, thanks to that, any slice creation without such pre-allocation tells you something too. For instance, it could say that the number of iterations is unpredictable, so you know to be more careful. You know one thing before you even looked at the loop’s content! To make such a habit consistent across Prometheus and Thanos codebase, we even added a related entry to Thanos Go style guide.

READABILITY IS NOT WRITTEN IN STONE. IT IS DYNAMIC.

The ability to understand certain software code can change over time, even if the code never changes. Conventions come and go as the language community tries new things. With strict consistency, you can help the reader to understand even more complex pieces of your program by introducing a new, clear convention.

Finally, to understand Knuth’s “premature optimization is the root of all evil” quote, we need to apply a specific context. While we can learn a lot about general programming from the past, there are many things we improved enormously from what engineers had in 1974. For example, back then, it was popular to add information about the type of the variable to its name as showcased in Example 1-511.

Example 1-5. Example of System Hungarian Notation applied to Go code.

type structSystem struct {

sliceU32Numbers []uint32

bCharacter      byte

f64Ratio        float64

}

Hungarian notation was useful because compilers and Integrated Development Environments (IDEs) were not very mature at that point. However, nowadays, on our IDEs or even repository websites like GitHub, we can hover over the variable to immediately know its type. We can go to the variable definition in milliseconds, read the commentary, find all invocations and mutations. With smart code suggestions, advanced highlighting, and dominance of object-oriented programming started in the mid-1990s, we have tools in our hands that allow us to add features and efficiency optimizations (so complexity) without significantly impacting the practical readability12. Furthermore, the accessibility and capabilities of observability and debugging tools have grown enormously, which we will explore in XREF HERE. It still does not permit clever code but allow us to understand bigger codebases much quicker.

To sum up, performance optimization is like another feature in our software, and we should treat it accordingly. It can add complexity, but there are ways to minimize the cognitive load required to understand our code13. As we learned in this chapter, there are even cases when a more efficient program is often a side effect of the simple, explicit, and understandable code. All practical suggestions and code examples you will see in this book have readability in mind.

WARNING

We need performance, but our goal is to make the code both readable and efficient. Ideally, we should never sacrifice readability. Simplify, use abstractions, encapsulation, and commentaries to avoid surprises and hard-to-follow code.

You Aren’t Going to Need It (YAGNI)

YAGNI is a powerful and popular rule that I use very often while writing or reviewing any software.

One of the most widely publicized principles of XP [Extreme Programming] is the You Aren’t Going to Need It (YAGNI) principle. The YAGNI principle highlights the value of delaying an investment decision in the face of uncertainty about the return on the investment. In the context of XP, this implies delaying the implementation of fuzzy features until uncertainty about their value is resolved.

Erdogmus, Hakan; Favaro

In principle, it means avoiding doing the extra work that is not strictly needed for the current requirements. It relies on the fact that requirements constantly change, and we have to embrace iterating rapidly on our software.

Let’s imagine a potential situation where Katie, a senior software engineer, was assigned the task of creating a simple webserver. Nothing fancy, just an HTTP server that exposes some REST endpoint. Katie is an experienced developer who created probably a hundred similar endpoints in the past. She went ahead, programmed functionality, and tested the server in no time. Given some time left, she decided to add extra functionality, a simple bearer token authorization ( Simple token-based authorization technique) layer. Katie knows that such change is outside of the current requirements, but she had written hundreds of REST endpoints, and each of those had a similar authorization. Experience tells her it’s highly likely such requirements will come soon, too, so she will be prepared. Do you think such a change would make sense and should be accepted?

While Katie had shown a good intention and solid experience, we should refrain from merging such change to preserve the quality of the webserver code and overall development cost-effectiveness. In other words, we should apply the YAGNI rule. Why? In most cases, we cannot predict the feature. Sticking to requirements allows us to save time and complexity. There is a risk that the project will never need an authorization layer. For example, if the server is running behind a dedicated authorization proxy. In such a case, the extra code Katie wrote can bring a high cost even if not used. It is an additional code to read, which adds up to the cognitive load. Furthermore, it will be harder to change or refactor such code when needed.

Now, let’s step into a more grey area. We explained to Katie why we need to reject the authorization code. She agreed, and instead, she decided to add some critical monitoring to the server by instrumenting it with a few vital metrics. Does this change violate the YAGNI rule too?

If monitoring is part of the requirements, it does not violate YAGNI and should be accepted. If it’s not, without knowing the full context, it’s hard to say. Critical monitoring should be explicitly mentioned in requirements. Still, even if not, webserver observability is the first thing that will be needed when we will run such code anywhere. Otherwise, how will we know that it is even running? In this case, Katie is technically doing something important that is immediately useful. In the end, we should apply common sense and judgment and add or explicitly remove monitoring from the software requirements before merging such change.

After such a change, in her free time, Katie (who writes solid code very quickly!) decided to add a simple cache to the necessary computation that enhances the performance of the separate endpoint reads. She even wrote and performed a quick benchmark to verify the endpoint’s latency and resource consumption improvements. Does that violate the YAGNI rule?

The sad truth about software development is that performance efficiency and response time are often missing from stakeholders’ requirements. The target performance goal for an application is to “just work” and be “fast enough”, without details on what that means. We will discuss on how to define practical software performance requirements in XREF HERE, commonly known nowadays as Service Level Objectives (SLO). For this example, let’s assume the worst. There was nothing in the requirements list about performance. Should we then apply YAGNI and reject Katie’s change?

Again, hard to tell without full context. Implementing a robust and usable cache is not trivial, so how complex is the new code? Is the data we are working on easily “cachable”?14 Do we know how often such an endpoint will be used (is it a critical path)? How far should it scale? On the other hand, computing the same result for a heavily used endpoint is highly inefficient, so cache is a good pattern.

I would suggest Katie takes a similar approach as she did with monitoring change—consider discussing with the team to clarify the performance guarantees that web service should offer. That will tell us if the cache is required now or it is violating the YAGNI rule.

As the last change, Katie went ahead and applied a simple efficiency rule mentioned in Thanos Style Guide. For instance, in relevant places, she implemented the slice pre-allocation improvement you learned from example Example 1-4. Should we accept such a change?

I would be strict here and say yes. Maybe you noticed that when we talked about readability, I suggested optimizing Example 1-4 always when you know the number of elements upfront. Isn’t that violating the core statement behind the YAGNI rule? Even if something is generally applicable, you shouldn’t do it before you are sure You Are Going to Need It?

I would argue that such small efficiency habits that do not reduce code readability (some even improve it) should generally be an essential part of the developer job, even if not explicitly mentioned in requirements. Similarly, no project requirements state basic best practices like code versioning, having small interfaces, or avoiding big dependencies.

The main takeaway here is that using the YAGNI rule helps, but it is not permission for developers to completely ignoring performance efficiency. As you will learn from XREF HERE, it’s usually thousands of small things that make up excessively resource usage and latency of an application, not just a single thing we can fix later. Ideally, well-defined requirements help clarify your software’s efficiency needs, but they will never cover all details and best practices we should try to apply nevertheless.

Hardware is Getting Faster and Cheaper

Undoubtedly hardware is more powerful and less expensive than ever before. We see technological advancement on almost every front every year or month. From single-core Pentium CPUs with a 200 MHz clock rate in 1995 to smaller, energy-efficient CPUs capable of 3-4 GHz speeds. RAM sizes increasing from dozens of MB in 2000 to 64 GB in personal computers 20 years later, with faster access patterns. In the past, small capacity hard disks, moving to SSD, then 7GB/s fast NVME SSD disks with few TB of space. Network interfaces achieving 100 Gigabits throughput. In terms of remote storage, I remember Floppy Disks with 1.44 MB of space, then read-only CD-ROMs with a capacity of up to 553 MB, next we had Blue-Ray, read-write capability DVDs, now it’s easy to get SD cards with TB sizes.

Now let’s add to the above facts the popular opinion that amortized hourly value of typical hardware is cheaper than the developer hour. With all of this, one would say that it does not matter if a single function in code takes 1 MB more or does excessive disk reads. Why should we delay features, educate or invest in performance-aware engineers if we could buy bigger servers and overall pay less?

As you probably imagine, this is not that simple. Let’s unpack this quite harmful argument descoping the efficiency and performance from the software development todo list.

First of all, stating that spending more money on hardware is cheaper than investing the expensive developer time into efficiency topics is very short-sighted. It is like claiming that we should buy a new car and sell an old one every time something breaks, only because we don’t want to care, learn or pay for an automotive mechanics job. Sure, that can work, but it would not be a very efficient approach. In many cases, that is simply a misleading oversimplification.

Let’s assume a software developer’s annual salary oscillates around $100,000. With other employment costs, let’s say the company has to pay $120,000 yearly, so $10,000 monthly. For $10,000 in 2021, you can buy a server with 1 TB of DDR4 memory, two high-end CPUs, 1 Gigabit network card, and 10 TB of hard disk space. Let’s ignore for now the energy consumption cost. Such a deal means that our software can over-allocate terabytes of memory every month, and we would still be better off than hiring an engineer to optimize this, right? Unfortunately, it does not work like this.

First of all, terabytes of allocation are more common than you think, and you don’t need to wait a whole month! Figure 1-2 shows a screenshot of the heap memory profile of a single replica (of six totally), of a single service (of dozens) running in a single cluster, and we run thousands of clusters at Red Hat. We will discuss how to read and use profiles in XREF HERE, but the particular screenshot in Figure 1-2 shows the total memory allocated since the last restart of the process five days before.

Figure 1-2. Snippet of memory profile showing all memory allocations within five days made by high traffic service. See the full [profile here]( https://share.polarsignals.com/378a246)

Most of that memory was already released, but notice that this software from the Thanos project used 17.61 TB in total for only five days of running15. If you write desktop applications or tools instead, you will hit a similar scale issue sooner or later. Taking the previous example, if one function is over-allocating 1MB, it is enough to run it 100 times for critical operation in our application with only 100 desktop users to get to 10 TB wasted in total. Not in a month, but on a single run done by 100 users. As a result, slight inefficiency can create overabundant hardware resources quickly.

There is more. To afford an over-allocation of 10 TB, it is not enough to buy a server with that much memory and pay for energy consumption. The amortized cost, among other things, has to include writing, buying, or at least maintaining firmware, drivers, operating system, and software to monitor, update and operate such server. Since for extra hardware, we need additional software, by definition, it requires spending money on engineers, so we are back where we were. We might have saved engineering costs by avoiding focusing on performance optimizations. In return, we would spend more on other engineers required to maintain over-used resources or pay a cloud provider which already calculated such extra cost, plus their profit into the cloud usage bill.

On the other hand, today, 10 TB of memory costs a lot, but tomorrow it might be a marginal cost due to technological advancements. What if we ignore performance problems and wait until server cost decreases or more users replace their laptops or phones with faster ones. Waiting is easier than debugging tricky performance issues!

Unfortunately, we cannot skip software development efficiency and expect hardware advancements to mitigate needs and performance mistakes. Hardware is getting faster and more powerful, yes. But, unfortunately, not fast enough. Let’s go through three main reasons behind that this not intuitive effect.

First of all, there is a saying that “software expands to fill the available memory”. This effect is known as Parkinson’s law16. It states that no matter how many resources we have, the demands tend to match the supply. For example, Parkinson’s Law is heavily visible in universities. No matter how much time the professor would give for the assignments or exam preparations, students will always use all of it and probably do most of it last minute17. We can see similar behaviour in software development too.

The second reason is that software tends to get slower more rapidly than hardware becomes faster. Niklaus mentions a “Fat Software” term that explains why there will always be more demand for more hardware.

Increased hardware power has undoubtedly been the primary incentive for vendors to tackle more complex problems (…). But it is not the inherent complexity that should concern us; it is the self-inflicted complexity. There are many problems that were solved long ago, but for the same problems, we are not offered solutions wrapped in much bulkier software.

Niklaus Wirth, A Plea for Lean Software (1995)

Software is getting slower faster than hardware is getting more powerful because products have to invest in a better user experience to get profitable. Prettier operating systems, glowing icons, complex animations, high definition videos on websites, or fancy emojis that mimic your face expression, thanks to facial recognition techniques. It’s a never-ending battle for clients, which brings more complexity, thus computational demands.

On top of that, rapid democratization of software thanks to better access to computers, servers, mobile phones, IoT devices, and any other kind of electronics. As Marc Andreessen said, “Software is eating the world”. The COVID-19 pandemic that started in late 2019 accelerated digitalization even more as remote, internet-based services became the critical backbone of modern society. We might have more computation power available every day, but more functionalities and user interactions consume all of it and demand even more. In the end, I would argue that our overused 1MB in the aforementioned single function might become a critical bottleneck on such a scale pretty quickly.

If that feels still very hypothetical to you, just look at the software around you. We use social media, where Facebook alone generates 4 PB18] of data per day. We search online, causing Google to process 20 PB of data per day. However, one would say those are rare, planet-scale systems with billions of users. Typical developers don’t have such problems, right? When I looked at most of the software co-created or used, they hit some performance issue related to significant data usage sooner or later. Prometheus UI page, written in React, was performing a search on millions of metric names or tried to fetch hundreds of MBs of compressed samples, causing browser latencies and explosive memory usage. Single Kubernetes at our infrastructure, with low usage, are generating 0.5 TB of logs per day (most of those never used). The excellent grammar checking tool I used to write this book is making too many network calls when text has more than 20 000 words, slowing my browser considerably. Our simple script for formatting our documentation in markdown and link checking took minutes to process all elements. Our Go static analysis job and linting exceeded 4GB of memory and crashed our CI jobs. It used to take 20 minutes for my IDE to index all code from our mono-repo, despite doing it on a top-shelf laptop. I still haven’t edited my 4K ultra-wide videos from GoPro because the software was too laggy. I could go forever with those examples, but the point is that we live in a really “Big Data” world.

When I started programming we not only had slow processors, we also had very limited memory — sometimes measured in kilobytes. So we had to think about memory and optimize memory consumption wisely.

V. Simonov; https://va.lent.in/optimize-for-readability-first/[Optimize for readability first (2014)]

While we can acknowledge that hardware was more constrained in the past, this does not change the fact that we can now ignore optimizations. Because of big data, we have to optimize memory and other resources wisely, if not wiser than before. It also will be much worse in the future. Our software and hardware have to handle the data growing at extreme rates, faster than any hardware development. We are just on the edge of introducing 5G networks capable of transfers up to 20 Gigabits per second. We introduce mini-computers in almost every item we buy like TVs, bikes, washing machines, freezers, desk lamps, or even deodorants!. We call this movement “Internet of Things” (IoT). Data from those devices are estimated to grow from 18.3 ZB in 2019 to 73.1 ZB by 202519. The industry can produce 8K TVs, rendering resolutions of 7,680 by 4,320, so approximately 33 million pixels. If you ever wrote any computer game, you are probably as scared as me. It will take a lot of efficient effort to render so many pixels in highly realistic games with immersive, highly destructive environments at 60+ frames per second. Modern cryptocurrencies and blockchain algorithms also pose challenges in computational energy efficiencies, with Bitcoin energy consumption increased in February 2021 to roughly 130 Terawatt-hours of energy (0.6% of global electricity consumption).

The last reason, but not least, behind not fast enough hardware progression is that hardware advancement stalled on some fronts like CPU speed (clock-rate) or memory access speeds. We will cover some challenges of that situation in XREF HERE, but I believe every developer should be aware of the fundamental technological limits we are hiting right now.

It would be odd to read a modern book about efficiency that does not mention Moore’s Law, right? You probably heard of it somewhere before. It was first stated in 1965 by former CEO and co-founder of Intel, Gordon Moore.

The complexity for minimum component costs [author: the number of transistors, with minimal manufacturing cost per chip] has increased at a rate of roughly a factor of two per year. (…) Over the longer term, the rate of increase is a bit more uncertain, although there is no reason to believe it will not remain nearly constant for at least 10 years. That means by 1975, the number of components per integrated circuit for minimum cost will be 65,000.

Gordon E. Moore, Cramming more components onto integrated circuits

Mr Moore’s observation had a big impact on the semiconductor industry. But decreasing the transistors’ size would not have been that beneficial if not for Rober H. Dennard and his team. In 1974 their experiment revealed that power use stays proportional to the transistor dimension (constant power density)20. This means that smaller transistors were more power-efficient. In the end, both laws promised exponential performance per watt growth of transistors. It motivated investors to continuously research and develop ways to decrease the size of MOSFET21 transistors. We can also fit more of them on even smaller, more dense microchips, which reduced manufacturing costs. The industry continuously decreased the amount of space needed to fit the same amount of computing power, enhancing any chip, from CPU, through RAM and Flash memory to GPS receivers and high definition camera sensors.

In practice, Moore’s prediction lasted not ten years as he thought, but nearly 60 so far and still holds. We continue to invent tinier, microscopic transistors, currently oscillating around ~70nm. Probably we can make them even smaller. Unfortunately, as we can see on ??? we reached the physical limit of Dennard’s Scaling around 200622.

1. from Why we cannot have faster transistors: Moore’s Law vs Dennard’s Rule image::images/hardware.png[""]

While technically, power usage of the higher density of tiny transistors remains constant, such dense chips heat up quickly. Beyond 3-4 GHz of clock speed, it takes significantly more power and other costs to cool those transistors to keep them running. As a result, unless you plan to run software on the bottom of the ocean23, you are not getting CPUs with faster instruction execution anytime soon. We only can have more cores.

So, what we have learned so far? Hardware speed is getting capped, the software is getting bulkier, and we have to handle continuous growth in data and users. Unfortunately, that’s not the end. There is a vital resource we tend to forget about while developing the software: power. Every computation of our process takes electricity, which is heavily constrained on many platforms like mobile phones, smartwatches, IoT devices, or laptops. Non-intuitively there is a strong correlation between energy efficency and software speed and efficiency. I love the Chandler Carruth presentation, which explained this surprising relation well:

If you ever read about “power-efficient instructions” or “optimizing for power usage”, you should become very suspicious. (…) This is mostly total junk science. Here is the number one leading theory about how to save battery life. Finish running the program. Seriously, race to sleep. The faster your software runs, the less power it consumes. (…) Every single general-usage microprocessor you can get today, the way it conserves power is by turning itself off. As rapidly and as frequently as possible.

Chandler Carruth, Efficiency with Algorithms, Performance with Data Structures

To sum up, avoid the common trap of thinking about hardware as a continuously faster and cheaper resource that will save us from optimizing our code. It’s a trap. Such a broken loop makes engineers gradually lower their coding standards in performance and demand more and faster hardware. Cheaper and more accessible hardware then creates even more mental room to skip efficiency and so on. There are amazing innovations like recent Apple’s M1 Silicon24, RISC-V standard25 and more practical Quantum computing appliances, which promise a lot. Unfortunately, as of 2021, hardware is growing slower than software efficiency needs.

If you are not yet convinced that we should take a little bit more thought on software efficiency, there is one more very important and, sadly, often forgotten point. Software developers are often “spoiled” and detached from the typical human reality. It’s often the case that engineers create and test the software on premium, high-end laptop or mobile devices. We need to realize that many people and organizations are and will be utilizing older hardware or worse internet connections26. It might be the case that people will have to run your applications on slower computers. It might be worth considering efficiency in our development process to improve our software’s overall accessibility and inclusiveness.

We Can Scale Horizontally Instead

As we learned in the previous sections, we expect our software to handle more data sooner or later. But it’s unlikely your project will have billions of users from day one. We can avoid enormous software complexity and development cost by pragmatically choosing a much lower target number of users, operations, or data sizes to aim for at the beginning of our development cycle. We usually simplify the initial programming cycle by assuming a low number of notes in your mobile note-taking app, a lower number of requests per second in the proxy you build, or smaller files in the data converter tool your team is working on. It’s ok to simplify things, but as you will learn in XREF HERE, it’s also important to roughly predict performance requirements in the early design phase. Similarly, it’s essential to find the expected load and usage in the mid to long term of software deployment. The software design that guarantees similar performance levels, even with increased traffic, is called “scalable”. Generally, scalability is very difficult and expensive to achieve in practice.

Even if a system is working reliably today, that doesn’t mean it will necessarily work reliably in the future. One common reason for degradation is increased load: perhaps the system has grown from 10,000 concurrent users to 100,000 concurrent users, or from 1 million to 10 million. Perhaps it is processing much larger volumes of data than it did before. Scalability is the term we use to describe a system’s ability to cope with increased load.

Martin Kleppmann, Designing Data-Intensive Application

Inevitability, while talking about performance, we might touch on some scalability topics in this book. For this chapter’s purpose, we can distinguish the scalability of our software into two types.

The first and sometimes the simplest way of scaling our application is by running the software on hardware with more resources (“vertical” scalability). For example, we could introduce parallelism for software to use not one but three CPU cores. If the load would increase, we provide more CPU cores. Similarly, if our process is memory intensive, we might bump running requirements and ask for bigger RAM space. The same with any other resource like disk, network or power. Obviously, that does not go without consequences. In the best case, you have that room in the target machine. Potentially you can make that room by re-scheduling other processes to different machines (e.g. when running in the cloud) or closing them temporarily (useful when running on the laptop or a smartphone). In the worse case, you need to buy a bigger computer, a more capable smartphone or laptop. The latter option is usually very limited, especially if you provide software for customers to run on their non-cloud premises. In the end, the usability of resource-hungry applications or websites that scale only vertically is much lower. If you or your customers run your software in the cloud, the situation is a little bit better. You can “just” buy a bigger server. As of 2021, you can scale up your software on the AWS platform up to 128 CPU cores, almost 4 TB of RAM and 14 Gbps of bandwidth27. In extreme cases you can also buy an IBM mainframe with 190 cores and 40TB of memory.

It does not take much time to notice that vertical scalability has its limits on many fronts. Even in cloud or datacenters, we simply cannot infinitely scale up the hardware. First of all, giant machines are rare and expensive. Secondly, as we will learn in XREF HERE, bigger machines run into complex issues caused by many hidden single points of failures. Pieces like memory bus, network interfaces, NUMA nodes, and the operating system itself can be overloaded and too slow28.

This is why engineers found a different way of fulfilling the demand. All thanks to the Internet and advancement in network technologies. Instead of a bigger machine, we might try to offload and share the computation across multiple remote, smaller, less complex, and much cheaper devices! How does it look in practice? To search for messages with the word “home” in a mobile messaging app, we could fetch millions of past messages (or store them locally in the first place) and run regex matching on each. Instead, we can design an API and call remotely a backend system that splits the search into 100 jobs matching 1/100 of the dataset. Instead of building “monolith” software, we could distribute different functionalities to separate components and move to a “microservice” design. Instead of running a game that requires expensive CPUs and GPUs on a personal computer or gaming console, we run it in a cloud and stream the input and output in high resolution.

Figure 1-3. Vertical vs Horizontal scalability.

With horizontal and vertical scalability in mind, let me show you a specific scenario from the past. Many modern databases rely on the compaction process to store and look up the data efficiently. We can reuse many indices during this process, deduplicate the same data, and gather fragmented pieces into the sequential stream of data for quicker reads. At the beginning of the Thanos project, we decided to reuse a very naive compaction algorithm for simplicity. We calculated that, in theory, we don’t need to make the compaction process parallel within a single block of data. Given a steady stream of 100+GB of eventually compacted data from a single source, we could rely on a single CPU and a minimal amount of memory and some disk space. The implementation was very naive and unoptimized at the very beginning, following good patterns D. Knuth mentioned. We wanted to avoid the complexity and effort of optimizing the project’s reliability and functionality features. As a result, users who deployed our project quickly hit compactions problems being either too slow to cope with incoming data or consuming hundreds of GB of memory per operations. The cost was the first problem, but not the most urgent. The bigger issue was that many Thanos users did not have bigger machines in their datacentres to scale the memory vertically.

At first glance, the compaction problem looked like a scalability problem. The compaction process was depending on resources that we could not just add up infinitely. As users wanted a solution fast, together with the community, we started brainstorming potential horizontal scalability techniques. We talked about introducing a compactor scheduler service that would assign compaction jobs to different machines or intelligent peer network using a gossip protocol. Without going into details, both solutions would add an enormous amount of complexity, probably doubling or tripling the complication of running the whole Thanos system. Luckily, it took a few days of brave and experienced developer time to redesign the code for efficiency and performance. It allowed the newer version of Thanos to make compactions twice faster and stream data directly from the disk, allowing minimal peak memory consumption. Three years later, the Thanos project still does not need to bring horizontal scalability for this operation.

It might feel funny now, but, personally, this story is quite scary. We were so close to bringing enormous, distributed system-level complexity. It would be fun to develop for sure, but it would also potentially kill the project’s adoption. A similar situation repeated many times in my career in both open and closed source for smaller and bigger projects.

We follow two rules in the matter of optimization: Rule 1. Don’t do it. Rule 2 (for experts only). Don’t do it yet — that is, not until you have a clear and unoptimized solution. (…) optimization makes a system less reliable and harder to maintain, and therefore more expensive to build and operate.

Michael A. Jackson, Principles of program design

Hopefully, previous misconceptions advised you to be careful with the strong “don’t do optimization” words. Michael had an excellent intention of deferring the unnecessary complexity. Unfortunately, as presented by the “lucky” Thanos compaction situation, if you follow that rule blindly, you can quickly end up with the premature “pessimization” that forces premature horizontal scalability. In other words, avoiding complexity can bring even bigger complexity. This appears to me as an unnoticed but critical problem in the industry. It is also one of the main reasons why I wrote this book. We might hit the need for scalability practices significantly quicker than we thought if we defer software performance and efficiency improvements. It is a massive trap because, with some optimization effort, we might even altogether avoid jumping into complications of the scalability methods.

The complications come from the fact that complexity has to live somewhere. We don’t want to complicate code, so we have to complicate the system, which, if built from non-efficient components, wastes not only resources but also an enormous amount of developer or operator time. Horizontal scalability is complex. By design, it involves network operations. As we might know from CAP Theorem29, we inevitably hit either availability or consistency issues as soon as we start distributing our process. Trust me, mitigating those elemental constraints, dealing with race conditions, and understanding the world of network latencies and unpredictability is a hundred times more difficult than extra complexity in our code hidden behind io.Reader interface.

It might feel to you that this section touches only infrastructure systems. That’s not true. It applies to all software. If you write a frontend software or dynamic website, there might be a temptation to move small client computations to the backend. We should probably only do that if such computation depends on the load and grows out of userspace hardware capabilities. Moving it to the server prematurely might cost you in the complexity caused by extra network calls, more error cases to handle and server saturations causing DOS 30. Another example comes from my experience. My master thesis was about “Particle Engine Using Computing Cluster”. In principle, the goal was to add to the game in the Unity3D a particle engine. The trick was that such a particle engine was not supposed to operate on client machines but offloading “expensive” computation to a nearby supercomputer in my University called “Tryton”31. Guess what? Despite the ultra-fast InfiniBand network32, all particles I tried to simulate (realistic rain and crowd) was much slower and less reliable when offloaded to our supercomputer. It was not only less complex but also much faster to compute all locally.

Summing up, when someone says, “don’t optimize, we can just scale horizontally”, be very suspicious. Generally, it is simpler and cheaper to start from efficiency improvements before we escalate to a scalability level. On the other hand, there should also be a judgment that tells you when optimizations are becoming too complex, and scalability might be a better option. You will learn more about that in XREF HERE.

Time to Market is More Important

Time is expensive. One aspect of this is that software developer time and expertise cost a lot. The more features you want your application or system to have, the more time is needed to design, implement, test, secure and optimize the solution’s performance. The second aspect is that the more time a company or individual spends to deliver the product or service, the longer their “time to market” is, which can hurt the financial results.

Once time was money. Now it is more valuable than money. A McKinsey study reports that, on average, companies lose 33% of after-tax profit when they ship products six months late, as compared with losses of 3.5% when they overspend 50% on product development.

Charles H. House and Raymond L. Price, The Return Map: Tracking Product Teams (1991)

It’s hard to measure such impact, but your product might no longer be pioneering when you are “late” to market. You might miss valuable opportunities or respond too late to a competitor’s new product. That’s why companies mitigate this risk by adopting agile methodologies or POC (Proof of Concept) and MVP (Minimal Viable Product) patterns.

Agile and smaller iterations help, but in the end, to achieve faster development cycles, companies try other things too: scale their teams (hire more people, redesign teams), simplify the product, do more automation or do partnerships. Sometimes they try to reduce the product quality. As Facebook’s proud initial motto was “Move fast and break things”33, it’s very common for companies to descope software quality in areas like code maintainability, reliability and performance to “beat” the market.

This what our last fifth misconception is all about. Descoping your software’s efficiency and performance quality to get to the market faster is not always the best idea. It’s good to know the consequences of such a decision. Know the risk first.

Optimization is a difficult and expensive process. Many engineers argue that this process delays entry into the marketplace and reduces profit. This may be true, but it ignores the cost associated with poor-performing products (particularly when there is competition in the marketplace).

Randall Hyde, The Fallacy of Premature Optimization (2009)

Bugs, security issues, and poor performance happen, but it might damage the company. Without looking too far, let’s look at a game released in late 2020 by the biggest polish game publisher, CD Project. The “Cyberpunk 2077” game was known to be a very ambitious, open world, massive and high-quality production. Well marketed, from a publisher with a good reputation, despite the delays, excited players around the world bought 8 million pre-orders. Unfortunately, when released in December 2020, the otherwise excellent game had massive performance issues. It had bugs, crashes, and a low frame rate (slowness) on all consoles and most PC setups. The game on some older consoles like PS4 or Xbox One was claimed to be unplayable.

Interestingly, for me, the game was fabulous despite all the problems. I pre-ordered Cyberpunk 2077 and played it on PS4 pro from day 1. The game had some issues, but it was not making the product unplayable, as others were admitting. It was crashing once every second hour, it was sometimes super slow and laggy, but it was a fully playable, fun and otherwise polished game. There were, of course, follow up updates with plenty of fixes and drastic improvements over the following months.

Unfortunately, it was too late. The damage was done. The issues, which for me felt somewhat minor, were enough to shake the CD Project’s financial perspectives. Five days after launch, the company lost one-third of its stock value costing founders more than one billion dollars. Millions of players were asking for game refunds. Investors sued the CD Project over game issues by investors and famous lead developers were leaving the company. Perhaps the publisher will survive and recover. Still, one can only imagine the implications of broken reputation impacting future productions.

More experienced and mature organizations know well the critical value of software performance, especially the client-facing ones. Amazon found that if their website were loading one second slower, they would lose $1.6 billion annually. They also reported that “100ms of latency costs 1% of profit”34. Google realized that slowing down web search from 400ms to 900ms caused a 20% drop in traffic. For some businesses, it’s even worse. It was estimated that if a broker’s electronic trading platform is five milliseconds slower than the competition, it could lose 1% of its cash flow, if not more. If 10ms slower, this number grows to a 10% drop in revenue.

Realistically speaking, it’s true that millisecond-level slowness might not matter in most software cases. For example, let’s say we want to implement a file converter from PDF to DOCX. Does it matter if the whole experience last 4 seconds or 100 milliseconds? In many cases, it does not. However, when someone puts that as a market value and a competitor’s product has a latency of 200 milliseconds, code efficiency and speed suddenly will be a matter of winning or losing customers. And if it’s physically possible to have such fast file conversion, competitors will try to achieve that sooner or later. This is also why so many projects, even in the open-source, are very loud about their performance results. While sometimes it feels like a cheap marketing trick, this works because if you have two similar solutions with similar feature sets, you will pick the fastest one.

It’s not all about the speed too. During my experience as a consultant around infrastructure systems, I saw many cases when customers migrated away from solutions requiring a larger amount of RAM or disk storage, even if that meant some loss in functionalities35.

To me, the verdict is simple. If you want to win the market, skipping efficiency and performance in your software might not be the best idea. Don’t wait with optimization for the last moment. On the other hand, time to market is critical, so the crucial aspect is to balance a good enough amount of performance work into your software development process. This book aims to help you find that healthy threshold and reduce the time required to improve the efficiency of your software.

At this point, you should be aware of five main misconceptions and why they can mislead us when planning software development. There is a single pattern to all of them - over simplifications. Let’s look at what that means.

Be Vigilant to Simplifications

All those misconceptions are not malicious in any form. It’s only natural for humans to oversimplify complex concepts and processes. Such generalization is often damaging, in the best case, causing unnecessary battles on which programming language or IDE is the best or if we should put this performance optimization ticket on the current sprint or not. In the worst case, simplifications are a source of more serious conflicts and stereotypes. Through the millenniums, we developed the tactic of reducing complex topics to a simple heuristic that can be intuitively executed by the “feeling” part of the brain when needed36. Such simplifications might feel helpful at first glance. However they can also be incredibly misleading.

We talked through five misconceptions that were excuses to avoid thinking about efficiency or speed. To reinforce this, let’s quickly unpack the simplification that was created based on Donald Knuth’s famous “premature optimization is the root of all evil” quote:

The full version of the [Donald’s Knuth] quote is “We should forget about small efficiencies, say about 97% of the time: premature optimization is the root of all evil.” and I agree with this. It’s usually not worth spending a lot of time micro-optimizing code before its obvious where the performance bottlenecks are. But, conversely, when designing software at a system level, performance issues should always be considered from the beginning. A good software developer will do this automatically, having developed a feel for where performance issues will cause problems. An inexperienced developer will not bother, misguidedly believing that a bit of fine-tuning at a later stage will fix any problems.

Randall Hyde, The Fallacy of Premature Optimization (2009)

Interestingly enough, if one would read the whole article where Donald Knuth put the quote, one would notice that it was more in favour of optimizations than against. The article was basically about writing efficient but also readable programs without goto statements. Nevertheless, premature optimization can be evil. And the essence of that statement is to understand when efficiency and speed optimization are premature. We will discuss that balance in details in XREF HERE.

Nothing is purely white or black. There is always something in the middle. Simplifications are natural and will always be tempting. The only thing we can do is teach others and ourselves not to take things to the extreme. We cannot categorize performance focus as something incredibly wrong or good without full context.

Let’s now look on what is the pragmatic way to think about performance.

Efficiency: The Key to Pragmatic Code Performance

In “Behind Performance”, we learned that performance splits into accuracy, speed and efficiency. I mentioned that in this book, I use efficiency in replacement for both speed and efficiency. The reason is that there is a practical suggestion hidden in that effort regarding how we should think about our code performing in the production. The lesson here is to stop thinking about the code performance regarding how fast it does things. Generally, for non-specialized software, speed only marginally matters. It is the waste and unnecessary consumption of resources, which translates to wasting time itself, which often stops us from achieving software goals like usability in a cost-effective way. Sadly, efficiency is often overlooked.

Let’s say you want to travel from city A to city B across the river. You can grab a fast car and drive around through the nearby bridge to get to city B quickly. But if you jump into the water and slowly swim across the river, you will be much faster in the city. Slower actions can still be faster when done efficiently, for example, by picking a shorter route. One could say, to improve travel performance and beat the swimmer, we could get a faster car, improve the road surface to reduce drag, or even add a rocket engine. We could potentially beat the swimmer, yes, but those drastic changes might be more expensive than simply doing less work and renting a boat instead.

Similar patterns exist in software. Let’s say our algorithm performs search functionality for certain words against specific input, and it performs slowly. Given that we operate on persistent data, the slowest operation is usually the data access, especially if our algorithm does them extensively. It’s very tempting to not think about efficiency and instead find a way to convince users to use SSD instead of HDD storage. This way, we could potentially reduce latency even up to 10x. That would be improving performance by increasing the speed element of the equation. On the contrary, if we would find a way to improve the current algorithm to read data only a few times instead of a million, we could achieve even lower latencies. That would mean we do less work to achieve the same effect, improving efficiency.

I want to propose focusing our efforts on efficiency instead of mere execution speed. That is also why this book’s title is “Efficient Go”, not something more general and catchy37 like “Ultra Performance Go”.

It’s not that speed is less relevant. It is important, and as you will learn in XREF HERE, you can have more efficient code that is much slower and vice versa. Sometimes it’s a tradeoff you will need to make. Both speed and efficiency are essential. Both can impact each other. In practice, when the program is doing less work on the critical path, it will most likely have lower latency. The other way around sometimes applies too. In the HDD vs SDD example above, changing to a faster disk might allow you to remove some caching logic, which results in better efficiency: less memory and CPU time used. As we learned in “Hardware is Getting Faster and Cheaper”, the faster your process is, the less energy it consumes, improving battery efficiency.

I would argue that we generally should focus on improving efficiency before speed as the first step when improving performance. In the context of this book and daily programming in Go, the efficiency element is typically the most impactful and essential. You might be surprised that sometimes after improving efficiency, you achieved desired latency! Let’s go through some further reasons why efficiency might be superior:

It is much harder to make efficient software slow

This is similar to the fact that readable code is easier to optimize. However, as I mentioned before, efficient code usually performs better simply because less work has to be done. In practice, this also translates to the fact that slow software is often inefficient.

Speed is more fragile

The latency of the software process depends on a huge amount of external factors. One can optimize the code for fast execution in a dedicated and isolated environment, but it can be much slower when left running for a longer time. At some point, CPUs might be throttled due to thermal issues with the server. Other processes (e.g. periodic backup) might surprisingly slow your main software38 by preempting it from the CPU core or exhausting memory bus (throughput between your CPU and RAM is limited too). The network might be throttled. There are tons of hidden unknowns to consider when we program for mere execution speed. This is why efficiency is usually what we, as programmers, can control the most.

Speed is less portable

Like above, if we optimize only for speed, we cannot assume it will work the same when moving our application from the developer machine to a server or between various client devices. Different hardware, environment, operating systems can diametrally change the latency of our application. That’s why it’s critical to design software for efficiency. First of all, there are fewer things that can be affected. Secondly, if you make two calls to the database on your developer machine, the chances are that you will do the same number of calls, no matter if you deploy it to an IoT device in the space station or ARM-based mainframe.

You can’t tell if the software is fast by looking at that code

Efficiency is easier to tell with just static analysis. You either do one iteration or thousands. You either allocate memory for an array of a hundred integers or billions. We can still be surprised about underlying inefficiencies, so we have to understand the runtime resource usage fully. However, we can find things and improve just faster than when improving for pure speed. As you will learn in XREF HERE, we cannot assess speed of our code with our own eyes. Experience helps, but we might be surprised by what’s slow and fast in many cases. We need to spend time on benchmarking and load testing to be sure, and that takes time.

Generally, efficiency is something we should do right after or together with readability. We should start thinking about that from the very beginning of the software design. The healthy efficiency awareness, when not taken to the extreme, results in robust development hygiene. It allows us to avoid silly performance mistakes that are hard to improve on in later development stages. Doing less work also often reduces the overall complexity of the code and improve code maintainability and extensibility.

Summary

I think it’s very common for developers to start their development process with compromises in mind. We often sit down with the attitude to negotiate to compromise certain software qualities from the very beginning. We are taught that we have to sacrifice efficiency, readability, testability etc., to accomplish our goals.

In this chapter, I would like to encourage you to hold out and not sacrifice or compromise any quality until it was demonstrated that there is no reasonable way we can achieve all of our goals. Maybe it’s not visible at the start, but many things they do have solutions. // https://youtu.be/3WBaY61c9sE?t=2872

Let’s try to be greedy in software development. Hopefully, at this point, you are aware that we have to think about performance, ideally from the early development stages. We learned what performance consists of. In addition, we learned that there are many misconceptions mentioned in “Common Performance Misconceptions” that are worth reconsidering or challenging when appropriate.

We need to be aware of the risk of premature pessimization and premature scalability as much as we need to think about avoiding premature optimizations.

Finally, we learned that efficiency in the performance equation might give us a bit of an advantage. It is easier to improve performance by improving efficiency first. It helped my students and me many times to effectively approach the subject of performance optimizations.

In the next chapter, we will walk through a detailed and opinionated introduction to Go. Of course, programming for efficiency is essential, but the key to that is knowledge. Let’s walk through things to help us achieve all our software quality goals!

1   One example could be an expensive software error causing Boeing 737 Max pilots to take control over the automation, causing two fatal crashes.

2   Out of memory is an undesired state of application where it uses more memory than the host have or the caller allocated for this process. We will learn about this mechanism more in XREF HERE

3   Donald Knuth stated this twice, once in Structured Programming with goto Statements (1974), second in Computer Programming as an Art (1974). There were debates if this quote should be attributed to C. A. R. Hoare instead, but more recent researches proved against that. The fact that people were trying to find the actual author of the quote speaks of the matter’s importance.

4   In a practical sense, there are limits to how fast our software can be. H. J. Bremermann in 1962 suggested there is a computational physical limit that depends on the mass of the system. We can estimate that 1 kg of the ultimate laptop can process ~1050 bits per second, while the computer with the mass of the Earth planet can process at a maximum of 1075 bits per second. While those numbers feel enormous, even such a large computer would take ages to brute force all chess movements estimated to 10^120 complexity. Those numbers have practical use in cryptography to assess the difficulty of cracking certain encryption algorithms.

5   I even did a small experiment on Twitter, proving this point.

6   UK Cambridge Dictionary defines the performance noun as “How well a person, machine, etc. does a piece of work or an activity”.

7   I would even recommend, with your changelog, sticking to common standard formats like https://keepachangelog.com/en/1.0.0/. This material also contains valuable tips on clean release notes.

8   It’s worth to mention, that hiding features or optimization can sometimes lead to lower readability. Sometimes explicitness is much better and avoids surprises.

9   As the part of the interface “contract”, there might be a comment stating that implementations should cache the result. Hence, the caller should be safe to call it many times. Still, I would argue that it’s better to avoid relying on something not assured by a type system to prevent surprises.

10   All of those three examples of Get implementations could be considered costly to invoke. Input-output (I/O) operations against the filesystem are significantly slower than reading or writing something from memory. Something that involves mutexes means you potentially have to wait on other threads before accessing it. Call to database usually involves all of those, plus potentially communication over the network.

11   This type of style is usually referred to as Hungarian Notation, which used extensively used in Microsoft. There are two types of this notation, too: App and System. Literature indicates that App’s Hungarian can still give many benefits.

12   It is worth highlighting that these days, it is recommended to write code in a way that is easily compatible with IDE functionalities, e.g. your code structure should be a “connected” graph. This means that you connect functions in a way that IDE can assist. Any dynamic dispatching, code injection and lazy loading disables those functionalities and should be avoided unless strictly necessary.

13   Cognitive load is the amount of “brain processing and memory” a person must use to understand a piece of code or function.

14   “Cachability” is often defined as an ability for being cached. It is possible to cache (save) any information to retrieve it later faster. However, the data might be valid only for a short time or only for a tiny amount of requests. If the data depends on external factors (e.g. user or input) and changes frequently, it’s not well cachable.

15   That is a simplification, of course. The process might have used more memory. Profiles do not show memory used by memory maps, stack, and many other caches required for modern application to work. We will learn more about this in XREF HERE

16   Cyril Northcote Parkinson was a British historian who articulated the management phenomenon, which is now known as Parkinson’s law. Stated as “Work expands to fill the time available for its completion,” it was initially referred to as the government office efficiency that highly correlates to the official’s number in the decision-making body.

17   At least that’s what my studying looked like. This phenomenon is also known as “Student Syndrome”.

18   PB means petabyte. One petabyte is 1000 TB. If we assume an average two hours long 4K movie takes 100GB, this means with 1 PB, we could store 10,000 movies, translating to roughly 2,3 years of constant watching.

19   1 zettabyte is 1 million PB, one billion of TB. I won’t even try to visualize this amount of data. (:

20   Dennard, Robert H.; “Design of ion-implanted MOSFET’s with very small physical dimension” (October 1974)

21   MOSFET stands for “metal–oxide–semiconductor field-effect transistor”, which is simply speaking an insulated-gate allowing to switch electronic signals. This particular technology is behind most memory chips and microprocessors produced between 1960 and now. It has proven to be highly scalable and capable of miniaturization. It is the most frequently manufactured device in history, with 13 sextillion pieces produced between 1960 and 2018, https://en.wikipedia.org/wiki/MOSFET

22   Funnily enough, marketing reasons led companies to hide the inability to reduce the size of transistors effectively by switching CPU generation naming convention from transistor gate length to size of the process. 14nm generation CPUs still have 70nm transistors, similar to 10, 7, and 5nm processes.

23   I am not joking, Microsoft has proven that running servers 40 meters underwater is a great idea that improves energy-efficiency.

24   M1 chip is a great example of interesting tradeoff, choosing speed and both energy and performance efficiency over the flexibility of hardware scaling

25   RISC-V is an open standard for the instruction set architecture allowing easier manufacturing compatible “reduced instruction set computer” chips. Such a set is much simpler and allows more optimized and specialized hardware than general-usage CPUs

26   To ensure developers understand and emphasize with users that have a slower connection, Facebook introduced “2G Tuesdays” that turn on the simulated 2G network mode on the Facebook app

27   That option is not as expensive as we might think. Instance type x1e.32xlarge costs $26.6 per hour, so “only” $19,418 per month

28   Even hardware management has to be different for machines with extremely large hardware. That’s why Linux kernels have special hugemem type of kernels that can manage up to 4 times more memory and ~8 times more logical cores for x86 systems.

29   CAP used to be core system design principle. Its acronym comes from Consistency, Availability and Partition Tolerance. It defines a simple rule that only two of those three can be achieved.

30   Denial of Service, is a state of the system that makes the system unresponsive, usually due to malicious attack. It can also be trigged “accidentally” by the unexpectedly large load.

31   Around 2015, it was the faster supercomputer in Poland, offering 530,5 TFLOPS and almost 2000 nodes, most of them with dedicated GPUs

32   InfiniBand is a high-performance network communication standard, especially popular before fabric optic was invented.

33   Funny enough, Mark Zuckerberg on F8 conference in 2014 announced a change of the famous motto to “Move fast with stable infra”.

34   http://radar.oreilly.com/2008/08/radar-theme-web-ops.html

35   “One example I see often, in the cloud-native world is moving logging stack from Elasticsearch to simpler solutions like Loki. Despite the lack of configurable indexing, the Loki project can offer better logging read performance with a smaller amount of resources

36   There are many good books around split brain phenomenons and its consequences. Some of them I can recommend are: “Thinking Fast and Slow” by Daniel Kahneman and “Everything is F*ucked” by Mark Manson

37   There is also another reason. “Efficient Go” name is very close to one of the best documentation pieces you might find about the Go programming language: “Effective Go”! It might also be one of the first pieces of information I have read about Go. It’s specific, actionable and I recommend reading it if you haven’t.

38   This situation is often called a “noisy neighbour”.

Chapter 2. Efficient Introduction to Go

Go is efficient, scalable, and productive. Some programmers find it fun to work in; others find it unimaginative, even boring. (…) those are not contradictory positions. Go was designed to address the problems faced in software development at Google, which led to a language that is not a breakthrough research language but is nonetheless an excellent tool for engineering large software projects.

Rob Pike, Go at Google: Language Design in the Service of Software Engineering (2012)

A NOTE FOR EARLY RELEASE READERS

With Early Release ebooks, you get books in their earliest form—the author’s raw and unedited content as they write—so you can take advantage of these technologies long before the official release of these titles.

This will be the 2nd chapter of the final book. Please note that the GitHub repo will be made active later on.

If you have comments about how we might improve the content and/or examples in this book, or if you notice missing material within this chapter, please reach out to the editor at mpotter@oreilly.com.

BEFORE WE START, A SMALL DISCLAIMER

I am a huge fan of Go. I think Go achieves an amazing balance between ease of reading or writing and staying relatively low-level, allowing some control of runtime efficiency. All while ensuring best practices to ensure our code works reliably. It might be the most practical, all-rounder programming language in the world.

The number of things developers around the world were able to achieve with Go is impressive. For a few years in a row, Go has stayed on the list of top 5 languages people love or want to learn. It is used in many businesses, including the biggest tech companies like Apple, American Express, Cloudflare, Dell, Google, Netflix, Red Hat, Twitch and more. Of course, as with everything, nothing is perfect. I would probably change, remove or add a couple of things to Go, but If you would wake me up in the middle of the night and ask to write quickly, reliable backend code, I would write it in Go. CLI? In Go. Quick, reliable script? In Go as well. The first language to learn as a junior programmer? Go. Code for IoT, robots and microprocessors? The answer is also Go1. Infrastructure configuration? As of 2021, I don’t think there is a better tool for robust templating than Go too.2.

Don’t get me wrong, there are languages with a specialized set of capabilities or ecosystems that are superior to Go. For example, think about Graphic User Interfaces (GUIs), advanced rendering parts of the game industry or code running in browsers.3 However, once you realize many advantages of the Go language, it is pretty painful to jump back to others.

In Chapter 1, we spent some time establishing a particular efficiency awareness for our software. As a result, we learned that our goal is to write efficient code with the least development effort and cost. This chapter will explain why the Go programming language can be a solid option to achieve this balance between performance and other software qualities.

We will start with a brief introduction to Go in “Basics You Should Know About Go” then with a set of more advanced features in “Advanced Language Elements”. Both sections list the short but essential facts everyone should know, something I wish I had when I started my journey with Go in 2014. It will cover much more than just basic information related to efficiency and can be used to introduce yourself to Go. However, if you are entirely new to the language, I would still recommend reading those sections, then check other resources like “A Tour of Go”, write your first program in Go and then get back to this book. On the other hand, if you consider yourself a more advanced user or expert, I would suggest not skipping “Basics You Should Know About Go” and “Advanced Language Elements”. I explain few less known facts about Go that you might find interesting (or controversial!).

Last but not least, we will finish with answering the tricky question about the overall. Go performance capabilities in “Is Go “Fast”?”, compared to other languages.

Basics You Should Know About Go

Go is an open-source project maintained by Google within a distributed team called “Go Team”. The project consists of the programming language specification, compilator, tooling, documentation and standard libraries.

To quickly understand Go basics and its characteristics in fast forward mode, let’s go through some facts and basic best practices. While some advice here might feel opinionated, this is all based on my experience working with Go since 2014—a background full of incidents, past mistakes and lessons learned the hard way. I’m sharing them here, so you don’t need to make those errors.

Imperative, Compiled and Statically Typed Language

The central part of the Go project is the general-purpose language with the same name, primarily designed for systems programming. As you notice in our Example 2-1 below, Go is an imperative language, so we have (some) control over how things are executed. In addition, it’s statically typed, and compiled which means that the compiler can perform many optimizations and checks before the program has ever run. Those characteristics alone are an excellent start to make Go suitable for reliable and efficient programs.

Example 2-1. Simple program printing “Hello World” and exiting.

package main

import "fmt"

func main() {

fmt.Println("Hello World!")

}

Both project and language are called Go, yet sometimes you can refer to them with Golang.

GO VS GOLANG.

As the rule of thumb, we should always use the Go name everywhere, unless it’s clashing with the English word go, or an ancient game called Go. Golang came from the domain choice (https://golang.org) since go was unavailable to authors. So use Golang when doing web searches for resources about this programming language.

Go also has its own mascot, called “Go gopher”, presented in Figure 2-1.

Figure 2-1. Go Gopher (credits: Renee French)

We see this cute Gopher in various forms, situations, and combinations, i.e., conference talks, blog posts, or project logos. Sometimes Go developers are called “gophers”, too!

Designed to Improve Serious Codebases

It all started when three experienced programmers from Google sketched the idea of the Go language around 2007.

Rob Pike

Co-creator of UTF-8 and Plan 9 operating system. Co-author of many programming languages before Go e.g. Limbo for writing distributed systems and Newsqueak for writing concurrent applications in graphical user interfaces. Both were inspired by Hoare’s Communicating Sequential Processes (CSP).4

Robert Griesemer

Among other work, he developed Sawzall language and did a doctorate with Niklaus Wirth. (The same Niklaus that was mentioned many times in Chapter 1).

Ken Thompson

One of the original authors of the first Unix system. Sole creator of grep command-line utility. Ken co-created UTF-8 and Plan 9 together with Rob Pike. He wrote a couple of languages, too, e.g. Bon and B programming languages.

They aimed to create a new programming language that was meant to improve mainstream programming, led by C++, Java and Python at that point. After a year, it became a full-time project, with Ian Taylor and Russ Cox joining what was later referenced as “Go Team” in 2008. Go Team announced the public Go project in 2009, with version 1.0 released in March 2012.

The main frustrations related to C++ mentioned in the design of Go were:

- Complexity, many ways of doing the same thing, too many features.

- Ultra-long compilation times, especially for bigger codebases.

- Cost of updates and refactors in large projects.

- Not easy to use and prone to the errors memory model.

Those elements are why Go was born, from the frustration of existing solutions and ambition to allow more by doing less. The guiding principles were to make a language that does not trade off safety for less repetition yet allows simpler code. It does not sacrifice execution efficiency for faster compilation or interpreting yet ensures build times are quick enough. https://talks.golang.org/2012/splash.article#TOC_5 [Go tries to compile as fast as possible, e.g. thanks to explicit imports]. Especially with caching enabled by default, only changed code is compiled, so build times are rarely longer than a minute.

YOU CAN TREAT GO CODE AS SCRIPT!

While technically, Go is a compiled language, you can run it like you would run Javascript, Shell or Python. It’s as simple as invoking go run <executable package> <flags>. It works great because the compilation is ultra-fast. You can treat it like a scripting language while maintaining the advantages of compilation.

In terms of syntax, Go was meant to be simple, light on keywords, familiar. Basing syntax on C with type derivation (automatic type detection, like auto in C++), no forward declarations, no header files. Concepts are kept orthogonal, which allows easier combination and reasoning about them. Orthogonality for elements means that, for example, we can add methods to any type, any data definition (adding methods is separate to creating types). Interfaces are orthogonal to types too.

Governed by Google, Yet Open Source

Since announcing Go, all development is done in open, with public mailing lists and bug trackers. Changes go to the public, authoritative source code, held under the BSD style license. The Go team reviews all contributions. The process is the same if the change or idea is coming from Google or not. Project roadmap and proposals are developed in public too.

Unfortunately, the sad truth is that there are many open-source projects, but some projects are less open than others. Google is still the only company stewarding Go and have the last decisive control over it. Even if anyone can modify, use and contribute, projects coordinated by a single vendor risk selfish decisions or relicensing. While there were some controversial cases where the Go team decision surprised the community5, overall, the project is very reasonably well-governed. Countless changes came from outside of Google, and especially the Go 2.0 draft proposal process has been well respected and community-driven. In the end, I believe consistent decision making and stewarding from the Go team brings a lot of benefits too. Conflicts and different views are inevitable, and having one consistent overview, even if not perfect, might be better than no decision or many ways of doing the same thing.

So far, this project setup is proven to work well for adoption and language stability. For our software efficiency goals, such alignment couldn’t be better too. We have a big company invested in making sure each release does not bring any performance regressions. These days lots of internal Google software depends on Go, e.g. Google Cloud Platform. And many people rely on Googe Cloud Platform to be reliable. On the other hand, we have a vast Go community that gives feedback, finds bugs, contributes ideas and optimizations. And if that’s not enough, we have open source code, allowing us, mere mortal developers, to dive into the actual Go libraries, runtime (“Go Runtime”) etc., to understand the performance characteristics of the particular code.

Simplicity, Safety and Readability are Paramount

Robert Griesemer mentioned in GopherCon 2015 that they, first of all, when they first started building Go, they knew what things NOT to do. The main guiding principle was that simplicity, safety, and readability are paramount. In other words, Go follows the pattern of Less is More. This is a potent idiom that spans many areas. In Go, there is only one idiomatic coding style6, and a tool called gofmt ensures most of it. In particular, code formatting (next to naming) is an element that is almost never settled among programmers. We spend time arguing about it and tuning it to our specific needs and beliefs. Thanks to a single style enforced by tooling, we save an enormous amount of time. As one of Go proverb goes, "Gofmt’s style is no one’s favourite, yet Gofmt is everyone’s favourite.“. Overall, the Go authors planned the language to be minimal so that there is essentially one way to write a particular construct. This takes away a lot of decision making when you are writing a program. There is one way of handling errors, one way of writing objects, one way of running things concurrently etc.

A huge number of features might be “missing” from Go, yet one could say it is more expressive than C or C++. Such minimalism allows maintaining simplicity and readability of Go code, which improves software reliability, safety and overall higher velocity towards application goals.

IS MY CODE IDIOMATIC?

The Idiomatic word is heavily overused in the Go community. Usually, it means Go patterns that are “often” used. Since Go adoption has grown a lot, there are many creative ways people have improved the initial “idiomatic” style. Nowadays, it’s as clear as the “This is the way” saying from Mandalorian Series. Use this word with care and avoid it unless you can elaborate further.

Interestingly, the “Less is More” idiom can help our efficiency efforts for this book’s purpose. As we learned in Chapter 1, if you do less work in runtime, it usually means faster, lean execution and less complex code. In this book, we will try to maintain this aspect while improving our code performance.

Packaging and Modules

Code is organized into packages and modules. Source code can be placed in different directories. A package is a collection of source files (with the .go suffix) in the same directory. The package name is specified with the package statement at the top of each source file, as seen in Example 2-1. All files in the same directory have to have the same package name7 (the package name can be different from the directory name). Multiple packages can be part of a single Go Module. A module is a directory with go.mod file that states all dependent modules with their versions required to build the Go application. This file is then used by the dependency management tool Go Modules. Each source file in such a module can then import packages from the same module or external modules. Some packages can also be “executable”. For example, if a package is called main and has func main() in some file, we can execute it. Sometimes such a package is placed in the cmd directory for easier discovery. Note that you cannot import the executable package. You can only build or run it.

Within the package, you can decide which functions, types, interfaces and methods are exported to package users and which are accessible only in package scope. This is important because it’s better to export the minimal amount of API possible for readability, reusability, and reliability. Go does not have any private or public keywords for this. Instead, it takes a slightly new approach. As Example 2-2 shows, if the construct name starts with an upper case letter, any code outside the package can use it. If the element name begins with a lower case letter, it’s private. It’s worth noting that this pattern works for all constructs equally, e.g. functions, types, interfaces, variables etc. (orthogonality).

Example 2-2. Construct accessibility control using naming case.

package main

const privateConst = 1

const PublicConst = 2

var privateVar int

var PublicVar int

func privateFunc() {}

func PublicFunc()  {}

type privateStruct struct {

privateField int

PublicField  int

}

func (privateStruct) privateMethod() {}

func (privateStruct) PublicMethod()  {}

type PublicStruct struct {

privateField int

PublicField  int

}

func (PublicStruct) privateMethod() {}

func (PublicStruct) PublicMethod()  {}

type privateInterface interface {

privateMethod()

PublicMethod()

}

type PublicInterface interface {

privateMethod()

PublicMethod()

}

Careful readers might notice tricky cases of exported fields or methods on private type or interface. Can someone outside of the package use them if the struct or interface itself is private? This is quite rarely used, but the answer is yes, you can return a private interface or type in public function e.g. func New() privateStruct { return privateStruct{}}. All its public fields and methods are usable to the user of our package.

INTERNAL PACKAGES

You can name and structure your code directories as you want to form packages, but one directory name is reserved for special meaning. If you want to make sure only the given package can import other packages, you can create a package subdirectory named internal. Any package under the internal directory can’t be imported by any other package than ancestor (and other packages in internal).

Dependencies Transparency by Default

In my experience, it was common to import some libraries, e.g. in C++, C# or Java in compiled form (e.g. JAR file), and use exported functions and structures/classes mentioned in some headers or documentation. Importing compiled code had some benefits. Notably, it was hiding hard to obtain, indirect dependencies, special compilation tooling or extra resources to build it. It was also easier to sell a coding library for close source purposes without exposing the source code.8 In principle, this is meant to work well. Developers of the library maintain specific programmatic contracts (API), and users of such libraries do not need to worry about the complexities of implementations.

Unfortunately, in practice, this is rarely that perfect. Implementation can be broken or inefficient, the interfaces can mislead, and documentation can be missing. In such cases, access to the source code is invaluable, allowing us to understand implementation deeper. We can find issues based on code, not by guessing, and propose a fix to the library author or fork the package and use it immediately. We can extract the required pieces and use them to build something else.

Go assumes this imperfection by requiring each library’s parts (in Go: module’s packages) to be explicitly imported using a package URI called “import path”. Such import is also strictly controlled, i.e. unused imports or cyclic dependencies are causing a compilation error. Let’s see some of the different ways to declare those imports in Example 2-3.

Example 2-3. Portion of import statements from github.com/prometheus/prometheus module, main.go file.

import (

"context"

"net/http"

_ "net/http/pprof"

"github.com/oklog/run"

"github.com/pkg/errors"

"github.com/prometheus/common/version"

"go.uber.org/atomic"

"github.com/prometheus/prometheus/config"

promruntime "github.com/prometheus/prometheus/pkg/runtime"

"github.com/prometheus/prometheus/scrape"

"github.com/prometheus/prometheus/storage"

"github.com/prometheus/prometheus/storage/remote"

"github.com/prometheus/prometheus/tsdb"

"github.com/prometheus/prometheus/util/strutil"

"github.com/prometheus/prometheus/web"

)

If the import declaration does not have a domain with a path structure, it means that the “standard” package is imported. Such a package has to have its source available in the Go version you have installed in your environment. This particular import, allows to use code from $(go env GOROOT)/src/context/ directory with context reference e.g context.Background()

Package can be imported explicitly without any identifier. This means that we don’t want to reference any construct from this package, but we want to have some global variables initialized. In this case, the pprof package will add debugging endpoints to the global HTTP server router. While allowed, in practice, we should avoid reusing global, modifiable variables.

Non-standard packages can be imported using an import path in a form of an internet domain name and an optional path to the package in a certain module. go tooling integrates well with https://github.com so if you host your Go code in a git repository it will find a specified package for you. In this case, it’s the https://github.com/oklog/run git repository with the run package in the github.com/oklog/run module.

If the package is taken from the current module (in this case, our module is github.com/prometheus/prometheus), packages will be resolved from your local directory. In our example, <module root>/config.

It might be confusing, but github.com/prometheus/common import is from a different module, so it’s downloaded upstream.

This model focuses on open and clearly defined dependencies. It works exceptionally well with the open-source distribution model, where the community can collaborate on robust packages in the public git repositories. Of course, a module or package can also be hidden using standard version control authentication protocols. Furthermore, at the current moment, the official tooling does not support distributing packages in binary form, so dependency source is highly encouraged to be present for compilation purposes.

The challenges of software dependency are not easy to solve. Go learnt by mistakes of C++ and others, takes a careful approach to avoid long compilation times, but also an effect commonly called a “dependency hell”.

Through the design of the standard library, great effort was spent on controlling dependencies. It can be better to copy a little code than to pull in a big library for one function. (A test in the system build complains if new core dependencies arise.) Dependency hygiene trumps code reuse. One example of this in practice is that the (low-level) net package has its own integer-to-decimal conversion routine to avoid depending on the bigger and dependency-heavy formatted I/O package. Another is that the string conversion package strconv has a private implementation of the definition of printable characters rather than pull in the large Unicode character class tables; that strconv honors the Unicode standard is verified by the package’s tests.

Rob Pike, Go at Google: Language Design in the Service of Software Engineering (2012)

Again, with efficiency in mind, potential minimalism in terms of dependencies and transparency bring enormous value. Fewer unknowns means we can quickly detect main bottlenecks and focus on the most significant value optimizations first. If we notice a potential room for optimization in our dependency, we don’t need to work around it. Still, instead, we are usually welcome to contribute the fix directly to the upstream, which helps both sides!

Consistent Tooling

From the very beginning of Go, it had a powerful and consistent set tool as a part of its command-line interface tool called go. Let’s enumerate a few utilities:

- go bug opens a new browser tab with the correct place where you can fill an official bug report (go repository on GitHub).

- go build -o <output path> <packages> builds given Go packages.

- go env shows all Go-related environment variables currently set in your terminal session.

- go fmt <file, packages or directories> formats given artefacts to the desired style, clean whitespaces, fix wrong indentations etc. Note that the source code does not need to be even valid and compilable Go code. You can also install an extended official formatter called

- goimports also cleans and formats your import statements.

TIP

For the best experience, set your programming IDE to run goimports -w $FILE on every file save, to not worry about the indentation anymore.

- go get <package@version> allows you to install the desired dependency with the expected version. Use the @latest suffix to get the latest version of @none to uninstall dependency.

- go help <command/topic> prints documentation about the command or given topic. For example, go help environment tells you all about possible environment variables Go uses.

- go install <package> similar to go get and install the binary if the given package is “executable”.

- go list lists Go packages and modules. It allows flexible output formatting using Go templates (explained later) e.g go list -mod=readonly -m -f '{{ if and (not .Indirect) (not .Main)}}{{.Path}}{{end}}' all lists all direct non-executable dependant modules.

- go mod allows managing dependant modules.

- go test allows running unit, fuzz test and benchmarks. We will discuss the latter in detail in XREF HERE.

- go tool hosts a dozen of more advanced CLI tools. We will especially take a close look at `go tool pprof ` in XREF HERE for performance optimizations.

- go vet runs basic static analysis checks.

In most cases, go CLI is all you need for effective Go programming.9

Single Way of Handling Errors

Errors are an inevitable part of every running software. Especially in distributed systems, they are expected by design, with advanced researches and algorithms for handling different types of failures.10 Despite the commonality of errors, most programming languages do not recommend or enforce a particular way of failure handling. For example, in C++ you see programmers using all means possible to return an error from a function:

- Exceptions

- Integer return codes (if the returned value is non zero, it means error)

- Implicit status codes11

- Other sentinel values (if the returned value is null, then it’s an error)

- Returning potential error by argument

- Custom error classes

- Monads12

Each option has its pros and cons, but just the fact that there are so many ways of handling errors can cause severe issues. It causes surprises by potentially hiding the fact that some statements can return an error, introduce complexity and, as a result, can make our software unreliable.

Undoubtedly, the intention for so many options was good. It allows a developer choice. Maybe the software you create is non-critical, or it is its first iteration, so you want to make “a happy path” crystal clear. In such cases masking some “bad paths” completely sounds like a good short-term idea, right? Unfortunately, as with many shortcuts, it poses numerous dangers. Software complexity and demand for functionalities causes the code to never go out of the “first iteration”, and non-critical code quickly becomes a dependency for something critical. This is one of the most important causes of unreliability or hard to debug software.

Go takes a unique path by treating the error as a first citizen language feature. It assumes we want to write reliable software, making error handling explicit, easy and uniform across libraries and interfaces. Let’s see some examples in Example 2-4.

Example 2-4. Defining error flow.

func noErrCanHappen() int {

// ...

return 204

}

func doOrErr() error {

// ...

if shouldFail() {

return errors.New("ups, XYZ failed")

}

return nil

}

func intOrErr() (int, error) {

// ...

if shouldFail() {

return 0, errors.New("ups, XYZ2 failed")

}

return noErrCanHappen(), nil

}

The critical aspect here is that functions and methods define the error flow as part of their signature. In this case, the noErrCanHappen function states that there is no way any error can happen during its invocation.

Just by looking at the doOrErr function signature, we know some errors can happen. We don’t know what type of error yet though, we only know it is implementing a built-in error interface. We also know that if the error is nil, there was no error.

The fact that Go functions can return multiple arguments is leveraged when they want to calculate some result in a “happy path”. If the error can happen, it should be the last return argument (always). From the caller side, we should then only touch the result if the error is nil.

It’s worth noting that Go also has an exception mechanism called panics. Panics are recoverable using the recover() built-in function. While useful or necessary for certain cases (e.g. initialization), you should never use panics for conventional error handling in your production code in practice. They are less efficient, hide failures and overall surprise the programmers. Having errors as part of invocation allows both the compilator and programmer to be prepared for error cases in the normal execution path. Example 2-5 shows how we can handle errors if they occur in our functions’ execution path.

Example 2-5. Checking and handling errors.

import "github.com/pkg/errors"

func main() {

ret := noErrCanHappen()

if err := nestedDoOrErr(); err != nil {

// handle error

}

ret2, err := intOrErr()

if err != nil {

// handle error

}

// ...

}

func nestedDoOrErr() error {

// ...

if err := doOrErr(); err != nil {

return errors.Wrap(err, "do")

}

return nil

}

Notice that we did not import the built-in error package, but instead, we used external, open-source drop-in replacement github.com/pkg/errors. This allows a bit more advanced logic like wrapping errors you will see in <3>

To tell if an error happened, we need to check if the err variable is nil or not. If an error occurs, we can follow with error handling. Usually, it means logging it, exiting the program, incrementing metrics, or even explicitly ignoring it.

In some cases, it’s appropriate to delegate error handling to the caller. If the function can fail from many errors, consider wrapping it with a errors.Wrap function to add a short context of what exactly is wrong.

ERROR WRAPPING

Notice that I recommended errors.Wrap (or errors.Wrapf) instead of the built-in way of wrapping errors. Go defines the` +w` identifier for the Sprintf type of functions that allows passing an error. Currently, I would not recommend +w because it’s not type-safe and as explicit as Wrap, which was causing non-trivial bugs in the past.

The one way of defining errors and handling them is one of Go’s best features. Interestingly, it is one of the language disadvantages due to verbosity and certain boilerplate involved. It sometimes might feel repetitive, but tools are allowing you to mitigate the boilerplate.

TIP

Some Go IDEs defines code templates. For example, in JetBrain’s GoLand product, writing err and pressing the tab button will generate a valid if err != nil statement. You can also collapse/un-collapse error handling blocks for readability.

Another common complaint is that it can feel very “pessimistic” to write Go because the errors which may never occur are visible in plain sight. The programmer has to decide what to do with them at every step, which takes mental energy and time. Yet, in my experience, it’s worth our work and makes our programs much more predictable and easier to debug.

NEVER IGNORE ERRORS!

Due to the verbosity of error handling, it’s tempting to skip err != nil checks. Consider not doing it unless you know that a function will never return an error (and in future versions!). If you don’t know what to do with the error, consider passing it to the caller by default. If you have to ignore the error, consider doing it explicitly with `_ = ` syntax. Also, always use liners, which will warn you about not checked errors.

Are there any implications of the error handling for general Go code runtime efficiency? Yes! Unfortunately, it’s much more significant than developers usually anticipate. In my experience, error paths are frequently an order of magnitudes slower and more expensive to execute than happy paths. One of the reasons is we tend not to monitor or test error paths correctly (we will talk about the importance of it in XREF HERE), so not many realize their impact. Another common reason is that construction errors often involves heavy string manipulation for creating human-readable messages. It can be costly, especially with lengthy debugging tags. We will touch on those elements in XREF HERE. Understanding those implications and ensuring consistent and efficient error handling is essential in every software, and we will take a detailed look at that in the following chapters.

Strong Ecosystem

A commonly stated strong point of Go is that its ecosystem is exceptionally mature for such a “young” language. While things listed in this section are not mandatory for solid programming dialect, they improve the whole development experience. This is also why the Go community is so large and still growing.

First of all, Go allows the programmer to focus on business logic without necessarily reimplementing or importing third-party libraries for basic functionalities like YAML decoding or cryptographic hashing algorithms. Go standard libraries are high quality, robust, ultra backwards compatible and rich in features. They are well benchmarked, have solid APIs and good documentation. As a result, you can achieve most things without importing external packages. For example, running an HTTP server is dead simple as visualized by Example 2-6:

Example 2-6. Minimal code for serving HTTP requests (not recommended for production, though13).

package main

import  "net/http"

func handle(w http.ResponseWriter, _ *http.Request) {

w.Write([]byte("It kind of works!"))

}

func main() {

http.ListenAndServe(":8080", http.HandlerFunc(handle))

}

In most cases, the efficiency of standard libraries is good enough or even better than third party alternatives. For example, especially lower-level elements of packages, net/http for HTTP client and server code or crypto, math, sort parts (and more!), have a good amount of optimizations to serve most of the use cases. This allows developers to build more complex code on top while not worrying about the performance of basics like sorting. Yet, that’s not always the case. Some libraries are meant for specific usage, and misusing them may result in significant resource waste. We will look at all things you need to be aware of in XREF HERE.

Another highlight of the mature ecosystem is a basic, official in-browser Go editor called Go Playground. It’s a fantastic tool if you want to test something out quickly or share an interactive code example. It’s also straightforward to extend, so the community often publish variations of Go playground to try and share experimental language features like generics.

Last but not least, the Go project defines its own templating language called Go Templates. In some way, it’s similar to Python’s Jinja2 language. While it sounds like a side feature of Go, it’s beneficial in any dynamic text or HTML generation. It is also often used in popular tools like Helm or Hugo. We will discuss how to use Go Templates efficiently in XREF HERE.

Unused Import or Variable Causes Build Error

If you define a variable in Go, but you never read any value from it or don’t pass it to another function, compilation will fail. Similarly, if you added a package to the import statement, but you don’t use that package in your file.

From what I see, Go developers got used to this feature and love it, but it might be surprising for newcomers. Failing on unused constructs can be frustrating if you want to play with the language quickly, e.g. create some variable without using it for debugging purposes. There are, however, ways to handle those cases explicitly! You can see a few examples of dealing with those usage checks in Example 2-7.

Example 2-7. Various examples of unused and used variables.

package main

func use(_ int) {}

func main() {

var a int // error: a declared but not used

b := 1 // error: b declared but not used

var c int

d := c // error: d declared but not used

e := 1

use(e)

f := 1

_ = f

}

Variables a, ,b, and c are not used, so they cause a compilation error.

Variable e is used.

Variable f is technically used for explicit no identifier (_). Such an approach is useful if you explicitly want to tell the reader (and compiler) that you want to ignore such value.

Similarly, unused imports will fail the compilation process, so tools like goimports (mentioned in “Consistent Tooling”) automatically removes unused ones. Overall, failing on unused variables and imports effectively ensures that code stays clear and relevant. Note that only internal function variables are check. Elements like unused struct fields, methods or types are not checked.

Unit Testing and Table Tests

Tests are the mandatory part of every application, small or big. In Go, tests are a natural part of the development process—easy to write, focused on simplicity and readability. If we want to talk about efficient code, we need to have solid testing in place, allowing us to iterate over the program without worrying about regressions. Add a file with the _test.go suffix to introduce a unit test to your code within a package. You can write any Go code within that file, which won’t be reachable from the production code. There are, however, four types of different functions you can add that will be invoked for different parts of testing. A certain signature distinguishes types, notably function name prefix: Test, Fuzz, Example or Benchmark and specific argument. Let’s walk through the unit test type in Example 2-8. To make it more interesting, it’s a table test. Examples and benchmarks are explained in “Code Documentation as a First Citizen” and XREF HERE.

Example 2-8. Example unit table test.

--- max_test.go ---

package max

import (

"math"

"testing"

"github.com/efficientgo/tools/core/pkg/testutil"

)

func TestMax(t *testing.T) {

for _, tcase := range []struct {

a, b     int

expected int

}{

{a: 0, b: 0, expected: 0},

{a: -1, b: 0, expected: 0},

{a: 1, b: 0, expected: 1},

{a: 0, b: -1, expected: 0},

{a: 0, b: 1, expected: 1},

{a: math.MinInt64, b: math.MaxInt64, expected: math.MaxInt64},

} {

t.Run("", func(t *testing.T) {

testutil.Equals(t, tcase.expected, max(tcase.a, tcase.b))

})

}

}

If function inside _test.go file is named with Test word and takes exactly t *testing.T it is considered a “unit test”. You can run them through go test command.

Usually, we want to test a specific function using multiple test cases (often edge cases) that define different input and expected output. This is where I would suggest using table tests. Define your input, output and run the same function in an easy to read loop.

Optionally, you can invoke t.Run, which allows you to specify a subtest. It’s a good practice to define those on dynamic test cases like in table tests. It will enable you to navigate to the failing case quickly.

Go testing.T type gives useful methods like Fail or Fatal to abort and fail the unit test, or Error to fail but continue running and check other potential errors. In our example, I propose using a simple helper, called testutil.Equals, giving you a nice diff.14

I would recommend you writing tests often. It might surprise you, but writing unit tests for critical parts upfront will generally help you implement desired features much faster. This is why I would recommend following some reasonable form of Test-Driven Development, covered in XREF HERE.

To sum up, the above information should give you a good overview of the language goals, strengths, and features before moving to more advanced features in “Advanced Language Elements” next.

Advanced Language Elements

Let’s now discuss more advanced features of Go. Similarly to the basics mentioned in the previous section, it’s crucial to overview core language capabilities before discussing efficiency improvements.

Code Documentation as a First Citizen

Every project at some point needs solid API documentation. For library-type projects, the programmatic APIs are the main entry point. Robust interfaces with good descriptions allow developers to hide complexity, bring value and avoid surprises. A code interface overview is essential for applications, too, allowing anyone to understand the codebase quicker. It’s also not uncommon to reuse an application’s Go packages in other projects.

Instead of relying on the community to create many potentially fragmented and incompatible solutions, the Go project developed a tool called godoc from the start. In some way, it behaves similarly to Python’s Docstring and Java’s Javadoc. godoc generates consistent documentation HTML website directly from the code and its comments.

The amazing part is that you don’t have many special conventions that would make the code comments less readable from the source code directly. To use this tool effectively, you need to remember five things. Let’s go through them using Example 2-9. The resulting HTML page, when godoc is invoked, can be seen in Figure 2-2.

Example 2-9. Example code with godoc compatible documentation.

--- block.go ---

// Package block contains common functionality for interacting with TSDB blocks

// in the context of Thanos.

package block

import ...

const (

// MetaFilename is the known JSON filename for meta information.

MetaFilename = "meta.json"

)

// Download downloads directory that is meant to be block directory. If any of the files

// have a hash calculated in the meta file and it matches with what is in the destination path then

// we do not download it. We always re-download the meta file.

// BUG(bwplotka): No known bugs, but if there was one, it would be outlined here.

func Download(ctx context.Context, id ulid.ULID, dst string) error {

// ...

// cleanUp cleans the partially uploaded files.

func cleanUp(ctx context.Context, id ulid.ULID) error {

// ...

--- block_test.go ---

package block_test

import ...

func ExampleDownload() {

if err := block.Download(context.Background(), ulid.MustNew(0, nil), "here"); err != nil {

fmt.Println(err)

}

// Output: downloaded

}

Optional package level description must be placed right on top of the package entry with no intervening blank line. If many source files have those entries, godoc will collect them all.

Any public construct should have a full sentences commentary starting with the name of the construct (it’s important!), similarly right before its definition.

Known bugs can be mentioned with // BUG(who) statements.

Private constructs can have comments, but they will never be exposed in documentation anyway since they are private. Be consistent and start them with a construct name, too, for readability.

If you write a function named Example<ConstructName> in the test file e.g block_test.go (package name has to be different too e.g block_test), godoc will generate an interactive code block with desired examples. Since they are part of the unit test, they will be actively tested against, so your examples stay updated.

Figure 2-2. godoc output of Example 2-9

I highly recommend sticking to those five simple rules. Not only because you can manually run godoc and generate yourself documentation web page, but the additional benefit is that those rules make your Go code comments structured and consistent. Everyone knows how to read them and where to find them.

TIP

I recommend using whole English sentences in all comments, even if those will not appear in godoc. It will help you keep your code commentary self-explanatory and explicit. After all, comments are for humans to read.

Furthermore, Go Team maintains a public documentation website capable of scraping all public repositories called https://pkg.go.dev. Thus, if your public code repository is compatible with godoc it will be rendered correctly, and users can read the autogenerate documentation for every module/package version. For example, see our Example 2-9(TODO: Link once code examples are published) web page here.

Backwards Compatibility and Portability

Go has a strong take on backwards compatibility guarantees. This means that core APIs, libraries, and language spec should never break old code created for Go 1.015. This was proven to be well executed. There is a lot of trust in upgrading Go to the latest minor or patch versions in practice. Upgrades are, in most cases, smooth and without significant bugs and surprises.

In terms of efficiency compatibility, it’s hard to talk about any guarantees. For both the Go project and any library, there is (usually) no guarantee that the function that does two memory allocations now will not use hundreds in the next version. There have been surprises between the version in the efficiency and speed characteristics. The community is working hard on improving the compilation and language runtime (more in “Go Runtime” and XREF HERE). Since the hardware and operating systems are developed too, the Go team is experimenting with different optimizations and features to allow everyone to have more efficient execution. Of course, we don’t speak here about major performance regression, as those are usually noticed and fixed in the release candidate period. Yet if we want our software to be deliberately fast and efficient, we need to be more vigilant and aware of the changes Go is introducing. We will discuss this in XREF HERE.

Source code is compiled into binary code that is targeted to each platform. Yet Go tooling allows cross-platform compilation, so you can build binaries to almost all architectures and operating systems.

TIP

When you execute, Go binary which was compiled for a different operating system or architecture, it can return cryptic error messages. A common one is Exec format error when you try to run binary for Darwin (macOS) on Linux.

Speaking about portability, we can’t skip mentioning the Go runtime and its characteristics.

Go Runtime

Many languages decided to solve portability across different hardware and operating systems using Virtual Machines. Typical examples are Java Virtual Machine (JVM) for Java bytecode compatible languages (e.g. Java or Scala) and Common Language Runtime CLR for .NET code, e.g. C#. Such a virtual machine allows building languages that do not need to worry about complex memory management logic (allocation and releasing), differences between hardware and operating systems, etc. JVM or CLR is interpreting the intermediate byte code and transfers program instructions to the host. Unfortunately, while making it easier to create a programming language, they also introduce some overhead and a significant amount of unknowns.16 To mitigate the overhead, those virtual machines often use complex optimizations like Just in time (JIT) compilation to process chunks of specific virtual machine byte code to machine code on the fly.

Go does not need any “virtual machine”. Our code and used libraries compile fully to machine code during the compilation time. Thanks to standard library support of large operating systems and hardware, our code, if compiled against particular architecture, it will run there with no issues.

Yet, something is running in the background (concurrently) when our program starts. It’s the Go runtime logic that among other, minor features of Go is responsible for:

Managing Concurrency

Go runtime implements one of the most known, unique features of Go. Robust and easy to use concurrency framework called “Go Routines”. This framework allows you to run your code concurrently (not parallel!17) while still maintaining a single process from the machine point of view. The unique part about it is its simplicity. The statement go func() { /* some code */}() starts a new routine. This is one of the key elements of how we can make our code run faster (not necessarily more efficiently in terms of resource usage), and we will explore it in detail in XREF HERE.

Memory Management with Garbage Collection

Go wouldn’t be as simple to write as it is now if runtime would not implement solid memory management. Every new construct in Go allocates some memory because it has to be stored somewhere for the program execution duration. Go follows the standard memory model and splits the process virtual memory into two areas stack and heap. The latter is the place for elements that lives longer than the duration of function execution. Go does not require (and allow) manually releasing memory for such items. Instead, Go runtime implements the Garbage Collection routine, which does that as efficiently as possible. This area impacts memory resource consumption and will be discussed in detail in XREF HERE.

Go runtime is similar to libc (C runtime) that performs similar duties for languages like C or C++. It is primarily written in Go itself too.18 In fact, you can tell Go to be compiled with cgo, which then allows packages that use C functions and thus are depending and using C runtime.

Object Oriented Programming in Go

Undoubtedly Object-oriented programming (OOP) got enormous traction over the last decades. It was invented around 1967 by Alan Key, and it’s still the most popular paradigm in programming.19 In principle, it allows us to think about code as some objects with attributes (in Go fields) and behaviours (Methods) telling each other what to do. Most OOP examples talk about some high-level abstractions like an animal that exposes the Walk() method or car that allows to Ride(), but in practice, objects are usually less abstract, yet still helpful to be encapsulated and described by a class. As we can see in Example 2-10, you can have function Compact that takes a list of Block objects. Compact then translates that slice into Sortable object that implements sorting interface sort.Sort accepts. After sorting, Compact instantiates an object of the Group class and uses the Group Add() method to add all blocks. Since Group is also a Block, it returns the Group object as the compacted block. Object-Oriented Programming allows us to leverage advanced concepts like Encapsulation, Abstraction, Polymorphisms and Inheritance. Example 2-10 shows how we can achieve those vital programming mechanisms in Go.

Example 2-10. Example of the Object-Oriented Programming in Go Part 1.

type Block struct {

id         string

start, end time.Time

// ...

}

func (b Block) String() string {

return fmt.Sprint(b.id, ": ", b.start.Format(time.RFC3339), "-", b.end.Format(time.RFC3339))

}

type Group struct {

Block

// ...

}

func (g *Group) Add(b Block) {

if g.end.IsZero() || g.end.Before(b.end) {

g.end = b.end

}

if g.start.IsZero() || g.start.After(b.start) {

g.start = b.start

}

// ...

}

func Compact(blocks []Block) Block {

sort.Sort(ToSortable(blocks))

g := &Group{}

for _, b := range blocks {

g.Add(b)

}

return g.Block

}

In Go there is no separation between structures and classes. So, on top of basic types like integer, string etc., a struct type can have methods (behaviours) and fields (attributes). Use structures and methods to encapsulate more complex logic under a more straightforward interface, e.g. String() method on Block.

If we add some struct, e.g. Block into another struct, e.g. Group without any field name, such a Block struct is considered embedded. Embedding allows us in a simple way to get the most valuable part of inheritance, so borrowing the embedded structure fields and methods. In this case, Group will have Block's fields and String method. This way, we can reuse a significant amount of code.

There are two types of methods you can define in Go. One with value receiver (e.g String() method) and Add() with pointer receiver. “Receiver” is the variable after func of the type you add a method to, e.g. (g *Group). It might sound convoluted, but the rule regarding which one to use is straightforward. Use pointer receiver (add *) if your method is meant to modify the local receiver state or if any other method does that (for consistency, don’t mix different receiver types within one struct). In our example, if the Group Add() method would be a value receiver, we will not persist potentially injected g.min and g.max values. That’s why we need to add a pointer receiver instead.

Standard library sorting function used with our Sortable type explained in Example 2-12.

This is the only thing missing for Go to support inheritance fully. Go does not allow casting specific types into another type unless it’s an alias or strict single-struct embedding (shown in Example 2-12). You can only cast interface into some type. That’s why here we need to specify embedded struct Block explicitly.

TIP

You can embed as many unique structures as you want within one struct. If the compilator can’t tell which method to use, compilation will fail because of the name clash. Use type name to explicitly tell compilator what should be used. For example, taking Example 2-10, you can reference the id variable from Group ’s embedded Block struct by writing Group{}.Block.id.

Go also allows defining interfaces that tell what methods struct has to implement to match it. Thus, there is no need to mark a specific struct explicitly that it implements a particular interface. It’s enough just to implement specified methods. Let’s see an example sorting interface exposed by the standard library in Example 2-11.

Example 2-11. Sorting interface from standard sort Go library.

// A type, typically a collection, that satisfies sort.Interface can be

// sorted by the routines in this package. The methods require that the

// elements of the collection be enumerated by an integer index.

type Interface interface {

// Len is the number of elements in the collection.

Len() int

// Less reports whether the element with

// index i should sort before the element with index j.

Less(i, j int) bool

// Swap swaps the elements with indexes i and j.

Swap(i, j int)

}

In order to use our type in sort.Sort function, it has to implement all sort.Interface methods. Example 2-12 shows how Sortable type do it.

Example 2-12. Example of the Object-Oriented Programming in Go Part 2.

type Sortable []Block

func ToSortable(blocks []Block) sort.Interface {

var s Sortable = blocks

return &s

}

func (b *Sortable) Len() int           { return len(*b) }

func (b *Sortable) Less(i, j int) bool { return (*b)[i].start.Before((*b)[j].start) }

func (b *Sortable) Swap(i, j int)      { (*b)[i], (*b)[j] = (*b)[j], (*b)[i] }

We can embed another type (e.g. slice of Block's) as the only thing in our `Sortable struct. This allows easy (but explicit) casting between Sortable and []Block.

This function shows that we can return a pointer to an instance of the Sortable type as the sorting interface itself, thanks to three implemented methods.

To sum up, those facts are what I found crucial when teaching others programming in Go, based on my own experience with the language. Moreover, it will be helpful when diving more into the runtime performance of Go in the following sections of this book.

However, if you never programmed in Go before, it’s worth going through other materials like Tour of Go before jumping to the following sections and chapters of this book. Make sure you try writing on your own basic Go program, write a unit test, use loops, switches, concurrency mechanisms like channels and routines. Learn common types and standard library abstraction. As a person coming to the new language, we need to produce a program returning valid results before ensuring it executes fast and efficiently.

We learned about some basic and advanced characteristics of Go, so it’s time to unwrap the efficiency aspects of the language. How is it easy to write high or good enough performance code in Go?

Is Go “Fast”?

Across the recent years, many companies have rewritten their products (e.g. from Ruby, Python, Java) to Go20. Two of the repeatedly stated reasons for the move to Go or starting a new project in Go were readability and excellent performance. Readability comes from simplicity and consistency (e.g. single way of error handling as you remember from “Single Way of Handling Errors”), and it’s where Go excels, but what about performance? Is Go fast in comparison to other languages like Python, Java or C++?

In my opinion, this question is badly formed. Given time and room for complexities, any language can be as fast as your machine and operating system allow. This is because, in the end, the code we write is compiled to machine code that uses the exact CPU instructions. Additionally, most languages allow delegating execution to other processes, e.g. written in optimized Assembly. Unfortunately, all we sometimes use to decide if language is “fast” are raw, semi-optimized short program benchmarks that compare execution time and memory usage across languages. While it tells something, it effectively does not show practical aspects of it, e.g. how complex programming for efficiency was.21

Instead, we should look at programming language in terms of how hard and practical it is to write efficient code (not just fast) and how much readability and reliability such a process sacrifices. I believe the Go language has a superior balance between those elements while keeping it fast and trivial to write basic functional code.

One of the reasons for being able to write efficient code easier is the hermetic compilation stage, relatively small amount of unknowns in Go runtime (as mentioned in “Go Runtime”), ease to use concurrency framework and maturity of debugging, profiling and benchmarking tools (discussed in XREF HERE and XREF HERE accordingly).

Those Go characteristics did not appear from thin air. Not many know, but Go was designed on the shoulders of giants: C, Pascal and CSP.

In 1960, language experts from America and Europe teamed up to create Algol 60. In 1970, the Algol tree split into the C and the Pascal branch. ~40 years later, the two branches join again in Go.

Robert Griesemer, The Evolution of Go (2015)

As we can see on Figure 2-3 many names mentioned in Chapter 1 are grandfathers of Go. Great concurrency language CSP created by Sir Hoare, Pascal declarations and packages created by Wirth and C basic syntax contributed to how Go looks today.

Figure 2-3. Go genealogy

But not everything can be perfect. In terms of efficiency, Go has its own Achilles heel. As we will learn in XREF HERE, the Go memory model can sometimes be hard to control. Allocations in our program can be surprising (especially for new users), and the automatic memory release process through garbage collection has some overhead and eventual behaviour. Especially for data-intensive applications, it takes some effort to ensure memory or CPU efficiency. Similar for machines with strictly limited RAM capacities (e.g. IoT).

Yet, the decision to automate this process is highly beneficial, allowing the programmer to not worry about memory cleanup, which has proven to be even worse and sometimes catastrophic (e.g. deallocating memory twice). An excellent example of alternative mechanisms other languages use is Rust. It implements a unique memory ownership model, which replaces automatic, global garbage collection. Unfortunately, while more efficient, it turns out that writing code in Rust is much more complicated than in Go. That’s why we see higher adoption of Go. This reflects the ease of use tradeoff the Go team picked in this element.

Fortunately, there are ways to mitigate the garbage collection mechanism’s negative performance consequences in Go and keep our software lean and efficient. We will go through those in the following chapters.

Summary

Frankly speaking, I initially considered that perhaps I could be lazy and skip most of the Go introduction—that I could assume if you are reading my Efficient Go book that you know the Golang basics already. There are many resources that go into more details for elements I could spend only a subchapter.

However, in the end, I realized that the true power of the Go optimizations, benchmarking, and basic efficiency practices come when used in practice, in everyday programming. Therefore, I want to empower you to merry efficiency with other good practices around reliability, abstractions or reliability for practical use. While sometimes, some fully dedicated approach has to be built (we will touch on this in XREF HERE), the basic, but often good enough, efficiency comes from understanding simple rules and language capabilities. That’s why I focused on giving you a better overview of Go and its features in this chapter.

With this knowledge, we can now move to XREF HERE where we will learn how to start our journey that aims to improve the efficiency and overall performance of our program’s execution.

1   New frameworks on tools for writing Go on small devices are emerging, e.g. GoBot and TinyGo

2   It’s a controversial topic. There is quite a battle in the infrastructure industry for the superior language for configuration as code. For example, between HCL, Terraform, Go templates (Helm), Jsonnet, Starlark and Cue. In 2018, we even open-sourced one tool for writing configuration in Go, called “mimic” Arguably, the loudest arguments against writing configuration in Go are that it feels too much like “programming” and requires programming skills from sysadmins.

3   WebAssembly is meant to change this, though, but not soon

4   CSP is a formal language that allows describing interactions in concurrent systems. Introduced by C.A.R Hoare in Communications of the ACM (1978), it was an inspiration for the Go language concurrency system.

5   One notable example is the controversy behind dependency management work

6   Of course, there are some inconsistencies here and there, that’s why the community created more strict formatters, linters or style guides. Yet the standard tools are good enough to feel comfortable in literally every Go codebase.

7   There is one exception, which are unit test files that has to end with _test.go, those files can have either the same package name or <package_name>_test name allowing to mimic external users of the package.

8   In practice, you can quickly obtain the C++ or Go code (even when obfuscated) from the compiled binary anyway, especially if you don’t strip binary from debugging symbols.

9   While go is improving every day, sometimes you can add more advanced tools like goimports or bingo to improve the development experience further. go in some areas can’t be opinionated and is limited by stability guarantees.

10   CAP Theorem mentions an excellent example of treating failures seriously. It states that you can only choose two from three system characteristics: consistency, availability, and partition. As soon as you choose to distribute your system, you must deal with network partition (communication failure). As an error handling mechanism, you can either design your system to wait (loose availability) or operate on partial data (loose consistency).

11   bash has many methods for error handling, but the default one is implicit. Programmer can optionally print or check ${?} that holds the exit code of the last command executed before any given line. An exit code of 0 means the command executed without any issues.

12   In principle, a monad is an object that holds some value optionally. For example some object Option<Type> with methods Get() and IsEmpty(). Furthermore, an “error monad” is an Option object that holds an error if the value is not set (sometimes referred to as Result<Type>).

13   The only things that would need to change is avoiding using global variables and checking all errors

14   This assertion pattern is also typical in other third party libraries like very popular testify package. However, I am not a fan of the testify package. Mainly because there are too many ways of doing the same thing (all you typically need is Ok and Equals), and the split of require and assert packages are tend to be inconsistently mixed and not used correctly.

15   https://golang.org/doc/go1compat

16   Since programs, e.g. in Java, compile to Java bytecode, many things happen before the code is translated to actual machine understandable code. The complexity of this process is too great to be understandable by a mere mortal, so machine learning “AI” tools we created to auto-tune JVM.

17   The difference between concurrency and parallelism is straightforward. Task runs in parallel when they are being executed simultaneously on two different CPU cores (CPU can only execute one instruction at a time). Concurrent tasks can run parallel from time to time, but they are executed one after another most of the time. The context switch is often so quick that it looks like tasks are run simultaneously from the outside.

18   This function starts up garbage collection loop in a separate routine.

19   Survey in 2020 shows that among the top ten used programming languages, two mandates object-oriented programming (Java, C#), six encourage it, two does not implement OOP. I personally almost always favour object-oriented programming for algorithms that have to hold some context larger than three variables between data structures or functions.

20   To name a few public changes, we seen salesforce case, AppsFlyer or Stream

21   For example, when we look on some benchmarks, we see Go sometimes faster, sometimes slower than Java. Yet if we look at CPU loads, every time Go or Java is faster, it’s simply quicker because the implementation allows more CPU use. But we don’t measure how much time one spent to optimize each of the Go code, how easy it is to read or extend such code etc.

About the Author

Bartłomiej (Bartek) Płotka is a Principal Software Engineer at Red Hat, and the current technical lead of the CNCF SIG Observability group. He has helped to build many popular, reliable, performance- and efficiency-oriented distributed systems in Go with a focus on observability. He is a core maintainer of various open-source projects, including Prometheus, libraries in the gRPC ecosystem, and more. In 2017, together with Fabian Reinartz, he created Thanos, a popular open-source distributed time series database. Focused on cheap and efficient metric monitoring, this project went through hundreds of performance and efficiency focused improvements. Bartek’s passion has always been to focus on the readability, reliability, and efficiency of Go. On the way, Bartek helped to develop many tools, wrote many blog posts, and created guides to teach others on writing pragmatic yet efficient Go applications.

Table of Contents

Preface

Why I Wrote This Book?

Who This Book is For?

How This Book is Organized?

Conventions Used in This Book

Using Code Examples

O’Reilly Online Learning

How to Contact Us

Acknowledgments

O’Reilly Interactive Katacoda Scenarios

1. Software Efficiency Matters

Motivation For This Book

Behind Performance

Common Performance Misconceptions

Optimized Code is Not Readable

You Aren’t Going to Need It (YAGNI)

Hardware is Getting Faster and Cheaper

We Can Scale Horizontally Instead

Time to Market is More Important

Be Vigilant to Simplifications

Efficiency: The Key to Pragmatic Code Performance

Summary

2. Efficient Introduction to Go

Basics You Should Know About Go

Imperative, Compiled and Statically Typed Language

Designed to Improve Serious Codebases

Governed by Google, Yet Open Source

Simplicity, Safety and Readability are Paramount

Packaging and Modules

Dependencies Transparency by Default

Consistent Tooling

Single Way of Handling Errors

Strong Ecosystem

Unused Import or Variable Causes Build Error

Unit Testing and Table Tests

Advanced Language Elements

Code Documentation as a First Citizen

Backwards Compatibility and Portability

Go Runtime

Object Oriented Programming in Go

Is Go “Fast”?

Summary