Post

Learning a Programming Language pt 2a: Go Language Mini-Course (90 Days of DevOps)

Learning a Programming Language pt 2a: Go Language Mini-Course (90 Days of DevOps)

In this blog post, we continue the 90 Days of DevOps series, by learning the basics of the Go programming language. We’ll do this by following along with a free course available from TechWorld with Nana on Youtube:

Golang Tutorial for Beginners | Full Go Course

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 web references that I found helpful.

Background

The Go programming language was developed in 2007 by Google in part to take advantage of new infrastructure trends at the time:

  • Multicore processors, allowing for the usage of multithreading to create more performant apps
  • Cloud computing

Some key design features of the Go language include:

  • The readable syntax of a high-level language like Python
  • The speed of a lower-level language like C++
  • It’s a compiled language, and its compiled binaries are cross-platform compatible; the same binary works to deploy across Lin, Win, Mac (??).

Setup

To prepare for the course, we are asked to install the following (steps will vary by OS):

Writing our first program & structure of a Go file

With everything installed, we start setting up a new project. We:

  • Make a new directory for our project called booking-app
  • Open this new directory in VSCode
  • Open a terminal and in there, initialize a new Go module:

    1
    
    $ go mod init booking-app
    

    A Go module is a collection of Go packages, which are themselves collections of individual functions. This structure helps keep larger projects organized and maintainable.

    The go mod init command above creates a new Go module named booking-app. Behind the scenes, a new file named go.mod is created in our project’s directory, with contents similar to the below:

    1
    2
    3
    
    module booking-app
    
    go *VERSION_NUMBER* 
    

    As we can see, the file contains our new Go module’s name, as well as the version of Go being used.

With our new booking-app module created, it is time to create the main Go package. We do this by creating a new source file named main.go.

1
2
3
4
5
6
7
  package main

  import "fmt"

  func main() {
    fmt.Println("Hello World")
  }

This is the same “Hello, World” code covered in the official Go quickstart tutorial as well as in the previous post in this series.

As review, here is what is happening in this code:

  • package main adds our program to our own package. The main package is required(??)
  • import "fmt" gives our program access to the core functions contained in the fmt package from the Go standard library. We will use its functions to provide core I/O capabilities to our program.
  • The main() function is required and is where Go will start executing instructions when our program runs. It calls the Println() function from fmt, and uses that function to output our chosen text to the console.

With our source file in place, we compile and run it with the command go run <filename>.

1
2
$ go run main.go
Hello World

With a minimal Go project structure in place, the next step is to build something a bit more interesting. As the name of our project suggests, this is meant to be a “booking app” of some sort.

It turns out this program will be for booking conference tickets, and we’ve decided to start by greeting the user when the program is run. We simply remove the “Hello World” line and replace it with the following two lines:

1
2
    fmt.Println("Welcome to our conference booking application.")
    fmt.Println("Get your tickets here to attend!")

After saving the changes to main.go, and compiling/running our app again, we get the new greeting as output:

1
2
3
$ go run main.go
Welcome to our conference booking application.
Get your tickets here to attend!

Variables & constants in Go

Variables

We declare a variable using the syntax var <variable_name> = <variable_value>, and reference its value in our user greeting. Our updated main() function looks like this:

1
2
3
4
5
6
7
8
9
func main() {
    var conferenceName = "Go Conference" // Declare the variable

    // Pass the variable's value as an argument to our first Println() call.
    //  Println() automatically handles combining and adding spaces between
    //  multiple arguments.
    fmt.Println("Welcome to the", conferenceName, "booking application.")
    fmt.Println("Get your tickets here to attend!")
}

After recompiling our code and running it, our end-user greeting includes the value of our new conferenceName variable:

1
2
3
$ go run main.go
Welcome to the Go Conference booking application.
Get your tickets here to attend!

Constants

We can declare a constant in Go using the syntax const <const_name> = <const_value>.

As an example, below we add a constant and a second variable to our main() function, and update the user greeting to include those values:

1
2
3
4
5
6
7
8
9
func main() {
    var conferenceName = "Go Conference"
    const conferenceTickets = 50 // This value will not be allowed to change.
    var remainingTickets = 50

    fmt.Println("Welcome to the", conferenceName, "booking application.")
    fmt.Println("We have a total of", conferenceTickets, "tickets and", remainingTickets, "are still available.")
    fmt.Println("Get your tickets here to attend!")
}

We compile and run again to see our new greeting.

1
2
3
4
$ go run main.go       
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!

Formatted output - Printf()

As an alternative to the Println() function used above, we can instead output messages from our code by calling Printf(), which is another function from the Go Standard Library’s fmt package. A benefit of using Printf() is that it can give us more control over how our variable values appear when they are output. See the code example below:

1
2
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, remainingTickets)

The general syntax as shown above is fmt.Printf(<what_to_print>, <first_variable_name>, <second_variable_name>, <third_variable_name>, ...), where each additional variable name to be substituted into the first argument, is added to our Printf() call as an additional argument.

We use % symbols within the first argument to indicate one or more placeholders where some variable’s value should be substituted in. This templating can be used to help make code more readable. The % symbols are read left-to-right by Printf(), and each time one is encountered, Printf() substitutes in the value of the next variable in our argument list.

The letters that we place immediately after the % symbols are how we tell Printf() what “format” the substituted values should be displayed in. The sequence %v, as used in the example above, indicates the default display format, but there are other options.

For example, %q (quoted format):

1
fmt.Printf("Welcome to the %q booking application.\n", conferenceName)

The above would result in the following quoted output at runtime:

1
Welcome to the "Go Conference" booking application.

A full reference of formatting options is available in the fmt package’s documentation.

Data types in Go

Go is statically typed. All variables must be assigned a specific type, such as string, int, float, etc. This explicit “typing” can help prevent bugs by:

  • enforcing a predictable range of possible values for each variable (e.g. we can count on a variable of type uint (unsigned integer), not to contain as its value an alphabetical character or a negative number of any kind).
  • preventing “type errors” (e.g. accidentally trying to calculate the sum of a string such as "lorem ipsum" and an integer such as 3).

Even though Go is statically typed, notice that we have not been required to specify any variable data types so far. That is because up to this point, the Go compiler has inferred each variable’s type based on the values we assigned during declaration:

1
2
3
var conferenceName = "Go Conference"  // Go infers a string data type, so this variable is statically typed as a string.
const conferenceTickets = 50  // Go infers an int data type, so this constant is statically typed as an int.
var remainingTickets = 50  // Go infers an int data type, so this variable is statically typed as an int.

In cases where we declare a variable or constant without specifying an initial value, Go will not be able to infer a data type. We need to explicitly specify a type as in the example below:

1
var userName string  //Since there is no value assigned, Go cannot infer a type. We explicitly declare a type of string.

Even in situations where Go has the ability to infer a new variable’s type, it’s possible that Go might infer a type that doesn’t match our actual use case. For example, Go might infer an int in a case where we specifically have a reason to use another numeric type like uint (unsigned integer).

Getting user input

To collect end-user input directly via the CLI, we can use Scan(), yet another function from Go’s fmt package. The syntax is fmt.Scan(<pointer to variable>) .

What is a pointer?

Sometimes in Go, as is the case when using Scan(), we need to work with a variable’s memory address instead of the value it contains. By placing a & before a variable’s name in our code, we tell Go to reference the variable’s address rather than reading its value.

1
2
firstName // this resolves to the current VALUE of the variable named, e.g. "Tom".
&firstName // this refers to the MEMORY ADDRESS of the variable named, e.g. 0xc000014080.

The Scan() function expects a variable’s pointer as an argument. That way, when the user enters input, Scan() knows where in the computer’s memory to write that new data.

1
fmt.Scan(&firstName) // takes user input and saves it to the memory address of the variable referenced

Booking app Example 1

At this point, we have covered enough tools within the Go language to simulate a simple booking process in our main() function.

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
package main

import "fmt" // provides the I/O functions called in this program

func main() {
    const conferenceName string = "Go Conference" // this value will not be allowed to change.
    const conferenceTickets uint = 50             // this value will not be allowed to change. It also cannot be negative.
    var remainingTickets uint = 50                // this value will change after the user specifies how many tickets they are booking. It cannot be negative.

    // greet user
    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, remainingTickets)
    fmt.Println("Get your tickets here to attend!")

    // get info from user
    var firstName string // explicit type declaration is required when declaring empty variables
    var lastName string
    var email string
    var userTickets uint
    fmt.Println("Enter your first name: ")
    fmt.Scan(&firstName) // user types input

    fmt.Println("Enter your last name: ")
    fmt.Scan(&lastName) // user types input

    fmt.Println("Enter your email address: ")
    fmt.Scan(&email) // user types input

    fmt.Println("How many tickets are you purchasing? : ")
    fmt.Scan(&userTickets) // user types input

    // update available tickets count for this conference
    remainingTickets = remainingTickets - userTickets

    // provide confirmation to user
    fmt.Printf("Thank you %v %v for booking %v tickets.\nYou will receive a confirmation at %v.\n", firstName, lastName, userTickets, email)
    fmt.Printf("There are %v tickets remaining for %q.\n", remainingTickets, conferenceName)
}

We compile and run our program to see it in action.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ go run main.go 
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: 
Example
Enter your last name: 
User
Enter your email address: 
email@example.com
How many tickets are you purchasing? : 
1
Thank you Example User for booking 1 tickets.
You will receive a confirmation at email@example.com.
There are 49 tickets remaining for "Go Conference".

Arrays & slices

While variables each will hold a single value, arrays and slices in Go consist of a series of values.

Arrays

Arrays have a fixed length; they contain a specific number of elements, and that number is declared at the same time as the array. The syntax to declare an array is var <variable_name> [<size>]<variable_type>. For example:

1
2
3
// declares an array named "bookingFullName", containing 50 elements of type string,
//  with each element containing Go's default string element value of "".
var bookingFullName [50]string

Just as with a variable, we can assign values to an array at the same time as we declare it. This is done using the syntax var <variable_name> = [<size>]<variable_type>{<value_1, value2, value3, ...>}. For example:

1
2
3
4
// declares an array named "bookingFullName", containing 50 elements of type string,
//  where the first three elements are assigned the values listed between curly braces,
//  and the rest contain Go's default string element value of "".
var bookingFullName = [50]string{"Philip Fry", "Leela Turanga", "Amy Wong"}

An alternative syntax to use is <variable_name> := [<size>]<variable_type>{<value_1, value2, value3, ...>} :

1
2
// alternative syntax for the previous example:
bookingFullName := [50]string{"Philip Fry", "Leela Turanga", "Amy Wong"}

Array elements are indexed numerically, starting from 0, so the arrays declared in the examples above contain indexes numbered from 0-49. To refer to a specific array element, we use the syntax <array_name>[<index_number>]. This syntax can be used both for accessing an element’s value and for changing it.

In the most recent code example, the value of bookingFullName[0] is “Philip Fry”. If we wanted to assign a value to the array’s fourth element, we could use bookingFullName[3] = "Hermes Conrad".

Slices

Unlike arrays, slices do not have a fixed length. Otherwise, they are very similar to arrays; in Go, a slice is technically a special type of array. Slices are more commonly used than arrays thanks to this variable length as well as some additional flexibility they provide over standard arrays.

The syntax to declare a slice is similar to the array examples, but no length is specified in between the square brackets.

Loops in Go

Unlike many popular programming languages, Go only uses only one type of loop, which is the for loop. More info about the Go for loop is available in this official interactive demo.

As detailed in the linked demo, the for loop in Go uses a syntax fairly similar to that encountered in languages like Java, C, and JavaScript.

1
2
3
4
5
6
7
8
9
10
11
12
13
for <init_statement>; <condition_expression>; <post_statement> {
    // Example init statement: i := 0
    // Example condition expression: i < 10
    // Example post_statement: i++

    // Instructions written between the curly braces will iterate based on the
    //  condition expression and init/post statements provided, if any.

    // If only a condition expression is provided, the loop behaves like C's
    //  "while" loop.

    // If none of the above are provided, the loop repeats indefinitely.
}

Conditionals (if / else) and boolean data type

Conditional logic in Go is handled via the if/else syntax:

1
2
3
4
5
6
7
8
9
10
if /*condition*/ { // condition will be evaluated as true or false
    // Code in this section will be executed if the "if" condition evaluated as true
} else if /*condition*/ { // optional statement; can appear many times in block
    // Code in this section will be executed if all preceding conditions
    //  evaluated as false AND the newly-introduced "else if" condition evaluated
    //  as true; used when multiple conditions are to be tested
} else { // optional statement; can appear once in block
    // Code in this section will be executed if all previous conditions evaluated
    //  as false
}

Booking app Example 2

By using slices, looping, and boolean conditions, we can expand our Go program to track conference ticket bookings across multiple users. The example below also performs limited user input validation, to ensure that a user doesn’t book more tickets than are currently available.

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
package main

import (
    "fmt"     // provides the I/O functions called in this program
    "strings" // provides the text parsing functions called in this program
)

func main() {
    const conferenceName string = "Go Conference" // this value will not be allowed to change.
    const conferenceTickets uint = 50             // this value will not be allowed to change. It also cannot be negative.
    var remainingTickets uint = 50                // this value will change after the user specifies how many tickets they are booking. It cannot be negative.
    bookedFullNames := []string{}                 // this is a new, empty slice

    for { // infinite loop
         // greet user
    	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, remainingTickets)
    	fmt.Println("Get your tickets here to attend!")
    	// get info from user
    	var firstName string // explicit type declaration is required when declaring empty variables
    	var lastName string
    	var email string
    	var userTickets uint

    	fmt.Printf("\n***\n") // visual separator between transactions
    	fmt.Println("Enter your first name: ")
    	fmt.Scan(&firstName) // user types input
    	fmt.Println("Enter your last name: ")
    	fmt.Scan(&lastName) // user types input
    	fmt.Println("Enter your email address: ")
    	fmt.Scan(&email) // user types input
    	fmt.Println("How many tickets are you purchasing? : ")
    	fmt.Scan(&userTickets) // user types input

    	if userTickets <= remainingTickets {
              // book the user
              bookedFullNames = append(bookedFullNames, firstName+" "+lastName)
              // update available tickets count for this conference
              remainingTickets -= userTickets
              // provide confirmation to user
              fmt.Println()
              fmt.Printf("Thank you %v %v for booking %v tickets.\nYou will receive a confirmation at %v.\n", firstName, lastName, userTickets, email)
              fmt.Printf("There are %v tickets remaining for %q.\n", remainingTickets, conferenceName)
              firstNames := []string{} // declare a new slice which will collect only attendees' first names
              // the for-range syntax is covered at https://go.dev/tour/moretypes/16
              for _, v := range bookedFullNames { // index argument omitted because not used in loop
              	var names = strings.Fields(v)
              	firstNames = append(firstNames, names[0])
              }

              fmt.Printf("These are our current attendees: %v\n", firstNames)
              fmt.Println()

              if remainingTickets == 0 {
              	// end program
              	fmt.Println("Our conference is fully booked. Please join us next year.")
              	fmt.Println()
              	break // exits current loop (the infinite for loop)
              }
    	} else { // user entered too many tickets
              fmt.Printf("You've requested %v tickets, but there are only %v tickets available.\n", userTickets, remainingTickets)
              fmt.Printf("Please book %v or fewer tickets.\n", remainingTickets)
              fmt.Println("Restarting booking process.")
              fmt.Println()
    	}
    }
}

We test the program again by compiling/running it and entering some user input.

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
$ go run main.go
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.
There are 49 tickets remaining for "Go Conference".
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: 
Leela
Enter your last name: 
Turanga
Enter your email address: 
lt@example.com
How many tickets are you purchasing? : 
50
You've requested 50 tickets, but there are only 49 tickets available.
Please book 49 or fewer tickets.
Restarting booking process.

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: 
Leela
Enter your last name: 
Turanga
Enter your email address: 
lt@example.com
How many tickets are you purchasing? : 
49

Thank you Leela Turanga for booking 49 tickets.
You will receive a confirmation at lt@example.com.
There are 0 tickets remaining for "Go Conference".
These are our current attendees: [Philip Leela]

Our conference is fully booked. Please join us next year.

As we can see, the program above performs some validation to check that the user doesn’t book more tickets than are currently available.

Validating user input

Next, we want to add more input validation, to test user-provided values other than ticket quantity. This is possible using boolean conditions just as we did with the ticket quantity.

To test strings (like the values of firstName, lastName, and email), we can use the len() built-in function, as well as the Contains() function, which is part of the strings package we’ve already imported.

1
2
3
4
5
6
7
8
9
10
11
// evaluates as true if the value of firstName contains two or more characters;
//  otherwise evaluates as false
firstNamePassesValidation := len(firstName) >= 2

// evaluates as true if the value of lastName contains two or more characters;
//  otherwise evaluates as false
lastNamePassesValidation := len(lastName) >= 2

// evaluates as true if the value of email contains the "@" character;
//  otherwise evaluates as false
emailPassesValidation := strings.Contains(email, "@")

The resulting boolean variables can then be used in an if statement to determine whether to proceed with the booking process, or to prompt the user to input new values.

Switch statements

A switch statement is a shorter way to write a sequence of if-else statements. Switch statements in Go can be useful when we anticipate that a value will likely match one of a predetermined set of values, and we want to execute different code depending on which of those values is matched.

As pointed out in the linked documentation, Go’s switch statement is different from that found in many commonly-used languages, because Go’s switch statement will automatically break after the code for the first matching case is executed.

Switch statements use the following syntax:

1
2
3
4
5
6
7
switch expression {
    case exp1: statement1
    case exp2: statement2
    case exp3: statement3
    ...
    default: statement4
}

For example:

1
2
3
4
5
6
7
8
9
10
11
12
13
switch city {
    case "New York":
        // code here will execute if city == "New York";
        //  switch statement will automatically be exited afterward
    case "London":
        // code here will execute if city == "London";
        //  switch statement will automatically be exited afterward
    case "Tokyo":
        // code here will execute if city == "Tokyo";
        //  switch statement will automatically be exited afterward
    default:
        // code here will execute if city does not evaluate to any of the above cases
}

Encapsulating logic with functions

To improve code readability and maintainability, we identify parts of our code that perform a specific action and can be reused. These include:

  • Greeting the user
  • Gathering user input
  • Validating user input
  • Booking tickets for a user based on their input
  • Outputting the first names of all currently booked attendees

A function declaration without parameters uses the syntax:

1
2
3
func functionName() {
    // code here will be executed when the function is called
} 

When required, function parameters can be defined between the parentheses that immediately follow a function’s name in its declaration:

1
2
3
func functionName(param1 type, param2 type, param3 type, ...) {
    // code here will be executed when the function is called
} 

Function return values can be declared to the right of the a function’s parameter list (parentheses), by specifying the data type of the value to be returned. Inside the function’s body, the keyword return instructs Go to return those values when the function is called.

1
2
3
4
func functionName() type {
    // code here will be executed when the function is called
    return /*expression resulting in a value of the type specified*/
} 

Return values can also be named as part of the function declaration. Named return values can directly be assigned values within the function as if they were variables. Note that Go functions have the ability to return multiple values.

1
2
3
4
5
6
7
func functionName() (retVal1 type, retVal2 type, retVal3 type, ...) {
    // code here will be executed when the function is called
    retVal1 = /*expression resulting in a value of the type specified*/
    retVal2 = /*expression resulting in a value of the type specified*/
    retVal3 = /*expression resulting in a value of the type specified*/
    return // returns all of the named values
} 

Booking app Example 3

Below is an example of the booking app after it has been broken into a number of functions. This (ideally) makes the main function more readable and the overall program easier to maintain.

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
package main

import (
    "fmt"
    "strings"
)

// All functions in this package can access the two values below directly by name
// (because they are declared outside of any function).
const conferenceName string = "Go Conference"
const conferenceTickets uint = 50

func main() {
    // The two values below cannot be accessed directly by other functions in this
    // program. Values are passed as needed between functions using arguments
    // and return values.
    var remainingTicketCount uint = 50
    bookedFullNames := []string{}

    for { // infinite loop
        var ticketsAreAvailable bool = remainingTicketCount != 0
        var noTicketsAreAvailable bool = !ticketsAreAvailable

        greetUser(remainingTicketCount, ticketsAreAvailable)

        if noTicketsAreAvailable {
            break // exits current loop (the infinite for loop)
        }

        // The two booking functions called immediately below take advantage
        // of Go functions' ability to return multiple values.
        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 { // all booking info is valid
            // Book user's tickets and update the remaining ticket count and attendee list
            remainingTicketCount, bookedFullNames = bookUserTickets(firstName, lastName, email, quantity, remainingTicketCount, bookedFullNames)
        }
    }
} // This is the end of the main() function

// Below this point, all of the new functions are declared

func greetUser(remainingTicketCount uint, ticketsAreAvailable bool) {
    fmt.Println("***") // visual separator between transactions
    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) // user types input

    fmt.Println("Enter your last name:")
    fmt.Scan(&lastName) // user types input

    fmt.Println("Enter your email address:")
    fmt.Scan(&email) // user types input

    fmt.Println("How many tickets are you purchasing?:")
    fmt.Scan(&quantity) // user types input

    return firstName, lastName, email, quantity
}

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
}

func bookUserTickets(firstName, lastName, email string, quantity, remainingTicketCountIn uint, bookedFullNamesIn []string) (remainingTicketCountOut uint, bookedFullNamesOut []string) {
    bookedFullNamesOut = append(bookedFullNamesIn, firstName+" "+lastName)
    remainingTicketCountOut = remainingTicketCountIn - quantity

    // provide confirmation to user
    fmt.Println()
    fmt.Printf("Thank you %v %v for booking %v tickets.\nYou will receive a confirmation at %v.\n", firstName, lastName, quantity, email)
    firstNames := getFirstNames(bookedFullNamesOut)
    printAttendeeFirstNames(firstNames)
    fmt.Println()

    return // returns updated remaining ticket count and attendees list for this conference
}

func getFirstNames(bookedFullNames []string) (firstNames []string) {
    firstNames = []string{} // declare a new slice which will collect only attendees' first names
    // the for-range syntax is covered at https://go.dev/tour/moretypes/16
    for _, v := range bookedFullNames { // index argument omitted because not used in loop
        var names = strings.Fields(v)
        firstNames = append(firstNames, names[0])
    }

    return
}

func printAttendeeFirstNames(names []string) {
    fmt.Printf("These are our current attendees: %v\n", names)
}

The program above contains six new functions in addition to the main function, with each of the new functions written to perform a specific task.

When creating functions, taking a moment to ensure descriptive variable and function names should go a long way toward keeping the overall program readable and maintainable.

This blog post ended up growing longer than originally planned, so the remainder of the Go language topics will be covered in separate posts instead. The upcoming Go topics are:

  • Packaging
  • Scope rules
  • Maps
  • Structs
  • Goroutines / concurrency

See you in the next post!

<< Back to 90 Days of DevOps posts

<<< Back to all posts

This post is licensed under CC BY-NC-SA 4.0 by the author.