If you're stuck look at examples on Go by Example
There are two ways to use the lab sheet, you can either:
- create a new repo from this template - this is the recommended way
- download a zip file
Each question is rated to help you balance your work:
- 🔴⚪⚪⚪⚪ - Easy, strictly necessary.
- 🔴🔴⚪⚪⚪ - Medium, still necessary.
- 🔴🔴🔴⚪⚪ - Hard, necessary if you're aiming for higher marks.
- 🔴🔴🔴🔴⚪ - Hard, useful for coursework extensions.
- 🔴🔴🔴🔴🔴 - Hard, beyond what you need for any part of the coursework.
go someFunc()
Start a new goroutine. someFunc
has to be a void function.
channel := make(chan int)
Make a new channel to be used to communicate between two goroutines. You can think of make
as Go's version of malloc
. It allocates memory for objects. However, you don't have to worry about freeing the memory as Go has a built-in garbage collector.
channel := make(chan int, 6)
Make a new buffered channel with buffer of size 6 (i.e. capable of storing 6 ints).
channel <- 5
Send one message (the integer 5) to the channel.
number := <-channel
Receive one message from a channel and assign it to a new variable called number
.
sendOnly chan<- int
Channel of type chan<-
is send-only. Receiving a message from a channel of that type will cause a compiler error.
receiveOnly <-chan int
Channel of type <-chan
is receive-only. Sending a message to a channel of that type will cause a compiler error.
Open messages.go
. In messages.go
we create a channel for strings and then use it to pass 3 strings between sendMessages
and main
.
Run the program and make sure you fully understand the output.
Modify the function main()
so that only 2 messages are received. Do not modify the sendMessages()
function.
Run the program and explain the output. Look at which messages have been received and which ones have been sent.
Modify the function main()
so that 4 messages are received. Do not modify the sendMessages()
function.
Run the program and explain what has happened and why.
Modify the function main()
so that 3 messages are received. Modify the make
function call in main so that messages
is a buffered channel of strings with buffer size 3. Do not modify the sendMessages()
function.
Run the program and explain how the result differs compared to the original solution with no buffered channels.
Watch the video on 'Debugging with Delve' and play around with some of the debug commands on the programs that you have written in Lab 1.
Open ping.go
. It's a skeleton for a "ping-pong" program. Implement one round of ping-pong: foo()
should send a string ping
to bar()
and bar()
should respond with the string pong
. After that foo()
should receive a pong
.
Complete your solutions by implementing foo()
and bar()
. Don't forget to wire things up in the pingPong()
function. Do not modify main()
.
Include Println
s to show what has been sent and received. Example output:
$ go run ping.go
Foo is sending: ping
Bar has received: ping
Bar is sending: pong
Foo has received: pong
Make both foo()
and bar()
run in infinite loops - i.e. After receiving a pong
the goroutine foo()
should send another ping
. foo()
and bar()
should continue doing this infinitely (until the program terminates from time.Sleep(500 * time.Millisecond)
).
Example output:
$ go run ping.go
Foo is sending: ping
Bar has received: ping
Bar is sending: pong
Foo has received: pong
Foo is sending: ping
Bar has received: ping
Bar is sending: pong
Foo has received: pong
Foo is sending: ping
Bar has received: ping
Bar is sending: pong
Foo has received: pong
Foo is sending: ping
Bar has received: ping
Bar is sending: pong
Foo has received: pong
...
You may have noticed that main
talks about tracing. An execution trace in go is similar to a stack trace, except now we have multiple threads of execution and therefore a simple linear trace is no longer very clear. An execution trace allows us to see exactly when each goroutine was running, in which function and how it communicated with other goroutines.
Running your ping.go
should have generated a trace.out
file. You can open the trace file with the command go tool trace trace.out
. This will provide you with a link that you must open in Chromium or Chrome - the trace viewer will not work in other browsers.
Explain how the trace relates to the code you have written.
WATCH THE VIDEO ON TRACING IN GO - it is available on OneDrive
Hint
Click on Goroutine analysis
, main.foo
, and then whatever number it says under the Goroutine
heading. You should see a trace like this:
Firstly, enable flow events
under view options
in the top-right corner. This will display arrows that indicate channel communication and use of the go
keyword.
To navigate the trace you will need to use your keyboard. Use W and S to zoom in and out (it will zoom to where your mouse is), and A and D to move left and right. Now zoom in so that you see a part of the trace that looks like this:
If your ping-pong code is correct your trace should look very similar to the one above. Each block means that the goroutine was running and each arrow represents a message on a channel.
Sometimes we do not know which channel to receive from. The example program select.go
illustrates this problem. We have two goroutines: fastSender
and slowSender
. In main
we would like to receive all messages from the two goroutines but we do not know when each message arrives. A select statement is a solution to this problem. Given a list of cases it will wait until it finds one that can be executed and then it will run the code after the :
. In this program the two possible cases are "string received from strings
" and "integer received from ints
" :
for { // An empty for is Go's equivalent of 'while(true).'
select {
case s := <-strings:
fmt.Println("Received a string", s)
case i := <-ints:
fmt.Println("Received an int", i)
}
}
Run the program and explain the output.
Write a new function func fasterSender(c chan<- []int)
that sends the slice [1, 2, 3]
to main
every 200 ms. Add another case in the select statement for receiving these slices from the fasterSender
.
Run your program. Ensure that your result looks similar to this example output:
$ go run select.go
Received a slice [1 2 3]
Received a slice [1 2 3]
Received an int 0
Received a slice [1 2 3]
Received a slice [1 2 3]
Received an int 1
Received a slice [1 2 3]
Received a slice [1 2 3]
Received a slice [1 2 3]
Received an int 2
Received a slice [1 2 3]
Received a slice [1 2 3]
Received a string I am the slowSender
Received an int 3
Received a slice [1 2 3]
Received a slice [1 2 3]
Received a slice [1 2 3]
Received an int 4
Received a slice [1 2 3]
Received a slice [1 2 3]
Received an int 5
Received a slice [1 2 3]
Received a slice [1 2 3]
Received a slice [1 2 3]
Received an int 6
Received a slice [1 2 3]
Received a slice [1 2 3]
Received a string I am the slowSender
Received an int 7
Received a slice [1 2 3]
Received a slice [1 2 3]
Received a slice [1 2 3]
Received an int 8
Received a slice [1 2 3]
Received a slice [1 2 3]
Received an int 9
...
A default case is a special case in the select statement that will be executed if and only if no other cases could be executed. Add a default case to the select statement containing the following code:
fmt.Println("--- Nothing to receive, sleeping for 3s...")
time.Sleep(3 * time.Second)
Run the program and explain the output.
Make all of your channels buffered. Set the buffer size to 10.
Run the program and explain the output.
Create traces for all versions of the select program and explain how they correspond to the code you have written.
Open quiz.go
. It's a sample solution to the quiz task that you might remember from lab 1. Today we will explore advanced concurrency concepts and language features to improve our quiz.
Your task is to use the for-select construct demonstrated in select.go
to satisfy the following new quiz requirements:
-
The quiz should only run for 5 seconds. After that time the final score should be printed to the user.
-
The quiz program must terminate immediately after the 5 seconds have elapsed. Specifically, it must not wait for the user's answer to the current question before terminating.
As part of the ask()
function, we have to wait for the user's input. The function call scanner.Scan()
will block until it receives a line of text (a string with a newline character at the end). An operation that may need to wait for some event is known as a blocking operation. Other blocking operations include channel send/receive operations (on a non-buffered channel send operation may block and wait for the matching receive and vice versa).
However, we described above that we would like to check the timer and terminate the program while the user is thinking about the answer. Goroutines offer a solution to this problem. If we turn ask
into a goroutine it will be able to block and wait for user input while main
will be able to continue to work.
Modify the quiz program so that ask is always called as a goroutine - i.e. ask is always of the form go ask(...)
. Verify that the quiz still works as expected and that the score printed out is correct.
Hint 1
You have to modify both main
and ask
.
Hint 2
Recall that to return from a goroutine you have to use a channel.
Hint 3
Look at the time package to find an appropriate function to help you measure the 5 second timeout.
Using your new ask
goroutine modify main
so that it satisfies the 2 new requirements.
Play the quiz. State the number of different threads your solution uses and explain how they communicate with each other.
Hint 1
In this question, you do not need to modify ask
.
Hint 2
Your new program should use 3 threads:
- A blocking goroutine
ask
that waits for the user's input. - The
main
goroutine which will hold state such as score and index of the current question. - A timer thread that will send a message on a channel after 5s.
Hint 3
Your select statement will have 2 cases. One for an updated score and one for a timeout message from the timer. You do not need to use a default case here.