Nathan Wailes - Blog - GitHub - LinkedIn - Patreon - Reddit - Stack Overflow - Twitter - YouTube
Go (Programming Language)
Random things I'm learning
- Apparently Go devs don't like people calling the language "Golang". They want it called "Go".
- "There are no reference types in Go."
Reviews of the language
- 2015.04.21 - Evan Miller - Four Days of Go
- 2017.08.22 - Michal Konarski - 5 things about programming I learned with Go
- HN discussion
- It is possible to have both dynamic-like syntax and static safety
- Go is not an object-oriented language. But it does have interfaces. And they are pretty much the same as these you can find in Java or C++. They have names and define a set of function signatures.
- Then we have Go’s equivalent of classes - structs. Structs are simple things that bundle together attributes.
- We can add a function to a struct.
- Go uses a concept called “automatic interface implementation”. A struct containing all methods defined in the interface automatically fulfills it. There is no implements keyword. Isn’t that cool? A friend of mine even likes to call it “a statically typed duck typing”. Thanks to that feature and type inference that allows us to omit the type of a variable while defining, we can feel like we’re working in a dynamically typed language. But here we get the safety of a typed system too.
- It’s better to compose than inherit
- if we want to mitigate the risk of getting lost inside the dark forest of code complexity we need to avoid inheritance and prefer composition instead.
- Go doesn’t support inheritance at all.
- There is a feature called embedding. Every method in an embedded interface is accessible directly on the struct that the interface is embedded in.
- Channels and goroutines are powerful way to solve problems involving concurrency
- Don’t communicate by sharing memory, share memory by communicating.
- Instead of using locks to control access to a shared resource, Go coders can simply use channels to pass around its pointer. Then only a goroutine that holds the pointer can use it and make modifications to the shared structure.
- There is nothing exceptional in exceptions
- there is nothing exceptional in exceptions. They are usually just one of possible return values from a function....it’s good to think about exceptions like they were regular return values. Don’t pretend that they just won’t occur.
- f, err := os.Open("filename.ext")
- Quora - Why did Google create the Go language? Isn't Python good enough?
"Python is a dynamically typed language, and as such it can present, er, challenges for working on large programs, in large teams. A quick example, is that if you make a function in Python to call, and call it from a few places, you’ll find that if you change the number of parameters, or types of parameters that the function takes, there is no compile time error, only a runtime error. Now that’s no big deal on a small program of only a few hundred or even few thousand lines, but once you go up to hundreds of thousands of lines, or millions of lines, working with hundreds of other people, it’s a major problem.Statically typed language like Go turn that runtime error into a compile time error, and it’ll point out each and every time that function is called with the wrong parameters. This is a huge difference, when working on medium or large scale projects."
"Python is very good at making programming simple and easy, but it suffers a performance hit compared to compiled languages. Compiled languages such as C or C++ are very fast, but they're not as simple and easy to use as Python. Go aims to be almost as easy to use as Python while being compiled, and almost as fast as traditional compiled languages. It's also very good at concurrency by design, which is its main strength."
- Go FAQ - Why are you creating a new language?
- We wanted the ease of programming of an interpreted, dynamically typed language...
- ...and the efficiency and safety of a statically typed, compiled language.
- Finally, working with Go is intended to be fast: it should take at most a few seconds to build a large executable on a single computer.
Cool Go projects
Libraries / frameworks
- Just look through this: https://github.com/avelino/awesome-go
Learning resources
- Official Go Tour
- Official 'How to Write Go Code' tutorial
- Official 'Effective Go' tutorial
- Udemy - A search for "golang"
- Looks like there are a couple of good options...
- Learn X in Y Minutes - Go
- How I Start - Go
- 50 Shades of Go
Misc "Go for Python developers" links
- Go vs. Python for Senior Developers
- This is great.
- Summary:
- Similarities to Python:
- A large standard library.
- Automatic memory management.
- A huge ecosystem (libraries, frameworks, IDEs, and other tooling).
- Differences to Python:
- Go is a compiled language
- Better performance
- Easier deployment and cross-compilation
- Go is statically-typed
- Go supports call by reference (via pointers)
- "Python is call-by-reference for dicts/lists/objects but doesn't support call-by-reference for primitives." But so what? I haven't run into a situation where that mattered.
- Go doesn't have classes or "class implements interface" syntax
- Go (officially) does not support sub-classing / inheritance
- Go does not have exceptions
- Go uses
defer
instead oftry-finally
- It's just different syntax. - Go has decentralized dependency management
- import statements in Go typically look like this:
import "github.com/some-user-or-org/some-go-module/some-go-package-that-you-need"
- The equivalent to Python’s
requirements.txt
is Go’sgo.mod
file.
- import statements in Go typically look like this:
- Go has excellent support for concurrency
- "Goroutines are a bit like threads, but much more efficient"
- "Python’s support for concurrency is abysmal. While you can have multiple threads, they won’t make proper use of your CPU cores (due to the Global Interpreter Lock). If you want to use multiple CPU cores, you must fork extra processes."
- Go comes with built-in, opinionated formatting
- Similarities to Python:
- https://golang-for-python-programmers.readthedocs.io/en/latest/
- This seems unfinished(?)
Go: The Complete Developers Guide
- https://www.udemy.com/go-the-complete-developers-guide/learn/v4/content
- This was the highest-rated intro to Go on Udemy.
- I liked it, I would recommend it to anyone who wants to learn Go.
- I think the main thing I found less-than-ideal about it was that I'm already an experienced Python developer and so I found the pace slower than it needed to be for me. But I can see how this pace would be perfect for someone who isn't as familiar with programming. So basically it doesn't leave anyone behind. But I feel like I would've benefited from seeing him digging through some more-complicated programs, like the Axis & Allies simulator, to get a sense of how to work with stuff like that.
Summary of key ideas from the course I want to remember
- An executable package (in contrast to a non-executable library) must be named
main
and have a function namedmain
. Hello World:
package main import "fmt" func main() { fmt.Println("Hi there!") }
- Use
go run <files-to-compile>
to run your code. - The
:=
syntax is an abbreviated syntax used when defining a variable for the first time; it has the variable infer its type from its initial value (which must be assigned on the same line). - Instead of using the Python syntax for creating classes, you create types (which have no inherent methods attached to them), and then to create methods you create receiver functions (functions which only accept the new type as one of their arguments).
- Example type declaration:
type deck []string
- Example receiver declaration:
func (d deck) print(<args go here>) { <code goes here> }
- Note how the receiver part is in front of the function name and separate from the parameter list.
- Example receiver usage:
cards_variable.print()
- Example type declaration:
- A lot of learning Go is learning how the various packages in the standard library work.
- Go has arrays (fixed-length) and slices (variable length, similar to Python lists):
- Create an empty array:
cards := [<size>]string
- Create an array with initial values:
cards := [<size>]string{<put-initial-contents-here-if-you-want>}
- Create an array with initial values:
- To create a slice just omit the <size>:
cards := []string{<put-initial-contents-here-if-you-want>}
- Append to a slice:
cards = append(cards, newElement)
- Append to a slice:
- Selecting a subarray/subslice works just like Python.
- The code
[]byte
is read as "slice byte", not "array of bytes". - Arrays are rarely used directly; slices are used 99% of the time for lists of items.
- Create an empty array:
- For loop syntax:
for i, card := range cards { <do-stuff> }
- If you're not going to use the iterator replace its name with an underscore:
for _, card
- If you're not going to use the iterator replace its name with an underscore:
- Unlike Python, you don't need to add import statements for stuff that's defined in the same package but in separate files.
- To convert between types, write the type you want and put the object to convert in parentheses after it:
[]byte("Hello World!")
- This is similar to Python, where you have str(), int(), class constructors like MyClass(), etc.
When importing multiple packages in a file, use this syntax:
import ( "package name one in quotes" "package name two in quotes" )
- Golang uses
nil
, notnull
orNone
. - You can do
Println(err)
to print an error. - To immediately quit a program we can call the
os
package functionExit()
with an argument of1
which indicates that something went wrong. - We can use
len()
to get the length of a slice. - Random number generation is kind of complicated in Go.
- Testing:
- Make a file that ends in
_test.go
and then rungo test
- The first argument to every test function should be
t *testing.T
- Call
t.Errorf("Description of problem")
to notify the test handler of a failure.
- Make a file that ends in
- Structs are like dicts or named tuples or classes-without-methods in Python: a collection of attributes describing a single thing. The values can be changed afterwards, unlike named tuples. The fields can't be added to afterwards, unlike a dict.
- Pointers:
- Go is a "pass-by-value" language by default. It will copy the object and pass the copy to a function.
- To create a pointer:
myStructMemoryAddress := &myStruct
- To dereference a pointer:
myStruct := *myStructMemoryAddress
- A potentially very confusing thing: When you see * in a type description, like in a list of function parameters, like *person, it isn't being used as an operator. So the variable name you'll see to the left of the type description is still just a pointer instead of a dereferenced pointer.
- If we define a receiver function of type pointer-to-X, Go will allow us to call it either with a pointer or with the referenced type itself. Basically letting us skip defining a pointer variable when calling the function if we want the function to modify the type instance it's called on.
- Slices and other "reference types" are tricky because the slice/etc. instance itself is a different thing in memory from the underlying array. So if you don't want a function to modify the original, you need to manually make a copy before sending that to the function.
- Maps:
- They're like dicts in Python, except:
- The keys and values must all be of the same type within their respective set.
- Defining a map:
my_map := map[string]string { }
- Iterating over a map:
for keyVar, valueVar := range myMap {
- Interfaces
- Interfaces are a way of writing functions that can be used by multiple different types.
Example interface:
type myInterface interface { myRequiredFunction(string) string }
- Any type that has all of the functions specified in the interface definition (including exactly matching the input types and return types) will thereby be treated by the Go compiler as a member of the interface type as well, and be able to use any functions defined for that interface type.
- In Go, interface types can't ever be directly instantiated, whereas "concrete types" are those that can be.
- The Reader interface is a common interface in Go; it lets us take input from any of various sources as a slice of bytes.
- He goes through the line of reasoning that would lead us to understand that
os.Stdout
would work as aWriter
type:os.Stdout
is of typeFile
, and typeFile
has a function of typeWrite
, and since we know that any type that implements that function (including the required parameter types and return types) is of typeWriter
, we therefore know thatos.Stdout
would be accepted byio.Copy()
. He describes knowing how to do this as "hard-won knowledge". - Go routines & channels
- They're like Python threads except they can be assigned to multiple cores like when doing multiprocessing. So basically they have the ease-of-communication of threads but the speed of multiple cores. So they're really fast.
- To run a function as a Go routine just add the
go
keyword before the call:go doMyThing()
- Channels are a way to communicate between Go routines; think of them as a messaging system.
- To create a channel:
c := make(chan string)
- Channels need to specify the type of the messages they'll be handling.
- To push or pop a message to/from a channel, use the
<-
operator:myNumber <- myChannel
- Receiving a message from a channel is a blocking operation (the program will wait until a message arrives).
- Function literals are equivalent to anonymous functions in JavaScript or lambda functions in Python and are straightforward to define:
func() { myCodeGoesHere() }
Section 1: Getting Started
How to Get Help
- No video, just text.
- There are three ways to get help:
- Post in the Q&A discussion
- Email me: <email address redacted>
- Message me on Udemy.
Link to Completed Code
- No video, just text.
- You can find the completed version of all the examples in this course here: https://github.com/StephenGrider/GoCasts
Environmental Setup
- We are going to install Go, then we'll install VSCode (an IDE), then we'll configure VSCode, then we'll write some code.
- He highly recommends trying VSCode because it has the best integration with Go.
- To install Go, go to golang.org/dl
- Just keep hitting "Next" for the installer, there's nothing we need to configure.
VSCode Installation
- To verify Go was installed correctly, start a terminal / command prompt window and type "go" and hit Enter. You should see a help message show up on the screen.
- To install VSCode, go to code.visualstudio.com
Go Support in VSCode
- VSCode doesn't start out with any support for Go.
- To add this support, go to View → Extensions, and search for "go".
- Download the extension with the title "Go", the description "Rich Go language support..." and over 1 million downloads.
- Restart VSC.
- Make sure you have a code editor window open (do File → New File if you don't).
- In the bottom right corner, click "Plaintext" and switch it to "Go", and in the same place where "Plaintext" showed up you'll see an error saying "Analysis tools missing". Click the error message and then click "Install" on the dialogs that pop up.
Section 2: A Simple Start
Boring Ol' Hello World
- We're going to start with a simple "Hello World" example in this video but in the next few videos we're going to do a deep dive on how Go is working behind the scenes.
- He goes to File → Open and creates a new folder named "helloworld" to house this project.
- NW: In my editor I didn't have an "Open" option, just "Open File" and "Open Folder", so I chose the latter.
- You should see the folder show up in the left sidebar (the "Explorer tab").
- He clicks a little "Add file" button next to the name of his project in the Explorer tab and names it "main.go".
- He writes out the code and says we'll discuss each line in-depth in the next few videos:
package main import "fmt" func main() { fmt.Println("Hi there!") }
- He says you must use double-quotes, not single quotes, in both the import statement and the Println statement.
Five Important Questions
- He studied the Hello World program ahead of time and came up with five basic questions we can ask that will give us a good sense of what the program is doing:
- How do we run the code in our project?
- What does 'package main' mean?
- What does 'import "fmt"' mean?
- What's that 'func' thing?
- How is the main.go file organized?
- He then starts answering question 1.
- He switches to a terminal navigated to his project folder.
- He runs
go run main.go
- NW: When I tried this I ran into a minor problem in that my code had not been auto-saved like it is in PyCharm, so it didn't work until I figured out that that was happening.
- He shows several other Golang CLI commands that we'll be using:
go run
is used to compile and immediately execute one or two files, without generating an executable file.go build
compiles, but does not execute, go files. It will generate an executable file.go fmt
formats all of the code in each file in the current directory.go install
compiles and 'installs' a package.go get
downloads the raw source code of someone else's package.go test
runs any tests associated with the current project.
Go Packages
- We're going to talk about what "package" means and why we chose the name "main" for the package.
- What a package is:
- A package can be thought of as a project or workspace.
- A package can have many Go source code files in it.
- Every file that belongs to a package must have its first line declare the package it is a part of.
- As for why we chose the name "main" for the package:
- There are two types of packages: executable packages and reusable packages. Executable packages generate an executable we can run. Reusable packages are used as helpers / libraries.
- The name of the package that you use determines whether you're creating an executable or reusable package. In particular, the word "main" for a package is what designates a package as executable. Any other name for a package will create a reusable package.
- An executable package must have a function called
main
.
Import Statements
- The import statement is used to give our package access to code contained in another package.
- "fmt" is a standard library included with Go. It stands for "format". It's used for printing out information.
- We can import both standard packages that are included with Go as well as packages that are not official and have been authored by other individuals.
- We can learn more about the standard packages by going to golang.org/pkg
- We are going to be looking at these official docs a lot, because a lot of learning Go is learning how these standard packages work.
File Organization
- The
func
in our hello world program is short for "function". Functions in Go work just like functions in other languages. - The way a file is organized always follows the same pattern:
- At the very top we have a "package ..." declaration.
- Then we'll import other packages we need.
- Then we'll declare functions.
How to Access Course Diagrams
- No video, just text.
How to use the course diagrams:
Go to https://github.com/StephenGrider/GoCasts/tree/master/diagrams
Open the folder containing the set of diagrams you want to edit
Click on the ‘.xml’ file
Click the ‘raw’ button
Copy the URL
Go to https://www.draw.io/
On the ‘Save Diagrams To…’ window click ‘Decide later’ at the bottom
Click ‘File’ -> ‘Import From’ -> ‘URL’
Paste the link to the XML file
Section 3: Deeper Into Go
Project Overview
- We're going to create a
Cards
package that simulates a deck of playing cards. - We're going to create the following functions:
- newDecks - Create a list of playing cards, just an array of strings.
- shuffle
- deal - I'll get one list for the 'hand' and a second one for the remaining cards.
- saveToFile
- newDeckFromFile
New Project Folder
- Create a new folder named
cards
. - Create a
main.go
file. Add some boilerplate code:
package main func main() { }
Variable Declarations
- We're going to play with Go a bit first to get a sense of the language.
- I want to create a variable in our
main()
function and assign it a string that represents a playing card, and then print out that string.Add the following code to
main()
:var card string = "Ace of Spades"fmt.Println(card)
var
is short for "variable" and lets Go know that we're going to create a new variable.card
is the name of the variable.string
is telling the Go compiler that only a value of typestring
will ever be assigned to this variable."Ace of Spades"
is the initial value we want to assign to the variable.
- Golang is a "statically typed language", like C++ and Java, and in contrast to "dynamically typed languages" like JavaScript, Ruby, and Python.
- He opens the Chrome console to show how JavaScript will let you assign a number to a variable and then later assign a string to the same variable.
- Some basic Go types:
bool
,string
,int
,float64
- The
:=
syntax:- He points out that the IDE shows a warning letting you know that you don't need to include
string
in the variable definition if you're assigning an initial value of that type. - An alternative way to define the variable is
card := "Ace of Spades"
- We only use the
:=
syntax when assigning a new variable. If we're assigning a new value to an existing variable, we don't include the colon. - If you accidentally use
:=
a second time, you'll get an error message.
- He points out that the IDE shows a warning letting you know that you don't need to include
Functions and Return Types
He starts with the following code:
package main import "fmt" func main() { card := "Ace of Spades" fmt.Println(card) }
- He wants to modify this code to create a new function that will return the value of the card to be created, instead of just assigning the value directly in
main()
. - To return a value, just use
return <value>.
He modifies the code to look like this:
package main import "fmt" func main() { card := newCard() fmt.Println(card) } func newCard() { return "Five of Diamonds" }
- He gets an error on the return statement saying
too many arguments to return. have (string), want ()
. - This error is happening because our function definition is saying we don't expect this function to return anything, but we are returning something (a string).
- To fix the error we update the function definition to read:
func newCard() string {
, adding the wordstring
. - He tries changing the
string
in the function definition toint
, and shows how that pops up an error on thereturn
line letting you know you're returning the wrong type of value.
Slices and For Loops
- Go has two basic data structures for handling lists of records: Array and Slice.
- an array is a fixed-length list of things.
- a slice is an array that can grow or shrink, like a Python list.
- Both arrays and slices must be defined with a single particular data type; every element must be of the same type.
- To declare a slice, we use the following syntax:
cards := []string{}
- Note he doesn't write
strings
(plural). - You can place whatever initial records you want to include in the curly braces:
{"Ace of Diamonds", newCard()}
- Note he doesn't write
- To add a new value to a slice:
cards = append(cards, newElement)
- This returns a new slice. It doesn't modify the existing slice.
To iterate over a slice use
range
:for i, card := range cards { fmt.Println(i, card)}
- We use the
:=
syntax for defining the variables (which we earlier saw should only be used the first time you define a variable) because Go "throws away" the variables at the end of every loop.
- We use the
- In the section "Reference vs Value Types" later in the course he writes that arrays are rarely used directly; slices are used "99% of the time for lists of elements".
OO Approach vs. Go Approach
- We're now going to start on our
Cards
project in earnest. - We're going to see how we might create this project in an OO language and then see how we do it in Go.
- Go is not an object-oriented programming language. There's no idea of "classes" in Go.
- In an OO language like Python we would define a
Deck
class and use it to create aDeck
instance variable. The instance would have deck-related methods attached to it. - In Go what we do instead is create a new type:
type deck []string
, and then to create deck-related functions we create "functions with a receiver" that only work with that new type. We'll talk more about this later. - We're going to aim to have the following file structure in our package:
main.go
- Contains the code used to create and manipulate a deck.deck.go
- Contains the code that describes what a deck is and how it works.deck_test.go
- Contains code that automatically tests the deck.
Custom Type Declarations
- In
deck.go
we'll create the new type:type deck []string
- This is telling Go that the identifier
deck
in our code should be treated as being exactly the same thing as if it saw[]string
(a slice of typestring
).
- This is telling Go that the identifier
- Note: we don't use the terms "extends" or "subclass" in Go.
- We can now replace
[]string
where it appears in ourmain.go
file withdeck
. - When we run the program now, we need to include
deck.go
:go run main.go deck.go
To define a receiver function, use this syntax:
func (d deck) print() {}
- To use the defined receiver function, use it as if it was a method in Python:
cards.print()
Receiver Functions
- When we define a receiver function, any variable in our package of the specified type will get access to that function. So basically it works like a Python class method.
- The receiver syntax "(d deck)" has two parts: a variable name and a type. So in our example, "d" is a variable name we'll be able to use in our method, and "deck" is the required type of that variable.
- You can think of the receiver variable as being very similar to the word "this" or "self" in JavaScript and Python.
- By convention in Go we never use the word "this" or "self"; we always pick a variable name that refers to the actual object.
- By convention in Go we always use a one or two letter abbreviation as the receiver variable name.
- Personally he's not convinced it's a great idea to use one or two letter variables names as it could be confusing, but he follows the convention.
- Officially in Go we never use the terms "class" and "method" even though you can think about this way of defining types with receivers as being similar to those.
Creating a New Deck
- We're going to implement the newDeck function.
- We need to annotate the function definition with the type that the function returns, we put it after the param list: func newDeck() deck { }
- We're not going to add a receiver because we'll want to call this function when we don't already have a deck object to work with.
- Each card is going to be a string of the form "<Value> of <Suit>", like "Ace of Spades".
- To create the cards we're going to create
cardSuits
andcardValues
slices and then use two for-loops to create all the different combinations. - This code should look very familiar if you've programmed before. He's seen some people feel intimidated when switching to Go but the language honestly doesn't have a lot of special features. You can put together good programs without a lot of study of the language.
- We don't use the iterator variables in our for loops and so we see a warning in the IDE. By convention in Go we replace the names of unused variable definitions in for loops with underscores.
Slice Range Syntax
- We're now going to implement the
deal
function. It's going to return two decks: a "hand" deck and a "remaining" deck. - Selecting a subset from a slice works just like Python:
- it's zero indexed
- the second index is not included in the result. So
[0:2
]
is the first two elements. - We can leave off either of the two numbers.
Multiple Return Values
- He spends a few minutes repeating information from previous lessons.
- To declare that a function returns multiple values, wrap them in parentheses:
func deal(d deck, handSize int) (deck, deck) { }
- Unlike Python, you don't need to add import statements for stuff that's defined in the same package but in separate files.
- Receiving multiple values from a function call works just like in Python: just have comma-separated variable names:
hand, remainingDeck := deal(cards, 5)
Byte Slices
- Now we're going to implement
saveToFile()
. - We're going to use the
WriteFile
function in theioutil
package to write to the hard drive. - He uses the word "slice byte" when the code says
[]byte
which makes me wonder how people distinguish between an array and a slice in function definitions. - Whenever you head "slice byte" he wants you to think "string of characters".
Deck to String
- We need to convert our card strings to bytes to be able to use the
WriteFile
function. - To do this conversion we'll do "type conversion". We just write the type we want, add parentheses, and then put the object we want to convert in the parentheses:
[]byte("Hello World!")
- To convert our deck to a slice of bytes, we're going to take our deck, which is a slice of strings, and join the elements into a single string, and then convert that single string into a slice of bytes.
- We're going to make a helper function
toString
that turns a deck into a string. We will make it a receiver function.
Joining a Slice of Strings
- In our
toString
function we first convert thedeck
object to a slice of string:[]string(d)
- To join the elements of the slice of strings, we're going to use the
strings
default package, specifically theJoin
function. To import multiple packages we use this syntax:
import ( "package name one in quotes" "package name two in quotes" )
Saving Data to the Hard Drive
- We're now going to implement a
saveToFile()
receiver function. - We'll allow the user of our function to specify the
filename
thatWriteFile
takes as an argument. - We're going to say that it returns an
error
since that's something that can be returned from theWriteFile
function we're going to use. - For the
permission
parameter toWriteFile
we'll use 0666. - You can see in the import statement that
ioutil
is a subpackage of theio
package. - He runs the code and confirms that it writes a file to the hard drive.
Reading From the Hard Drive
- We're now going to implement a
newDeckFromFile()
function. - Golang uses
nil
instead ofnull
orNone
. - A common pattern in Go code after calling a function that can return
nil
is to add an if statement to add code to handle the case when the value isnil
. - He spends a few minutes explaining two different options for handling an error when trying to load a file: 1) return an empty deck, or 2) quit the program.
- To immediately quit the program we can call the
os
package functionExit()
with an argument of1
which indicates that something went wrong.
Error Handling
- We'll use the
Split()
function in thestrings
package to split up the comma-joined list of cards that we load from the file:s = strings.Split(string(bs), ",")
- He converts the list of string to a deck with
deck(s)
- He tries running
newDeckFromFile()
and confirms it works.
Shuffling a Deck
- Golang doesn't have any built-in function for shuffling a slice.
- To implement our own shuffle function, we will iterate over the length of the deck, and for each index we'll generate a random different index in the deck and swap the two cards at those indices.
- We can use the
Intn()
function in therand
subpackage of themath
package to generate random numbers. - We'll make the
shuffle()
function a receiver function that modifies the deck we call it from. - We can use
len()
to get the length of a slice. - We can do a one-line swap like this:
d[i], d[newPosition] = d[newPosition], d[i]
- One weird thing about our initial implementation of the
shuffle()
function is that the last four elements in our deck are always the same.
Random Number Generation
- The reason we're getting the same random results every time is that by default, the Golang random number generator always uses the same seed.
- It's kind of hard to figure out how to change the seed just by reading the docs.
- Let's take a look at the type
Rand
in the docs. It's "a source of random numbers". When we create an object of this type with theNew()
function, we must specify a source (which is of typeSource
). - A
Source
is "a source of uniformly-distributed pseudo-random int64 values in the range 0-2^62". - To create a new
Source
we need to pass in a randomly-chosenint64
. - Again, a lot of learning Go is just learning to navigate the documentation around the standard packages.
- To generate an int64 that'll be different every time, we'll use the current time with
UnixNano()
in thetime
package:time.Now().UnixNano()
Creating a go.mod file
- To run tests we need a
.mod
file. To create one, rungo mod init cards
Testing With Go
- Go testing is not like using testing frameworks in other languages. Go has a very limited number of functions to help us test our code.
- To make a test in Go, make a file that ends in
_test.go
- To run all tests in a package run
go test
- In your test file remember to start it with
package <your-package-name>
- VS Code will detect that you've created a test file and will put links at the top of the file to run all the tests in the package or just the tests in that file.
Writing Useful Tests
- A common question in testing is: how do we know what to test? With Go this ends up being straightforward to answer.
- Basically you want to try to find some easy assertions that should be true of the output of your functions and that capture attributes of the output you care about.
- For the
newDeck
function there are three things he can think of:- The returned deck should have the expected number of items.
- The first card of the deck should be the expected first card.
- The last card should be the expected last card.
- For each file we want to test we create a
_test.go
file, and for each function in that file that we want to test, we create a function namedTest<YourFunctionName>
. - But we don't have to have one test function for each of our functions. For example, we can test both saving a deck to a file and reading a deck from a file in a single test function:
TestSaveToDeckAndNewDeckFromFile
- Our tests can often follow a similar pattern:
- Get the return value from the function we're testing.
- Have various
if
statements to check the output. - If we find a problem, tell the test handler that something went wrong.
- The first (and only?) argument to a test function should be
t *testing.T
which is our test handler. - To notify our test handler that something went wrong, use
t.Errorf("Write a description of the problem here")
- To include variables in the error description, use string formatting with the percentage sign:
("Your message: %v", len(d))
Asserting Elements in a Slice
- He just implements the checks he talked about in the last lesson:
if d[0] != "Ace of Spades" { t.Errorf("...") }
if d[len(d) - 1] != "Four of Clubs" { t.Errorf("...") }
- Unlike the test frameworks you see in other languages,
go test
doesn't know how many tests we wrote, so you won't see an output that says something like "Ran 60 tests, 5 failed".
Testing File IO
- He's now going to test our functions that save data to a file and load data from a file.
- When writing tests with Go we need to make sure we write code that will do any necessary clean-up in all cases. Go doesn't handle cleanup for us.
- In our case, we'll have our code attempt to delete the
_decktesting
file we'll be using both before running and after running. - So, how do we delete a file? Let's check the Golang standard library docs. It's the
Remove
function in theos
package. - The
Remove
function can return an error, but we can ignore that for our purposes. - Our test function will be named
TestSaveToDeckAndNewDeckFromFile
- We're using a long name because it will make it easier for us to find it in our code if it causes an error in the future. (This doesn't make any sense to me if all your tests are in one file but I could see it making sense if you have tests across multiple files and want to avoid using duplicate test names).
- He just has a single assertion, that the deck he gets from loading the saved file has the expected number of items.
- When writing a test you should make a change to the assertion to make sure that it actually fails when it should.
Project Review
- He summarizes the steps he took to get the Deck project done.
- There are some weird things about the project he wants to point out.
- For the
deal
function we didn't use a receiver. The reason is that it might create some ambiguity about what it does. If we had code that saidcards.deal(5)
it would look like we're modifying thecards
slice by removing 5 cards. - We pass in the
t *testing.T
argument to each of our tests, but we don't know yet what the*
is for. - These two things are actually at the core of the next thing he wants to talk about.
- For the
Section 4: Organizing Data With Structs
Structs in Go
- When creating our Deck class it would've been awkward to access just the suit or just the number since they're joined into a single string.
- A struct is like a collection of properties that are related.
- So it's like a JavaScript object or a Python dictionary (or really a named tuple).
- He creates a new folder called
/structs
and a newmain.go
Defining Structs
- We're going to create a simple project / demo to show how to use a struct.
- Whenever you use a struct you need to first define the type and then you can create an object of that type.
- He gives an example of a
person
struct with afirstName
field and alastName
field. - Note we don't have any colons or commas in the definition.
Declaring Structs
- We can define a new
person
object with syntax like the following:alex := person("Alex", "Anderson")
- By default, if you omit the field names Go will assume the provided arguments match the order of the struct definition.
- Personally he can't stand that Go allows this.
- The other way to create an instance of a struct is to specify the keys like in Python.
- If you
Println
the struct you'll get a list of the values.
Updating Struct Values
- Another way o create an instance is with
var alex person
- If you don't specify the values, Go will assume what it calls a "zero value": an empty string, 0, or false, depending on the type.
- This is different from JavaScript where such fields would have a value of
null
orundefined
- We can use
%+v
in ourPrintln
call to get a printout of both the keys and values of the struct. - You'll sometimes see the
var
syntax when the coders want the instantiated struct to use the default values of the struct. - To update the value in a struct we can just do
alex.firsname = 'new name'
Embedding Structs
- You can have attributes in structs that are themselves structs.
- Example: a
contactInfo
struct that is used for acontact
attribute of aperson
struct.
- Example: a
He goes through an example of creating a
person
struct and having an embedded definition of acontactInfo
struct:jim := person{ firstName: "Jim", lastName: "Party", contactInfo: contactInfo{ email: "jim@gmail.com", zipCode: 94000, }, }
- Every line within the definition must have a comma at the end.
Structs with Receiver Functions
- When writing a struct definition, you can leave off the field name and just specify the value, and the field name will be the same as the name of the type of the value.
- Example: instead of
contactInfo: contactInfo
you can just havecontactInfo
.
- Example: instead of
- This might seem like a minor thing but it'll be important later when we look at code reuse with Go.
- Defining a receiver function works the same way as when doing it for a type:
func (p person) print() { }
- He creates an
updateName(newName string)
receiver function and shows that it doesn't actually modify the object that calls theupdateName()
function. This is a segue into the next section.
Pass By Value
- This discussion is going to revolve around the concept of 'pointers'.
- Pointers in Go are relatively straightforward.
- We should talk about how RAM on your computer works: it's like a bunch of cubbies, each of which has an address.
- When we create a
person
object, the computer takes that object and puts it at a particular address (cubby) in RAM. - Go is a "pass by value" language by default. It will copy the entire object when passing it to the function being called.
Structs with Pointers
- He reiterates / summarizes what he discussed in the previous section.
- He's going to change the code we've been working on to make it pass the value of the
person
struct to theupdateName
receiver function.- The first thing is that he adds this line of code:
jimPointer := &jim
- He then changes the call to
updateName
to instead be called by the pointer:jimPointer.updateName("jimmy")
- He updates the
updateName
function definition:func (pointerToPerson *person) updateName(newFirstName string) { }
- He updates the line of code in
updateName
that changes the name:(*pointerToPerson).firstName = newFirstName
- The first thing is that he adds this line of code:
- He's going to explain what is going on with this new code in the next section.
Pointer Operations
- The
&
is an operator.&variable
means "give me the memory address that this variable is pointing at". *
is also an operator.*variable
means "give me the value that this memory address is pointing at".- A very important distinction you need to understand that can be very confusing in Go is the difference between
*
when seen in a type description vs.*
when seen on a 'normal' line of code.- He thinks this is one of the most confusing things about pointers. When you see a star in front of a type it means something completely different than when you see a star in front of an actual instance of a pointer.
- When you see
*
in a type description, like*person
, it isn't being used as an operator. So the variable name you'll see to the left of the type description is still just a pointer instead of a dereferenced pointer. - When you see
*
on a line of code like*pointerToPerson.updateName("jimmy")
then it is being used as an operator and is actively dereferencing the pointer.
- So we're working with two different kinds of variables: variables that produce an address to a particular type of data structure, and variables that produce an actual value of a particular type of data structure.
- He spends a few minutes reiterating / summarizing the above ideas.
- This is revision 1 of our understanding of pointers.
Pointer Shortcut
- With Go, if we define a receiver function of type pointer-to-X, Go will allow us to call it either with a pointer or with the referenced type itself. Basically letting us skip defining a pointer variable.
- So instead of needing to do
jimPointer := &jim; jimPointer.updateName("jimmy")
we can just keep it asjim.updateName("jimmy")
and it will work (it will pass to the receiver function by reference) as long as the receiver is typepointerToPerson *person
Gotchas With Pointers
- He creates an example of creating a string slice, passing it to a function, modifying the first element of the slice within that function, and then doing a Println of the slice outside of the function. Given what we said before about Go being pass-by-value, we would expect that the slice would not have been modified.
Reference vs. Value Types
- We rarely use arrays directly; we almost always use slices when we want to have lists of items.
- Slices actually store the items in an array behind the scenes.
- When we create a slice, Go is actually creating two separate data structures:
- the first is what we call a slice. It has a pointer to the underlying array, a capacity number, and a length number.
- The second is the underlying array.
- So what's happening when we pass a slice to a function is that Go is still behaving the same: it's still passing by value. But it's copying the slice and passing the slice by value, but the underlying array is not being copied because it's not what is being passed.
- In Go, slices aren't the only data structure that behave this way. There are "value types" and "reference types".
- Value types: int, float, string, bool, structs - You need to use a pointer to modify this value from a function.
- Reference types: slices, maps, channels, pointers, functions
- He writes "Don't worry about pointers with these". You don't need to use a pointer to modify the values from a function.
- NW: My question: What happens if you grow one of these reference types within the function to such a degree that they need to reference a new array?
- A: I asked about this here and apparently the answer is that this is a potential problem for slices, but not for other "reference types" because there's another level of separation between the underlying array and the root type, and actually there are no "reference types" in Go (see links at the top of this wiki page).
Section 5: Maps
What's a Map?
- A map is a collection of key-value pairs, like an object in JavaScript or dict in Python.
- The keys must all be of the same type.
- The values must all be of the same type.
The syntax for defining a map is
map[key_type]value_type
my_map := map[string]string { }
Manipulating Maps
To create an empty map:
var colors map[string]string or colors := make(map[string]string)
- To add a key-value pair:
colors["white"] = "#ffffff"
- You can't access keys using dot notation as with structs because the keys don't have to be strings.
- To delete an entry:
delete(mapName, keyName)
Iterating Over Maps
To iterate over a map:
for keyVar, valueVar := range myMap { }
Differences Between Maps and Structs
- Keys in a map don't need to be a string.
- All values must be of the same type in a map.
- You can't update the keys of a struct; you need to define them all in your code.
- There's no built-in way to iterate over the keys of struct like how you can with a map.
- A struct is used to represent a single thing that has multiple attributes, whereas a map is used as a collection of different things.
- We need to use pointers to update a struct from within a function, but we don't need to do that with maps. So maps are a "reference type".
- In his experience, when writing professional Go code you'll end up using structs a lot more than maps.
Section 6: Interfaces
Purpose of Interfaces
- We're going to look at the code we've written through this course and see an issue with it that interfaces help solve.
- He gives an example of the
shuffle()
receiver function we created, and how the logic in it seems fairly generic. But what if we want to use it with a slice of a different type (other than string)? Do we need to copy-paste all that code?- This is one of the problems that interfaces helps us solve.
- We're going to write a "bad version" of a program without interfaces, then update it to use interfaces.
- We're going to create a chatbot program. It'll have an
englishBot
struct and aspanishBot
struct.- Both bots are going to have a
getGreeting
receiver function that returns a greeting in the bot's language, and aprintGreeting
receiver function. - The
printGreeting
function will probably just do something likePrintln(bot.getGreeting())
- The
getGreeting
functions' implementations will be very different, but theprintGreeting
functions will have very similar logic.
- Both bots are going to have a
Problems Without Interfaces
- He creates a new project and creates the
englishBot
struct (no fields), thespanishBot
struct (no fields), andgetGreeting
receiver functions for each that return different strings. - If your receiver function isn't going to use the received object, you can omit its variable name and just specify the type:
func (englishBot) getGreeting() string { }
- Go doesn't support overloading: you can't have functions with identical names but different parameters.
Interfaces in Practice
- In this video he's going to do the refactor and then discuss what he did.
He defines the interface:
type bot interface { getGreeting() string }
He defines the generic form of
printGreeting
:func printGreeting(b bot) { fmt.Println(b.getGreeting()) }
- He has a good "plain English" explanation of what the interface definition is saying: "Hey, every type in this program, pay attention: our program now has a new type called 'bot'. If you're a type in this program with a receiver function named
getGreeting
that returns a string, you are now an honorary member of type 'bot', and you now get access to the functionprintGreeting
."- When he showed his refactor I was wondering how the connection was happening between the new interface and the existing types, and this explanation answered my question.
Rules of Interfaces
- In our interface definition we list all of the functions that we expect the matching types to have along with their expected argument types and expected return types.
- He mainly makes the point that you can specify multiple parameter types and multiple functions within an interface, all of which need to be matched exactly for a type to qualify as the interface type.
- In TypeScript we have the terms "concrete type" and "interface type". You can directly instantiate a concrete type, but you can't directly instantiate an interface type.
Extra Interface Notes
- Interfaces are not "generic types" like you'd see in some other languages. Go doesn't have support for generic types.
- Interfaces are implicit. You don't need to write code that explicitly connects an interface and the qualifying types.
- It's nice because the code is shorter, but it can make it harder to know what interfaces a given type implements.
- Interfaces are a "contract to help us manage types". But they're not some kind of guarantee that your implementation of that interface is going to be correct. If you feed the interface garbage, you'll get garbage back.
- Interfaces are tough to use and understand. At the beginning, just focus on trying to understand the standard library documentation when it says it expects an interface. Later, when you're comfortable with them, you can think about writing your own.
The HTTP Package
- Next we're going to take a look at a more-realistic example of using interfaces: working with stuff from the standard library.
- Our program will make an HTTP request to google.com and print the response to the terminal.
- He creates a new
http
directory to hold the project code, and creates amain.go
file. - He navigates to the
http
package in the official Go docs and sees how to create a GET request, and then navigates to thetype Response
section of the docs to arrive at the section dedicated to theGET
function. - You have to include the protocol (
http://)
when making a request. - He writes code that will just
Println
the response object and explains that while this would work in many other languages, it won't work in Go. - He runs the code and seems to get something like a slice of header information, but no actual HTML.
Reading the Docs
- He reads the official docs on the
Response
type and sees that the stuff we were getting in the output last time seem to be the first few fields of the type. - He sees that the
Response
type has aBody
property of typeio.ReadCloser
- He checks the docs for the
ReadCloser
type and sees that it's an interface, and that it looks weird, because it doesn't seem to have a list of functions with their required argument types and return types, but instead justReader
andCloser
- He reads the docs on the
Reader
type and sees it's an interface that needs to define aread
function that takes a slice of bytes and returns anint
and an error. - You can end up going down a rabbit hole when trying to make sense of the docs.
More Interface Syntax
- First we're going to talk about why an interface was used as a type inside a struct: what it means is that we can assign any type we want to that field that satisfies that interface.
- Next: why did we see that weird syntax in the definition of
ReadCloser
that just hadReader
andCloser
fields? It's a way for us to define an interface in terms of another; what it means is, "in order to satisfy theReadCloser
interface, you need to satisfy theReader
interface and theCloser
interface".
Interface Review
- Quick review of what interfaces are and why we care about them: (he summarizes the bot example and how interfaces allowed us to re-use the
printGreeting
function).
The Reader Interface
- You're going to see this
Reader
interface all over Go. - He gives an example of how the Reader interface is useful (and how interfaces are useful): Go programs could receive input from a variety of sources: HTTP requests, text files on the hard drive, image files, users entering information on the command line, or even data from an analog sensor plugged into the computer. We could imagine that these could all return different data types, and therefore without interfaces we'd need to have separate "print" functions for each of them.
- The Reader interface allows us to take the input from all of these different sources and get a slice of bytes as a result.
- So you can think of the Reader interface as being like an "adapter".
More on the Reader Interface
- Something that implements the
Reader
interface needs to implement aRead
function. - The request body has already implemented this for us.
- The way the
Read
function works is that the code that wants to callReaderObject.Read()
passes it a byte slice, and then the Reader takes its source of data and pushes it to that provided byte slice.- This may seem weird you if you're used to not needing to supply the data structure that the output will be put into.
- The integer that gets returned from
Read()
is the number of bytes that were put into the slice.
Working with the Read Function
- Let's try to actually write some code that uses the
Read()
function. - To create the slice he does this:
bs := make([]byte, 99999)
- This initializes the slice so that its underlying array can immediately handle the specified number of bytes.
- We have to do this because the
Read()
function isn't set up to automatically resize the slice if it's already full. (NW: Ugh, what a nightmare.)
- To load the HTTP response into the byte slice he does this:
resp.Body.Read(bs)
- To print it out he does
fmt.Println(string(bs))
- In the next video we'll see a way to simplify this code we wrote.
The Writer Interface
- Let's simplify the code from the last section: He replaces the three lines of code he wrote in the previous section with just this:
io.Copy(os.Stdout, resp.Body)
- How did this work? First, you need to understand that Go has a
Writer
interface which does the opposite of theReader
interface: it takes a slice of bytes and writes it to some form of output (HTTP request, text file, etc.).
The io.Copy Function
- He navigates to the official docs on the
Writer
interface and sees that to satisfy it, a type needs to implement aWrite
function. - He then navigates to the docs on the
Copy()
function and sees that it requires two arguments: first, something that implements theWriter
interface, and secondly something that implements theReader
interface. - He then goes through the line of reasoning that would lead us to understand that
os.Stdout
would work as aWriter
type:os.Stdout
is of typeFile
, and typeFile
has a function of typeWrite
, and since we know that any type that implements that function (including the required parameter types and return types) is of typeWriter
, we therefore know thatos.Stdout
would be accepted byio.Copy()
. - He describes this as "hard-won knowledge", suggesting there was no easily-available help out there on the Internet that made this stuff clear to him.
- "It took me a lot of time when I was learning Go to understand all this stuff."
- He says that even though this stuff seems "nasty", it will save you "so much time when you go write your own code".
- Next, as a way to get more experience with interfaces, we'll take a look at the implementation of
io.Copy()
so we can understand how we'd implement theWriter
interface in our own type.
The Implementation of io.Copy
- He holds down the Command key (OSX, maybe Ctrl on Windows) and hovers over the
Copy
function in the code, and it shows the source code in a little window. - He then clicks on
Copy
and a new tab opens to the source code. - He sees that the
Copy
function immediately passes the arguments it receives to acopyBuffer
function, and navigates to that function's source code. - He points out in the source code the line that has the
copyBuffer
function doing what we did manually in an earlier section: it creates an empty 32kb byte slice to contain information from the input source (whatever is implementing theReader
interface). - He then shows how the function is just looping pulling information from the
Reader
and piping it to theWriter
32kb at a time until theReader
doesn't have anything left to give. - Next, let's try to create our own type that implements the
Writer
interface.
A Custom Writer
- We're going to try to create our own custom
Writer
and pass it toio.Copy()
He creates an empty
logWriter
type with the necessaryWrite
function:type logWriter struct{} (...) func (logWriter) Write(bs []byte) (int, error) {}
- He points out (as he did earlier in the course) that there's nothing about the "interface" feature of Go that guarantees that a given type will actually do the intended action of the interface. It just guarantees the input and output types.
- As an example, he has the
Write
function just return1, nil
but not do anything else. He runs the code (using a logWriter object withio.Copy
) and shows that it doesn't raise any error.
- As an example, he has the
He then does a working implementation of
Write
:func (logWriter) Write(bs []byte) (int, error) { fmt.Println(string(bs)) return len(bs), nil }
- He says interfaces are "some of the really top-end, heavy Go stuff", so it's normal if it feels tough.
Assignment: Interfaces
- Create two struct types: triangle and square, both should have a
getArea()
receiver function. - Add a
shape
interface that requires a getArea()
receiver function and provides aprintArea()
function.
Assignment: Hard Mode Interfaces
- Create a program that reads a file from the hard drive and prints out the contents to the terminal.
- The filename should be provided as a command-line argument.
os.Args
is a slice of string that has the command line arguments passed to our program.- The first element will be the path to the compiled executable.
Section 7: Channels and Go Routines
Website Status Checker
- Channels and Go Routines are both used for writing concurrent programs.
- We're going to build a program without using these features and then improve the program by rewriting it to use these features.
- The program we'll build will make HTTP requests to a list of sites periodically to check if they're online or not.
- Our first approach will be to iterate one-at-a-time through a slice of strings of the URLs we want to check.
- He implements the "outer" code of the function that loops through the list of strings.
Printing Site Status
- He writes a function
checkLink
that takes a link and makes an HTTP request. - He runs the program and sees the output coming out one site at a time.
Serial Link Checking
- It seems in our first approach there's a bit of a delay between when we receive the output for each site.
- This is because we're needing to wait for the HTTP response for each site we make a request for before we can move on to handle the next site.
- If we had many, many links, we might having to wait a long time between when we could check a given website.
- We could use Go Routines to run these requests in parallel.
Go Routines
- First we'll discuss the theory of Go Routines, and then we'll actually implement them.
- When we run a Go program, Go creates a Go Routine that runs our code line-by-line.
- When it runs the
http.Get(link)
call, the main Go routine is blocked from continuing until it receives a response. - To fix this, we'll add a
go
keyword in front of thecheckLink
function to have it run in parallel in a new Go routine:go checkLink(link)
- When the child routine hits a blocking function, control is passed to other Go routines that are unblocked.
- In the next section we'll look at some of the edge cases we can run into with Go routines.
Theory of Go Routines
- Let's talk about what Go routines are doing on our machine when we run them.
- Behind the scenes, there's something called the "Go scheduler". By default it works with one CPU on our machine, even if you have multiple cores.
- What's important to understand is that only one Go routine is ever running at any given time.
- The purpose of the Go scheduler is to monitor the status of each Go routine and switch the 'active' one depending on which ones are blocked and unblocked.
- If you have multiple CPUs, the scheduler will assign routines to different CPUs to make them run truly in parallel.
- In Go there is a saying you'll run into a lot: "concurrency is not parallelism". Parallelism is when you have code for the same program running at the exact same time on different CPUs. Concurrency is when you can start new work before having totally finished old work.
- Child go routines aren't given 100% exactly the same treatment as the main Go routine.
Channels
- He updates the
checkLink
project code we'd written to work with Go routines by just adding the keywordgo
in front of the call tocheckLink(link)
- He runs the code and there's no output (we'd expect to see the URLs we're checking).
- He explains that in our existing code, when the main routine finishes creating all the child routines, it doesn't see anything else for it to do, so it exits entirely. It doesn't care that the child routines haven't finished their work.
- We're going to fix this by using channels. Channels are a way to communicate between Go routines, and they're the only way to communicate between Go routines.
- In our case, we're going to create one channel that will let our main routine know when the child routines have finished their work.
- You can think of the channels as working like text/instant messaging: you can send data into a channel and it'll automatically get sent to all other routines that have access to that channel.
- A channel is an actual value in Go that you can pass around between functions like any data structure: int, string, struct.
- You must specify a type of data that the channel will be passing around.
Channel Implementation
- To create a channel:
c := make(chan string)
string
can be whatever type you want.
- For a function to be able to make use of the channel, you have to pass the channel variable to that function.
- When you list a channel in a function's list of arguments, you also need to list the type of data that the channel expects:
func checkLink(link string, c chan string) {
- To send data into a channel use the
<-
operator:myChannel <- 5
- To receive data from a channel use the same operator:
myNumber <- myChannel
- The routine will wait for a value to be sent into the channel.
- You don't have to assign it to a variable, for example you can do
fmt.Println(<- myChannel)
- He updates the
checkLink
project code we've been working on to just send a string message into the channel from each child go routine after the GET request, and then waiting for a single message in the main routine. - He runs the code and notes that we only see one message in the terminal before the program exits, and says we'll discuss why this is happening in the next section.
Blocking Channels
- He steps through the code, explaining that the main routine stops at the
fmt.Println(<- c)
line of code that receives string messages from the child routines, and then when it receives a message it runs that line of code and exits.- He stresses that the main thing to understand is that receiving data from a channel is a blocking operation in the same way that an HTTP request is.
- He shows a timeline diagram he created to show how the main routine stops when it has finished creating all the child routines and then restarts when it receives a message in the channel from the child routine that had sent a request to google.com.
- He adds another
fmt.Println(<- c)
line after his first one and runs the code again and sees two messages in the output.- He shows a new timeline diagram showing how the main routine would go to sleep after printing the first string and wake up when receiving the second string through the channel.
- He adds three more
Println
calls, bringing the number of calls up to the same number of links, and runs the code again and sees all of the expected messages in the output. - He adds one more
Println
call and sees how all of the expected output messages get printed out but then the program doesn't exit because it's waiting for one more that will never come. - In the next section we'll see how to print out all of the messages without copy-pasting
Println
statements.
Receiving Messages
- Instead of copy-pasting the
Println
calls, we're going to use a for loop:for i := 0; i < len(links); i++ {
Repeating Routines
- We're now going to update our program to ping each website repeatedly instead of only once.
- To have an infinite loop use this syntax:
for {
This is the full code for the repeating part:
for { go checkLink(<- c, c) }
- This loop runs after our initial loop through the links. The
<-c
is receiving links sent into the channel by finished routines and then immediately passing them to a new routine. - He runs the code and we can see how quickly it's querying each site.
- In the next section we'll add a gap of time between each request for a given site.
Alternative Loop Syntax
An equivalent syntax for our loop in the previous section is:
for l := range c { checkLink(l, c)}
- The
range c
will wait for a value to arrive on the channelc
- This syntax might make it easier for other coders to understand that the for loop is iterating over values coming in from the channel.
Sleeping a Routine
- He navigates the official docs to find the 'Sleep' function.
- 'Sleep' takes a 'Duration', so he navigates to the docs to see what that is. He sees that a
time.Second
is of typeDuration
- The final code:
time.Sleep(5 * time.Second)
- He points out that having the sleep statement in the main routine wouldn't achieve the parallel behavior we want.
Function Literals
- He initially moves the
time.Sleep()
call to thecheckLink()
function but then points out that this kind of "pollutes" the nature ofcheckLink()
His proposed solution is to instead have the
go
call initiate a function literal that contains the call totime.Sleep()
and the call tocheckLink()
:for l := range c { go func() { time.Sleep(5 * time.Second) checkLink(l, c) }() }
Channels Gotcha!
- He notes that we're getting a warning underneath the call to
checkLink()
saying "range variable l captured by func literal". - When he runs the code, we see that after the initial loop through the sites, all of the HTTP calls seem to be going to facebook.com. So something is wrong.
- He explains that what's happening is that the function literal is running within a child routine and is referencing the actual value of l (not a copy), which is also being referenced and updated by the main routine.
The solution is to pass the for loop variable as an argument to the function literal:
for l := range c { go func(link string) { time.Sleep(5 * time.Second) checkLink(link, c) }(l) }