├── 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 | 34 | 35 | 36 | 37 | 38 | 39 | 40 |
RankNameScore
41 | 42 |

Update Score

43 |
44 | 45 | 46 | 47 |
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 | --------------------------------------------------------------------------------