Practical introduction to Go

20.10.2020 Raffael Schneider
Cloud Tech golang microservices handson foss

Go is a popular language in the cloud environment. Many well-known applications are now indispensable in the cloud, like for example Docker, Kubernetes, Istio or Terraform were written in Go. This is also attested to by Steve Francia, product owner of Go at Google, in an interview from 2019 in which he addresses a conscious alignment of Go in the cloud environment and that too newly developed Go Cloud Development Kit. So it seems appropriate to take a closer look at the Lingua franca.

First things first

Go (also called Golang) is a compilable language that supports concurrency and has automatic garbage collection. In addition, the language syntax is minimalistic and based on the hardware-related C. The Go Programming Language Specification is just 50 pages long. The idea behind Go is to provide the smallest possible number of simple orthogonals instructions that can be assembled into a manageable number of patterns. This reduces the overall complexity. This makes code easier to write, understand, and maintain, as there is often only one way to go.

The features of Go result in a language that is particularly well suited for scalable, cluster-capable applications that are designed to solve exactly one task particularly well and efficiently. The Go compiler generates small binary artifacts that guarantee small footprints of Docker images. In addition, the Go Cloud Development Kit provides a library that covers common, cloud-typical operations such as reading blob storage (AWS S3) or health checks. For these reasons, Go is suitable for microservices.

Snippet under magnifying glass

To get a feel for the language right away, let's examine two short code examples that illustrate Golang's features. The first example is a simple object-oriented class that demonstrates the declaration of a data type and functions. The second example shows how to use concurrency in Golang with Goroutines and Channels. The snippets contain inline comments that explain the process.

A simple object-oriented class

The following snippet implements a simple, abstract data type Stack in the package collection using golang:

// This is a comment. The snippet is part of the 'collection' class
package collection

// The null value of stack is an empty array 'data'
type Stack struct {
    data []string
}

// Function Push() adds 'x' to array 'data' in stack 's'
func (s *Stack) Push(x string) {
    s.data = append(s.data, x)
}

// The Pop() function removes the top element in array 'data' in stack 's'
func (s *Stack) Pop() string {
    n := len(s.data) - 1
    res := s.data[n]
    // nötig um einen Memory-Leak zu vermeiden
    s.data[n] = ""
    s.data = s.data[:n]
    return res
}

// The Size() function returns the number of elements in array 'data' in stack 's'
func (s *Stack) Size() int {
    return len(s.data)
}
Implementation of goroutines and channels

Concurrency in Go relies on goroutines and channels. A goroutine is a parallel thread. Channels are connections that let goroutines communicate with each other. This allows certain parts of a program to run side by side and synchronously from other points.

The following snippet constantly writes out "ping". The pinger() function writes "ping" to the channel variable c. The printer() function outputs the content of c every second via an intermediate variable msg. You can stop the constant output with Enter.

package main

import (
  "fmt"
  "time"
)

func pinger(c chan string) {
  for i := 0; ; i++ {
    c <- "ping"
  }
}

func printer(c chan string) {
  for {
    msg := <- c
    fmt.Println(msg)
    time.Sleep(time.Second * 1)
  }
}

func main() {
  var c chan string = make(chan string)

  go pinger(c)
  go printer(c)

  var input string
  fmt.Scanln(&input)
}

The <- operator sends values into the channel variable c. The value from the channel variable is read out again using the := <- operator. This value is routine-aware and is therefore always synchronous with the current value.

Microservice with Go

The basics of Go are covered, now let's get our hands a little dirty. A simple microservice typically exposes a set of data via an API using a REST interface. In our case, an array of cactus objects []Cactus is provided through the interface.

Requirements

In order to carry out the following hands-on, the following tools are required:

  • Go compiler installed
  • Notepad (IntelliJ, VSCode, Atom, vim, ...)
  • Postman to test the API
  • 5 minutes of your time

Resolve first the external library gorilla/mux with the following command:

$ go get -u github.com/gorilla/mux

After that, create a file named cactusService.go in any directory. The go file must have the following content:

package main

import (
	"encoding/json"
	"log"
	"net/http"
	"strconv"

	"github.com/gorilla/mux"
)

type Cactus struct {
	ID          string `json:"id"`
	ImageNumber string `json:"imageNumber"`
	Name        string `json:"name"`
	Features    string `json:"features"`
}

var cacti []Cactus

func getCacti(w http.ResponseWriter, r *http.Request) {
	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(cacti)
}

func getCactus(w http.ResponseWriter, r *http.Request) {
	w.Header().Set("Content-Type", "application/json")
	params := mux.Vars(r)
	for _, item := range cacti {
		if item.ID == params["id"] {
			json.NewEncoder(w).Encode(item)
			return
		}
	}
}

func createCactus(w http.ResponseWriter, r *http.Request) {
	w.Header().Set("Content-Type", "application/json")
	var newCactus Cactus
	json.NewDecoder(r.Body).Decode(&newCactus)
	newCactus.ID = strconv.Itoa(len(cacti) + 1)
	cacti = append(cacti, newCactus)
	json.NewEncoder(w).Encode(newCactus)
}

func updateCactus(w http.ResponseWriter, r *http.Request) {
	// tbd
}

func deleteCactus(w http.ResponseWriter, r *http.Request) {
	// tbd
}

func main() {
	cacti = append(cacti, Cactus{ID: "1", ImageNumber: "8", Name: "San Pedro Cactus", Features: "small, middle-sized,
	 tasty, red"}, Cactus{ID: "2", ImageNumber: "6", Name: "Palo Alto Cactus", Features: "big, tall, juicy, green"})

	router := mux.NewRouter()

	router.HandleFunc("/cactus", getCacti).Methods("GET")
	router.HandleFunc("/cactus", createCactus).Methods("POST")
	router.HandleFunc("/cactus/{id}", getCactus).Methods("GET")
	router.HandleFunc("/cactus/{id}", updateCactus).Methods("POST")
	router.HandleFunc("/cactus/{id}", deleteCactus).Methods("DELETE")

	log.Fatal(http.ListenAndServe(":5000", router))
}

Now the go file can be compiled as follows. To do this, run the command in the same directory:

$ go build -o cactusService

The resulting binary is 6.7 MB in size. Now the REST interface can be called under localhost:5000/cactus/. Postman can be used to conveniently set the query parameters.

Congratulations, your first microservice in Go is functional! Now it is important to deepen this knowledge and to achieve a corresponding use case in your company. With Go, maintainable, concise services can be written that trump with small footprints and the Best Breeds of the cloud world.

Related Links:

Web-Frameworks

Buffalo | A Go web development eco-system, designed to make your life easier

Revel Framework | A high productivity, full-stack web framework for the Go language

Learning material about Go

Educative | Getting started with Golang: a tutorial for beginners

Golang | How to Write Go Code


This text was automatically translated with our golang markdown translator.

Raffael Schneider – crafter, disruptor, free spirit. As a fervent software craftsmanship, Raffael likes to write about programming languages and software resilience in modern distributed systems. Be it DevOps, SRE or systems architecture, he always got a new way of approaching things.