A Taste of Go

August 14, 2014

Robert Griesemer

Google

The Go programming language

Designed by programmers for programmers!

Hello, World!

package main

import "fmt"

func main() {
    fmt.Println("Hello, 世界!")
}

Hello, World! Internet-style

package main

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

func HelloServer(w http.ResponseWriter, req *http.Request) {
    log.Println(req.URL)
    fmt.Fprintf(w, "Hello, 世界!\nURL = %s\n", req.URL)
}

func main() {
    fmt.Println("please connect to localhost:7777/hello")
    http.HandleFunc("/hello", HelloServer)
    log.Fatal(http.ListenAndServe(":7777", nil))
}

Program elements

Constants

const e = 2.71828182845904523536028747135266249775724709369995957496696763
const third = 1.0/3
const M64 int64 = 1<<20
const M = 1<<20
const big = 1<<100 / 1e30  // valid constant expression

Compiler complains if a constant doesn't fit where it is used.

Variables

var x int
var s, t string
var x int
var s, t string = "foo", "bar"  // multiple assignment

var x = 42                      // int
var s, b = "foo", true          // string, bool
x := 42
s, b := "foo", true
return &x

Types

uint8 (byte), uint16, uint32, uint32, uint64,
int8, int16, int32, int32 (rune), int64,
float32, float64,
complex64, complex128,
uint, int, uintptr,
bool, string,
error  // not so usual
array, struct, pointer, function,
slice, map, channel
interface

Type declarations

[10]byte  // array of 10 bytes

struct {
    name        string
    left, right *Node
    action      func(*Node)
}

func(a, b, c int)
func(http.ResponseWriter, *http.Request) error
type Weekday int

type Point struct {
    x, y int
}

Slices

[]T  // slice of T

Common slice operations:

len(s)
s[i]
s[i:j]
append(s, x)  // append element x to slice s and return new slice

Maps

map[K]V  // map K -> V

Common map operations:

make(map[K]V)
len(m)
m[k]
delete(m, k)
for key, value := range m {
    // order of key sequence different each time
}

Statements

a, b = b, a                 // swap
f, err = os.Open(filename)

if x < y {
    return x
} else {
    return y
}

switch day {
case Mon:
    ...
    // break is implicit
case Tue, Wed:
    ...
}

A few words on syntax

Syntax doesn't matter unless you are a programmer.
-- Rob Pike

Corollary:

Compactness of syntax doesn't matter unless you are reading programs.

Compact is not the same as terse. Readability is crucial.

An example: IndexOfAny in Java

public static int IndexOfAny(String str, char[] chars) {
    if (isEmpty(str) || ArrayUtils.isEmpty(chars)) {
        return -1;
    }
    for (int i = 0; i < str.length(); i++) {
        char ch = str.charAt(i);
        for (int j = 0; j < chars.length; j++) {
            if (chars[j] == ch) {
                return i;
            }
        }
    }
    return -1;
}

299 chars (100%), 101 tokens (100%)

IndexOfAny in Go

func IndexOfAny(str string, chars []rune) int {
    if len(str) == 0 || len(chars) == 0 {
        return -1
    }
    for i, ch := range str {
        for _, match := range chars {
            if ch == match {
                return i
            }
        }
    }
    return -1
}

217 chars (73%), 62 tokens (61%)

Almost 30% less text and a surprising 40% fewer tokens to read!

Less clutter means reduced cognitive load.

Functions

func Sin(x float64) float64
func AddScale(x, y int, f float64) int
func Write(data []byte) (written int, err error)
func Printf(format string, args ...interface{})
var delta int
return func(x int) int { return x + delta }

Function values: An example

// walkStdLib calls f with the filename of each .go
// file in the std library until f return false.
func walkStdLib(f func(filename string) bool)

Calling walkStdLib with a closure:

    n := 0
    println := func(s string) bool {
        fmt.Println(n, s)
        n++
        return n < 10
    }
    walkStdLib(println)

More directly:

// +build OMIT

package main

import (
	"fmt"
	"io/ioutil"
	"path/filepath"
	"runtime"
	"strings"
)

func walk(dir string, f func(string) bool) bool {
	fis, err := ioutil.ReadDir(dir)
	if err != nil {
		panic(err)
	}
	// parse all *.go files in directory;
	// traverse subdirectories, but don't walk into testdata
	for _, fi := range fis {
		path := filepath.Join(dir, fi.Name())
		if fi.IsDir() {
			if fi.Name() != "testdata" {
				if !walk(path, f) {
					return false
				}
			}
		} else if strings.HasSuffix(fi.Name(), ".go") && !strings.HasPrefix(fi.Name(), ".") {
			if !f(path) {
				return false
			}
		}
	}
	return true
}

func walkStdLib(f func(filename string) bool) {
	walk(filepath.Join(runtime.GOROOT(), "src"), f)
}

func _() {
	// example START OMIT
	n := 0
	println := func(s string) bool {
		fmt.Println(n, s)
		n++
		return n < 10
	}
	walkStdLib(println)
	// example END OMIT
}

func main() {
    n := 0
    walkStdLib(func(s string) bool {
        fmt.Println(n, s)
        n++
        return n < 10
    })
}

Methods

Methods are functions with a receiver parameter:

func (p Point) String() string {
    return fmt.Sprintf("(%d, %d)", p.x, p.y)
}

The receiver binds the method to its base type (Point):

type Point struct {
    x, y int
}

Methods are invoked via the usual dot notation:

// +build OMIT

package main

import "fmt"

// Point START OMIT
type Point struct {
	x, y int
}

// Point END OMIT

// String START OMIT
func (p Point) String() string {
	return fmt.Sprintf("(%d, %d)", p.x, p.y)
}

// String END OMIT

func main() {
    p := Point{2, 3}
    fmt.Println(p.String())
    fmt.Println(Point{3, 5}.String())
}

Methods can be defined for any user-defined type!

For the Weekday type:

type Weekday int

Define String method on Weekday:

func (d Weekday) String() string { // ...
// +build OMIT

package main

import "fmt"

// type START OMIT
type Weekday int

// type END OMIT

const (
	Mon Weekday = iota
	Tue
	Wed
	Thu
	Fri
	Sat
	Sun
)

var names = [...]string{"Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"}

// String START OMIT
func (d Weekday) String() string { // ...
	// String END OMIT
	return names[d]
}

func main() {
    fmt.Println(Mon.String())
    fmt.Println()

    for d := Mon; d <= Sun; d++ {
        fmt.Println(d.String())
    }
}

Method calls via non-interface types are statically dispatched.

Interface types

Examples:

interface{}  // empty interface

interface {
    String() string
}

interface {
    Len() int
    Swap(i, j int)
    Less(i, j int) bool
}

Using interfaces

type Stringer interface {
    String() string
}

Both Weekday and Point define a String method, so values of both can be assigned to
a variable of Stringer type:

// +build OMIT

package main

import "fmt"

type Point struct {
	x, y int
}

func (p Point) String() string {
	return fmt.Sprintf("(%d, %d)", p.x, p.y)
}

type Weekday int

const (
	Mon Weekday = iota
	Tue
	Wed
	Thu
	Fri
	Sat
	Sun
)

var names = [...]string{"Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"}

func (d Weekday) String() string { // ...
	return names[d]
}

// Stringer START OMIT
type Stringer interface {
	String() string
}

// Stringer END OMIT

func main() {
    var x Stringer
    x = Point{2, 3}
    fmt.Println("A", x.String())

    x = Tue
    fmt.Println("B", x.String())

    fmt.Println("C", Point{2, 3}) // fmt.Println knows about Stringer!
    fmt.Println("D", Tue)
}

Method calls via interface types are dynamically dispatched ("virtual function call").

A larger example

Top 10 identifiers in std library

package main // idents.go

import (
    "fmt"
    "os"
    "text/scanner"
)

func main() {
    var s scanner.Scanner
    s.Init(os.Stdin)
    for {
        switch s.Scan() {
        case scanner.EOF:
            return // all done
        case scanner.Ident:
            fmt.Println(s.TokenText())
        }
    }
}
$ cat $(find $GOROOT -name '*.go') | ./idents | sort | uniq -c | sort -nr | sed 10q

A variation: Histogram of Go statements

A histogram is a map from statement name ("if", "for", etc.) to use count:

type histogram map[string]int

Algorithm:

func main() {
    h := make(histogram)
    walkStdLib(func(filename string) bool {
        h.add(filename) // does all the hard work
        return true
    })
    h.print()
}

Processing a Go source file

func (h histogram) add(filename string) {
    f, err := parser.ParseFile(token.NewFileSet(), filename, nil, 0)
    if err != nil {
        panic(err)
    }

    ast.Inspect(f, func(n ast.Node) bool {
        if n, ok := n.(ast.Stmt); ok { // type test: is n an ast.Stmt?
            h[fmt.Sprintf("%T", n)]++
        }
        return true
    })
}

Printing the histogram

// +build OMIT

package main

import (
	"fmt"
	"go/ast"
	"go/parser"
	"go/token"
	"io/ioutil"
	"path/filepath"
	"runtime"
	"strings"
)

func walk(dir string, f func(string) bool) bool {
	fis, err := ioutil.ReadDir(dir)
	if err != nil {
		panic(err)
	}
	// parse all *.go files in directory;
	// traverse subdirectories, but don't walk into testdata
	for _, fi := range fis {
		path := filepath.Join(dir, fi.Name())
		if fi.IsDir() {
			if fi.Name() != "testdata" {
				if !walk(path, f) {
					return false
				}
			}
		} else if strings.HasSuffix(fi.Name(), ".go") && !strings.HasPrefix(fi.Name(), ".") {
			if !f(path) {
				return false
			}
		}
	}
	return true
}

func walkStdLib(f func(filename string) bool) {
	walk(filepath.Join(runtime.GOROOT(), "src"), f)
}

// histogram START OMIT
type histogram map[string]int

// histogram END OMIT

// add START OMIT
func (h histogram) add(filename string) {
	f, err := parser.ParseFile(token.NewFileSet(), filename, nil, 0)
	if err != nil {
		panic(err)
	}

	ast.Inspect(f, func(n ast.Node) bool {
		if n, ok := n.(ast.Stmt); ok { // type test: is n an ast.Stmt?
			h[fmt.Sprintf("%T", n)]++
		}
		return true
	})
}

// add END OMIT

func (h histogram) print() {
    // determine total number of statements
    total := 0
    for _, count := range h {
        total += count
    }

    // print map entries
    i := 0
    percent := 100 / float64(total)
    for key, count := range h {
        fmt.Printf("%4d.  %5.2f%%  %5d  %s\n", i, float64(count)*percent, count, key)
        i++
    }
}

// main START OMIT
func main() {
	// body START OMIT
	h := make(histogram)
	walkStdLib(func(filename string) bool {
		h.add(filename) // does all the hard work
		return true
	})
	// body END OMIT
	h.print()
}

// main END OMIT

Note: Histogram (map) iteration order is not specified.

Sorting

sort.Sort operates on any type that implements the sort.Interface:

interface {
    Len() int
    Swap(i, j int)
    Less(i, j int) bool
}

For instance, to sort a slice of strings lexically, define:

type lexical []string

func (a lexical) Len() int           { return len(a) }
func (a lexical) Swap(i, j int)      { a[i], a[j] = a[j], a[i] }
func (a lexical) Less(i, j int) bool { return a[i] < a[j] }

And sort:

sort.Sort(lexical(s))  // where s is a []string slice

Sorting histogram entries

type entry struct {
    key   string
    count int
}

type byCount []entry

func (s byCount) Len() int      { return len(s) }
func (s byCount) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
func (s byCount) Less(i, j int) bool {
    x, y := s[i], s[j]
    if x.count != y.count {
        return x.count > y.count // want larger count first
    }
    return x.key < y.key
}

Improved histogram printing

// +build OMIT

package main

import (
	"fmt"
	"go/ast"
	"go/parser"
	"go/token"
	"io/ioutil"
	"path/filepath"
	"runtime"
	"sort"
	"strings"
	"time"
)

func walk(dir string, f func(string) bool) bool {
	fis, err := ioutil.ReadDir(dir)
	if err != nil {
		panic(err)
	}
	// parse all *.go files in directory;
	// traverse subdirectories, but don't walk into testdata
	for _, fi := range fis {
		path := filepath.Join(dir, fi.Name())
		if fi.IsDir() {
			if fi.Name() != "testdata" {
				if !walk(path, f) {
					return false
				}
			}
		} else if strings.HasSuffix(fi.Name(), ".go") && !strings.HasPrefix(fi.Name(), ".") {
			if !f(path) {
				return false
			}
		}
	}
	return true
}

func walkStdLib(f func(filename string) bool) {
	walk(filepath.Join(runtime.GOROOT(), "src"), f)
}

type histogram map[string]int

func (h histogram) add(filename string) {
	f, err := parser.ParseFile(token.NewFileSet(), filename, nil, 0)
	if err != nil {
		panic(err)
	}

	ast.Inspect(f, func(n ast.Node) bool {
		if n, ok := n.(ast.Stmt); ok {
			h[fmt.Sprintf("%T", n)]++
		}
		return true
	})
}

func (h histogram) print() {
    var list []entry
    var total int
    for key, count := range h {
        list = append(list, entry{key, count})
        total += count
    }
    sort.Sort(byCount(list))

    percent := 100 / float64(total)
    for i, e := range list {
        fmt.Printf("%4d.  %5.2f%%  %5d  %s\n", i, float64(e.count)*percent, e.count, e.key)
    }
}

// byCount START OMIT
type entry struct {
	key   string
	count int
}

type byCount []entry

func (s byCount) Len() int      { return len(s) }
func (s byCount) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
func (s byCount) Less(i, j int) bool {
	x, y := s[i], s[j]
	if x.count != y.count {
		return x.count > y.count // want larger count first
	}
	return x.key < y.key
}

// byCount END OMIT

// main START OMIT
func main() {
	start := time.Now()
	h := make(histogram)
	walkStdLib(func(filename string) bool {
		h.add(filename)
		return true
	})

	h.print()
	fmt.Println(time.Since(start))
}

// main END OMIT

Concurrency

Goroutines

go f()
go f(x, y, ...)

A simple example

func f(msg string, delay time.Duration) {
    for {
        fmt.Println(msg)
        time.Sleep(delay)
    }
}

Function f is launched as 3 different goroutines, all running concurrently:

// +build OMIT

package main

import (
	"fmt"
	"time"
)

// f START OMIT
func f(msg string, delay time.Duration) {
	for {
		fmt.Println(msg)
		time.Sleep(delay)
	}
}

// f END OMIT

func main() {
    go f("A--", 300*time.Millisecond)
    go f("-B-", 500*time.Millisecond)
    go f("--C", 1100*time.Millisecond)
    time.Sleep(20 * time.Second)
}

Communication via channels

A channel type specifies a channel value type (and possibly a communication direction):

chan int
chan<- string  // send-only channel
<-chan T       // receive-only channel

A channel is a variable of channel type:

var ch chan int
ch := make(chan int)  // declare and initialize with newly made channel

A channel permits sending and receiving values:

ch <- 1   // send value 1 on channel ch
x = <-ch  // receive a value from channel ch (and assign to x)

Channel operations synchronize the communicating goroutines.

Communicating goroutines

Each goroutine sends its results via channel ch:

func f(msg string, delay time.Duration, ch chan string) {
    for {
        ch <- msg
        time.Sleep(delay)
    }
}

The main goroutine receives (and prints) all results from the same channel:

// +build OMIT

package main

import (
	"fmt"
	"time"
)

// f START OMIT
func f(msg string, delay time.Duration, ch chan string) {
	for {
		ch <- msg
		time.Sleep(delay)
	}
}

// f END OMIT

func main() {
    ch := make(chan string)
    go f("A--", 300*time.Millisecond, ch)
    go f("-B-", 500*time.Millisecond, ch)
    go f("--C", 1100*time.Millisecond, ch)

    for i := 0; i < 100; i++ {
        fmt.Println(i, <-ch)
    }
}

Putting it all together

Analyze files concurrently, map-reduce style

Mapper:

        go func() {
            h := make(histogram)
            h.add(filename)
            ch <- h
        }()

Reducer:

    h := make(histogram)
    for count > 0 {
        h.merge(<-ch)
        count--
    }
func (h histogram) merge(h1 histogram) {
    for key, count := range h1 {
        h[key] = h[key] + count
    }
}

From sequential program...

// +build OMIT

package main

import (
	"fmt"
	"go/ast"
	"go/parser"
	"go/token"
	"io/ioutil"
	"path/filepath"
	"runtime"
	"sort"
	"strings"
	"time"
)

func walk(dir string, f func(string) bool) bool {
	fis, err := ioutil.ReadDir(dir)
	if err != nil {
		panic(err)
	}
	// parse all *.go files in directory;
	// traverse subdirectories, but don't walk into testdata
	for _, fi := range fis {
		path := filepath.Join(dir, fi.Name())
		if fi.IsDir() {
			if fi.Name() != "testdata" {
				if !walk(path, f) {
					return false
				}
			}
		} else if strings.HasSuffix(fi.Name(), ".go") && !strings.HasPrefix(fi.Name(), ".") {
			if !f(path) {
				return false
			}
		}
	}
	return true
}

func walkStdLib(f func(filename string) bool) {
	walk(filepath.Join(runtime.GOROOT(), "src"), f)
}

type histogram map[string]int

func (h histogram) add(filename string) {
	f, err := parser.ParseFile(token.NewFileSet(), filename, nil, 0)
	if err != nil {
		panic(err)
	}

	ast.Inspect(f, func(n ast.Node) bool {
		if n, ok := n.(ast.Stmt); ok {
			h[fmt.Sprintf("%T", n)]++
		}
		return true
	})
}

// print START OMIT
func (h histogram) print() {
	var list []entry
	var total int
	for key, count := range h {
		list = append(list, entry{key, count})
		total += count
	}
	sort.Sort(byCount(list))

	percent := 100 / float64(total)
	for i, e := range list {
		fmt.Printf("%4d.  %5.2f%%  %5d  %s\n", i, float64(e.count)*percent, e.count, e.key)
	}
}

// print END OMIT

// byCount START OMIT
type entry struct {
	key   string
	count int
}

type byCount []entry

func (s byCount) Len() int      { return len(s) }
func (s byCount) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
func (s byCount) Less(i, j int) bool {
	x, y := s[i], s[j]
	if x.count != y.count {
		return x.count > y.count // want larger count first
	}
	return x.key < y.key
}

// byCount END OMIT

func main() {
    start := time.Now()
    h := make(histogram)
    walkStdLib(func(filename string) bool {
        h.add(filename)
        return true
    })

    h.print()
    fmt.Println(time.Since(start))
}

... to concurrent program

// +build OMIT

package main

import (
	"fmt"
	"go/ast"
	"go/parser"
	"go/token"
	"io/ioutil"
	"path/filepath"
	"runtime"
	"sort"
	"strings"
	"time"
)

func walk(dir string, f func(string) bool) bool {
	fis, err := ioutil.ReadDir(dir)
	if err != nil {
		panic(err)
	}
	// parse all *.go files in directory;
	// traverse subdirectories, but don't walk into testdata
	for _, fi := range fis {
		path := filepath.Join(dir, fi.Name())
		if fi.IsDir() {
			if fi.Name() != "testdata" {
				if !walk(path, f) {
					return false
				}
			}
		} else if strings.HasSuffix(fi.Name(), ".go") && !strings.HasPrefix(fi.Name(), ".") {
			if !f(path) {
				return false
			}
		}
	}
	return true
}

func walkStdLib(f func(filename string) bool) {
	walk(filepath.Join(runtime.GOROOT(), "src"), f)
}

type histogram map[string]int

func (h histogram) add(filename string) {
	f, err := parser.ParseFile(token.NewFileSet(), filename, nil, 0)
	if err != nil {
		panic(err)
	}

	ast.Inspect(f, func(n ast.Node) bool {
		if n, ok := n.(ast.Stmt); ok {
			h[fmt.Sprintf("%T", n)]++
		}
		return true
	})
}

// merge START OMIT
func (h histogram) merge(h1 histogram) {
	for key, count := range h1 {
		h[key] = h[key] + count
	}
}

// merge END OMIT

type entry struct {
	key   string
	count int
}

func (h histogram) print() {
	var list []entry
	var total int
	for key, count := range h {
		list = append(list, entry{key, count})
		total += count
	}
	sort.Sort(byCount(list))

	percent := 100 / float64(total)
	for i, e := range list {
		fmt.Printf("%4d.  %5.2f%%  %5d  %s\n", i, float64(e.count)*percent, e.count, e.key)
	}
}

type byCount []entry

func (s byCount) Len() int      { return len(s) }
func (s byCount) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
func (s byCount) Less(i, j int) bool {
	x, y := s[i], s[j]
	if x.count != y.count {
		return x.count > y.count // want larger count first
	}
	return x.key < y.key
}

func init() {
	n := runtime.NumCPU()
	//fmt.Println(n, "cores")
	runtime.GOMAXPROCS(n)
}

func main() {
    start := time.Now()
    ch := make(chan histogram)
    count := 0 // goroutine count
    walkStdLib(func(filename string) bool {
        count++
        go func() {
            h := make(histogram)
            h.add(filename)
            ch <- h
        }()
        return true
    })

    h := make(histogram)
    for count > 0 {
        h.merge(<-ch)
        count--
    }

    h.print()
    fmt.Println(time.Since(start))
}

There's a lot more!

Thank you

Robert Griesemer

Google

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.)