CheckWebsites
, that checks the status of a list of URLs.true
for a good response, false
for a bad response.WebsiteChecker
which takes a single URL and returns a boolean. This is used by the function to check all the websites.CheckWebsites
so that we can see the effect of our changes.CheckWebsites
using a slice of one hundred urls and uses a new fake implementation of WebsiteChecker
. slowStubWebsiteChecker
is deliberately slow. It uses time.Sleep
to wait exactly twenty milliseconds and then it returns true. We use b.ResetTimer()
in this test to reset the time of our test before it actually runsgo test -bench=.
(or if you're in Windows Powershell go test -bench="."
):CheckWebsites
has been benchmarked at 2249228637 nanoseconds - about two and a quarter seconds.CheckWebsites
faster. Instead of waiting for a website to respond before sending a request to the next website, we will tell our computer to make the next request while it is waiting.doSomething()
we wait for it to return (even if it has no value to return, we still wait for it to finish). We say that this operation is blocking - it makes us wait for it to finish. An operation that does not block in Go will run in a separate process called a goroutine. Think of a process as reading down the page of Go code from top to bottom, going 'inside' each function when it gets called to read what it does. When a separate process starts it's like another reader begins reading inside the function, leaving the original reader to carry on going down the page.go
statement by putting the keyword go
in front of it: go doSomething()
.go
in front of a function call, we often use anonymous functions when we want to start a goroutine. An anonymous function literal looks just the same as a normal function declaration, but without a name (unsurprisingly). You can see one above in the body of the for
loop.()
at the end of the anonymous function is doing. Secondly they maintain access to the lexical scope they are defined in - all the variables that are available at the point when you declare the anonymous function are also available in the body of the function.WebsiteChecker
function) each of which will add its result to the results map.go test
:CheckWebsites
, it's now returning an empty map. What went wrong?for
loop started had enough time to add their result to the results
map; the WebsiteChecker
function is too fast for them, and it returns the still empty map.url
is reused for each iteration of the for
loop - it takes a new value from urls
each time. But each of our goroutines have a reference to the url
variable - they don't have their own independent copy. So they're all writing the value that url
has at the end of the iteration - the last url. Which is why the one result we have is the last url.u
- and then calling the anonymous function with the url
as the argument, we make sure that the value of u
is fixed as the value of url
for the iteration of the loop that we're launching the goroutine in. u
is a copy of the value of url
, and so can't be changed.fatal error: concurrent map writes
. Sometimes, when we run our tests, two of the goroutines write to the results map at exactly the same time. Maps in Go don't like it when more than one thing tries to write to them at once, and so fatal error
.race
flag: go test -race
.WARNING: DATA RACE
is pretty unambiguous. Reading into the body of the error we can see two different goroutines performing writes on a map:Write at 0x00c420084d20 by goroutine 8:
Previous write at 0x00c420084d20 by goroutine 7:
/Users/gypsydave5/go/src/github.com/gypsydave5/learn-go-with-tests/concurrency/v3/websiteChecker.go:12
/Users/gypsydave5/go/src/github.com/gypsydave5/learn-go-with-tests/concurrency/v3/websiteChecker.go:11
WebsiteChecker
function with the url.results
map we now have a resultChannel
, which we make
in the same way. chan result
is the type of the channel - a channel of result
. The new type, result
has been made to associate the return value of the WebsiteChecker
with the url being checked - it's a struct of string
and bool
. As we don't need either value to be named, each of them is anonymous within the struct; this can be useful in when it's hard to know what to name a value.map
directly we're sending a result
struct for each call to wc
to the resultChannel
with a send statement. This uses the <-
operator, taking a channel on the left and a value on the right:for
loop iterates once for each of the urls. Inside we're using a receive expression, which assigns a value received from a channel to a variable. This also uses the <-
operator, but with the two operands now reversed: the channel is now on the right and the variable that we're assigning to is on the left:result
received to update the map.wc
, and each send to the result channel, is happening in parallel inside its own process, each of the results is being dealt with one at a time as we take values out of the result channel with the receive expression.CheckWebsites
function; the inputs and outputs never changed, it just got faster. But the tests we had in place, as well as the benchmark we wrote, allowed us to refactor CheckWebsites
in a way that maintained confidence that the software was still working, while demonstrating that it had actually become faster.