Go Primer
In this chapter, we will take a look at a couple of Go related topics. The chapter is not meant as a general introduction to the language but instead emphasizes a couple of things we will touch on in subsequent chapters.
Installing Go
Head over to https://golang.org/doc/install where you will find clearly written installation instructions. I installed Go on Windows 10, which by default installs Go in C:\Go. The installer also sets the environment variable GOROOT to c:\Go. The root folder where you will organize your code is in your user directory, usually C:\Users\user-name\golang. The environment variable GOPATH refers to that folder.
When I create a new application, I usually create it in a folder like %GOPATH%\src\github.com\github-id\projectname. From that folder, I issue the command code . to open Visual Studio Code. Get Visual Studio Code from https://code.visualstudio.com/download.
Hello World
Like any text that discusses a language, let's take a look at a Hello World program with a bit of a twist. Create a new folder as explained above and open Visual Studio Code. Next, create a file called main.go:
package main
import (
"fmt"
"os"
"time"
)
func main() {
sign := make(chan struct{})
// exit program on enter
go func() {
os.Stdin.Read(make([]byte, 1))
os.Exit(1)
}()
go func() {
fmt.Printf("Hello ")
<-sign
fmt.Printf("World ")
}()
// calculate 44th Fibonacci
_ = fib(44)
sign <- struct{}{}
time.Sleep(time.Second)
}
func fib(n int) int {
if n < 2 {
return n
}
return fib(n-1) + fib(n-2)
}
The program prints Hello World to the screen, as all Hello World programs do, but also uses goroutines and channels. Let's go through it step by step.
Go organizes code in packages. On the first line we specify the main package, which contains the main() function that serves as the program's entry point. You can add other files with a .go extension to the same folder that belong to the same package if you would like. For instance, we could put the fib function in a separate file called fib.go and remove it from main.go:
package main
func fib(n int) int {
if n < 2 {
return n
}
return fib(n-1) + fib(n-2)
}
To run the program, you can then use go run:
go run main.go fib.go
The import statement is used to import other packages. Those packages can be written by yourself, others or just be part of Go. The packages fmt, os and time are part of the base Go libraries.
The main() function starts by defining a variable of type chan (a channel). The use of := defines a variable and immediately assigns it a value. The type of the variable is derived from the value. In this case, the variable is a channel, which is used as a communications mechanism between goroutines. In the Hello World program, main() itself is a goroutine in addition to two other functions. For now, just remember that goroutines run concurrently and that we can use channels to communicate between them.
The channel we create is an unbuffered channel that uses an empty struct type, which is useful for signaling only because such a struct consumes 0 bytes of memory. If you look at the code, you can see the channel is used in two places. The first time, it is used in the following anonymous function:
go func() {
fmt.Printf("Hello ")
<-sign
fmt.Printf("World ")
}()
That function prints Hello to the screen followed by <-sign which means we want to read from the channel but discard the results. We do not assign the channel's value to a variable because the channel is merely used for signaling. The use of <-sign where sign is an unbuffered channel means that the code in that function blocks, waiting for communications. The anonymous function was called with the keyword go in front of it which runs the function concurrently with the rest of your code.
Great, we have printed Hello to the screen and wait for communications. Because it is a goroutine, the rest of the code continues so we end up at the line that calls the fib() function:
_ = fib(44)
fib() calculates Fibonacci numbers recursively which is inefficient and takes a while (exponential time). fib() actually returns the result but we are not interested in it. In Go, you can use _ to indicate you want to discard the result.
When the calculation finishes, we send an empty struct into the sign channel. Note that struct{} is the type (struct without fields) and struct{}{} is the empty struct value. By sending the empty struct into the channel, <-sign unblocks so the word World can be printed to the screen.
Although it is not terribly elegant, we need to give some time to the program to actually print World to the screen. Without the time.Sleep call, your program would exit too early. Using time.Sleep only increases the likelihood that fmt.Printf("World ") can finish. Instead of using time.Sleep, we should synchronize main() with our function a second time. I will leave that exercise to you!
We have skipped another Goroutine in the explanation:
go func() {
os.Stdin.Read(make([]byte, 1))
os.Exit(1)
}()
The above function just waits for user input. If it detects input, it exists the program setting the exit code to 1. The function runs concurrently so you can press ENTER while the program is calculating the Fibonacci number to exit early if it takes too long. You can of course press CTRL-C as well which exits the program with exit code 2.
Try to calculate the 100th Fibonacci. I bet you will press ENTER after a while because it takes too long! Iteration is a better solution in this case and totally besides the point of this book. Nevertheless, we will use variations of the iterative method in subsequent examples.
package main
import (
"fmt"
"math/big"
)
func main() {
count, max := 0, 100
i := big.NewInt(0)
j := big.NewInt(1)
for count < max {
i.Add(i, j)
i, j = j, i
count++
}
fmt.Println(i)
}
Building an executable
If you want to build your code, just run go build. On Windows, this will create a file with the .exe extension. The name of the file will be the folder name. If you want to specify another name use -o as in:
go build -o fib.exe
Go can build an executable for other operating systems as well. When we later build Go programs for Linux containers, you will want to build such executables. Use the GOOS environment variable to set the operating system to build for. In a Windows command prompt, you would use:
SET GOOS=Linux
After setting GOOS, build your executable as usual. For instance:
go build -o fib
Working with net
Go has great support for building network applications. In this section, we will create a simple TCP server that returns a Fibonacci number. How's that for usefulness? You can use nc (netcat) to connect and type a number n to get the nth Fibonacci:
λ geba:~ nc localhost 5000
10
55
20
6765
1000
4346655768693745643568852767504062580256466051737178040248172908953655541794905189040...
In a new folder, add fib.go. We will use an iterative algorithm to calculate the Fibonacci numbers because we do not have forever:
package main
import (
"math/big"
)
func fib(n int) *big.Int {
count, max := 0, n
i := big.NewInt(0)
j := big.NewInt(1)
for count < max {
i.Add(i, j)
i, j = j, i
count++
}
return i
}
Among other things, the math/big package allows us to work with bigger integers. The NewInt method returns a pointer to an Int type (*Int). Because i is such an *Int, the fib() function needs to return *big.Int.
Now create main.go and add the following (first section of code):
package main
import (
"bufio"
"fmt"
"log"
"net"
"strconv"
)
func main() {
server, err := net.Listen("tcp", "localhost:5000")
if err != nil {
log.Fatal(err)
}
for {
conn, err := server.Accept()
if err != nil {
log.Print(err)
continue
}
go handleConnection(conn)
}
}
One of the things the net package can do is to create TCP servers. We simply use net.Listen to specify the protocol, host and port. A function that returns two values is typical for Go. If there is an error, err will not be nil so we handle that. If there is no error, net.Listen returns a Listener which is later used to wait for incoming connections with the Accept() method.
server.Accept() waits until a TCP connection on port 5000 comes in. It again uses the same technique with two return values. If there is an error we continue from the beginning of the for loop. If there is no error, we have a successful connection that needs to be handled. Note the use of go to execute handleConnection as a goroutine. Without it, the program will only be able to handle one connection at a time.
The conn variable is of type Conn (net.Conn), which allows us to read from and write to the connection. The handleConnection function looks like:
func handleConnection(c net.Conn) {
defer c.Close()
input := bufio.NewScanner(c)
for input.Scan() {
i, err := strconv.Atoi(input.Text())
if err != nil {
fmt.Fprintln(c, "\t", "Invalid input")
continue
}
fmt.Fprintln(c, "\t", fib(i))
}
}
The use of defer in Go makes sure that we close the connection when we exit the function, even if we return early. Next, we need to grab the input from the user:
input := bufio.NewScanner(c)
NewScanner is part of the bufio package and returns a Scanner. The parameter to NewScanner requires a type that implements the io.Reader interface. Indeed, the Conn type (c) implements the io.Reader interface via the Read method:
Read(b []byte) (n int, err error)
Great, so now we have a variable called input which is of type Scanner. Such a type has a method Scan() which returns true after a successful scan up to the next token which is a newline by default. When you launch nc localhost 5000, you enter some text and press ENTER (newline). input.Scan() finishes after you press ENTER and tries to convert the text to an integer. When that fails, you get feedback and the code will wait for the next input. If the text can be converted to a number, the Fibonacci number is printed to the connection using fmt.Fprintln which can take c as a first parameter. The type of the variable you use as the first parameter to Fprintln needs to implement the io.Writer interface which is the case for the Conn type of c.
Time to try it. Build the code and run it. Then use netcat to connect to port 5000 and type a number. I used the Ubuntu Bash shell on Windows because it has nc installed by default.
Although we will not build TCP or UDP servers in this book, this example made use of several techniques we will use in subsequent chapters:
- error checking by returning either the result or an error
- logging to stdout with the log package
- breaking up the code of a package in multiple files
Web serving with net/http
In this section, we will briefly look at creating a small http server. Create a new folder and copy fib.go from the previous section. Next, add main.go:
package main
import (
"fmt"
"log"
"net/http"
"strconv"
)
func main() {
http.HandleFunc("/", httpHandler)
log.Fatal(http.ListenAndServe("localhost:8888", nil))
}
func httpHandler(w http.ResponseWriter, r *http.Request) {
nparam := r.URL.Query().Get("n")
n, err := strconv.Atoi(nparam)
if err != nil {
fmt.Fprintf(w, "Parameter not valid")
return
}
fmt.Fprintf(w, "<H1>%v</H1>", fib(n))
}
The net/http package is built on top of the net package. It provides many features to build a full fledged web server. The code above only uses a tiny subset of the net/http package. With HandleFunc we setup a handler for / and all URLs below it. Indeed, navigating to http://localhost:8888/abc will give you the same result as http://localhost:8888/. After defining the handler, we start listening to incoming requests and serving pages. The ListenAndServe method of http takes care of that. The second parameter to ListenAndServe expects a Handler. When it is nil, a default Handler or DefaultServeMux is used. When you setup handlers with HandleFunc, you are actually adding Handlers to DefaultServeMux.
In this book, we use ListenAndServe in the chapter about monitoring, Application Monitoring. In that chapter, it is used as follows:
// metrics endpoint
func prometheusMetrics() {
log.Println("Prometheus metrics")
h := http.NewServeMux()
h.Handle("/metrics", promhttp.Handler())
http.ListenAndServe(":9100", h)
}
In the above snippet, a new Handler is created with NewServeMux() and a handler is added to respond to calls to /metrics. In ListenAndServe, the custom handler is used instead of nil. Note that the above function is called from main() using go prometheusMetrics() which allows the web server to run concurrently with the rest of the code. Another example of goroutines in action.
Continuing with our httpHandler, we just get the parameter n from the query string and convert it to a number. When that succeeds we write the Fibonacci number to the browser using the http.ResponseWriter which implements the io.Writer interface that fmt.Fprintf expects. This is similar to what we did with our TCP server.
In REST API to InfluxDB, we will write a REST API to request recent values written to InfluxDB, a time-series database. Although we could have implemented the API with the net/http package, I chose to use Goa which uses net/http under the hood. Goa uses a design first approach and takes care of much of the plumbing such as parameter validation, security with JWTs and more.
Now it is time to start with the actual IoT stuff. The next chapter deals with installing the Mosquitto MQTT server.