├── README.md
├── go.mod
├── go.sum
├── index.html
└── main.go
/README.md:
--------------------------------------------------------------------------------
1 | Leaderboard with Go and Redis
2 | ===
3 |
4 | 1. Start a local Redis server on port 6379
5 | 2. Start the leaderboard example server
6 |
7 | ```sh
8 | $ go run main.go
9 | ```
10 |
11 | Once the server is running, open http://localhost:8080 in your browser.
12 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/arpitbbhayani/leaderboard-go-redis
2 |
3 | go 1.22.1
4 |
5 | require (
6 | github.com/cespare/xxhash/v2 v2.1.2 // indirect
7 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
8 | github.com/go-redis/redis/v8 v8.11.5 // indirect
9 | github.com/gorilla/websocket v1.5.3 // indirect
10 | )
11 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE=
2 | github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
3 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
4 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
5 | github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI=
6 | github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo=
7 | github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
8 | github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
9 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Leaderboard
7 |
27 |
28 |
29 | Leaderboard
30 |
31 |
32 |
33 | | Rank |
34 | Name |
35 | Score |
36 |
37 |
38 |
39 |
40 |
41 |
42 | Update Score
43 |
48 |
49 |
79 |
80 |
81 |
--------------------------------------------------------------------------------
/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "encoding/json"
5 | "log"
6 | "net/http"
7 | "time"
8 |
9 | "github.com/go-redis/redis/v8"
10 | "github.com/gorilla/websocket"
11 | )
12 |
13 | var (
14 | client *redis.Client
15 | upgrader = websocket.Upgrader{
16 | CheckOrigin: func(r *http.Request) bool {
17 | return true
18 | },
19 | }
20 |
21 | connectedUsers []*websocket.Conn
22 | )
23 |
24 | type Score struct {
25 | Name string `json:"name"`
26 | Score int `json:"score"`
27 | }
28 |
29 | func main() {
30 | client = redis.NewClient(&redis.Options{
31 | Addr: "localhost:6379",
32 | })
33 | go watchLoop()
34 |
35 | http.HandleFunc("/", serveHome)
36 | http.HandleFunc("/ws", handleWebSocket)
37 | http.HandleFunc("/update", handleUpdate)
38 |
39 | log.Println("Server starting on :8080")
40 | log.Fatal(http.ListenAndServe(":8080", nil))
41 | }
42 |
43 | func serveHome(w http.ResponseWriter, r *http.Request) {
44 | http.ServeFile(w, r, "index.html")
45 | }
46 |
47 | func handleWebSocket(w http.ResponseWriter, r *http.Request) {
48 | conn, err := upgrader.Upgrade(w, r, nil)
49 | if err != nil {
50 | log.Println(err)
51 | return
52 | }
53 |
54 | connectedUsers = append(connectedUsers, conn)
55 | }
56 |
57 | func watchLoop() {
58 | for {
59 | scores, err := getTopScores()
60 | if err != nil {
61 | log.Println(err)
62 | return
63 | }
64 |
65 | for _, conn := range connectedUsers {
66 | if err := conn.WriteJSON(scores); err != nil {
67 | log.Println("websocket write error:", err)
68 | // TODO: remove the connection from the list
69 | }
70 | }
71 |
72 | time.Sleep(1 * time.Second)
73 | }
74 | }
75 |
76 | func handleUpdate(w http.ResponseWriter, r *http.Request) {
77 | var score Score
78 | if err := json.NewDecoder(r.Body).Decode(&score); err != nil {
79 | http.Error(w, err.Error(), http.StatusBadRequest)
80 | return
81 | }
82 |
83 | err := client.ZAdd(r.Context(), "leaderboard", &redis.Z{
84 | Score: float64(score.Score),
85 | Member: score.Name,
86 | }).Err()
87 |
88 | if err != nil {
89 | http.Error(w, err.Error(), http.StatusInternalServerError)
90 | return
91 | }
92 |
93 | w.WriteHeader(http.StatusOK)
94 | }
95 |
96 | func getTopScores() ([]Score, error) {
97 | cmd := client.ZRevRangeWithScores(client.Context(), "leaderboard", 0, 5)
98 | result, err := cmd.Result()
99 | if err != nil {
100 | return nil, err
101 | }
102 |
103 | var scores []Score
104 | for _, z := range result {
105 | scores = append(scores, Score{
106 | Name: z.Member.(string),
107 | Score: int(z.Score),
108 | })
109 | }
110 |
111 | return scores, nil
112 | }
113 |
--------------------------------------------------------------------------------