Session 08¶
Topic 2: The Go language¶
- tour of Go
- tour of Go (continued)
- Go’s programming syntax 👈
- many workers with Goroutines
- shared memory between workers
- message passing with Go channels
- case studies with concurrency
- performance and scalability
- case studies with parallel algorithms
Today¶
Take a tour of Go’s programming syntax¶
- basic syntax:
packages, imports, functions, variables, types, constants, for, if, … - deeper dive:
structs, pointers, methods
We will follow the official tutorial: go.dev/tour
what we’ll skip for now:
- slices, maps
- interfaces
- generics
- concurrency
Basics¶
Basic syntax¶
Learn the basic components of any Go program
→ Packages, variables, and functions
—
Learn how to control the flow of your code with conditionals, loops, switches and defers
→ Flow control statements: for, if, else, switch and defer
Packages¶
Every Go program is made up of packages.
Programs start running in package main.
Imports and exported names¶
Our program is using the packages with import paths "fmt" and
"math/rand".
import (
"fmt"
"math/rand"
)
In Go, a name is exported if it begins with a capital letter. For
example, Println is an exported name, which is exported from the fmt
package.
Functions¶
Arguments¶
A function can take zero or more arguments.
In our example, Deposit takes one parameters of type int.
func Deposit(amount int) {
balance = balance + amount
}
Notice that the type comes after the variable name (first amount then
int).
Single or multiple results¶
A function can return any number of results.
The Balance function returns one int.
func Balance() int {
return balance
}
The Withdrawal function returns an int and a bool. Multiple return
types must be in parentheses.
func Withdrawal(requested int) (int, bool) { ... }
Variables¶
The var statement declares one or more variables; and just like in
function argument lists, the type is last.
var balance int
var (
foo int
bar int
)
A var statement can be at package or function level.
We see in our example that balance is declared at the package level
(it’s not in any function).
Short variable declarations¶
Really cool in Go: Inside a function, the := short assignment
statement can be used in place of a var declaration.
The Go compiler figures out the type implicitly. We save time.
At the package level, every statement begins with a keyword (var,
func, and so on) and so the := construct is not available.
Zero values¶
Undefined variables don’t exist in Go: Variables declared without an explicit initial value are given their zero value.
The zero values are:
0for numeric types,falsefor the boolean type, and""(the empty string) for strings.- …
Types¶
Basic types¶
Go’s basic types are …
bool
int int8 int16 int32 int64
uint uint8 uint16 uint32 uint64 uintptr
float32 float64
complex64 complex128
byte // alias for uint8
string
rune // represents a Unicode code point
Type inference¶
When declaring a variable without specifying an explicit type (either by
using the := syntax or var = expression syntax), the variable’s type
is inferred from the value on the right hand side.
When the right hand side of the declaration is typed, the new variable is of that same type:
var i int
j := i // j is an int
Type conversions¶
The expression T(v) converts the value v to the type T.
Some numeric conversions:
var i int = 42
var f float64 = float64(i)
var u uint = uint(f)
Or, put more simply:
i := 42
f := float64(i)
u := uint(f)
Constants¶
Constants are declared like variables, but with the const keyword.
const Pi = 3.14
const Pie = "yum"
Constants can be character, string, boolean, or numeric values. Constants have no type specification.
// define multiple constants at once:
const (
Pi = 3.14
Pie = "yum"
)
For¶
Go has only one looping construct: the for loop.
The basic for loop has three components separated by semicolons:
for i := 0; i < 10; i++ {
// do something
}
- the init statement: executed before the first iteration
- the condition expression: evaluated before every iteration
- the post statement: executed at the end of every iteration
The init statement will often be a short variable declaration, and the variables declared there are visible only in the scope of the for statement.
The loop will stop iterating once the boolean condition evaluates to
false.
The init and post statements are optional. They can be empty. If init and post are emtpy, the semicolons can be dropped.
sum := 1
for sum < 1000 {
sum += sum
}
If you omit the loop condition, it loops forever, so an infinite loop is compactly expressed as:
for {
// this loop never ends, but you could stop it with `break`
}
If the iteration index is not used inside the loop, use a for range
loop.
for range 10 {
// do something 10 times
}
for range loops are very common for slices and maps, which we will
meet later.
If¶
Go’s if statements are like its for loops.
The expression need not be surrounded by parentheses ( ) but the
braces { } are required.
If with a short statement¶
Like for, the if statement can start with a short statement to
execute before the condition.
func pow(x, n, lim float64) float64 {
// squint your eyes: you see a for loop w/o increment
if v := math.Pow(x, n); v < lim {
return v
}
return lim
}
Variables declared by the statement are only in scope until the end of
the if. In the example, v does not exist outside the { } braces of
the if statement.
If and else¶
Variables declared inside an if short statement are also available
inside any of the else blocks.
if v := math.Pow(x, n); v < lim {
return v
} else {
fmt.Printf("%g >= %g\n", v, lim)
}
// can't use "v" here, though
Switch¶
A switch is a shorter way to write a sequence of if ... else
statements. It runs the first case whose value is equal to the condition
expression.
today := time.Now().Weekday()
// evaluate cases from top to bottom, stopping when a case succeeds
switch time.Saturday {
case today + 0:
fmt.Println("Today.")
case today + 1:
fmt.Println("Tomorrow.")
case today + 2:
fmt.Println("In two days.")
default:
fmt.Println("Too far away.")
}
Switch with no condition¶
A switch without a condition can be a clean way to write long if-then-else chains.
t := time.Now()
switch {
case t.Hour() < 12:
fmt.Println("Good morning!")
case t.Hour() < 17:
fmt.Println("Good afternoon.")
default:
fmt.Println("Good evening.")
}
Defer¶
A defer statement defers the execution of a function until the
surrounding function returns.
func main() {
defer fmt.Println("... world!")
fmt.Println("Hello ...")
}
This often useful for closing files:
defer file.Close()
Deeper dive¶
More types¶
Learn how to define types based on existing ones: structs, arrays, slices, and maps
→ More types: structs, slices, and maps
Structs¶
A struct is a collection of fields, for example, two int’s.
type Point2D struct {
X int
Y int
}
Struct fields are accessed using a dot:
p := Point{12, 34} // create a struct with X=12, Y=34
p.X = 56
fmt.Println(p.X)
Struct literals¶
A struct literal denotes a newly allocated struct value by listing the values of its fields.
You can list just a subset of fields by using the Name: syntax. (And
the order of named fields is irrelevant.)
var (
p1 = Point2D{1, 2} // has type Point2D, X=1, Y=2
p2 = Point2D{X: 1} // Y:0 is implicit
p3 = Point2D{} // X:0 and Y:0 is implicit
)
Pointers¶
A pointer holds the memory address of a value.
// this integer value, "99", is stored somewhere in computer memory
var i int = 99
Go can tell the location in memory:
var p *int = &i // I am the address of the value of "i"
We say generally: The type *T is a pointer to a T value.
The zero value of a pointer – no matter which type – is nil.
var p *int // I am the address of nobody
fmt.Println(p == nil) // prints "true"
The & operator generates a pointer from a variable.
i := 42
p := &i
The * operator denotes the pointer’s underlying value.
fmt.Println(*p) // read the value at "p" (prints "42")
*p = 21 // set a new value through the pointer "p"
fmt.Println(*p == 21) // prints "true"
fmt.Println(i == 21) // prints "true" ... do you understand why?
Using the * operator is known as “dereferencing” or “indirecting”.
Why pointers are useful¶
Pointers become attractive when they store the address of something that takes a lot of memory.
A pointer – no matter which type – always takes up as many Bytes as just one integer.
They are also useful if a variable is changed inside a function and that change should remain after the function finishes.
func Like(count *int) {
(*count)++
}
func main() {
var count int
// Alice:
Like(&count) // count is 1 after Like() finished
// Bob:
Like(&count) // count is 2 after Like() finished
// Ted ... does not like :/
}
Pointers to structs¶
Struct fields can be accessed through a struct pointer.
Take this struct, for example:
type Point2D struct {
X int
Y int
}
To access the field X of a struct when we have the struct pointer p
we could write (*p).X.
Since that notation is cumbersome, Go permits us instead to write just
p.X (without the explicit dereference). – 👍 Ted likes that!
Methods and interfaces¶
Methods and interfaces define objects and their behavior
(we’ll skip interfaces for now)
Methods¶
Go does not have classes.
Instead we define methods on types.
A method is a function with a special receiver argument. The receiver
appears in its own argument list between the func keyword and the
method name.
type Account struct {
balance int
}
// Balance - a method with receiver "a" of type Account
func (a Account) Balance() int {
return a.balance
}
Methods are functions¶
A method is just a function with a receiver argument.
This method and the function below work the same.
// Balance - a method with receiver "a" of type Account
func (a Account) Balance() int {
return a.balance
}
// Balance_too - a function with an additional argument
func Balance_too(a Account) int {
return a.balance
}
// difference of using a method and a function - same result
fmt.Println(a.Balance())
fmt.Println(Balance_too(a))
Pointer receivers¶
Methods with pointer receivers can modify the value to which the receiver points.
The receiver type has the literal syntax *T for some type T. (Also,
T cannot itself be a pointer such as *int.)
// Deposit - a method that modifies the receiver "a" of type *Account
func (a *Account) Deposit(amount int) {
a.balance = a.balance + amount
}
var a Account
fmt.Println(a.Balance()) // prints "0"
a.Deposit(100)
fmt.Println(a.Balance()) // prints "100"
Methods and pointer indirection¶
Calling a method with value receiver and another method with a pointer receiver looks the same:
a.Deposit(100) // calling with a pointer receiver
fmt.Println(a.Balance()) // calling with a value receiver
Even though a is a value and not a pointer, the method with the
pointer receiver is called automatically.
→ Convenience of Go! It automatically converts.
Remember: Methods often modify their receiver, that’s why pointer receivers are more common than value receivers.
Generics¶
Go supports generic programming using type parameters
→ Generics
(we’ll skip generics for now)
Concurrency¶
Go provides concurrency constructions as part of the core language
(we’ll see these soon)
Key takeaways¶
Readability over cleverness¶
- Go syntax: small, clear, and simple
- types are explicit
- type inference reduces code bloat
- zero values remove uninitialized-variable bugs
- control flow is simple and uniform:
for,if,switch - functions/methods support multiple return values
- pointers are explicit, safe, and improve performance
- no classes: methods on types give structure without complexity
Looking ahead¶
- goroutines: fork-join pattern
- goroutines \(<\) OS threads
- goroutines are designed for greater concurrency