Introduction to Go

The not so short version

Christian Joergensen

Co-founder, CTO at Ubivox Technologies

Agenda

2

Language features

3

Environment

4

Installing

wget https://.../go1.9.2.linux-amd64.tar.gz
tar -C /usr/local -xzf go1.9.2.linux-amd64.tar.gz

Contains:

5

Paths

Go finds your code using these environment variables:

Also, add these to your path to PATH:

Now we're ready:

$ go version
go version go1.9.2 linux/amd64
6

The language

7

Syntax

A few gotchas:

8

Syntax: Declarations

Coming from C, a declaration of two integers would read:

int a, b;

In Go, the order is reversed:

var a, b int

Short declarations using type inference with:

var s = "Hello world"

Or even shorter:

s := "Hello world"
9

Syntax: Less clutter

if a == 42 { 
  print("hello")
}

for i := 0; i < 10; i++ { ... }

var c *http.Client = &http.Client{}

c := &http.Client{}
10

Syntax: Loops

The naked loop (do)

for { ... }

The single condition loop (while)

for cond { ... }

The Regular loop (for)

for init; cond; post { ... }
11

Syntax: Condition expressions

Turns:

err := process()
if err != nil {
  logError(err)
  return err
}

Into:

if err := process(); err != nil {
  logError(err)
  return err
}
12

Types

13

Types: Zero values

Mostly what you would expect, though:

var s string  // ""
var i int     // 0
var b bool    // false
14

Types: Scalars

What one would expect:

15

Types: Collections

Slices, arrays:

var a []int
var b [][]int
var c [8]int

Maps:

var m map[string]int
var n map[string]map[string][]int

Must be initialized:

a := make([]int)
b := make([]int, length, capacity)
c := new([8]int)

m := make(map[string]int)
m := make(map[string]int, capacity)
16

Working with maps

package main

import "fmt"

func main() {

    m := make(map[string]string)
    m["a"] = "Hello"

    fmt.Printf("a: %s, b: %s\n", m["a"], m["b"])

    if value, found := m["b"]; !found {
        fmt.Println("'b' not found in map")
    } else {
        fmt.Printf("b: %s", value)
    }

    if _, found := m["b"]; !found {
        fmt.Println("'b' really not found in map")

    }

}
17

Working with arrays and slices

package main

import "fmt"

func main() {

    s := "Hello World"
    fmt.Printf("Hello %s\n", s[6:])
    fmt.Printf("%s World\n", s[:5])

    l := []int{5, 10, 15, 20}
    fmt.Printf("Slice: %v\n", l[1:3])

}
18

Types: Defined types

type Amount float64

func (a Amount) VAT() Amount  {
  return a * 0.25
}
19

Types: Pointer types

package main

import "fmt"

func main() {

    var a *int

    b := 42
    a = &b

    b = 60

    fmt.Printf("a: %d, b: %d\n", *a, b)

}
20

Types: Struct

Class'ish types composited of other types:

type Employee struct {
  Name       string
  Department string
  Salary     float64
}

To create a new instance, use either a composite literal:

e := Employee{
  Name:       "Christian",
  Department: "Development",
  Salary:     1000,
}

Or allocate a zero instance and get a pointer with new. Then access the fields:

e := new(Employee)
e.Name = ...
21

Types: Struct; cont'd

Pointer or non-pointer struct?

If a struct requires initialization, create a constructor function.
The naming convention is:

func NewEmployee(name, department string, salary float64) *Employee {
  ...
}

If it makes sense, strive to make the struct useful in a "zero" state. Examples:

22

Types: Function types

Typical function declaration:

func TwoInts(a int) (int, int) {
  return 60 + a, 80 - a
}

var otherName func(int) (int, int)

a, b := TwoInts(10)

Unused return values must be ignored:

_, b := TwoInts(10)
23

Types: Function types, cont'd

Varadic parameters:

func Sum(numbers ...int) int {
  // type of numbers will be []int
}

sum := Sum(42, 60)

Expanded:

numbers := []int{42, 60}
sum := Sum(numbers...)
24

Types: Method types

func (e *Employee) GiveRaise(percentage float64) {
  e.Salary *= 1 + percentage/100
}

func (e Employee) Greet() {
  fmt.Printf("Hello %s\n", e.Name)
}
25

Types: Interface types

type Greeter interface {
  Greet()
}

var g Greeter

g = Employee{...}

g.Greet()
g.GiveRaise(13) // Oops: g.GiveRaise undefined (type Greeter has no field or method GiveRaise)

e := g.(Employee)

e.GiveRaise(13)
26

The nil value

27

Packages

package main

import "fmt"

func main() {
    fmt.Println("Hello world!")
}
28

Packages: Paths

Example: I have a package for implementing SMTP servers living on Github:

$ go get github.com/chrj/smtpd

Checks out the master branch in: $GOPATH/github.com/chrj/smtpd

github.com/golang/dep

29

Goroutines

To start a new goroutine that executes a function concurrently, use the go keyword followed by a function call:

go expensiveCalculation()
30

Channels

var c chan int

c = make(chan int)      // Unbuffered
c = make(chan int, 10)  // Buffered

Send operator:

c <- 5

Receive operator:

a := <-c

Beware of edge cases: zero (nil) or closed channel send/receive

31

Channels: Select statements

package main

import (
	"errors"
	"fmt"
	"math/rand"
	"time"
)

func main() {

    valuec := make(chan int)
    errorc := make(chan error)

    go worker(valuec, errorc)

    for {
        select {
        case v := <-valuec:
            fmt.Printf("got value: %d\n", v)
        case err := <-errorc:
            fmt.Printf("got error: %v\n", err)
        }
    }

}

func worker(valuec chan int, errorc chan error) {

	for {
		select {
		case valuec <- rand.Intn(10):
		case errorc <- errors.New("error"):
		}
		time.Sleep(time.Second)
	}

}
32

Range loops

Use range loops for iteration:

for i := range s { ... }           // String
for i, r := range s { ... }        // String
for key := range m { ... }         // Map
for key, value := range m { ... }  // Map
for i := range s { ... }           // Slice, Array
for i, item := range s { ... }     // Slice, Array
for msg := range c { ... }         // Channel
33

Goroutine / Channel / Range example:

A small example using both a goroutine and a channel for communication, read from a range loop:

package main

import (
	"fmt"
	"time"
)

func main() {

    c := make(chan string)
    go pinger(c)

    for msg := range c {
        fmt.Println(msg)
    }

}

func pinger(c chan string) {
    for {
        time.Sleep(time.Second)
        c <- fmt.Sprintf("Ping: %s", time.Now().Format(time.RFC3339))
    }
}
34

Defer

Consider this:

var m sync.Mutex

func Update(...) {
  m.Lock()
  defer m.Unlock()

  // Critical area

}
35

Go Idioms

36

Names

var Author = "Christian"
var secret = "hunter2"
var e Employee
var rd io.Reader
var rw http.ResponseWriter
type Reader interface {
  Read(p []byte) (n int, err error)
}
37

Panics

For runtime errors, such as:

Aborts execution of the current goroutine. Can be recovered using recover() in a defer statement, for exception like semantics inside a function:

defer func() {
  if p := recover(); p != nil {
    log.Printf("run time panic: %v", p)
  }
}()
38

Errors

Error handling in Go feels very old fashioned at first. But it will grow on you.

The error type is a builtin interface:

type error interface {
  Error() string
}

Most often used in function declarations as the last parameter:

func Read(p []byte) (n int, err error)
39

Errors: Annotations

Common construct is wrapping a function in an if statement and on a failed call
return the error annotated with some extra details:

if err := ReadFromNetwork(buf); err != nil {
  return errors.Wrap(err, "read from network failed")
} 

handle(buf)
40

Errors: Inspecting

The net package defines its own Error type:

type Error interface {
  error
  Timeout() bool   // Is the error a timeout?
  Temporary() bool // Is the error temporary?
}

We can work with that:

for {
  if err := ReadFromNetwork(buf); err != nil {
    if nerr, ok := err.(net.Error); ok && nerr.Temporary() {
      time.Sleep(5 * time.Second)
      continue
    }
    return err
  } 
  break
}

handle(buf)
41

Errors: Ignoring

func ParseConfigFile(filename string) (*Config, error) { ... }

config, _ := ParseConfigFile("/etc/program.yaml)

A better solution: Turn errors into panics

config := MustParseConfigFile("/etc/program.yaml)
42

Working with I/O

The standard library has a couple of clever interfaces in the io package that should be embraced everywhere it makes sense.

These interface are abstractions for everything that deals with byte-based I/O:

package io

type Reader interface {
  Read(p []byte) (n int, err error)
}

type Writer interface {
  Write(p []byte) (n int, err error)
}

These are supported everywhere: File IO, Network IO, HTTP, RPC as well as for encoding/decoding various formats: JSON, XML, CSV, images.

43

Applications

Finally, I'm going to go through a couple of real world applications of Go that most modern programmers will use at some point. In particular:

44

HTTP Server

Go has a production ready HTTP server in the standard library:

package http

type Handler interface {
  ServeHTTP(ResponseWriter, *Request)
}  

func ListenAndServe(addr string, handler Handler) error
45

HTTP Server: The ResponseWriter interface

All comminication with the client goes through the ResponseWriter interface:

type ResponseWriter interface {
  Header() Header
  Write([]byte) (int, error)
  WriteHeader(int)
}

You may recall the io.Writer interface. This is satisfied by the ResponseWriter type. So everything that can write, can write to an HTTP response.

46

HTTP Server: Request multiplexing

Having only a single handler for a whole server is not very useful. So in order to do path or host based routing, we need a request multiplexer. The http package has a simple one:

package http

type ServeMux
func NewServeMux() *ServeMux
func (mux *ServeMux) Handle(pattern string, handler Handler)
func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Request))
func (mux *ServeMux) ServeHTTP(w ResponseWriter, r *Request)

A default instance is available with the Handle and HandleFunc methods exposed at package level.

package http

func Handle(pattern string, handler Handler)
func HandleFunc(pattern string, handler func(ResponseWriter, *Request))
47

HTTP Server: Request multiplexing, cont'd

The bundled ServeMux is very basic. I prefer working with the Gorilla toolkit, which also has support for:

There is a ton of other multiplexers available:

48

Serving HTTP: Hello world server

A complete example:

package main

import (
    "fmt"
    "log"
    "net/http"
)

func main() {

    http.HandleFunc("/hello", func(rw http.ResponseWriter, req *http.Request) {
        fmt.Fprintf(rw, "Hello World!\n")
    })

    log.Fatal(http.ListenAndServe(":8123", nil))

}
49

JSON encoding / decoding

Despite the dynamic nature of JSON, Go has excellent JSON support.

Annotate your structs with tags:

type Employee struct {
  Name       string  `json:"name"`
  Department string  `json:"department"`
  Salary     float64 `json:"salary"`
}
50

JSON encoding

Encode using the json.Marshal function:

package main

import (
	"encoding/json"
	"fmt"
	"log"
)

type Employee struct {
    Name       string  `json:"name"`
    Department string  `json:"department"`
    Salary     float64 `json:"salary"`
}

func main() {

    e := Employee{
        Name:       "Christian",
        Department: "Development",
        Salary:     1000,
    }

    if encoded, err := json.Marshal(e); err != nil {
        log.Fatal(err)
    } else {
        fmt.Printf("%s\n", encoded)
    }

}
51

JSON decoding

Decode using the json.Marshal function:

package main

import (
	"encoding/json"
	"fmt"
	"log"
)

type Employee struct {
    Name       string  `json:"name"`
    Department string  `json:"department"`
    Salary     float64 `json:"salary"`
}

func main() {

    data := []byte(`{"name":"Christian","department":"Development","salary":1000}`)

    e := Employee{}

    if err := json.Unmarshal(data, &e); err != nil {
        log.Fatalf("error while decoding data: %v", err)
    } else {
        fmt.Printf("Decoded: %s (salary: %.2f)\n", e.Name, e.Salary)
    }

}
52

JSON I/O

If you're encoding or decoding to something that supports the io.Reader or io.Writer interfaces use:

func (e *Employee) Write(wr io.Writer) error {
  return json.NewEncoder(wr).Encode(e)
}

func Load(rd io.Reader) (*Employee, error) {
  e := Employee{}
  return &e, json.NewDecoder(rd).Decode(&e)
}

Works very well with HTTP requests / responses.

53

Serving JSON

In this example I'm going to write a web service exposing a database of employees in JSON:

type Employee struct {
    Name       string  `json:"name"`
    Department string  `json:"department"`
    Salary     float64 `json:"salary"`
}

type EmployeeRegistry map[int]Employee

I will attach a ServeHTTP method to allow the map to implement the http.Handler interface:

func (er EmployeeRegistry) ServeHTTP(rw http.ResponseWriter, req *http.Request) { ... }
54

Serving JSON, cont'd

Here I configure the routes, and start the HTTP server

package main

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

	"github.com/gorilla/mux"
)

// BEGIN types OMIT

type Employee struct {
	Name       string  `json:"name"`
	Department string  `json:"department"`
	Salary     float64 `json:"salary"`
}

type EmployeeRegistry map[int]Employee

// END types OMIT

// BEGIN handler OMIT

func (er EmployeeRegistry) ServeHTTP(rw http.ResponseWriter, req *http.Request) {

	i, err := strconv.Atoi(mux.Vars(req)["id"])
	if err != nil {
		http.Error(rw, "Couldn't decode ID", http.StatusBadRequest)
		return
	}

	if e, found := er[i]; !found {
		http.NotFound(rw, req)
	} else {

		rw.Header().Set("Content-Type", "application/json")

		if err := json.NewEncoder(rw).Encode(e); err != nil {
			log.Printf("error encoding reply: %v", err)
		}

	}

}

// END handler OMIT

func main() {

    er := EmployeeRegistry{}

    er[60] = Employee{Name: "Christian", Department: "Development", Salary: 1000}
    er[80] = Employee{Name: "John", Department: "Management", Salary: 500}

    r := mux.NewRouter()
    r.Handle("/employee/{id:[0-9]+}", er)

    log.Fatal(http.ListenAndServe(":8123", r))

}
55

Serving JSON, cont'd

The Handler:

func (er EmployeeRegistry) ServeHTTP(rw http.ResponseWriter, req *http.Request) {

    i, err := strconv.Atoi(mux.Vars(req)["id"])
    if err != nil {
        http.Error(rw, "Couldn't decode ID", http.StatusBadRequest)
        return
    }

    if e, found := er[i]; !found {
        http.NotFound(rw, req)
    } else {

        rw.Header().Set("Content-Type", "application/json")

        if err := json.NewEncoder(rw).Encode(e); err != nil {
            log.Printf("error encoding reply: %v", err)
        }

    }

}
56

Compilation

57

Compilation commands

Differences between:

Build a single file:

go build -o employee-server main.go

Build and install a program:

go install github.com/chrj/employee-server

Fetch, build and install a program:

go get github.com/chrj/employee-server
58

Cross compilation

Examples:

GOOS=windows GOARCH=amd64 go install github.com/chrj/employee-server
GOOS=darwin GOARCH=amd64 go install github.com/chrj/employee-server
GOOS=linux GOARCH=amd64 go install github.com/chrj/employee-server
59

go fmt

Turns:

b = MyType{
Prop: "Value",
OtherProp: 60,
}

Into:

b = MyType{
    Prop:      "Value",
    OtherProp: 60,
}
60

go doc

$ go doc net/http.Server.Serve
func (srv *Server) Close() error
    Close immediately closes all active net.Listeners and any connections in
    state StateNew, StateActive, or StateIdle. For a graceful shutdown, use
    Shutdown.

    Close does not attempt to close (and does not even know about) any hijacked
    connections, such as WebSockets.

    Close returns any error returned from closing the Server's underlying
    Listener(s).

Webserver:

$ godoc -http=:8081
61

go vet

Example checks:

There is a lot of other tools for static analysis. I prefer to run them using:

Runs a lot of different linters concurrently.

62

go test

Let's test this:

func Sum(numbers ...int) int {

    result := 0

    for _, number := range numbers {
        result += number
    }

    return result

}
63

go test; cont'd

With this test case:

func TestSum(t *testing.T) {

    cases := []struct {
        Numbers  []int
        Expected int
    }{
        {[]int{2, 15, 25}, 42},
        {[]int{-5, 5, 20, 28, 32}, 80},
        {[]int{-2, -8, -10, -18, -22}, -60},
    }

    for _, c := range cases {
        if s := Sum(c.Numbers...); s != c.Expected {
            t.Errorf("got: '%d', expected: '%d' (input: %v)", s, c.Expected, c.Numbers)
        }
    }

}
$ go test .
ok    github.com/chrj/go-talks/2017/intro/sum  0.001s
64

Resources

Also, don't be afraid to dive into the standard library source code and learn:

65

Thank you

Christian Joergensen

Co-founder, CTO at Ubivox Technologies

Use the left and right arrow keys or click the left and right edges of the page to navigate between slides.
(Press 'H' or navigate to hide this message.)