1. Easy to learn, difficult to master

This chapter covers

  • Remind us of what makes Go an efficient, extensible, and productive language
  • Explore why GO is easy to learn and difficult to master

Programming has evolved a lot in the last few decades. Most modern computer systems are no longer written by one person, but by groups of programmers, or even thousands. Our code must be readable, expressive, and maintainable to make the system durable. At the same time, in today’s rapidly evolving world, maximizing agile travel and reducing time to market is critical for most organizations. Programming should follow this trend to ensure that software engineers are as efficient as possible in reading, writing, and maintaining code.

To address these challenges, Google conceived the Go programming language in 2007. Since then, many organizations have adopted the language to support a variety of scenarios: APIs, automation, databases, CLI (command line interface), and so on. Go is still considered by many to be a cloud language. A key factor in Go’s success is that it is a simple programming language. A beginner can learn all the major functions of the language in less than a day. However, as we have seen in this chapter, easy to learn does not necessarily mean easy to master.

This idea led me to write a book to help developers make the most of the Go programming language. However, there’s a question: Why would you want to read a book about Go’s common mistakes? Why not deepen your knowledge with a book that delves into different topics? Neuroscientists have shown that our brains grow best when we are faced with mistakes. Haven’t you experienced the process of learning from your mistakes and recalling relevant scenes months or even years later? As Janet Metcalfe argues in her book Learning From Mistakes, this trait is because mistakes are stimulative. The idea is that we can remember not only mistakes, but also the circumstances in which they occurred. This is one of the reasons why learning from mistakes is so effective.

Following these principles, the book will contain 100 common mistakes that developers make in key areas of the language. At the same time, in order to reinforce the facilitation we mentioned, each error will be as much an example of a real-world occurrence as possible. The book is not just about theory. Its main goal is to get you on the path to mastery of Go.

1.1 summary of Go

Let’s rethink what makes Go such a popular and efficient language in modern systems.

1.1.1 characteristics

In terms of features, Go has no type inheritance, no exceptions, no macros, no partial functions, no lazy evaluation or invariance, no operation overloading, no pattern matching, no implicit type conversion, and so on.

Why aren’t these features supported in the Go language? The official Go FAQ offers insights: Why does Go not feature X? Your favorite feature may be lost because it doesn’t fit, because > it affects compilation speed or design clarity, or because it makes the underlying system model too hard for >.

     --- Go FAQ

Therefore, the number of features in a programming language should not be our primary concern. At least, that’s not what the Go language advocates.

Type inheritance is a good example. The problem with type inheritance is that the path of code in a large code base can be more complex and difficult to understand. In fact, if most interactions are based on inheritance, then the mental model that developers maintain can quickly become complex. Programmers have long been advised to prefer composition to inheritance. Therefore, inheritance is not included in the Go language. This is one of many instances where the Go designers have intentionally leaned toward other aspects of the language rather than adding as many features as possible.

Another example is about data structures. Go has only three standard data structure types:

  • Array. You can store a fixed number of elements of the same type.
  • Slice. The dynamic version of arrays provides a more powerful and convenient way to store collections of elements.
  • The map. The implementation of a hash table used to store key-value pairs in Go.

Therefore, there are no linked lists, no binary trees, no binary heaps, etc. This lack of a standard data structure may come as a surprise to the novice. However, this is a deliberate effort by GO’s designers. For example, in most cases, slicing is the most efficient way for the CPU to access a list of dynamic elements. It involves predictable access patterns and relies on data locality, which makes it more efficient than linked lists in most cases. These three basic types handle most of the scenarios we encounter when developing with Go.

Stability is also an essential feature of GO. Although Go has received frequent updates (performance improvements, security patches, etc.), it has remained a very stable language over the years. Stability is an important aspect of adopting a language on an organizational scale. It is even considered to be the best feature of language.

All in all, Go is not the most feature-rich language. However, everything is about considering all aspects of the programming language and providing the best possible balance for developers.

1.1.2 Developer Productivity

Today, we build, test, and deploy faster than ever before. Software programming must adapt to this trend. Go is considered the most productive language for developers. Let’s see why.

simplicity

First, we mentioned that Go is a compact language: it has only 25 keywords. If you compare with other languages, Java and Rust have more than 50, C++ has more than 100, and so on.

For example, one can argue about whether a Go application is clean or not because of errors management. In general, however, Go’s simplicity is reflected in the fact that it has a shallow learning curve for beginners. In Go, developers can quickly learn about Go by injecting resources such as tour.golang.org.

expressive

We can emphasize that Go is expressive. Expressiveness in a programming language means that we can write and read code naturally and intuitively. As Robert C.Martin writes in his book The Tidy Code, the ratio of time spent reading to writing is much longer than 10:1. Therefore, working with expressive language is critical, especially in large organizations. In addition, the reduced number of ways to solve common problems compared to other languages also makes large Go codebases generally easier to work with.

compile

Another important aspect of developer productivity is compilation time. For example, as a developer, is there anything more annoying than having to wait for the build to complete before executing unit tests?

The goal of fast compilation has always been a conscious one of Go’s designers. First, Go is designed to provide a model for software building that simplifies dependency analysis and avoids the heavy overhead of C-style include files and libraries. As a result, it saves a lot of time for developers to compile.

In summary, Go is considered an efficient language for three main reasons: simplicity, expressiveness, and efficiency. However, as you might imagine, productivity is not the only aspect of the language to consider. Let’s look at some other aspects of the Go language that have made it so popular.

1.1.3 security

Go is a statically typed language. Therefore, type checking is done at compile time, not at run time. This ensures that the code we write is type-safe in most cases.

In addition, Go has a garbage collector to help developers deal with memory management. Direct memory management is not the responsibility of the developer. The garbage collector is responsible for keeping track of memory allocations and freeing memory when it is not needed. But there is also a bit of overhead during execution. For this reason, Go is not intended for use in real-time applications, as it is often impossible to make strict guarantees about execution time. However, this is an assumed balance, as it significantly reduces the development effort and reduces the risk of application crashes or memory leaks.

Another scary aspect for developers is Pointers. A pointer is a variable that contains the address of another variable. Pointers are a core aspect of the Go language. However, Pointers in Go are not complicated to handle because they are explicit (unlike references) and there is no such thing as pointer operations. What is the reason? Again, to reduce the risk of writing insecure applications.

Because of these features, Go is a very secure language, which has a positive impact on the overall reliability of Go applications.

1.1.4 concurrent

In 2005, noted C++ expert Herb Sutter wrote a blog post titled Free Lunch Is Over. He notes that CPU designers have made significant progress in three main areas over the past 30 years:

  • Clock speed
  • Perform optimization
  • The cache

Over the years, improvements in these three areas have led to improvements in the performance of sequential applications (non-parallel, single-threaded, single-process). However, according to Herb Sutter, it’s time to stop expecting CPUs to keep getting faster. This hypothesis has been tested in the past few years. As Figure 1.1 shows, starting around 2004, the speed increase for single-threaded execution stopped being linear. Worse, it has reached its ceiling.

Figure 1.1

Herb Sutter went on to say that now is the right time to change the way we develop applications. At the same time, CPU designers are no longer solely concerned with clock speed and optimization. Instead, they began to consider other approaches, such as multicore and noisy threads (multiple logical cores on the same physical core). Concurrency is going to be the next big revolution for software developers, rather than writing sequential applications and expecting CPUs to always get faster.

The Go programming language was designed with concurrency in mind. Its concurrency model is based on Communicating Sequential Processes (CSP). We will look at this model in the next section.

CSP model

The CSP model is a concurrency paradigm that relies on message passing. Processes do not have to share memory, but communicate by exchanging messages over channels. As shown in Figure 1.2:

Figure 1.2

In Figure 1.2, we can see the interaction between two CSP based processes. Each process is executed sequentially. The lack of callbacks complicates the overall interaction. The first process sends A message A and waits for A response at some point. The second process waits for message A, does some work, and, in response, sends A message B.

What is the rationale for facilitating messaging through memory sharing?

Today, all CPUs have different levels of caching to speed up access to main memory (RAM). Variables that are shared across different threads may be repeated multiple times. Therefore, shared memory is an illusion provided by modern CPUs (we’ll explore these concepts in more detail in the Concurrency section).

The adoption of messaging is consistent with the way modern CPUs are built, which can have a significant impact on performance in most cases. In addition, she makes complex interactions easier to reason about. We don’t have to deal with complex callback chains: everything is written in order.

Go implements the CSP model using two primitives: goroutine and channel.

Goroutine can be thought of as a lightweight thread. Unlike threads scheduled by the operating system, goroutines are scheduled by the Go runtime. A goroutine belongs to only one thread at a time, and a thread can process multiple goroutines, as shown in Figure 1.3:

Figure 1.3

The operating system is responsible for scheduling threads on the CPU core. At the same time, the Go runtime determines the most appropriate number of Go threads based on the workload and schedules goroutines on those threads. Compared to threads, the cost of creating goroutines is cheaper in terms of startup time and memory (only 2KB stack size). Context switching from one goroutine to another is also faster than context switching from thread to thread. As a result, it’s not uncommon to see applications create hundreds or even thousands of goroutines simultaneously.

A channel, on the other hand, is a data structure that allows data to be exchanged between different goroutines. Each message sent to a Channel is received by at most one goroutine. The only broadcast operation (1 vs N) is a Channel closure that propagates events received by multiple goroutines.

Making these primitives part of the core language is a remarkable feature. You don’t need to rely on any external libraries. Developers can write concurrent code in a clean, expressive, and standard way. Of course, we can still use mutexes to share memory. In most cases, however, we should support the messaging approach, mainly because, as discussed, it leverages the way modern CPUs are built.

Messaging is a powerful method of concurrency, but it does not prevent data contention. Fortunately, Go provides a powerful tool for detecting data races.

We have demonstrated that Go is powerful and easy to learn through multiple aspects of the design. So why would you want to expand your knowledge by reading a book about Go?

1.2 Simple doesn’t mean easy

There is a fine line between simplicity and ease. Simply applying a technology means it is not complicated to learn or understand. However, easy means that we can achieve everything effortlessly. Go is easy to learn but difficult to master.

Let’s take concurrency as an example. In 2019, a study on Concurrency errors was published: Understanding Real-World Concurrency Bugs in Go. This study is the first systematic analysis of concurrency errors and focuses on six popular Go code repositories: Docker, Kubernetes, ETCD, CockroachDB, BoltDB, and GRPC.

There are a number of interesting conclusions from this study. In all of these repositories, the authors show that, despite the cultivation achieved in GO, the use of fuses that deliver messages is less frequent than the shared memory approach. The study also highlighted that the majority of blocking errors are caused by inaccurate use of the passing message, despite the perception that the method of passing the message is easier to handle and less error-prone.

What conclusions will we draw about this study? Should we be afraid to use messaging methods in our applications? Of course not. First, the shared memory and messaging paradigms can coexist. This also means that we, the Go developers, need to make some progress and thoroughly understand the meaning of the messaging approach in order to avoid repeating the most common concurrency errors. However, this also means that messaging, while easy to learn and use in theory, is not easy to master in practice.

The idea that simple does not mean easy generalizes to many aspects of Go, not just concurrency; Such as:

  • When to use an interface?
  • When to use value reception and when to use pointer reception?
  • How to process slices efficiently?
  • How to handle error management cleanly and expressively?
  • How do I avoid memory leaks?
  • How do I write relevant tests and benchmarks?
  • How do I use the application to get ready for production?

To be a skilled Go developer, we should have a thorough understanding of many aspects of the language, which takes a lot of time, effort, and mistakes. This book is intended to help you become a skilled developer by collecting and demonstrating 100 common errors in every aspect of the Go language: basics, code organization, data and control structures, strings, functions and methods, error management, concurrency, testing, optimization, and production.

1.3 summarize

  • Go is a modern programming language that puts a lot of effort into developer productivity, which is of vital importance to most companies today.
  • Go is easy to learn, but difficult to master. That’s why we need to deepen our knowledge if we want to make the most of Go.
  • Learning through mistakes and concrete examples is a powerful tool.