Learning a Programming Language pt 2c: Go Language Mini-Course, conclusion (90 Days of DevOps)
This blog post is a continuation from an earlier post, which works through a popular, publicly-available mini-course covering the Go programming language.
As in the previous two posts, full credit for the material goes to the course’s creator, TechWorld with Nana; below are simply my own notes and interpretation of their freely available course material, with occasional links added to additional web references I found helpful.
Goroutines/concurrency
What if, for the sake of responsiveness, we needed to have our program perform its work across multiple processes?
We simulate such a scenario by creating a function that (artificially) takes a long time to complete. The idea is that each time this function is called, it takes about 50 seconds to generate a document (the conference ticket file itself), and email that document to the user.
Note that we’re not really generating any documents, just writing a function that takes a long time to complete:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// main.go
package main
import (
"fmt"
"time" // package required by our new "emailTickets" function
)
...
func main() {
...
// call the new function
emailTickets(firstName, lastName, email, quantity)
...
}
...
func emailTickets(firstName, lastName, email string, quantity uint) {
fmt.Printf("Sending %v conference tickets for %v %v to %v...\n", quantity, firstName, lastName, email)
// Simulates the document processing time
time.Sleep(50 * time.Second)
fmt.Println("############") // visual separator
fmt.Printf("Tickets sent to %v %v.\n", firstName, lastName)
fmt.Println("############") // visual separator
}
Running our program with the changes above introduces a program responsiveness issue; the program cannot do anything else for 50 seconds each time it calls emailTickets() to generate and email tickets to a user:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
$ go run .
***
Welcome to the Go Conference booking application.
We have a total of 50 tickets and 50 are still available.
Get your tickets here to attend!
Enter your first name:
Philip
Enter your last name:
Fry
Enter your email address:
pf@example.com
How many tickets are you purchasing?:
1
Thank you Philip Fry for booking 1 tickets.
You will receive a confirmation at pf@example.com.
These are our current attendees: [Philip]
Sending 1 conference tickets for Philip Fry to pf@example.com...
At this point, 50 seconds pass while the ticket generation/sending process is simulated. The program cannot move on to the booking the next conference attendee until this process completes. After the time passes, the following confirmation appears:
1
2
3
############
Tickets sent to Philip Fry.
############
Then, the program moves on to start the next booking request:
1
2
3
4
5
6
7
***
Welcome to the Go Conference booking application.
We have a total of 50 tickets and 49 are still available.
Get your tickets here to attend!
Enter your first name:
...
In the next version of our program, we will solve the problem of program non-responsiveness by using concurrency. The ease of implementing concurrency in Go is a core selling point of the language. Concurrency is achieved through the use of multiple threads, via goroutines.
Creating a new goroutine could not be much simpler; we only need to add the keyword go just before calling our time-intensive function, i.e.:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// main.go
package main
import (
"fmt"
"time"
)
...
func main() {
...
// call "emailTickets()" IN A NEW GOROUTINE
go emailTickets(firstName, lastName, email, quantity)
...
}
...
func emailTickets(firstName, lastName, email string, quantity uint) {
fmt.Printf("Sending %v conference tickets for %v %v to %v...\n", quantity, firstName, lastName, email)
time.Sleep(50 * time.Second)
fmt.Println("############")
fmt.Printf("Tickets sent to %v %v.\n", firstName, lastName)
fmt.Println("############")
}
With the go keyword added, the Go runtime will know to automatically create a separate thread for the specified function, and will also clean up that thread it when its work completes.
We test by booking three users in quick succession, noticing that there is no delay between booking individual users:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
$ go run .
***
Welcome to the Go Conference booking application.
We have a total of 50 tickets and 50 are still available.
Get your tickets here to attend!
Enter your first name:
Philip
Enter your last name:
Fry
Enter your email address:
pf@example.com
How many tickets are you purchasing?:
1
Thank you Philip Fry for booking 1 tickets.
You will receive a confirmation at pf@example.com.
These are our current attendees: [Philip]
***
Welcome to the Go Conference booking application.
We have a total of 50 tickets and 49 are still available.
Get your tickets here to attend!
Enter your first name:
Sending 1 conference tickets for Philip Fry to pf@example.com...
Leela
Enter your last name:
Turanga
Enter your email address:
lt@example.com
How many tickets are you purchasing?:
1
Thank you Leela Turanga for booking 1 tickets.
You will receive a confirmation at lt@example.com.
These are our current attendees: [Philip Leela]
***
Welcome to the Go Conference booking application.
We have a total of 50 tickets and 48 are still available.
Get your tickets here to attend!
Enter your first name:
Sending 1 conference tickets for Leela Turanga to lt@example.com...
Hermes
Enter your last name:
Conrad
Enter your email address:
hc@example.com
How many tickets are you purchasing?:
1
Thank you Hermes Conrad for booking 1 tickets.
You will receive a confirmation at hc@example.com.
These are our current attendees: [Philip Leela Hermes]
***
Welcome to the Go Conference booking application.
We have a total of 50 tickets and 47 are still available.
Get your tickets here to attend!
Enter your first name:
Sending 1 conference tickets for Hermes Conrad to hc@example.com...
...
About a minute later, three new goroutines (each spun out to a separate process to handle our time-intensive emailTickets() function for a single attendee), start completing:
1
2
3
4
5
6
7
8
9
10
11
############
Tickets sent to Philip Fry.
############
############
Tickets sent to Leela Turanga.
############
############
Tickets sent to Hermes Conrad.
############
...
Syncing / WaitGroups
One problem with the example above is that the program could possibly terminate before all of the emailTickets() goroutines have had a chance to complete.
For example, if the first user happened to book all 50 of the conference tickets, the program/main goroutine would have terminated very shortly afterwards, before the emailTickets() goroutine had enough time to successfully complete.
This scenario can be fixed using WaitGroups. A WaitGroup is a logical grouping of multiple goroutines. Once a WaitGroup is declared, we can:
- add goroutines to / release goroutines from the
WaitGroup - tell Go to wait until all of the
WaitGroup’s member goroutines have been released, before executing any further code.
That leads us to our final version of the conference ticket booking app:
Booking app Example 5
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
// main.go
package main
import (
"fmt"
"sync" // required to implement WaitGroup
"time"
)
const conferenceName string = "Go Conference"
const conferenceTickets uint = 50
var attendeesInfo = make([]UserData, 0)
type UserData struct {
firstName string
lastName string
email string
quantity uint
}
var wg = sync.WaitGroup{} // declares a new, empty WaitGroup
func main() {
var remainingTicketCount uint = 50
for {
var ticketsAreAvailable bool = remainingTicketCount != 0
var noTicketsAreAvailable bool = !ticketsAreAvailable
greetUser(remainingTicketCount, ticketsAreAvailable)
if noTicketsAreAvailable {
break
}
firstName, lastName, email, quantity := getUserBookingInfo()
firstNameValid, lastNameValid, emailValid, quantityValid := validateUserBookingInfo(firstName, lastName, email, quantity, remainingTicketCount)
fullNameValid := firstNameValid && lastNameValid
allInfoValid := fullNameValid && emailValid && quantityValid
if !allInfoValid {
fmt.Printf("\nPlease fix these issues identified in your booking info:\n")
fmt.Println("--------------------------------------------------------")
if !fullNameValid {
fmt.Println("*First and last name must each contain 2 or more characters.")
}
if !emailValid {
fmt.Println("*Email address must contain the \"@\" symbol.")
}
if !quantityValid {
fmt.Printf("*You can book a minimum of 1 and a maximum of %v tickets for this conference.\n", remainingTicketCount)
}
fmt.Println("--------------------------------------------------------")
fmt.Printf("The booking process will restart.\n\n")
continue // skips back to start of loop (the infinite for loop)
} else {
remainingTicketCount = bookUserTickets(firstName, lastName, email, quantity, remainingTicketCount)
wg.Add(1) // Adds a single goroutine to the WaitGroup declared earlier
go emailTickets(firstName, lastName, email, quantity)
}
}
// By calling our WaitGroup's "Wait()" method below, we ensure that the main()
// function will wait for all members of that WaitGroup to complete their work,
// before moving on (in this case, before exiting the program entirely)
wg.Wait()
}
func greetUser(remainingTicketCount uint, ticketsAreAvailable bool) {
fmt.Println("***")
fmt.Printf("Welcome to the %v booking application.\n", conferenceName)
fmt.Printf("We have a total of %v tickets and %v are still available.\n", conferenceTickets, remainingTicketCount)
if ticketsAreAvailable {
fmt.Println("Get your tickets here to attend!")
} else {
fmt.Println("Our conference is fully booked. Please join us next year.")
}
fmt.Println()
}
func getUserBookingInfo() (firstName, lastName, email string, quantity uint) {
fmt.Println("Enter your first name:")
fmt.Scan(&firstName)
fmt.Println("Enter your last name:")
fmt.Scan(&lastName)
fmt.Println("Enter your email address:")
fmt.Scan(&email)
fmt.Println("How many tickets are you purchasing?:")
fmt.Scan(&quantity)
return firstName, lastName, email, quantity
}
func bookUserTickets(firstName, lastName, email string, quantity, remainingTicketCountIn uint) (remainingTicketCountOut uint) {
userBookingInfo := UserData{
firstName: firstName,
lastName: lastName,
email: email,
quantity: quantity,
}
attendeesInfo = append(attendeesInfo, userBookingInfo)
remainingTicketCountOut = remainingTicketCountIn - quantity
fmt.Println()
fmt.Printf("Thank you %v %v for booking %v tickets.\nYou will receive a confirmation at %v.\n", userBookingInfo.firstName, userBookingInfo.lastName, userBookingInfo.quantity, userBookingInfo.email)
firstNames := getFirstNames()
printAttendeeFirstNames(firstNames)
fmt.Println()
return
}
func getFirstNames() []string {
firstNames := []string{}
for _, v := range attendeesInfo {
firstNames = append(firstNames, v.firstName)
}
return firstNames
}
func printAttendeeFirstNames(names []string) {
fmt.Printf("These are our current attendees: %v\n", names)
}
func emailTickets(firstName, lastName, email string, quantity uint) {
fmt.Printf("Sending %v conference tickets for %v %v to %v...\n", quantity, firstName, lastName, email)
time.Sleep(50 * time.Second)
fmt.Println("############")
fmt.Printf("Tickets sent to %v %v.\n", firstName, lastName)
fmt.Println("############")
// Using our WaitGroup's Done() method, we indicate that the goroutine that was
// started for the purpose of running this function, can be removed from our
// WaitGroup, since this function has now completed its work.
wg.Done()
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// helper.go
package main
import "strings"
func validateUserBookingInfo(firstName, lastName, email string, quantity, remainingTicketCount uint) (bool, bool, bool, bool) {
firstNameIsValid := len(firstName) >= 2
lastNameIsValid := len(lastName) >= 2
emailIsValid := strings.Contains(email, "@")
quantityIsValid := (quantity <= remainingTicketCount) && (quantity > 0)
return firstNameIsValid, lastNameIsValid, emailIsValid, quantityIsValid
}
We run the above:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
$ go run .
***
Welcome to the Go Conference booking application.
We have a total of 50 tickets and 50 are still available.
Get your tickets here to attend!
Enter your first name:
Philip
Enter your last name:
Fry
Enter your email address:
pf@example.com
How many tickets are you purchasing?:
50
Thank you Philip Fry for booking 50 tickets.
You will receive a confirmation at pf@example.com.
These are our current attendees: [Philip]
***
Welcome to the Go Conference booking application.
We have a total of 50 tickets and 0 are still available.
Our conference is fully booked. Please join us next year.
Sending 50 conference tickets for Philip Fry to pf@example.com...
...
Even though all conference tickets are quickly booked, our main process remains open. This is because we have instructed Go to wait for all goroutines that are part of the wg WaitGroup to complete their work. After 50 seconds pass, the single goroutine we added to wg completes.
1
2
3
############
Tickets sent to Philip Fry.
############
At the very end of the emailTickets() function, we inform Go that the goroutine running it, can be removed from our WaitGroup. Only then does the main() function terminate, since the WaitGroup it was waiting for is now empty.
As we see, thanks to Go’s sync package and its WaitGroup functionality, we can better manage multiple threads by making our main thread wait for others when necessary.
This blog post concludes the “Learning a Programming Language” objective in the “90 Days of DevOps” project. The next topic to be covered will be the Linux OS. See you in the next post!
<< Back to 90 Days of DevOps posts
<<< Back to all posts