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).
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…
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.
Go is much closer to languages you already know than Haskell. Go is…
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.
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
Go code is arranged into packages, and Go is opinionated about the file layout:
src
directory;.go
files in those directories (broken up however you like).The main function must be in a package main
.
package main func main() { … }
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
and package hello
main/*.go
must start with
.package main
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) }
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.
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 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
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 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)
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.
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.
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 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
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 }
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.
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
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.
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") }
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") }
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") }
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.
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
Actually, the for
condition is also optional. This is an infinite loop:
for { fmt.Println("hello world") }
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.
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 } }
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
.
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
.
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
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 (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:
poly
here). Choose a good name.uint
to float64
explicitly.Access methods like you'd expect from other languages:
fmt.Println(littleHexagon.Perimeter())
0.6000000000000001
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
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.
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
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.
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 int
s. 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 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.
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 }
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 }
If you need the value and its position, you can use two values before the
. The first will be the index, and the second the value::= range
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
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.
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
.
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.
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]
Realistically: most of the time, you'll work with slices in Go, and the language will take care of the underlying array for you.