Black Friday Sale!
Save 50% on Web Development with Go and Test with Go until Dec 3.
Both courses have lifetime access, a 30-day money back guarantee, and free updates. This is also the biggest discount I offer on the courses, and I only do it once a year around Black Friday.
Thank you for your continued support and happy coding!
Jon Calhoun
I recently needed to connect to the Desk.com API and realized that their API imposes a rate limit. There are several ways to get around this, but I decided to go with the simplest thing that would work for my code and wrote a bucket that keeps track of how many API calls I have available, and returns a “Not enough capacity” error if I can’t consume any at this time. Then in my code that makes API calls, I simply check if I can consume an API call, and if i can’t I sleep for a few seconds.
Simple enough, except keeping track of times events can be a pain. In my case I would need to keep track of how many API calls are available in my bucket, as well as when a new set was last added to the bucket. When a new API call attemps to consume an existing API call, I would need to first calculate how many many times the timed event had occured and update the remaining API calls accordingly. Only after doing these calculations could I determine if I could consume an API call.
Concurrency in Go makes this much simpler. Instead of keeping track of the last time API calls were added to my bucket, I can instead create a go routine with the sole responsibility of adding API calls to the bucket at a set interval. This ends up simplifying the code significantly, and the final rate limiter is roughly 65 lines of code. You can see it below, or you can check it out on GitHub.
package drip
import (
"errors"
"sync"
"time"
)
type Bucket struct {
Capacity int
DripInterval time.Duration
PerDrip int
consumed int
started bool
kill chan bool
m sync.Mutex
}
func (b *Bucket) Start() error {
if b.started {
return errors.New("Bucket was already started.")
}
ticker := time.NewTicker(b.DripInterval)
b.started = true
b.kill = make(chan bool, 1)
go func() {
for {
select {
case <-ticker.C:
b.m.Lock()
b.consumed -= b.PerDrip
if b.consumed < 0 {
b.consumed = 0
}
b.m.Unlock()
case <-b.kill:
return
}
}
}()
return nil
}
func (b *Bucket) Stop() error {
if !b.started {
return errors.New("Bucket was never started.")
}
b.kill <- true
return nil
}
func (b *Bucket) Consume(amt int) error {
b.m.Lock()
defer b.m.Unlock()
if b.Capacity-b.consumed < amt {
return errors.New("Not enough capacity.")
}
b.consumed += amt
return nil
}
Sign up for my mailing list and I'll send you a FREE sample from my course - Web Development with Go. The sample includes 19 screencasts and the first few chapters from the book.
You will also receive emails from me about Go coding techniques, upcoming courses (including FREE ones), and course discounts.
Jon Calhoun is a full stack web developer who teaches about Go, web development, algorithms, and anything programming. If you haven't already, you should totally check out his Go courses.
Previously, Jon worked at several statups including co-founding EasyPost, a shipping API used by several fortune 500 companies. Prior to that Jon worked at Google, competed at world finals in programming competitions, and has been programming since he was a child.
Related articles
Spread the word
Did you find this page helpful? Let others know about it!
Sharing helps me continue to create both free and premium Go resources.
Want to discuss the article?
See something that is wrong, think this article could be improved, or just want to say thanks? I'd love to hear what you have to say!
You can reach me via email or via twitter.
©2018 Jonathan Calhoun. All rights reserved.