Go basic: Understanding Timers, Tickers, Worker Pools, Waitgroups, Rate Limiting in Go
we delve into the details related to Timers, Tickers, Worker Pools, Waitgroups, and Rate Limiting.
This article was produced with the help of AI
full lessons here👇:
The journey commences from here:
1.1 Timers: Understand timers which play a crucial role in managing and scheduling tasks in Go.
1.2 Tickers: Learn about tickers that help you when you want to do something repeatedly at regular intervals.
1.3 Worker Pools: Deep dive into the concept of the worker pool which is a significant pattern in concurrent programming — a collection of threads/environments, waiting for tasks.
1.4 Waitgroups: Understand how WaitGroups help in synchronizing and handling multiple goroutines.
1.5 Rate Limiting: Discuss Rate Limiting which is an essential mechanism for controlling resource utilization and maintaining quality of service.
1.6 Case Studies: Discuss some case studies related to these concepts like building a job schedule, building a rate-limited service, etc.
1.7 Review and Assessments: Revisit the key points covered and assessments to validate understanding and application of these Advanced Concurrency concepts in Go.
Let’s get started. First up, we have Timers.
Topic: Timers
Timers represent a single event in the future. You tell the timer how long you want to wait, and it provides a channel that will notify you at that time. This is how you make a timer in Go:
timer := time.NewTimer(2 * time.Second)
<-timer.C
fmt.Println("Timer fired")In this example, we create a timer that is set to fire after 2 seconds. The <-timer.C line blocks the timer's C channel until it sends a value indicating that the timer fired.
What if you want to stop the timer before it fires? You can do that with the Stop function:
timer := time.NewTimer(2 * time.Second)
go func() {
<-timer.C
fmt.Println("Timer fired")
}()
stop := timer.Stop()
if stop {
fmt.Println("Timer stopped")
}In this case, we stop the timer before it has a chance to fire.
Topic: Tickers
Unlike timers, tickers are not for one-time events — they’re for repeated tasks at regular intervals. Here’s how you can create a ticker in Go:
ticker := time.NewTicker(500 * time.Millisecond)
done := make(chan bool)
go func() {
for {
select {
case <-done:
return
case t := <-ticker.C:
fmt.Println("Tick at", t)
}
}
}()
time.Sleep(1600 * time.Millisecond)
ticker.Stop()
done <- true
fmt.Println("Ticker stopped")In this example, we are creating a ticker that ticks every 500 milliseconds. We then create an anonymous goroutine that listens on the ticker.C channel in a loop. Every time a tick happens, the goroutine prints the tick's time.
After 1600 milliseconds, we stop the ticker and send a message on the done channel to tell the goroutine to stop too.
Now that you’ve got a handle on timers and tickers, let’s talk about Worker Pools.
Topic: Waitgroups
In Go language, sync package provides WaitGroup. It's designed to tackle a very common problem. In a situation, where we have n number of Goroutines. Now, we need to make sure that we proceed only after these n Goroutines finish their execution. This can easily be accomplished by WaitGroup.
You can think of a wait group like a token system or a bookkeeper, where each bookkeeper keeps track of the tasks to be done and at the end checks if all tasks are completed.
Consider this example:
package main
import (
"fmt"
"sync"
"time"
)
var wg sync.WaitGroup
func worker(msg string) {
// Schedule the call to WaitGroup's Done to tell we are done.
defer wg.Done()
fmt.Printf("Working on %s\n", msg)
time.Sleep(time.Second)
fmt.Printf("Done working on %s\n", msg)
}
func main() {
// Add a count of the total number of goroutines we're going to use.
wg.Add(2)
go worker("Task 1")
go worker("Task 2")
// Wait until the WaitGroup counter is zero i.e., all routines have finished
wg.Wait()
fmt.Println("All tasks completed!")
}In this code, main doesn't exit until worker has finished both its tasks.
Remember, the power of Go’s concurrency is best harnessed when coupled with synchronization tools like WaitGroup. In a real-world scenario, you’d be using these to do complex task orchestration.
Let’s move on to our next topic.
Topic: Rate Limiting
Rate limiting is a way to control how many requests are made to a server in a given amount of time. It’s important for maintaining the quality of service and keeping servers from being overwhelmed.
Here’s an example of how to implement rate limiting in Go:
func main() {
requests := make(chan int, 5)
for i := 1; i <= 5; i++ {
requests <- i
}
close(requests)
limiter := time.Tick(200 * time.Millisecond)
for req := range requests {
<-limiter
fmt.Println("request", req, time.Now())
}
}In this sample code, we’re running a loop sending 5 requests and storing them in a channel. Then, we set up a rate limiter that will tick every 200 milliseconds.
For each request, we receive a tick from the limiter before we print the request. This ensures that we’re not handling more than one request every 200 milliseconds.
Topic: Case Studies: Building a Job Scheduler and a Rate-limited Service
Case Study 1: Building a Job Scheduler
Job scheduling is a common task in software engineering where certain tasks need to be executed at a specific interval or at a specified time in the future.
Here’s some simple code that schedules a function to run every 2 seconds:
package main
import (
"fmt"
"time"
)
func printTime() {
fmt.Println(time.Now())
}
func main() {
tick := time.Tick(2 * time.Second)
go func() {
for range tick {
printTime()
}
}()
// Hold main from exiting
time.Sleep(10 * time.Second)
}In this Go code, we have scheduled the printTime function to execute every 2 seconds by using the built-in time.Tick function.
Case Study 2: Building a Rate-limited Service
Building upon the rate-limiter we discussed earlier, let’s consider an HTTP server that uses this rate limiter to handle incoming requests:
package main
import (
"fmt"
"net/http"
"time"
)
var limiter = time.Tick(200 * time.Millisecond)
func rateLimitedHandler(w http.ResponseWriter, r *http.Request) {
<-limiter // Block until we receive from the limiter channel (i.e., "rate limit" ourselves)
fmt.Fprint(w, "Web page content")
}
func main() {
http.HandleFunc("/", rateLimitedHandler)
http.ListenAndServe(":8080", nil)
}This would be a simplified sketch of a rate-limited HTTP server.
It’s crucial to note that the complexities of building real-world schedulers and rate-limited services go far and beyond this simple introduction. But with a strong grasp of these concurrency principles and mechanisms, you’re well on your way to building robust systems in Go.
Topic: Review and Assessment
Review:
We have covered a myriad of important “Advanced Concurrency” concepts:
- Timers: Useful for when you want to do something once in the future.
- Tickers: Useful when you want to do something repeatedly at regular intervals.
- Worker Pools: Implementing worker pools can be a good way to limit resource usage. They are used where we have a large number of goroutines to do similar kind of tasks.
- Waitgroups: Useful for waiting for a collection of goroutines to finish executing.
- Rate Limiting: Important to ensure our programs don’t overwhelm the resources they use too much. Also used to control the rate of actions that can be done; for instance, control the rate of incoming requests.
Done in a typically “Go” way, we used channels and goroutines to tackle these concurrency concepts.
Assessment:
If you now understand these advanced concepts of concurrency then it’s time for a quiz. Let’s see if you’re ready to work on complex problems that require the application of these skills.
- Timer Practice Problem: Write a program that prints a message “Timer expired” after 3 seconds using a timer.
- Tickets Practice Problem: Write a function that ticks every 100ms and stops after 1 second.
- Worker Pools Practice Problem: Create a worker pool of size 3. Submit jobs to print the squares of numbers. The job should print the square of inputs after a delay.
- Waitgroups Practice Problem: Write a program to sync the execution of two goroutines using WaitGroup.
- Rate Limiting Problem: Write a program that allows only 3 requests to be handled every second.
These problems should give you a practical understanding of how these advanced concurrency concepts play out. If you could come up with code for these, congratulations! If not, or if you have doubts, simply ask me and I’ll help you out.
Remember, the best way to learn Go or any programming language is by constant practice. Keep exploring, keep implementing. Happy coding! 🦌
Once you’ve tried solving these problems, let me know and we can review the answers together!
Stackademic
Thank you for reading until the end. Before you go:





