Skip to content

Commit 2389eee

Browse files
committed
✨ add rate limit sample
0 parents  commit 2389eee

File tree

15 files changed

+552
-0
lines changed

15 files changed

+552
-0
lines changed

.gitignore

+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# If you prefer the allow list template instead of the deny list, see community template:
2+
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
3+
#
4+
# Binaries for programs and plugins
5+
*.exe
6+
*.exe~
7+
*.dll
8+
*.so
9+
*.dylib
10+
11+
# Test binary, built with `go test -c`
12+
*.test
13+
14+
# Output of the go coverage tool, specifically when used with LiteIDE
15+
*.out
16+
17+
# Dependency directories (remove the comment below to include it)
18+
# vendor/
19+
20+
# Go workspace file
21+
go.work
22+
go.work.sum
23+
24+
# env file
25+
.env
26+
bin
27+
gg
28+
mage

README.md

+63
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
# golang-sample-with-rate-limiter
2+
3+
This repository is to demo how to use golang standard library to implement rate limiter
4+
5+
## logic
6+
7+
1. install package
8+
```shell
9+
go get golang.org/x/time/rate
10+
```
11+
12+
2. setup rate limiter
13+
```golang
14+
package rate_limiter
15+
16+
import (
17+
"encoding/json"
18+
"log"
19+
"net"
20+
"net/http"
21+
22+
"golang.org/x/time/rate"
23+
)
24+
25+
type RateLimiter struct{}
26+
27+
// getIP - 取得 request IP
28+
func (r *RateLimiter) getIP(req *http.Request) string {
29+
host, _, err := net.SplitHostPort(req.RemoteAddr)
30+
if err != nil {
31+
log.Printf("Error parsing IP: %v", err)
32+
return ""
33+
}
34+
return host
35+
}
36+
37+
// RateLimiterMiddleware - 建立 ratelimiter middleware
38+
func (r *RateLimiter) RateLimiterMiddleware(next http.Handler, limit rate.Limit, burst int) http.Handler {
39+
ipLimiterMap := make(map[string]*rate.Limiter)
40+
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
41+
// Fetch IP
42+
ip := r.getIP(req)
43+
// Create limiter if not present for IP
44+
limiter, exists := ipLimiterMap[ip]
45+
if !exists {
46+
limiter = rate.NewLimiter(limit, burst)
47+
ipLimiterMap[ip] = limiter
48+
}
49+
// return error if the limit has been reached
50+
if !limiter.Allow() {
51+
w.Header().Set("Content-Type", "application/json")
52+
w.WriteHeader(http.StatusTooManyRequests)
53+
json.NewEncoder(w).Encode(map[string]string{"error": "Too many requests"})
54+
return
55+
}
56+
next.ServeHTTP(w, req)
57+
})
58+
}
59+
60+
func NewRateLimiter() *RateLimiter {
61+
return &RateLimiter{}
62+
}
63+
```

Taskfile.yml

+42
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
version: '3'
2+
3+
dotenv: ['.env']
4+
5+
tasks:
6+
default:
7+
cmds:
8+
- echo "PORT=$PORT"
9+
silent: true
10+
11+
# build:
12+
# cmds:
13+
# - CGO_ENABLED=0 GOOS=linux go build -o bin/main cmd/main.go
14+
# silent: true
15+
# run:
16+
# cmds:
17+
# - ./bin/main
18+
# deps:
19+
# - build
20+
# silent: true
21+
22+
build-mage:
23+
cmds:
24+
- CGO_ENABLED=0 GOOS=linux go build -o ./mage mage-tools/mage.go
25+
silent: true
26+
27+
build-gg:
28+
cmds:
29+
- ./mage -d mage-tools -compile ../gg
30+
deps:
31+
- build-mage
32+
silent: true
33+
34+
coverage:
35+
cmds:
36+
- go test -v -cover ./...
37+
silent: true
38+
test:
39+
cmds:
40+
- go test -v ./...
41+
silent: true
42+

cmd/client/main.go

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
"net/http"
6+
"time"
7+
8+
"github.com/leetcode-golang-classroom/golang-sample-with-rate-limiter/internal/config"
9+
)
10+
11+
func main() {
12+
url := fmt.Sprintf("http://localhost:%s", config.AppConfig.Port)
13+
14+
for i := 1; i <= 50; i++ {
15+
resp, err := http.Get(url)
16+
if err != nil {
17+
fmt.Println("Error making request:", err)
18+
continue
19+
}
20+
fmt.Printf("Request: %2d: Status: %d\n", i, resp.StatusCode)
21+
time.Sleep(100 * time.Millisecond) // Adjust timing to test rate limiting
22+
}
23+
}

cmd/server/main.go

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package main
2+
3+
import (
4+
"context"
5+
"log"
6+
"os"
7+
"os/signal"
8+
"syscall"
9+
10+
"github.com/leetcode-golang-classroom/golang-sample-with-rate-limiter/internal/application"
11+
"github.com/leetcode-golang-classroom/golang-sample-with-rate-limiter/internal/config"
12+
)
13+
14+
func main() {
15+
// 建立 application instance
16+
app := application.New(config.AppConfig)
17+
// 設定中斷訊號監聽
18+
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt,
19+
syscall.SIGTERM, syscall.SIGINT)
20+
defer cancel()
21+
err := app.Start(ctx)
22+
if err != nil {
23+
log.Println("failed to start app:", err)
24+
}
25+
}

go.mod

+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
module github.com/leetcode-golang-classroom/golang-sample-with-rate-limiter
2+
3+
go 1.22.4
4+
5+
require (
6+
github.com/magefile/mage v1.15.0
7+
github.com/spf13/viper v1.19.0
8+
golang.org/x/time v0.10.0
9+
)
10+
11+
require (
12+
github.com/fsnotify/fsnotify v1.7.0 // indirect
13+
github.com/hashicorp/hcl v1.0.0 // indirect
14+
github.com/magiconair/properties v1.8.7 // indirect
15+
github.com/mitchellh/mapstructure v1.5.0 // indirect
16+
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
17+
github.com/sagikazarmark/locafero v0.4.0 // indirect
18+
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
19+
github.com/sourcegraph/conc v0.3.0 // indirect
20+
github.com/spf13/afero v1.11.0 // indirect
21+
github.com/spf13/cast v1.6.0 // indirect
22+
github.com/spf13/pflag v1.0.5 // indirect
23+
github.com/subosito/gotenv v1.6.0 // indirect
24+
go.uber.org/atomic v1.9.0 // indirect
25+
go.uber.org/multierr v1.9.0 // indirect
26+
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
27+
golang.org/x/sys v0.18.0 // indirect
28+
golang.org/x/text v0.14.0 // indirect
29+
gopkg.in/ini.v1 v1.67.0 // indirect
30+
gopkg.in/yaml.v3 v3.0.1 // indirect
31+
)

go.sum

+75
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
2+
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
3+
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
4+
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
5+
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
6+
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
7+
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
8+
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
9+
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
10+
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
11+
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
12+
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
13+
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
14+
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
15+
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
16+
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
17+
github.com/magefile/mage v1.15.0 h1:BvGheCMAsG3bWUDbZ8AyXXpCNwU9u5CB6sM+HNb9HYg=
18+
github.com/magefile/mage v1.15.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A=
19+
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
20+
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
21+
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
22+
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
23+
github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
24+
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
25+
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
26+
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
27+
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
28+
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
29+
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
30+
github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ=
31+
github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4=
32+
github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE=
33+
github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ=
34+
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
35+
github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
36+
github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8=
37+
github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY=
38+
github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0=
39+
github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
40+
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
41+
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
42+
github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI=
43+
github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg=
44+
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
45+
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
46+
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
47+
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
48+
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
49+
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
50+
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
51+
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
52+
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
53+
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
54+
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
55+
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
56+
go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
57+
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
58+
go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI=
59+
go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ=
60+
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g=
61+
golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k=
62+
golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
63+
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
64+
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
65+
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
66+
golang.org/x/time v0.10.0 h1:3usCWA8tQn0L8+hFJQNgzpWbd89begxN66o1Ojdn5L4=
67+
golang.org/x/time v0.10.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
68+
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
69+
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
70+
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
71+
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
72+
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
73+
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
74+
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
75+
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

internal/application/app.go

+68
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
package application
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"log"
7+
"net/http"
8+
"time"
9+
10+
"github.com/leetcode-golang-classroom/golang-sample-with-rate-limiter/internal/config"
11+
rateLimiter "github.com/leetcode-golang-classroom/golang-sample-with-rate-limiter/internal/service/rate_limiter"
12+
"golang.org/x/time/rate"
13+
)
14+
15+
type App struct {
16+
appConfig *config.Config
17+
router *http.ServeMux
18+
}
19+
20+
// New - 建立 App 物件
21+
func New(appConfig *config.Config) *App {
22+
router := http.NewServeMux()
23+
// create app instance
24+
app := &App{
25+
appConfig: appConfig,
26+
router: router,
27+
}
28+
// setup route
29+
app.setupGreetingRoute()
30+
return app
31+
}
32+
33+
// Start - 啟動 server
34+
func (app *App) Start(ctx context.Context) error {
35+
// setup rateLimiter
36+
rateLimitMux := rateLimiter.NewRateLimiter()
37+
handler := rateLimitMux.RateLimiterMiddleware(app.router, rate.Limit(1), 10)
38+
// setup server
39+
server := &http.Server{
40+
Addr: fmt.Sprintf(":%s", app.appConfig.Port),
41+
Handler: handler,
42+
}
43+
log.Printf("starting server on %s", app.appConfig.Port)
44+
errCh := make(chan error, 1)
45+
var err error
46+
go func() {
47+
err = server.ListenAndServe()
48+
if err != nil {
49+
errCh <- fmt.Errorf("failed to start server: %w", err)
50+
}
51+
CloseChannel(errCh)
52+
}()
53+
select {
54+
case err = <-errCh:
55+
return err
56+
case <-ctx.Done():
57+
timeout, cancel := context.WithTimeout(context.Background(), time.Second*10)
58+
log.Printf("stopping server, wait for 10 seconds to stop")
59+
defer cancel()
60+
return server.Shutdown(timeout)
61+
}
62+
}
63+
64+
func CloseChannel(ch chan error) {
65+
if _, ok := <-ch; ok {
66+
close(ch)
67+
}
68+
}

internal/application/route.go

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package application
2+
3+
import "github.com/leetcode-golang-classroom/golang-sample-with-rate-limiter/internal/service/greeting"
4+
5+
func (app *App) setupGreetingRoute() {
6+
greetingRoute := greeting.NewRoute()
7+
greetingRoute.SetupRoute(app.router)
8+
}

0 commit comments

Comments
 (0)