Concurrent Programming & Go

Concurrent Programming & Go

With ubiquitous multi-core processors, writing concurrent code is less optional that it might have been a decade (or two) ago.

We really want our language to help us get pieces of our logic running concurrently. Recent language designs prioritize this (again, more than languages a decade or two old).

Concurrent Programming & Go

In the Language Features section of the course, we saw a few features that programming languages can have to help us write concurrent code.

But we should take a deeper look at one…

Go

In particular, the Go Programming Language was designed with concurrent programming in mind.

We will be using it as an example of (1) another language where we can apply the concepts from the previous section of the course, and (2) how a language can help us write concurrent code.

Language Basics

Go is much closer to languages you already know than Haskell. Go is…

  • imperative;
  • statically typed, with types (often) explicitly declared;
  • typically compiled to machine code with the standard Go tools.

Language Basics

A Go hello world:

package main

import (
	"fmt"
)

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

Every file must start with a package declaration. The fmt package from the standard library is imported. The main() function is called as the entry point to a program.

Language Basics

The program can be run with the usual golang tools:

go run src/main/main.go

Or explicitly compiled with one of the tools:

go build src/main/main.go
./main
gccgo src/main/main.go -o main
./main

Language Basics

Go code is arranged into packages, and Go is opinionated about the file layout:

  • packages go in a src directory;
  • packages are directories within that;
  • package code is in .go files in those directories (broken up however you like).

The main function must be in a package main.

package main

func main() { … }

Language Basics

A project with packages hello and main might have a directory structure like:

src
├── hello
│   ├── constants.go
│   └── greeter.go
└── main
    └── main.go

The files hello/*.go must all start with package hello and main/*.go must start with package main.

Language Basics

If we have some code in src/hello/constants.go:

package hello

const Message = "Hello world!"

We can import and use that package in main.go:

package main

import (
	"fmt"
	"hello"
)

func main() {
	fmt.Println(hello.Message)
}

Language Basics

The current directory isn't in the package search path by default, so it must be added before we can import these packages.

export GOPATH=$(pwd)
go run src/main/main.go

Or let some IDE take care of it for you.

Language Basics

Names that start with a capital letter are exported: available when the package is imported. If we had named the constant lower-case:

const message = "Hello world!"

… then we can't see it from another package:

src/main/main.go:9:14: cannot refer to unexported name hello.message
src/main/main.go:9:14: undefined: hello.message

Variables

Variables must be explicitly created. That can be done with the var keyword.

var a float64
a = 6.2832
fmt.Println(a)
6.2832

Or the variable can be initialized along with its creation:

var b int = 383
fmt.Println(b)
383

Variables

But more beautiful and idiomatic, let Go initialize and do type inference from the initializer:

c := 6.2832
d := 383
fmt.Printf("Type: %T, Value: %v\n", c, c)
fmt.Printf("Type: %T, Value: %v\n", d, d)
Type: float64, Value: 6.2832
Type: int, Value: 383

The := operator (1) creates a variable, (2) infers its type, and (3) initializes it. Compare = which assigns to an existing variable.

Functions

Functions in Go have a fixed argument list, and arguments are statically-typed. A function that takes two int arguments and returns an int:

func doublePlus(x int, y int) int {
	dbl := 2 * x
	return dbl + y
}

Then this creates an int variable holding 10:

r1 := doublePlus(3, 4)

Functions

Another example, working with strings (which is another built-in basic type):

func stringDouble(s string) string {
	return s + s
}

We would get results like:

stringDouble("abc") == "abcabc"

String literals use double-quotes, like C/​Java/​etc.

Functions

A function that returns nothing:

func printWithX(s string) {
	fmt.Printf("X%sX\n", s)
}

Printf works like in C: calling printWithX("hello") produces and prints XhelloX, followed by a newline.

Functions

Or a function with no arguments (after the imports for the example):

import (
	"fmt"
	"math/rand"
	"time"
)
func randomDigit() int {
	return rand.Intn(10)
}
rand.Seed(time.Now().UnixNano())
fmt.Println(randomDigit())

Functions

Functions in Go can return multiple values. The interval function in this program returns two float32:

package main

import "fmt"

func Interval(centre, radius float32) (float32, float32) {
	return centre - radius, centre + radius
}

func main() {
	left, right := Interval(10.0, 2.3)
	fmt.Println(left)
	fmt.Println(right)
}
7.7
12.3

Functions

Return values can have names. Like arguments, the names can provide free documentation of what they are for. If there are named return values, variables for them get created automatically.

This is equivalent to the previous Interval function:

func Interval2(centre, radius float32) (start, end float32) {
	start = centre - radius
	end = centre + radius
	return start, end
}

Functions

With named return values, you can use a naked return to return the values corresponding to the named return:

func Interval3(centre, radius float32) (start, end float32) {
	start = centre - radius
	end = centre + radius
	return
}

This seems unlikely to make your code more readable, but it's possible.

Functions

A defer statement in a function will be delayed until the function returns. So, this:

func FunnyHelloWorld() {
	defer fmt.Println("world")
	fmt.Println("hello")
}

Prints:

hello
world

Functions

Why? So we can ensure cleanup work gets done, no matter how we leave the function:

func WriteFile(fileName string) {
	file, err := os.Create(fileName)
	if err != nil {
		return
	}
	defer file.Close()
	// ⋮ continue on to write to the file, possibly failing

We know that file.Close() will get called if WriteFile panics, exits early, or exits normally.

Conditionals

The if in Go looks like you might guess, without a set of parens:

if a < 10 {
	fmt.Println("a is small")
} else {
	fmt.Println("a is big")
}

Conditionals

And if/else if/else structures look like C/​Java/​etc, again without the parens around the condition:

if a < 10 {
	fmt.Println("a is small")
} else if a < 100 {
	fmt.Println("a is big")
} else {
	fmt.Println("a is very big")
}

Conditionals

There is also a switch/​case statement:

switch b {
case 1:
	fmt.Println("b is one")
case 2:
	fmt.Println("b is two")
default:
	fmt.Println("b is something else")
}

Loops

There is a for loop. Like if: it's like C but without parentheses around the setup components:

for a := 0; a < 10; a++ {
	fmt.Printf("%d ", a)
}
0 1 2 3 4 5 6 7 8 9 

The pieces are the same as C, Java, C#, JavaScript: initializing statement, condition expression, post-iteration statement, and the loop body in curly braces.

Loops

There's no while loop in Go, but the for loop's initializer and increment statement are optional:

total := 0
count := 1
for total < 100 {
	total += count
	count += 1
}
fmt.Printf("total=%d, count=%d\n", total, count)
total=105, count=15

So there is a while, you just spell it for.

Loops

Actually, the for condition is also optional. This is an infinite loop:

for {
    fmt.Println("hello world")
}

Loops

Go has only one loop structure, and it's enough to do everything you need. It's surprisingly elegant: why do other languages have so many loops?

This is why learning new programming languages is useful (even if they're quite similar to others you know): it forces you to think about what's you're doing in a different way.

Loops

There's no do…while structure, but you can break from a loop, so you can do the equivalent (or more):

count = 0
for {
	count += 1
	square := count * count
	if square%10 == 5 {
		break
	}
}

Structs

There are no classes in Go, but there's something almost as good…

In Go, structs are like in C: a collection of fields that are stored as one value.

Structs

An example: defining a struct Polygon that holds information about a regular polygon:

type Polygon struct {
	Sides   uint
	SideLen float64
}

i.e. two fields: one named Sides holding a uint and SideLen that holds a float64.

Structs

We can create a struct value by specifying the fields in order, and get them out again:

unitSquare := Polygon{4, 1.0}
fmt.Println(unitSquare.Sides)
4

Structs

A struct literal can also be written with field names. And structs have a default string representation that's reasonable:

littleHexagon := Polygon{
	SideLen: 0.1,
	Sides:   6,
}
fmt.Println(littleHexagon.SideLen)
fmt.Println(littleHexagon)
0.1
{6 0.1}

Structs as Classes

Structs (so far) are as good as classes at holding data, but can we have methods? Yes. You can define a function with a receiver argument and it becomes a method on that struct:

func (poly Polygon) Perimeter() float64 {
	return float64(poly.Sides) * poly.SideLen
}

Notes:

  • The receiver argument is named (poly here). Choose a good name.
  • You can't multiply an integer and float. We had to explicitly convert uint to float64 explicitly.

Structs as Classes

Access methods like you'd expect from other languages:

fmt.Println(littleHexagon.Perimeter())
0.6000000000000001

Structs as Classes

Methods can take arguments like any other function:

func (p Polygon) ScaledPerimeter(scale float64) float64 {
	true_perimeter := float64(p.Sides) * p.SideLen
	return true_perimeter * scale
}
bigPentagon := Polygon{5, 15}
fmt.Println(bigPentagon.ScaledPerimeter(5))
375

Types

The basic types in Go mostly have an explicit number of bits:

  • int8, int16, int32, int64
  • uint8, …, uint64
  • float32, float64
  • complex64, complex128
  • byte (== uint8)
  • bool

There are two others for the system-sized integers, usually 64-bits: int, uint. And a few more we'll meet later.

Types

Go types are very strongly checked: most operations happen on a specific type, and if you want to mix types in a calculation, you have to explicitly convert in the direction you want.

var i int = 12
var x float32 = 3.6
fmt.Println(float32(i) * x)
fmt.Println(i * int(x))
if i != 0 {
	fmt.Println("yes")
}
43.199997
36
yes

Types

Things that would have failed:

fmt.Println(i * x)  // mismatched types int and float32
if i { fmt.Println("yes") } // cannot convert… to type bool

That's the strongest type checking of any language I can think of.

Arrays and Slices

An array in Go is like an array in other languages where you have seen them: an ordered collection of one type, of a fixed size.

Really, a fixed size. For example, a function that takes an array of ten ints. The length is part of the type:

func SumTenInt(arr [10]int) int {
	sum := 0
	for i := 0; i < 10; i++ {
		sum += arr[i]
	}
	return sum
}

Arrays and Slices

Arrays are very inflexible, since they can have exactly one size. On the other hand a slice is a reference to an existing array (or part of an array) that can have a dynamic size.

Think of it this way: an array stores stuff in memory. A slice refers to an array in memory.

Arrays and Slices

A slice is specified like an array, but without specifying a size. A slice of a byte array has type []byte.

For example, a similar function that works on a slice of any size:

func SumInts(slice []int) int {
	sum := 0
	for i := 0; i < len(slice); i++ {
		sum += slice[i]
	}
	return sum
}

Arrays and Slices

Iterating over an array/​slice with a counter (as you would in C) is probably bad form in Go.

The range clause in a for loop is the nicer way to iterate through the values. This is equivalent:

func SumIntsBetter(slice []int) int {
	sum := 0
	for _, val := range slice {
		sum += val
	}
	return sum
}

Arrays and Slices

If you need the value and its position, you can use two values before the := range. The first will be the index, and the second the value:

values := []int{10, 20, 30}
for pos, val := range values {
	fmt.Printf("position %d value %d\n", pos, val)
}
position 0 value 10
position 1 value 20
position 2 value 30

Arrays and Slices

If you're writing a C-style for loop (with initializer, condition, increment) to iterate through an array/​slice, stop!

Write a for/range loop instead (if possible). It's easier to read, harder to get wrong, and generally nicer.

Arrays and Slices

Slices can be formed by specifying sections of an array (or slice) like Python:

digits := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
fmt.Println(digits)
fmt.Println(digits[3:7])
fmt.Println(digits[:])
[0 1 2 3 4 5 6 7 8 9]
[3 4 5 6]
[0 1 2 3 4 5 6 7 8 9]

First line: create an array with ten elements, and initialize digits as a slice of (all of) it. Next lines: create slices of some/​all elements of digits.

Arrays and Slices

So far, all slices have been of arrays that had a length determined at compile time. What if we want to allocate with a dynamic size?

The make function can create a slice (among other things) dynamically. It allocates the necessary array, and returns a slice referring to it.

Arrays and Slices

For example, allocate and print a slice of a dynamic size:

func makePrintSlice(size int) {
	values := make([]uint8, size)
	fmt.Printf("%T %v\n", values, values)
}

Calling makePrintSlice(4) prints:

[]uint8 [0 0 0 0]

Arrays and Slices

Realistically: most of the time, you'll work with slices in Go, and the language will take care of the underlying array for you.