Skip to content

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

  1. basic syntax:
    packages, imports, functions, variables, types, constants, for, if, …
  2. 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:

  • 0 for numeric types,
  • false for 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

→ Methods and interfaces

(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

→ Concurrency

(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

Prepare for the next session 🌱

sessions/09-todo