Notes on: The Go Programming Language (Donovan & Kernighan, 2016)
Links » https://gopl.io
Table of Contents
The following is my synopsis of the authoritative introduction to Go, namely The Go Programming Language authored by Google’s Alan Donovan and Bell Labs' Brian Kerningham (Donovan and Kernighan 2016).
Preface
- Go balances expressiveness with safety: fast runtime but few crashes due to type errors.
- Go runs pretty much anywhere.
The Origins of Go

The authors describe Go’s ancestry (from C) as follows:
From C, Go in herited its expression syntax, control-flow statements, basic data types, call-by-value parameter passing, pointers, and above all, C’s emphasis on programs that compile to efficient machine code and cooperate naturally wit h the abstractions of current operating systems.
From the lineage on the left of the tree, i.e. communicating sequential processes (CSP) Squeak, Newsquek and Alef, Go inherited the concept of concurrency. Concurreny is “the ability of different parts or units of a program, algorithm, or problem to be executed out-of-order or in partial order, without affecting the final outcome”.
Simplicity
- In the long run, simplicity is the key to good software. But, simplicity is multiplicative. With each fix that makes one part of the code more complex, other parts of the code are bound to become more complex as well.
- Simplicity is easy to neglect and requires more work in the beginning of a project. It also requires dscipline over the whole project lifetime.
Tutorial
Hello World
- Go handles unicode natively (see example below)
go build helloworld.go
builds an executable binary helloworld that can be run without any further processing
package main
import "fmt"
func main() {
fmt.Println("Hello, 世界")
}
- Go code is organized into packages, such as the
fmt
package above from which we take thePrintln
function to print a some values with a newline character at the end. - The
main
package is special. It contains the special functionmain
which defines what our program does. - The
import
statement defines which packages are needed for the compilation of the program. The program won’t compile if imports are missing or if there are superfluous ones.
Command-Line Arguments
- The
os
package provides functions and values provided by the OS of the user in a platform-independent manner os.Args
is a slice of strings. A slice is “a dynamically sized sequence of array elements where individual elements can be accessed ass[i]
and a contiguous subsequence ass[m:n]
”- Go uses half-open intervals which include the first index but exclude the last.
- The program below mimics the UNIX command
echo
, which simply prints its command-line arguments:
// Echo1 prints its command-line arguments.
package main
import (
"fmt"
"os"
)
func main() {
// implicitly initialise s and sep as empty strings
var s, sep string
// loop through os.Args
for i := 1; i < len(os.Args); i++ {
// use assignments statement to concatenate old value of s with sep and
// os.Args[i] and then assign it back to s
s += sep + os.Args[i]
// insert space after first loop
sep = " "
}
fmt.Println(s)
}
The for
loop is the only loop statement in Go! Its general form is:
for initialization; condition; post {
// zero or more statements
}
A traditional while loop has this form:
for condition {
// ...
}
- If you omit the condition entirely, you get an infinite loop.
- It is also possible to loop through a range of values from a string or a slice. The following rewrite of the Echo1 program above illustrates this:
// Echo2 prints its command-line arguments.
package main
import (
"fmt"
"os"
)
func main() {
// explicitly initialise s and sep as empty strings
s, sep := "", ""
// for each iteration, range gives us a pair of values, the index and the
// values of the element at that index
for _, arg := range os.Args[1:] {
s += sep + arg
sep = " "
}
fmt.Println(s)
}
In the code above, the blank identifier (_
) allows us to discard the index
of the range
that we loop through. I needed to read about the blank identifier
here to get the idea.
Variable Declarations
// compact but only allowed inside a function and not for package-level
// variables
s := ""
// implicit initilisation
var s string
// rarely used except for when declaring multiple variables
var s = ""
// explicit about the variables type, which is redundant if it is the same as
// that of the initial value but neccessary if they are not
var s string = ""
In practice, use either the first or second option.
Efficiency
A more efficient way to write the echo program would be one that does not need
so much garbage collection as the one above. Currently, we re-assign the
variable s
every time we iterate through the loop. This can be optimised using
the Join
function from the strings
package:
package main
import (
"fmt"
"os"
"strings"
)
//!+
func main() {
fmt.Println(strings.Join(os.Args[1:], " "))
}
Exercises
Exercise 1.2: Modify the echo program to also print os.Args[0]
, the name of
the command that invoked it.
package main
import (
"fmt"
"os"
"strings"
)
func main() {
// simply change 1 to 0
fmt.Println(strings.Join(os.Args[0:], " "))
}
Exercise 1.2: Modify the program to print the index and value of each of its arguments, one per line.
package main
import (
"fmt"
"os"
)
func main() {
// simply use both the index and the argument and print them
for i, arg := range os.Args[1:] {
fmt.Println(i, arg)
}
}
Exercise 1.3: Experiment to measure the difference in running time between our
potentially inefficient versions and the one that uses strings.Join
. (Section
1.6 illustrates part of the time
package, and Section 11.4 shows how to write
benchmark tests for systematic performance evaluation.)
I implemented benchmarking functions in files named main_test.go
for each
version (using string.Join
, using range
and the first, most simple
version):
package main
import (
"os"
"testing"
)
func BenchmarkLoop(b *testing.B) {
for i := 1; i < b.N; i++ {
os.Args = append(os.Args, `./some/resource/fred`)
main()
}
}
Turns out that the version with string.Joing
did consistently perform best.
The solution above might be
Finding Duplicate Lines
Let us introduce some more standard packages and useful functions. Consider the following piece of code that takes lines from standard input and outputs duplicate lines:
package main
import (
"bufio"
"fmt"
"os"
)
func main() {
counts := make(map[string]int)
input := bufio.NewScanner(os.Stdin)
// returns true if there is a line and false if there is no more input
// (Ctrl + D)
for input.Scan() {
// for each new line add a new key/value pair, the contents of the line
// being the key and the number of times it occured being the value
counts[input.Text()]++
}
// NOTE: ignoring potential errors from input.Err()
for line, n := range counts {
if n > 1 {
// produces formatted output from a list of expressions
fmt.Printf("%d\t%s\n", n, line)
}
}
}
Here, the authors make use of a map, which is the built-in associative data type. It holds a set of key/value pairs. The key can be of any type that can be compared; usually it is a string. The value can be of any type.
bufio
provides us with useful input and output tools. Above we use the
Scanner
type that reads input and then breaks it into lines or words.
verb | spec |
---|---|
%d | decimal integer |
%x, %o, %b | integer in hexadecimal, octal or binary |
%f, %g, %e | floating point number: 3.141593 3.141592653589793 3.141593e+00 |
%t | boolean |
%c | string |
%q | quoted string |
%v | any value in natural format |
%T | type of any value |
%% | literal percent sign (no operand) |
Now, we expand the program to handle not only standard input but also a list of
file names using os.Open
:
package main
import (
"bufio"
"fmt"
"os"
)
func main() {
counts := make(map[string]int)
files := os.Args[1:]
if len(files) == 0 {
countLines(os.Stdin, counts)
} else {
for _, arg := range files {
f, err := os.Open(arg)
// error handling
if err != nil {
fmt.Fprintf(os.Stderr, "dup2: %v\n", err)
continue
}
countLines(f, counts)
f.Close()
}
}
for line, n := range counts {
if n > 1 {
fmt.Printf("%d\t%s\n", n, line)
}
}
}
func countLines(f *os.File, counts map[string]int) {
input := bufio.NewScanner(f)
for input.Scan() {
counts[input.Text()]++
}
// NOTE: ignoring potential errors from input.Err()
}
Program Structure
Basic Data Types
Composite Types
Functions
Methods
Interfaces
Testing
Reflection
Low-level Programming
Resources
Bibliography
Donovan, Alan A. A., and Brian W. Kernighan. 2016. The Go Programming Language. First printing, October 2015. Addison-Wesley Professional Computing Series. New York: Addison-Wesley.