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)

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.

gopher
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 1
}

func (privateStruct) privateMethod() {}
func (privateStruct) PublicMethod()  {} 1

type PublicStruct struct {
   privateField int
   PublicField  int
}

func (PublicStruct) privateMethod() {}
func (PublicStruct) PublicMethod()  {}

type privateInterface interface {
   privateMethod()
   PublicMethod() 1
}

type PublicInterface interface {
   privateMethod()
   PublicMethod()
}
1

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" 1
   "net/http"
   _ "net/http/pprof" 2

   "github.com/oklog/run" 3
   "github.com/pkg/errors"
   "github.com/prometheus/common/version" 5
   "go.uber.org/atomic"

   "github.com/prometheus/prometheus/config" 4
   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"
)
1

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()

2

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.

3

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.

4

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.

5

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 <[email protected]> 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 { 1
   // ...
   return 204
}

func doOrErr() error { 2
   // ...
   if shouldFail() {
      return errors.New("ups, XYZ failed")
   }
   return nil
}

func intOrErr() (int, error) { 3
   // ...
   if shouldFail() {
      return 0, errors.New("ups, XYZ2 failed")
   }
   return noErrCanHappen(), nil
}
1

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.

2

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.

3

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" 1

func main() {
   ret := noErrCanHappen()
   if err := nestedDoOrErr(); err != nil { 2
      // handle error
   }
   ret2, err := intOrErr()
   if err != nil {
      // handle error
   }
   // ...
}

func nestedDoOrErr() error {
   // ...
   if err := doOrErr(); err != nil {
      return errors.Wrap(err, "do") 3
   }
   return nil
}
1

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>

2

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.

3

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 1

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

   var c int
   d := c // error: d declared but not used 1

   e := 1
   use(e) 2

   f := 1
   _ = f 3
}
1

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

2

Variable e is used.

3

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) { 1
   for _, tcase := range []struct { 2
      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) { 3
         testutil.Equals(t, tcase.expected, max(tcase.a, tcase.b)) 4
      })
   }
}
1

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.

2

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.

3

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.

4

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 1

import ...

const (
   // MetaFilename is the known JSON filename for meta information.  2
   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. 2
// BUG(bwplotka): No known bugs, but if there was one, it would be outlined here. 3
func Download(ctx context.Context, id ulid.ULID, dst string) error {
// ...

// cleanUp cleans the partially uploaded files. 4
func cleanUp(ctx context.Context, id ulid.ULID) error {
// ...

--- block_test.go ---
package block_test

import ...

func ExampleDownload() { 5
   if err := block.Download(context.Background(), ulid.MustNew(0, nil), "here"); err != nil {
      fmt.Println(err)
   }
   // Output: downloaded
}
1

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.

2

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

3

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

4

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.

5

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.

godoc
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 {  1
   return fmt.Sprint(b.id, ": ", b.start.Format(time.RFC3339), "-", b.end.Format(time.RFC3339))
}

type Group struct {
   Block 2
   // ...
}

func (g *Group) Add(b Block) { 3
   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)) 4

   g := &Group{}
   for _, b := range blocks {
      g.Add(b)
   }
   return g.Block 5
}
1

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.

2

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.

3

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.

4

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

5

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 1

func ToSortable(blocks []Block) sort.Interface { 2
   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] }
1

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.

2

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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset