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!

演讲者

Robert Griesemer

Google