Go 101

Wifi

Network:

Password:

HackerYou

Thisishackeryou

While you wait

Please sign up for a free Repl.it Account

Some additional light reading:

Welcome to Juno College

We’ve been teaching people to code since 2012, and we’ve mastered pairing the perfect students with the perfect material and learning environment. How do we know you’ll love Juno?

Don't just take our word for it.

What We're Going to Cover

The goal of this workshop is to introduce basic concepts of Go and to have students leave with a completed HTTP status checker!

Getting Started: Tools

Terminal

We generally make use of our terminal to compile and run go. In order to run and compile go, you'll have to install Go on your machine. You can follow the installation guide here to install go and start working with it locally.

Online Editor

We are going to use an online editor so that we do not have to install Go on our computers.

It can be found here: https://repl.it/languages/go

If you'd like to install Go on your computer for future use, instructions can be found here: https://golang.org/doc/install

Repl.it

We're going to use a free online Go IDE to speed up learning the language today. Let's sign up for an account!

Getting Started

As is customary, when learning a new language, let's write a "Hello World" example.


        package main

        import "fmt"

        func main() {
            fmt.Println("Hello from CrowdRiff!!!")
        }
    

Every .go file must begin with a package declaration at the top. Packages are a tool for organizing and grouping code together. Files with identical package declarations belong to the same package.

Very commonly a project will have multiple packages, each containing one or more files.

After package declaration, we can place our import statements. When we import dependencies in Go, we import by targeting entire packages, instead of individual files.

We can import from packages within our own project, or from third-party dependencies. In this first example, we're going to import from the "fmt" package, found in the standard library.

This gives us access to the Println() function, which is equivalent to console.log() in JS.

Finally, we declare a function main(). Our Go program will not execute without this function.

Unlike JavaScript, we cannot run each .go file in isolation. Instead, the main.go file is the entry-point of the entire program, and is what the Go compiler looks for each time it is instructed to run.

Let's try executing our file in the online editor.

💥Boom, you've just run your first Go program!

Types

Go is both a strong and statically typed language.

It is strong in that typing is strictly enforced. Variables must always be declared with a type, and implicit type conversions are not permitted.

It is static in that once a variable's type has been declared, it cannot change.

There are various ways to declare variables in Go:


        // Declared without a value.
        var color string
        color = "red"

        var days int
        days = 12
        
        var toBe bool
        toBe = true

        // Shorthand declarations.
        food := "pizza"
        lucky := 13
        toNotBe := false

    

If a variable is declared without a value, that variable is said to have a zero value.

Here are the zero values of our three primitive types:

When declaring a variable, we can choose between short and long-hand notations. Short-hand notations infer their type from its inital value.

Let's try some of them out now!

Data Structures

There are two common data structures we use in Go: slices and maps.

Slices are functionally the same as arrays in other languages. They are used to store one or more elements with consistent order. The only difference about slices in Go is that they must be declared with a type. Only elements of that type are eligible to be stored within that slice.


        // For example, the following will not compile.
        numbers := []int{1, 2, "three", 4, 5}

        // But this will!
        allNumbers := []int{13, 14, 15}
                        

Maps are similar to hashtables/dictionaries in other languages. They are used to store data in key-value format. Just like with slices, both the key and value types of a map must be declared.


      // Create an empty map.
      m := make(map[string]int)

      // Add an item to the map.
      m["one"] = 12
      m["two"] = 14

      fmt.Println(m["one"]) // Output: 12

      // Or create and add items to the map in one line
      n := map[string]int{"apple": 1, "orange": 2}
                                        
Let's take a crack at creating some data structures and storing information inside them.

Functions

When declaring a function, all parameters must by typed as well as their return values (Yes, you can have more than one return value!).

Calling a function is the same as most other languages, use the function name and pass in some appropriately typed arguments.


        func add(a int, b int) int {
            return a + b
        }

        func main() {
            res := add(1, 2)
            fmt.Println(res) -> Output: 3
        }
    

An example of a function with multiple return values.


        func add(a int, b int) (int, bool) {
            sum := a + b
            var isLarge bool
            if sum > 11 {
                isLarge = true
            }
            return sum, isLarge
        }

        func main() {
            sum, isLarge := add(1, 2)
            fmt.Println(sum, isLarge) // Output: 3, false
        }
    
Let's try creating our own functions from scratch.

Structs

Sometimes, primitive types aren't enough to communicate information. Go's answer to this problem is the struct. Structs allow us to define custom types that group other fields together.

To create a custom type, we start with the type declaration, followed by a list of typed fields:


        type Rectangle struct {
            Length  int
            Height  int
            Colour  string
        }
    

We now have a brand new type that we can utilize in our program, just as it if were a string or an integer.


        var r Rectangle
        fmt.Println(r)
    

We can access fields on a struct by using dot notation. This syntax will allow us to retrieve and assign values just like we would for a variable.

Note: Unpopulated fields will have zero values.


        fmt.Println(r.Color) // "" (zero value)

        sq.Color = "red" 
        fmt.Println(r.Color) // "red"

        r.height = 100
        fmt.Println(r.Height) // 100
    

Alternatively, we have the option of populating fields at the moment of instantiation. We can access the fields using dot notation, just like we did above!


        r := Rectangle{
            Color: "red",
            Height: 100,
            Length: 100,
        }

        fmt.Println(r.Color, r.Height, r.Length)
    
Let's define a struct from scratch.

Methods

Adding Methods to Structs

We can add methods to types in go like we might embed functions in objects literals in JavaScript. These function similarly to class methods in other languages and will only be available on the struct itself.

Let's add a method to the struct we just defined.

        type Rectangle struct {
            Length  int
            Height  int
            Colour  string
        }

        func (r Rectangle) Perimeter() int {
            return (r.Length * 2) + (r.Height * 2)
        }
    

We use some funny looking syntax above to add a method on the the rectangle struct. We are adding a something after the func keyword but before the method name! This is known as a receiver argument.

This small modification tells the function that it has been received by the Rectangle.

Adding this receiver argument to this struct allows us to access all fields on the struct just as if it were passed in as a parameter.

Now let's make use of this method in our code.


        type Rectangle struct {
            Length  int
            Height  int
            Colour  string
        }

        func (r Rectangle) Perimeter() int {
            return (r.Length * 2) + (r.Height * 2)
        }

        func main() {
            r := Rectangle {
                Length: 3,
                Height: 5,
                Colour: "red",
            }

            p := r.Perimeter()
            fmt.Println(p); // 16
        }
    

What if we wanted to write a method on a struct that mutated itself?

Let's try writing a method that changes the dimensions of our rectangle.

        type Rectangle struct {
            Length  int
            Height  int
            Colour  string
        }

        // IncHeight increases the height of the rectangle.
        func (r Rectangle) IncHeight(n int) {
            r.Height = r.Height + n
        }

        func main() {
            r := Rectangle {
                Length: 50,
                Height: 100,
                Colour: "red",
            }

            r.IncHeight(3)
            fmt.Println(r.Height); // Output: 100
        }
    

You'll see that we aren't able to mutate the original struct from within the method 😱. In Go, everything is passed by value. This means that variables will never be mutated from within a function unless they are given permission explicitly.

In order to simulate the behaviour of passing variables by reference, we need to utilize pointers.

Pointers

Working with Pointers

Whenever you declare a variable, its value is stored within some arbitrary cell of memory. Pointers give us the ability to access a variable through its memory address.

We can create variables that are of "pointer" type by using the * symbol.


        // 'p' is of type 'pointer to an integer'.
        var p *int

        // Create an integer.
        n := 42

        // This will not compile.
        // 'n' is not a pointer to an integer.
        p = n
    

If assigning standard values to 'p' will not work, how can we satisfy this type requirement?

If we defined pointers as variables that point to memory addresses, we need some way of finding the address of a variable. To accomplish this, we use the & symbol.

Placing the & before any variable will change it from a value to an address where it is stored. Let's see an example below.


        n := 42
        fmt.Println(n) // Output: 42
        fmt.Println(&n) // Output: 0xc000018038
                            

Simply adding an ampersand lets us access the memory address of any variable. Let's go back to our original example and see how we can apply this.


        var p *int

        n := 42

        // 'p' now holds the memory address of 'n'
        p = &n
        fmt.Println(p) // Output: 0xc000018038
    

If we can access the address of a variable, the reverse is also true. We can access the value stored at a specific address.


        var p *int

        n := 42

        p = &n
        fmt.Println(p) // Output: 0xc000018038
        fmt.Println(*p) // Output: 42
    

Here we use the * symbol again. Placing it before a variable is known as dereferencing, giving us the value for any given address.

But why would we want to use a pointer? Because p is a pointer to n, when we change n the value of p also changes. Pointers are very useful for enabling the mutation of state within variables that would otherwise be out of scope.

Let's play around with pointers.

Pointers, Methods, and Mutations

Now let's jump back to how we can make use of pointers in structs and methods! We weren't able to update the values of a struct previously because we were passing the receiver argument by its value - so we were simply mutating a copy, instead of the original.

Let's try changing those rectangle dimensions one more time.

        type Rectangle struct {
            Length  int
            Height  int
            Colour  string
        }

        // Now we add * to the receiver.
        func (r *Rectangle) IncHeight(n int) {
            r.height = r.height + int
        }

        func main() {
            r := rectangle {
                Length: 50,
                Height: 100,
                Colour: "red",
            }

            sq.IncHeight(300)
            fmt.Println(r.Height); // 400
        }
    

By changing the receiver to a pointer receiver we can alter the underlying struct from within its methods.

HTTP Status Checker

We're now going to take everything we've learned and apply it in the form of a HTTP status checker.

In this single exercise, we're going to utilize slices, iterations, maps, structs, functions, methods, http requests, and error handling.

The goal here is to accomplish the following:

Let's get to it!

Concurrency

One of Go's strengths is the ability to write programs that support running operations in parallel. It does this easily as a result of having built-in support for multithreading.

Unlike Node.js and its event loop, Go utilizes Goroutines (a play on the word co-routines) to perform asynchrounous operations on multiple threads. Operations, in this case, are functions!

In order to run a function asynchronously, we can simply add the word: go before any function.

Below is an example of a function being run synchronously.


        package main

        import (
            "fmt"
            "time"
        )

        func main() {
            for i := 1; i <= 10; i++ {
                sleepAndPrint(i)
            }
            // We expect this Go program to
            // take ~10 seconds to print out
            // all 10 numbers.
        }

        func sleepAndPrint(number int) {
            time.Sleep(1 * time.Second)
            fmt.Println(number)
        }
        

Let's see if we can increase the performance of this program by leveraging Goroutines.


        package main

        import (
            "fmt"
            "time"
        )

        func main() {
            for i := 1; i <= 10; i++ {
                go sleepAndPrint(i) // Added 'go'.
            }
            // This program should now take ~1 second
            // to finish printing all 10 numbers.

            // This sleep simulates the behavior
            // of Promise.All().
            time.Sleep(5 * time.Second)
        }

        func sleepAndPrint(number int) {
            time.Sleep(1 * time.Second)
            fmt.Println(number)
        }
                

We can now go back to our HTTP status checker example to see if we can improve the performance there.

Let's make some Goroutines!

Exercise Solutions

For your reference, below you'll find a list of completed exercises you've done:

Extra Resources