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 hellomain/*.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, int64uint8, …, uint64float32, float64complex64, complex128byte (== uint8)boolThere 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 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 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.