In Part 1, we saw the basics of installing Go compiler, running Go programs and theGo module system.
In Part 2, we developed a password manager program and learn about several language features and packages from standard library.
In Part 3, we developed a game while learning more of Go language features.
In this Part 4, we explore other interesting Go features like closures and channels.
Let’s build a small application that tracks moving average of cryptocurrencies every minute. Let us consider just three Bitcoin (BTC), Litecoin (LTC) and Ethereum (ETH). There are public APIs that provide real time values for these currencies. We will use one of them provided at https://www.coingecko.com/api/documentations/v3/swagger.json
The application gets the values for each of these currencies every minute, calculates and prints simple moving average.
Create a directory “coinstat” in workspace and create module:
mkdir coinstat
cd coinstatgo mod init antosara.com/coinstat
We use a third party library to simplify reading JSON from a HTTP response.
Run the following. This will add the new dependency in the go.mod created above.
go get -u github.com/tidwall/gjson
Create a file coinstat.go and start adding:
// Create an executable
package main
// Packages we use in this application
import (
"log"
"net/http"
"fmt"
"io/ioutil"
"time"
"github.com/tidwall/gjson"
)
// The CoinGecko API to talk with to fetch the market prices
const COIN_EP = "https://api.coingecko.com/api/v3/simple/price?ids=%s&vs_currencies=USD"
- Note the format string with “%s”. We substitute this to get prices for the specific coins. The API allows multiple ids, but we will use one id in each call for this program. Using these parameters, we could get the current price of the specified crypto currency in USD
func movAvg() func(float64) float64 {
var avg float64
count := 0.0
return func(price float64) float64 {
count = count + 1
avg = avg + (price - avg) / count
return avg
}
}
- movAvg is a function that returns an anonymous function.
- The anonymous function takes a price and returns the moving average so far. For a moving average, it needs to keep track of historical average and a count if values considered. How does it do that? It just uses the avg and count variables in the enclosing movAvg function. These do not go out of scope over multiple calls to the anonymous function.
- This anonymous function is called a closure: https://en.wikipedia.org/wiki/Closure_(computer_programming)
- The count is incremented whenever the closure is invoked. The moving average is calculated, stored and returned.
- We will see how this is used below
func fetch(channel chan float64, avgFunc func(float64) float64, coinId string) {
resp, err := http.Get(fmt.Sprintf(COIN_EP, coinId))
if err != nil {
log.Fatalln(err)
channel <- 0.0
return
}
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
log.Fatalln(err)
channel <- 0.0
return
}
val := gjson.Get(string(body), "*.usd")
resp.Body.Close()
channel <- avgFunc(val.Float())
}
- Fetch is a function that takes 3 parameters — a “channel”, a “function”, a currency name
- A channel in Go is a typed conduit through which you can send and receive values. Here, this channel deals with float values. Sends and receives to a channel are blocked until the receiver and sender on the other end is ready respectively. Why we need a channel will be clear soon, but we push our calculated average to the channel provided.
- The second parameter avgFunc is a function that takes a float value and returns a float value. In fact it has the same signature as the closure returned by moveAvg function we have introduced above. This parameter is like a function pointer. We invoke this function with the current price to get the moving average so far.
- In the implementation, we make a HTTP GET call to the endpoint. fmt.Sprinf() formats the COIN_EP with the given currency name string.
- After proper error handling, we read the response JSON body to a string. The response is of the form: {“bitcoin”:{“usd”:12335.060000}}
- Use the gjson library to pick the usd value from the json string.
- Now, call the avgFunc with this price as argument. The function returns the moving average calculated considering the new price at this time.
- This new moving average is now sent to the channel. Note the notation of sending to channel: “channel <- value”
func fetchAll(btc_chan, ltc_chan, eth_chan chan float64) {
btcAvg := movAvg()
ltcAvg := movAvg()
ethAvg := movAvg()
i := 10
for i > 0 {
fetch(btc_chan, btcAvg, "bitcoin")
fetch(ltc_chan, ltcAvg, "litecoin")
fetch(eth_chan, ethAvg, "ethereum")
i--
time.Sleep(1* time.Minute)
}
}
- fetchAll is a function that takes 3 channels. We want to track 3 currencies BTC, LTC, ETH
- Inside this, we call movAvg() to get the average calculating closure for each of the currencies
- Then, for every minute for the next 10 minutes, we call fetch on the 3 currencies passing the individual channel, the closure and the currency name
func printAll(btc_chan, ltc_chan, eth_chan chan float64) {
i := 10
for i > 0 {
fmt.Printf("BTC:%f, LTC:%f, ETH:%f\n", <-btc_chan, <-ltc_chan, <-eth_chan)
i--
}
}
- printAll function prints the values of each currency in a line.
- The function receives the values from each of the channels.
- Note the notation to retrieve value from channel. “<- channel”. Here we just print the value
- Also recall, we said receives are blocked until fetch sends the values to the channels.
- Since we fetch 10 times, we also try retrieving 10 times. Because this blocks, we effectively run for 10 minutes while receiving the values every minute.
Finally the main function:
func main() {
btcChan := make(chan float64)
ltcChan := make(chan float64)
ethChan := make(chan float64)
go fetchAll(btcChan, ltcChan, ethChan)
printAll(btcChan, ltcChan, ethChan)
}
- We make 3 channels: one for each currency.
- Note how make is used to create the channel
- Notice the “go” before fetchAll call. This means fetchAll() is invoked as “go routine” which is saying that Go uses a separate thread of execution for fetchAll
- The control goes to printAll which runs in the main thread and we have seen that the program waits until is printAll processes all the 10 values for each of currency.
- You probably have observed how channels are useful here. Since fetchAll and printAll run in separate threads, they communicate with the help of channels.
- While this may seem an overkill in this application, you could think of usage of channels where you may need to share data between multiple threads.
We will run this a bit differently. In coinstat directory, run
go install
This will build and install the executable “coinstat” in $GOBIN. Make sure you have this setup. If not you can build with “go build” which will create the executable in the current directory.
Run
./coinstat
or
$GOBIN/coinstat
You should see output like this every 1 min or whatever frequency you have configured.
BTC:12335.060000, LTC:67.500000, ETH:431.170000BTC:12335.060000, LTC:67.525000, ETH:431.415000
...
Great! I hope you have learnt enough basics to explore Go further on your own.
This concludes the Concise Go series.