The future of Go
The success of Go version 1 has attracted a lot of developers, most of them with prior experience in other languages that helped shape their mindset and expectations of what a programming language should deliver. The Go team has defined a process to propose, document, and implement changes to Go (Further reading), to give a way for these new contributors to voice their opinions and influence the design of the language. They would label any proposals that break the language-compatibility guarantee, described in the preceding section, as Go 2.
The Go team announced the start of the process of developing Go version 2 at GopherCon 2017 and with the blog post Go 2, here we come! (Further reading). The intention is to ensure the language continues to enable programmers to develop large-scale systems, and to scale to a sizable code base that big teams work on simultaneously. In Toward Go 2 (Further reading), Russ Cox said the following:
Any language change proposal needs to follow the Go 2 language change template (Further reading). They are shipping all Go 2 features that are backward-compatible incrementally in Go 1. After that is complete, they can introduce backward-incompatible changes (see Go 2 proposals: Further reading), in case they offer a significant benefit, into Go 2.0.
Support for generic data types is part of the Go 2 draft designs document (Further reading), along with improved error handling, and error-value semantics. The first implementation of generics has already made it into Go 1. The other items in the list are still under evaluation, pushing the release of 2.0 further into the future.
Technical reasons
Go's build speed is a top-of-the-chart aspect of Go that Go developers are more satisfied with, according to Go Developer Survey 2020 Results (Further reading). It's followed very closely by Go's reliability, in second place.
The list of technical aspects we could highlight is large, but aside from build speed and reliability, we cover performance, cross-compiling, readability, and Go's tooling.
Type safety
Most programming languages can be broadly categorized as either statically typed when variable types are checked at compile time or dynamically typed when this check happens during the program execution (runtime). Go belongs to the first category and requires programs to declare all variable types explicitly. Some beginners or people with a background in dynamically typed languages might see this as a detractor.
Type declarations increase the amount of code that you need to write, but in return, you not only get performance benefits but also protection from type errors occurring at runtime, which can be a source of many subtle and hard-to-troubleshoot bugs. For example, consider the program in the next code example at https://github.com/PacktPublishing/Network-Automation-with-Go/blob/main/ch01/type-safety/main.go:
func process(s string) string { return "Hello " + s } func main() { result := process(42) }
A process
function takes a string
data type as input and returns another string
that concatenates Hello
and the value of the input string. A dynamically typed program can crash if this function receives a value of a type different from string
, such as an integer, for example.
These errors are very common, especially when dealing with complex data structures that can represent a network configuration or state. Go's static type checking prevents the compiler from producing a working binary generating the following error:
cannot use 42 (type untyped int) as type string in argument to process
Readability also improves with Go's static typing. A developer might be able to keep the entire data model in mind when writing code from scratch, but as new users come into a project, code readability becomes critical to help them understand the logic to make their required code changes. No longer do they need to guess which value type a variable stores—everything is explicitly defined by the program. This feature is so valuable that some dynamically typed languages forgo the benefit of their brevity to introduce the support for type annotations (such as Python typing: Further reading), with the only goal to help integrated development environments (IDEs) and static linters catch obvious type errors.
Go builds are fast
Go is a compiled language that creates small binary files in seconds or a couple of minutes tops. Initial build time may be a bit longer, mostly because of the time it takes to download dependencies, generate extra code, and do other household activities. Subsequent builds run in a fraction of that time. For example, the next capture shows that it takes no more than 10 seconds to rebuild a 120-megabytes (MB) Kubernetes application programming interface (API) server binary:
$ time make kube-apiserver +++ [0914 21:46:32] Building go targets for linux/amd64: cmd/kube-apiserver > static build CGO_ENABLED=0: k8s.io/kubernetes/cmd/kube-apiserver make kube-apiserver 10.26s user 2.25s system 155% cpu 8.041 total
This allows you to iterate quickly through the development process and to keep focus, without spending minutes waiting for code to recompile. Some developer productivity tools, such as Tilt, take further actions to optimize the development workflow so that it takes seconds for changes to propagate from a developer's IDE to their local staging environment.
Reliability
Let's define this term as a set of properties of a programming language that help developers write programs that are less likely to fail because of bugs and other failure conditions, as Jiantao Pan from Carnegie Mellon University (CMU) describes in Software Reliability (Further reading). This is one of Go's core tenets, as its website (Further reading) highlights:
Go developers also say reliability is the second aspect of Go they are most satisfied with, only behind build speed, based on Go Developer Survey 2020 Results (Further reading).
A more reliable software means less time spent chasing bugs and more time invested in the design and development of extra features. We've tried to put together a set of features that we think contribute to increased program reliability. This is not a definitive list, though, as interpretation and attribution of such features can be very subjective. Here are the features we've included:
- Code complexity—Go is a minimalistic language by design. This translates into simpler and less error-prone code.
- Language stability—Go comes with strong compatibility guarantees, and the design team tries to limit the number and impact of newly added features.
- Memory safety—Go prevents unsafe memory access, which is a common source of bugs and exploits in languages with pointer arithmetic, such as C and C++.
- Static typing—Compile-time type-safety checks catch many common bugs that would otherwise go unnoticed in dynamically typed languages.
- Static analysis—An automatic way to analyze and report several errors, such as unused variables or unreachable code paths, comes built into the language tooling with
go vet
.
Performance
Go is a highly performant language. The Computer Language Benchmarks Game (Further reading) shows that its performance is in the vein of languages with manual memory management, such as C/C++ and Rust, and that it offers considerably better performance than dynamic type languages such as Python and Ruby.
It has native support for multi-core multithreaded central processing unit (CPU) architectures, allowing it to scale beyond a single thread and to optimize the use of CPU caches.
Go's built-in garbage collector helps you keep the memory footprint of your program low, and Go's explicit type declaration optimizes memory management and storage of values.
The Go runtime gives you profiling data, which you can visualize with pprof
to help you hunt for memory leaks or spot bottlenecks in your program and fine-tune your code to achieve better performance and optimize resource utilization.
For more details on this subject, we recommend checking out Dave Cheney's Five things that make Go fast blog post (Further reading).
Cross-platform compiling
Go can natively produce binaries for different target architectures and operating systems. At the time of writing, the go tool dist list
command returns 45 unique combinations with operating systems ranging from Android to Windows and instruction sets that go from PowerPC
to ARM
. You can change the default values inherited from the underlying operating system and architecture with GOOS
and GOARCH
environment variables.
You can build an operating system-native version of your favorite tool written in Go, regardless of which operating system you are currently on, as illustrated in the following code snippet:
ch01/hello-world$ GOOS=windows GOARCH=amd64 go build ch01/hello-world$ ls hello-world* hello-world.exe
The preceding output shows an example to create a Windows executable on a Linux machine.
Readability
This is, arguably, one of the best qualities of Go when compared to other high-performance languages such as C or C++. The Go programming language specification (Further reading) is relatively short, with around 90 pages (when other language specifications can span over 1,000 pages). It includes only 25 keywords, with only one for loop (for
). The number of features is intentionally low to aid code clarity and to prevent people from developing too many language idioms or best practices.
Code formatting is an active battleground in other languages, while Go prevented this problem early on by shipping automatic opinionated formatting as part of the go
command. A single run of go fmt
on any unformatted (but syntactically correct) code updates the source file with the right amount of indentation and line breaks. This way, all Go programs have a similar look, which improves readability by reducing the number of personal style preferences in code.
Some might say that explicit type declarations alone improve code readability, but Go takes this a step further by making comments an integral part of the code documentation. All commented lines preceding any function, type, or variable declaration gets parsed by the go doc
tool website (Further reading) or an IDE to autogenerate code documentation, as the following screenshot shows:
Figure 1.2 – Automatic code documentation
Most modern IDEs have plugins that support not only documentation but automatic code formatting with go fmt
, code linting and autocompletion, debugging, and a language server—a tool that allows developers to navigate through the code by going back and forth between type, variable, and function declarations and their references (gopls
, the Go language server: Further reading). This last feature not only allows you to navigate code bases of any complexity without having to resolve import statements manually or search for string patterns in text, but also highlights any type inconsistencies on the fly before you compile a program.
Tooling
When setting up a new environment, one of the first things a typical developer would do is download and install a set of their favorite language tools and libraries to help with testing, formatting, dependency management, and so on. Go comes with all these utilities included by default, which are part of the go
command. The following table summarizes some Go built-in tools and their purpose:
Table 1.1 – Go tools
These are just a few of the most popular tools that get shipped together with the Go binary. This certainly reduces the room for creativity in the tooling ecosystem by giving developers a default choice that is good enough for most average use cases. Another benefit of this artificial scarcity is not having to reinstall and relearn a new set of tools every time you switch between different Go projects.
Go for networking
Some network automation processes can trigger hundreds—if not thousands—of simultaneous connections to network devices. Being able to orchestrate this at scale is one of the things that Go enables us to do.
You can see Egon Elbre's Network Gopher mascot in the following screenshot:
Figure 1.3 – Network Gopher, by Egon Elbre
Go comes with a strong networking package that offers you all the constructs to create network connections, packages to encode and decode data from popular formats, and primitives to work with bits and bytes.
Concurrency
Go has first-class support for concurrency with the help of lightweight threads managed by the Go runtime, called goroutines. This language construct makes it possible to embed asynchronous functions into an otherwise sequential program.
Any function call that you prepend with the go
keyword runs in a separate goroutine—different from the main application goroutine—that does not block execution of the calling program.
Channels are another language feature that allows communication between goroutines. You can think of it as a first-in, first-out (FIFO) queue with sending and receiving ends existing in two different goroutines.
Together, these two powerful language constructs offer a way to write concurrent code in a safe and uniform way that allows you to connect to various networking devices simultaneously, without paying the tax of running an operating system thread for each one. For example, consider the following program in the next code example (https://github.com/PacktPublishing/Network-Automation-with-Go/blob/main/ch01/concurrency/main.go) that simulates interaction with remote network devices:
func main() { devices := []string{"leaf01", "leaf02", "spine01"} resultCh := make(chan string, len(devices)) go connect(devices, resultCh) fmt.Println("Continuing execution") for msg := range resultCh { fmt.Println(msg) } }
Connecting to remote devices can take a long time, and it would normally block the execution of the rest of the program. With the connect
function running in a goroutine, as illustrated in the following code snippet, our program can continue its execution, and we can come back and collect the responses at any point in the future:
ch01/concurrency$ go run main.go Continuing execution Connected to device "leaf01" Connected to device "spine01" Connected to device "leaf02"
As the remote devices process the requests and return a response, our program starts printing the responses in the order it receives them.
Strong standard library
Go has a versatile standard library that covers different areas that may be applicable to networking—from cryptography to data encoding, from string manipulation to regular expressions (regexes) and templating. Standard library packages such as net
and encoding
offer interfaces for both client- and server-side network interactions, including the following:
- Internet Protocol (IP) prefix parsing and comparison functions
- Client and server implementations for IP, Transmission Control Protocol/User Datagram Protocol (TCP/UDP), and HyperText Transfer Protocol (HTTP) connections
- Domain Name System (DNS) lookup functions
- Uniform Resource Locator (URL) parsing and manipulations
- Serializing data formats such as Extensible Markup Language (XML), binary, and JavaScript Object Notation (JSON) for storage or transmission
Unless you have unique performance requirements, for example, most Go developers recommend against using external libraries for logic that can otherwise be implemented natively with the standard library. All standard packages are thoroughly tested with each release and used extensively in several large-scale projects. All this creates a better learning experience for newcomers because most-often-used data structures and functions are there already.
Data streaming
Network services are I/O-bound in general—they read or write bytes from or to the network. This mode of operation is how data streaming works in Go, which makes it appealing to network engineers who are familiar with byte processing for network protocol parsing, for example.
I/O operations in Go follow a model where a Reader reads data from a source, which can stream as an array of bytes to a Writer that writes that data to a destination. The following diagram should give you a clearer picture of what this means:
Figure 1.4 – Streaming from a network connection to a file example
A Reader
is an interface that can read from a file, a cipher, a shell command, or a network connection, for example. You can then stream the data you capture to a Writer
interface, which could also be a file or most of the other Reader
examples.
The Go standard library offers these streaming interfaces, such as net.Conn
, that, in this case, allow you to read and write from a network connection, transfer data between interfaces, and transform this data if needed. We cover this topic in much more detail in Chapter 3, Getting Started with Go.
While there are other variables to consider when selecting a programming language to work with, such as which one your company is currently using or which one you feel more comfortable with, our goal is to equip you with all the resources to understand what makes Go so appealing to large-scale system developers. If you want to begin in familiar territory, we compare and contrast Go with Python next. Python is the most popular programming language used for network automation today.