Conversation
Full learning repository for WebSocket fundamentals using a Slither.io-style snake game as the teaching subject. Each step includes README with concept explanation, full Go server code, and HTML/JS client code. Steps covered: - Step 0: HTTP vs WebSocket theory - Step 1: Echo server (first connection) - Step 2: Broadcast (1-to-many) - Step 3: Input sending (intent over state) - Step 4: Server-authoritative state management - Step 5: Game loop (tick-based updates) - Step 6: Full snake game with food and growth - Step 7: Lag and sync drift experience - Step 8: Client-side interpolation - Step 9: 10-player room management - Step 10: Disconnection handling and heartbeat - Step 11: Optimization and size measurement - Step 12: Advanced topics (multi-server, Redis, WebRTC) https://claude.ai/code/session_01Cg4waeVYhPHpatXEsQMbfB
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughStep0〜Step12の段階的WebSocket学習カリキュラムを追加。各ステップは教材(README)、ブラウザクライアント(HTML/JS)、Goサーバ(Echo + gorilla/websocket)を用い、エコー→ブロードキャスト→入力同期→サーバ状態→ゲームループ→Snake→遅延→補間→ルーム→再接続→最適化→スケーリングを順に扱う。 Changes
Sequence Diagram(s)sequenceDiagram
participant Client
participant FrontendServer as Server
participant RoomLoop as Room/GameLoop
Client->>FrontendServer: WebSocket接続 /ws
FrontendServer->>FrontendServer: findOrCreateRoom()
FrontendServer->>RoomLoop: register client into room
Note right of RoomLoop: Room runs ticker loop
RoomLoop->>RoomLoop: tick → update game state
RoomLoop->>FrontendServer: broadcast state to room clients
FrontendServer->>Client: state (ws message)
Client->>FrontendServer: input (ws message)
FrontendServer->>RoomLoop: apply input (update direction)
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Pull request overview
This PR introduces a 12-step WebSocket learning curriculum (Go + Echo + gorilla/websocket backend, vanilla JS + Canvas frontend) that progressively builds toward a simple multiplayer snake game and then expands into real-world concerns like rooms, reconnection, and basic optimization.
Changes:
- Added Step 0–12 curriculum documentation, including quizzes/exercises and progression guidance in the root README.
- Implemented step-by-step server/client examples for echo, broadcast, input intent, server-authoritative state, tick loop, snake mechanics, lag simulation, interpolation, rooms, reconnect/heartbeat, and optimization instrumentation.
- Added per-step Go modules for reproducible builds.
Reviewed changes
Copilot reviewed 47 out of 47 changed files in this pull request and generated 21 comments.
Show a summary per file
| File | Description |
|---|---|
| README.md | Top-level curriculum overview and step index |
| step-00-http-vs-websocket/README.md | Conceptual foundation (HTTP vs WebSocket) |
| step-01-echo/README.md | Step 1 explanation and run instructions |
| step-01-echo/server/go.mod | Step 1 Go module dependencies |
| step-01-echo/server/main.go | Step 1 echo server implementation |
| step-01-echo/client/index.html | Step 1 client UI for echo testing |
| step-02-broadcast/README.md | Step 2 explanation (broadcast) |
| step-02-broadcast/server/go.mod | Step 2 Go module dependencies |
| step-02-broadcast/server/main.go | Step 2 multi-client broadcast server |
| step-02-broadcast/client/index.html | Step 2 client UI for broadcast chat |
| step-03-input/README.md | Step 3 explanation (send input/intent) |
| step-03-input/server/go.mod | Step 3 Go module dependencies |
| step-03-input/server/main.go | Step 3 input tracking + broadcast |
| step-03-input/client/index.html | Step 3 client UI for input sending/display |
| step-04-server-state/README.md | Step 4 explanation (server-authoritative state) |
| step-04-server-state/server/go.mod | Step 4 Go module dependencies |
| step-04-server-state/server/main.go | Step 4 authoritative state + broadcast |
| step-04-server-state/client/index.html | Step 4 client rendering of server state |
| step-05-game-loop/README.md | Step 5 explanation (tick loop) |
| step-05-game-loop/server/go.mod | Step 5 Go module dependencies |
| step-05-game-loop/server/main.go | Step 5 tick loop + periodic broadcast |
| step-05-game-loop/client/index.html | Step 5 client UI with tick/debug display |
| step-06-snake/README.md | Step 6 explanation (snake body/food) |
| step-06-snake/server/go.mod | Step 6 Go module dependencies |
| step-06-snake/server/main.go | Step 6 snake mechanics + food + state broadcast |
| step-06-snake/client/index.html | Step 6 client rendering + scoreboard |
| step-07-lag/README.md | Step 7 explanation (latency/jitter) |
| step-07-lag/server/go.mod | Step 7 Go module dependencies |
| step-07-lag/server/main.go | Step 7 server-side delay simulation + debug fields |
| step-07-lag/client/index.html | Step 7 client stats panel + interval graph |
| step-08-interpolation/README.md | Step 8 explanation (interpolation) |
| step-08-interpolation/server/go.mod | Step 8 Go module dependencies |
| step-08-interpolation/server/main.go | Step 8 server emitting tick/time for interpolation experiments |
| step-08-interpolation/client/index.html | Step 8 side-by-side raw vs interpolated rendering |
| step-09-rooms/README.md | Step 9 explanation (rooms) |
| step-09-rooms/server/go.mod | Step 9 Go module dependencies |
| step-09-rooms/server/main.go | Step 9 room partitioning + per-room loop |
| step-09-rooms/client/index.html | Step 9 client showing room metadata + gameplay |
| step-10-reconnect/README.md | Step 10 explanation (heartbeat/reconnect patterns) |
| step-10-reconnect/server/go.mod | Step 10 Go module dependencies |
| step-10-reconnect/server/main.go | Step 10 heartbeat ping/pong + disconnection detection |
| step-10-reconnect/client/index.html | Step 10 client with auto-reconnect + status/log |
| step-11-optimization/README.md | Step 11 explanation (message size / delta concepts) |
| step-11-optimization/server/go.mod | Step 11 Go module dependencies |
| step-11-optimization/server/main.go | Step 11 state size measurement + stats message |
| step-11-optimization/client/index.html | Step 11 client stats panel + size graph |
| step-12-advanced/README.md | Step 12 advanced discussion (multi-server, Redis Pub/Sub, alternatives) |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| data, _ := json.Marshal(GameState{Type: "state", Snakes: sc}) | ||
| for _, conn := range cc { | ||
| conn.WriteMessage(websocket.TextMessage, data) | ||
| } |
There was a problem hiding this comment.
broadcastState() と ping 送信用 goroutine が同じ *websocket.Conn に対して同時に WriteMessage を呼びうるため、gorilla/websocket の「同一コネクションへの同時書き込み禁止」に抵触します(データ競合/破損/panic の原因)。各接続ごとに write を 1 goroutine に集約するか、sync.Mutex 等で write を直列化し、ping は WriteControl(+ write deadline)で送る構成にしてください。
| case <-ticker.C: | ||
| // PingMessage を送信 | ||
| if err := conn.WriteMessage(websocket.PingMessage, nil); err != nil { | ||
| log.Printf("ping 送信エラー: %v", err) | ||
| return |
There was a problem hiding this comment.
ping 送信が conn.WriteMessage(websocket.PingMessage, ...) を呼ぶ一方で、他 goroutine(broadcastState や ack 送信)が同じ conn に書き込む可能性があり、同時 write になります。各接続で write を 1 goroutine に集約するか、write 用 mutex で WriteMessage/WriteControl を直列化してください。
| fullMsg := FullState{Type: "state", Snakes: sc} | ||
| fullData, _ := json.Marshal(fullMsg) | ||
|
|
There was a problem hiding this comment.
fullData, _ := json.Marshal(fullMsg) として Marshal エラーを無視しています。失敗すると fullData が nil(len=0)になり、以降の統計や送信が壊れるので、エラーを扱って失敗時はログ + 送信スキップ等にしてください。
| log.Printf("tick %d: full=%d bytes, light=%d bytes, ratio=%.1f%%, players=%d", | ||
| tick, len(fullData), len(lightData), | ||
| float64(len(lightData))/float64(len(fullData))*100, | ||
| count) |
There was a problem hiding this comment.
この ratio 計算は len(fullData) が 0 の場合に 0 除算になります(上で Marshal エラーを無視しているので発生し得ます)。fullSize > 0 を確認してから計算するか、0 の場合は ratio を 0/NA 扱いにしてください。
| // 全クライアントに送信(全量版 + 統計) | ||
| for _, conn := range cc { | ||
| conn.WriteMessage(websocket.TextMessage, fullData) | ||
| conn.WriteMessage(websocket.TextMessage, statsData) | ||
| } |
There was a problem hiding this comment.
クライアント送信で WriteMessage の戻り値エラーを無視しているため、切断済み接続が clients に残り続けたり、送信失敗が検知できません。送信エラー時はその接続を close して map から削除する等の処理を入れると、教材としても「切断処理」の重要性が伝わりやすいです。
| clients[snakeID] = conn | ||
| mu.Unlock() |
There was a problem hiding this comment.
clients[snakeID] = conn を先に登録してから ack を WriteMessage しているため、別 goroutine の gameLoop() が broadcastState() で同じ conn に書き込む可能性があり、接続直後に同時 write が発生します。ack 送信後に clients 登録するか、write を直列化してください。
| snakes[id] = &Snake{ID: id, Body: body, Direction: "right", Color: color} | ||
| clients[id] = conn | ||
| mu.Unlock() |
There was a problem hiding this comment.
clients[id] = conn を行った直後に ack を WriteMessage しているため、別 goroutine の gameLoop() が broadcastState() で同じ conn に書き込む可能性があり、接続直後に同時 write になります。ack 送信後に clients に登録するか、write を直列化してください。
| room.addPlayer(snakeID, conn, snake) | ||
| log.Printf("プレイヤー %s が部屋 %s に入室(現在%d人)", snakeID, room.ID, room.PlayerCount()) | ||
|
|
||
| // 自分のID と 部屋IDを通知 | ||
| ack, _ := json.Marshal(GameState{ |
There was a problem hiding this comment.
room.addPlayer(..., conn, ...) で接続を部屋の Clients に登録した直後に ack を conn.WriteMessage していますが、部屋のゲームループ(別 goroutine)が同じ conn に broadcastState() で書き込み得るため、接続直後に同時 write が起きます。ack 送信後に Clients 登録するか、write を直列化してください。
| snakes[id] = &FullSnake{ID: id, Body: body, Direction: "right", Color: color} | ||
| clients[id] = conn | ||
| mu.Unlock() | ||
|
|
||
| ack, _ := json.Marshal(FullState{Type: "state", MyID: id, Snakes: map[string]*FullSnake{}}) |
There was a problem hiding this comment.
clients[id] = conn で登録後すぐに ack を conn.WriteMessage しているため、別 goroutine の gameLoop() が broadcastWithStats() で同じ conn に書き込む可能性があり、接続直後に同時 write が起きます。ack 送信後に clients 登録するか、write を直列化してください。
| func generatePlayerID() string { | ||
| idCounter++ | ||
| return fmt.Sprintf("player-%d", idCounter) | ||
| } |
There was a problem hiding this comment.
generatePlayerID() がグローバル変数 idCounter をロックなしでインクリメントしているため、同時接続時にデータレース/ID重複が起こり得ます。mu で保護するか、atomic.AddInt64 などのアトミック操作にしてください。
There was a problem hiding this comment.
Actionable comments posted: 12
Note
Due to the large number of review comments, Critical severity comments were prioritized as inline comments.
🟠 Major comments (14)
step-08-interpolation/client/index.html-41-46 (1)
41-46:⚠️ Potential issue | 🟠 Majorサーバー座標系(800x600)とキャンバスサイズ(390x300)が不一致です。
step-08-interpolation/server/main.goの Line 29-30 と不整合で、描画が途中で切れます。少なくとも同一サイズに揃えるか、座標スケーリングを入れてください。🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@step-08-interpolation/client/index.html` around lines 41 - 46, The client canvas elements rawCanvas and interpCanvas are sized 390x300 but the server sends coordinates in an 800x600 coordinate system, causing clipped/incorrect drawing; either set both canvases to 800x600 to match the server coordinates or implement client-side scaling: compute scaleX = canvas.width / 800 and scaleY = canvas.height / 600 and multiply incoming x/y before drawing (apply same scaling for interpolation logic), and ensure any hit-testing or bounds checks use the scaled coordinates so rendering is not cut off.step-08-interpolation/client/index.html-89-91 (1)
89-91:⚠️ Potential issue | 🟠 Major接続URLの固定値と多重接続を同時に防いでください。
ws://localhost:8080/ws固定だと配信環境で壊れます。またonopen前に連打すると複数接続が張られます。🔧 提案差分
btnConn.addEventListener('click', () => { - ws = new WebSocket('ws://localhost:8080/ws'); + if (ws && (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING)) return; + const scheme = location.protocol === 'https:' ? 'wss' : 'ws'; + ws = new WebSocket(`${scheme}://${location.host}/ws`);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@step-08-interpolation/client/index.html` around lines 89 - 91, btnConn のクリックハンドラで直接 ws = new WebSocket('ws://localhost:8080/ws') としているため接続URLが固定され配信環境で壊れるのと、onopen 前に連打すると複数接続が張られる問題があります。修正: 接続先は location.protocol と location.host を使って動的に組み立てる(例: (location.protocol === 'https:' ? 'wss:' : 'ws:') + '//' + location.host + '/ws')し、既存の ws が存在して readyState が CONNECTING(0) や OPEN(1) の場合は新しく作らない(既存を使うか一度 close() してから再接続する)、またはボタンを一時無効化して onopen/onclose で状態を管理するように実装してください(参照: btnConn, ws, WebSocket, onopen, onclose)。step-08-interpolation/server/main.go-20-22 (1)
20-22:⚠️ Potential issue | 🟠 Major
CheckOriginが全許可になっており、クロスサイト接続を無制限に許可しています。Line 21 の
CheckOrigin: func(r *http.Request) bool { return true }は、学習用の環境であってもセキュリティ上危険なデフォルトです。許可するOriginを明示的に制限してください。🔧 提案差分
var upgrader = websocket.Upgrader{ - CheckOrigin: func(r *http.Request) bool { return true }, + CheckOrigin: func(r *http.Request) bool { + origin := r.Header.Get("Origin") + return origin == "http://localhost:8080" || origin == "http://127.0.0.1:8080" + }, }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@step-08-interpolation/server/main.go` around lines 20 - 22, The current websocket.Upgrader named upgrader uses CheckOrigin: func(r *http.Request) bool { return true }, which allows all cross-origin connections; replace this with a CheckOrigin implementation that validates r.Header.Get("Origin") against a whitelist of allowed origins (e.g., a slice or map of strings loaded from constants or environment config) and only returns true when the Origin header matches an entry; locate websocket.Upgrader upgrader and change CheckOrigin to call a helper like isAllowedOrigin(origin string) or allowedOrigin(r *http.Request) that performs the whitelist check and rejects everything else.step-08-interpolation/server/main.go-166-169 (1)
166-169:⚠️ Potential issue | 🟠 MajorWriteMessageのエラーを無視しており、切断済みクライアントが残留したまま毎tickで送信失敗が繰り返されます。
WriteMessageが失敗した接続は、handleWebSocketのReadMessageでエラーが検出されるまで、clientsマップに残ったままになります。その間、毎tickで同じ接続への送信が失敗し続けます。失敗時は接続をクローズして、clientsマップから即座に除去してください。
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@step-08-interpolation/server/main.go` around lines 166 - 169, WriteMessage errors are being ignored causing disconnected sockets to stay in the clients map and repeatedly fail; when websocket.WriteMessage returns an error you must close that connection and immediately remove it from the shared clients map (the same map used in handleWebSocket/ReadMessage) under the clients mutex. Modify the broadcast loop (where data is written to each conn from clientsCopy) to check the error return, call conn.Close(), acquire the clients mutex, delete the corresponding entry from clients, release the mutex, and continue so failed connections are cleaned up immediately.step-08-interpolation/server/main.go-214-218 (1)
214-218:⚠️ Potential issue | 🟠 Major
directionの許可値チェックが不足しています。逆方向禁止だけでは不十分で、
up/down/left/right以外を弾く必要があります。現在は不正値で移動計算が崩れます。🔧 提案差分
if msg.Type == "input" { + valid := map[string]bool{"up": true, "down": true, "left": true, "right": true} + if !valid[msg.Direction] { + continue + } opposites := map[string]string{"up": "down", "down": "up", "left": "right", "right": "left"} mu.Lock() if s, ok := snakes[id]; ok && opposites[s.Direction] != msg.Direction { s.Direction = msg.Direction } mu.Unlock() }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@step-08-interpolation/server/main.go` around lines 214 - 218, The code only prevents reversing direction but doesn't validate that msg.Direction is one of the allowed values; update the check around the opposites map and snake update (the block using opposites, mu.Lock(), snakes[id], s.Direction and msg.Direction) to first verify msg.Direction is in the allowed set {"up","down","left","right"} (e.g., by checking membership in the opposites map) and only then apply the existing reverse-direction guard before assigning s.Direction; ensure you still hold mu while reading/updating snakes[id].step-06-snake/client/index.html-67-70 (1)
67-70:⚠️ Potential issue | 🟠 Major接続ボタン連打でソケットを多重生成できます。
Line 67 で
CONNECTING/OPENのガードがないため、onopen前の連打で接続リークが起きます。接続開始時点で再入防止を入れてください。🔧 修正例
btnConn.addEventListener('click', () => { + if (ws && (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING)) { + return; + } + btnConn.disabled = true; ws = new WebSocket('ws://localhost:8080/ws'); ws.onopen = () => setUI(true);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@step-06-snake/client/index.html` around lines 67 - 70, ボタン連打で複数の WebSocket が作られる問題は、btnConn の click ハンドラで既に接続中か開いているかをチェックしていないため発生しています;btnConn の click ハンドラ内でグローバル変数 ws の readyState を確認し(WebSocket.CONNECTING / WebSocket.OPEN)、接続中または既に開いている場合は新規接続処理を無視するか btnConn を一時無効化してから ws = new WebSocket(...) を作成し、onopen/onclose で setUI とボタン有効化を制御するように変更してください(参照シンボル: btnConn, ws, WebSocket, setUI)。step-06-snake/server/main.go-242-246 (1)
242-246:⚠️ Potential issue | 🟠 Majorブロードキャストの同期的な書き込みがゲームループをブロックする可能性があります。
Line 242 の
json.Marshalと Line 245 のconn.WriteMessageのエラーが無視されています。より深刻なのは、broadcastState()が 150ms ごとのゲームループから同期的に呼ばれ、遅延クライアントへの WriteMessage が他すべてのプレイヤーへの配信を遅延させることです。SetWriteDeadlineでタイムアウトを設定し、失敗した接続をclientsマップから削除してください。🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@step-06-snake/server/main.go` around lines 242 - 246, broadcastState() currently ignores errors from json.Marshal and conn.WriteMessage and performs synchronous writes which can block the 150ms game loop; change it to set a per-connection write deadline using conn.SetWriteDeadline before each WriteMessage, check and handle errors from json.Marshal and conn.WriteMessage, and if a WriteMessage or deadline set fails remove that connection from the shared clients map (safely, e.g. via the same mutex used when iterating clients). Ensure json.Marshal error is handled (return/log and skip broadcasting on failure) and treat per-connection write failures as dead connections to be cleaned up to avoid blocking the loop.step-10-reconnect/README.md-71-77 (1)
71-77:⚠️ Potential issue | 🟠 Majorping ループは接続終了時に停止する必要があります。
Line 71-77 の例は WriteMessage のエラーをチェックしておらず、接続が切断された後も goroutine が実行され続けるため、goroutine リークのリスクがあります。
ticker.Stop()を defer で呼び出し、write エラー時に goroutine を終了させるべきです。go func() { ticker := time.NewTicker(pingInterval) defer ticker.Stop() for { select { case <-ticker.C: if err := conn.WriteMessage(websocket.PingMessage, nil); err != nil { return } } } }()🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@step-10-reconnect/README.md` around lines 71 - 77, The ping goroutine uses a Ticker and calls conn.WriteMessage(websocket.PingMessage, nil) but doesn't check for errors or stop the ticker, causing a goroutine leak when the connection closes; update the anonymous goroutine that creates ticker := time.NewTicker(pingInterval) to defer ticker.Stop(), perform the WriteMessage call inside a select or directly check its returned error, and return from the goroutine when WriteMessage returns an error so the goroutine exits cleanly on connection close.step-01-echo/server/main.go-16-18 (1)
16-18:⚠️ Potential issue | 🟠 Major
CheckOrigin: return trueは CSRF 脆弱性を招くため避けてください。gorilla/websocket 公式ドキュメントは、CheckOrigin で「リクエストオリジンを慎重に検証して CSRF を防ぐ」ことを明記しており、すべてのオリジンを許可する実装は本番環境で推奨されていません。教材コードでも localhost に制限するのが安全な初期値です。
提案差分
var upgrader = websocket.Upgrader{ CheckOrigin: func(r *http.Request) bool { - return true + origin := r.Header.Get("Origin") + return origin == "http://localhost:8080" || + origin == "http://127.0.0.1:8080" }, }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@step-01-echo/server/main.go` around lines 16 - 18, The current CheckOrigin function unconditionally returns true and allows any origin; replace it with origin validation that only permits trusted origins (e.g., localhost or an explicit allowlist). In the websocket Upgrader's CheckOrigin callback, read r.Header.Get("Origin") and compare it against a set of allowed origins (or validate the host is "localhost" / "127.0.0.1") and return true only on match; otherwise return false. Update the Upgrader's CheckOrigin implementation (the CheckOrigin function) to perform this check so CSRF is mitigated.step-07-lag/server/main.go-124-140 (1)
124-140:⚠️ Potential issue | 🟠 Major未知の
directionを弾かないと蛇が(0,0)に飛びます。ここは逆方向だけを拒否しているので、
"foo"や空文字でもs.Directionに入ります。するとmoveSnake()のswitchがどのcaseにも入らず、次 tick でゼロ値のnewHeadが使われます。up/down/left/rightだけを受け付けてください。最小修正例
if msg.Type == "input" { + switch msg.Direction { + case "up", "down", "left", "right": + default: + continue + } opposites := map[string]string{ "up": "down", "down": "up", "left": "right", "right": "left", }Also applies to: 241-249
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@step-07-lag/server/main.go` around lines 124 - 140, The moveSnake function currently assumes snake.Direction is one of "up","down","left","right" and falls back to a zero-valued newHead for unknown values; update moveSnake to explicitly validate Direction and return early (or ignore the tick) when it is not one of the four allowed values, e.g., check snake.Direction against the set {"up","down","left","right"} before the switch and bail out if invalid; also apply the same strict validation where direction changes are handled (the reverse-direction check code referenced around lines 241-249) so only these four literals can be assigned to Snake.Direction.step-03-input/server/main.go-96-117 (1)
96-117:⚠️ Potential issue | 🟠 Major参加/離脱が他クライアントへ反映されません。
このステップは入力時にしか
broadcast()していないため、接続直後のクライアントは既存inputsを受け取れず、既存クライアントも新規参加や切断を次の入力まで知れません。tick 配信がない構成なので、参加/離脱時にもスナップショットを送ってください。Also applies to: 144-145
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@step-03-input/server/main.go` around lines 96 - 117, The server only calls broadcast() on input messages, so new connections and disconnects are not propagated; after registering a new client in the connection handler (where you set clients[playerID] and playerInputs[playerID] and send the ack via ServerMessage) call the same broadcast() path to send a full snapshot to all clients so the newcomer receives current inputs and others see the join; likewise, inside the deferred disconnect cleanup (where you delete from clients and playerInputs) invoke broadcast() after the deletion to notify others of the leave (ensure broadcast() reads the current playerInputs map under mu to build the snapshot).step-07-lag/server/main.go-95-111 (1)
95-111:⚠️ Potential issue | 🟠 Majorジッター模擬がサーバー tick 自体を止めています。
ここで
time.Sleepすると通信遅延ではなくgameLoop()全体が停止します。delay > tickRateでは ticker の tick も取りこぼすので、全クライアントのゲーム速度まで変わってしまいます。遅らせたいのが配信だけなら、その tick のスナップショットを別 goroutine / 送信キューで遅延送信してください。🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@step-07-lag/server/main.go` around lines 95 - 111, The current gameLoop uses time.Sleep inside the tick loop which blocks the ticker and halts updateGame; instead, capture the tick's state snapshot immediately after updateGame (use the same variables/struct you pass to broadcastState), then enqueue or spawn a separate goroutine to perform the simulated send delay and call broadcastState with that snapshot; keep the ticker loop non-blocking (remove time.Sleep from gameLoop), reference gameLoop, updateGame, broadcastState, ticker and maxRandomDelayMs and implement a send-queue or goroutine-per-tick that sleeps and sends so ticks are never lost.step-05-game-loop/server/main.go-132-160 (1)
132-160:⚠️ Potential issue | 🟠 Majortick ループでブロッキングな送信を直列実行しないでください。
broadcastState()は game loop の中で全接続へ順番にWriteMessageしていますが、write deadline も失敗時の退避もありません。1 本の遅い接続で tick が止まり、全プレイヤーの進行が巻き込まれます。送信は専用 goroutine に切り出すか、最低でも deadline を付けて失敗接続を外してください。🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@step-05-game-loop/server/main.go` around lines 132 - 160, broadcastState currently does blocking sequential WriteMessage calls that can stall the game loop; for each connection (from clients or clientsCopy) set a short write deadline (conn.SetWriteDeadline(time.Now().Add(...))) before WriteMessage, check the error return and on failure close the connection and remove its entry from the shared clients map under mu.Lock (use mu.Lock()/Unlock() when mutating clients), and optionally offload sends to per-connection goroutines so slow clients don't block the tick loop—ensure you still enforce the deadline and clean up closed connections (references: broadcastState, clients/clientsCopy, WriteMessage, mu, tickCount, ServerMessage).step-10-reconnect/server/main.go-98-113 (1)
98-113:⚠️ Potential issue | 🟠 Major
directionのホワイトリスト検証が抜けています。逆方向以外は何でも通るので、不正値で
s.Directionが壊れます。moveSnake()は default を持たないため、次 tick でnhのゼロ値(0,0)が使われます。Step 05 と同様にup/down/left/rightのみ受け付けてください。最小修正例
if msg.Type == "input" { + switch msg.Direction { + case "up", "down", "left", "right": + default: + continue + } opposites := map[string]string{ "up": "down", "down": "up", "left": "right", "right": "left", }Also applies to: 239-247
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@step-10-reconnect/server/main.go` around lines 98 - 113, The switch on s.Direction in moveSnake must whitelist allowed directions and avoid falling through to a zero-value nh; update the switch in moveSnake to explicitly handle only "up", "down", "left", "right" and add a default branch that returns (or otherwise aborts movement) if s.Direction is invalid so nh is never left as (0,0); apply the same whitelist+default fix to the other identical direction-switch block elsewhere in this file (the other snake-movement switch around the later movement code).
🟡 Minor comments (12)
step-08-interpolation/client/index.html-119-120 (1)
119-120:⚠️ Potential issue | 🟡 Minor初回ACK受信時に
tickがundefined表示になります。
tickCountが数値のときだけUIを更新してください。🔧 提案差分
- tickEl.textContent = msg.tickCount; + if (typeof msg.tickCount === 'number') { + tickEl.textContent = String(msg.tickCount); + }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@step-08-interpolation/client/index.html` around lines 119 - 120, 初回ACKで msg.tickCount が undefined のときに UI を更新してしまうため、tickEl.textContent = msg.tickCount の代わりに msg.tickCount が数値であることを確認してから更新するよう修正してください(例: typeof msg.tickCount === 'number' または Number.isFinite(msg.tickCount) を使ってチェック)。対象箇所は tickEl.textContent を代入している行(tickEl.textContent = msg.tickCount;)を探して、条件判定を追加して数値の場合のみ textContent を設定するようにしてください。step-08-interpolation/client/index.html-123-129 (1)
123-129:⚠️ Potential issue | 🟡 Minor切断時にID/Tick/補間係数の表示をリセットしてください。
状態は消えているのに表示だけ残るので、再接続時に混乱しやすいです。
🔧 提案差分
ws.onclose = () => { setUI(false); ws = null; + myId = null; rawState = { snakes: {} }; prevSnapshot = null; nextSnapshot = null; + myIdEl.textContent = 'なし'; + tickEl.textContent = '0'; + tEl.textContent = '0.00'; };🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@step-08-interpolation/client/index.html` around lines 123 - 129, ws.onclose ハンドラで接続切断時に表示だけ残る「ID / Tick / 補間係数」をリセットしていないため、再接続時に混乱します。ws.onclose の中(または setUI(false) を呼ぶ直後)で画面表示をクリアする処理を追加してください:具体的には ws.onclose ハンドラ内で ID 表示、Tick 表示、補間係数(alpha) 表示に対応する DOM 要素や表示変数を空文字またはデフォルト値に設定してクリアする(あるいは setUI を拡張してこれらのフィールドもリセットする)ように修正してください。参照箇所:ws.onclose ハンドラ、setUI、prevSnapshot/nextSnapshot をリセットしている箇所。step-06-snake/client/index.html-97-104 (1)
97-104:⚠️ Potential issue | 🟡 Minor切断後の表示リセットが不足しています。
Line 97 以降で state は空に戻していますが、
myScore・playerCount・scoreboardが前回値のまま残ります。切断時にUI表示も初期化してください。🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@step-06-snake/client/index.html` around lines 97 - 104, In the ws.onclose handler you reset state and UI partially but forget to clear myScore, playerCount and the scoreboard display; update the ws.onclose block (the handler for ws.onclose) to reset myScore and playerCount variables to their initial values, clear or rebuild the scoreboard DOM (scoreboard element) and any score display elements (e.g., myScoreEl, playerCountEl or equivalents), and call setUI(false) / draw() after those resets so the UI shows the initial empty state on disconnect.step-11-optimization/README.md-15-15 (1)
15-15:⚠️ Potential issue | 🟡 Minorコードフェンスに言語指定を追加してください(MD040)。
Line 15 / Line 38 / Line 116 のフェンスは言語未指定です。
textなどを付けて markdownlint 警告を解消してください。📝 修正例
-``` +```text ... -``` +```Also applies to: 38-38, 116-116
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@step-11-optimization/README.md` at line 15, README.md contains three fenced code blocks that use plain ``` without a language tag; update each of those fences (the plain "```" occurrences in the step-11-optimization README) to include a language specifier such as "text" (i.e., change ``` to ```text) so markdownlint MD040 warnings are resolved.step-06-snake/server/main.go-17-19 (1)
17-19:⚠️ Potential issue | 🟡 Minor
CheckOriginの無条件許可は開発用途に限定してください。Step 1 で既に「開発用に全てのオリジンを許可(本番では制限すること)」とコメント記載されていますが、本番環境へのデプロイ時には必ず許可オリジンを明示してください。現在はローカルホストのみ(http://localhost:8080)での使用のため許容範囲ですが、Web に公開する際には以下のように修正が必要です。
修正例
var upgrader = websocket.Upgrader{ - CheckOrigin: func(r *http.Request) bool { return true }, + CheckOrigin: func(r *http.Request) bool { + origin := r.Header.Get("Origin") + return origin == "https://yourdomain.com" + }, }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@step-06-snake/server/main.go` around lines 17 - 19, upgrader の CheckOrigin を無条件許可から限定許可に変更してください: websocket.Upgrader の変数名 upgrader とその CheckOrigin ハンドラを修正して、r.Header.Get("Origin") を取得し whitelist(例: "http://localhost:8080" または環境変数で指定したホスト群)と照合して合致する場合のみ true を返すように実装し、本番では環境変数や設定ファイルで許可オリジンを管理するようにしてください。step-04-server-state/README.md-15-15 (1)
15-15:⚠️ Potential issue | 🟡 Minorコードブロックに言語指定を追加してください(MD040)。
Line 15 / 29 / 46 / 129 のフェンスは
text等を指定してください。ドキュメント品質と CI 安定性が上がります。Also applies to: 29-29, 46-46, 129-129
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@step-04-server-state/README.md` at line 15, README.md の複数のフェンス付きコードブロックに言語指定が抜けているため MarkdownLint(MD040) に引っかかっています。該当するすべてのフェンス付きコードブロックを探し、開始バックティックの直後に適切な言語タグ(例: text, json, js, bash など)を追加して下さい(例: ```text)。これによりドキュメント品質と CI の安定性が改善されます。step-03-input/README.md-16-16 (1)
16-16:⚠️ Potential issue | 🟡 Minorフェンス言語未指定を解消してください(MD040)。
Line 16 / 30 / 57 / 104 / 116 のフェンスは言語指定を付けると lint を通せます(例:
json,go,text)。Also applies to: 30-30, 57-57, 104-104, 116-116
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@step-03-input/README.md` at line 16, README.md の複数のフェンス付きコードブロックが言語指定なしで MD040 を引いているため、該当するフェンス(現在バックティック3つで始まっている未指定のコードブロック)に適切な言語タグ(例: json, go, text など)を追加して lint を通すようにしてください; 具体的には各未指定フェンスの開始行の ``` の後に適切な言語名を付与し(例: ```json)、内容に合わせた言語を選んで保存してください。step-06-snake/README.md-15-15 (1)
15-15:⚠️ Potential issue | 🟡 Minor未指定フェンス言語の追記が必要です(MD040)。
Line 15 / Line 27 / Line 75 / Line 104 のフェンスに言語指定がありません。図やツリーは
text指定で十分です。Also applies to: 27-27, 75-75, 104-104
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@step-06-snake/README.md` at line 15, The README's fenced code blocks that contain diagrams/trees are missing a language tag (MD040); update each bare fence to include a language (use "text" for diagrams/trees) so replace occurrences of triple-backticks like ``` with ```text in the README's fenced blocks (the diagram/tree code blocks) to satisfy the linter.step-02-broadcast/README.md-15-15 (1)
15-15:⚠️ Potential issue | 🟡 Minorコードフェンスに言語指定を付けてください(MD040)。
Line 15 / Line 67 / Line 83 のフェンスが言語未指定です。
textを付けるだけで markdownlint 警告を解消できます。🛠️ 例(抜粋)
-``` +```text クライアントA → サーバー → クライアントA → クライアントB → クライアントC-
+text
step-02-broadcast/
├── README.md
├── server/
│ ├── main.go
│ └── go.mod
└── client/
└── index.html-``` +```text 新しい接続 → clients に追加 → goroutine でメッセージ待機 ↓ メッセージ受信 ↓ 全クライアントに送信 ↓ 切断 → clients から削除</details> Also applies to: 67-67, 83-83 <details> <summary>🤖 Prompt for AI Agents</summary>Verify each finding against the current code and only fix it if needed.
In
@step-02-broadcast/README.mdat line 15, README.md contains triple-backtick
code fences without a language tag; update each offending fence (the three shown
in the diff) by replacing the barewithtext so the markdownlint MD040
warnings are resolved (i.e., change each opening fence liketotext in
the README.md code blocks).</details> </blockquote></details> <details> <summary>step-11-optimization/client/index.html-142-143 (1)</summary><blockquote> `142-143`: _⚠️ Potential issue_ | _🟡 Minor_ **ゼロ除算の可能性があります** `msg.fullSize` が 0 の場合、`msg.lightSize / msg.fullSize` でゼロ除算が発生し、`NaN` や `Infinity` が表示されます。サーバー側で常に正の値が送られることが想定されていても、防御的なチェックを追加することを検討してください。 <details> <summary>🛡️ 修正案</summary> ```diff - const ratio = ((1 - msg.lightSize / msg.fullSize) * 100).toFixed(1); + const ratio = msg.fullSize > 0 + ? ((1 - msg.lightSize / msg.fullSize) * 100).toFixed(1) + : '0.0'; ``` </details> <details> <summary>🤖 Prompt for AI Agents</summary> ``` Verify each finding against the current code and only fix it if needed. In `@step-11-optimization/client/index.html` around lines 142 - 143, The current ratio calculation can divide by zero when msg.fullSize is 0; update the logic around the ratio computation (the lines using msg.lightSize, msg.fullSize and setting ratioEl.textContent) to defensively check msg.fullSize > 0 before doing the division and to assign a sensible fallback (e.g., "0.0" or a placeholder) when fullSize is zero or invalid so ratioEl.textContent never becomes NaN/Infinity. ``` </details> </blockquote></details> <details> <summary>step-07-lag/client/index.html-155-160 (1)</summary><blockquote> `155-160`: _⚠️ Potential issue_ | _🟡 Minor_ **RTT(往復遅延)ではなく片道遅延です。** `Date.now() - msg.serverTime` はサーバー送信時刻からクライアント受信時刻までの片道遅延(+クロックスキュー)を測定しています。コメントの「往復遅延」は技術的に不正確です。実際のRTTを測定するにはクライアントからpingを送信し、pongを受け取る時間を計測する必要があります。 教育目的のコードなので、コメントを「片道遅延(概算)」に修正するか、RTTの意味について注釈を追加することを検討してください。 <details> <summary>📝 コメント修正の提案</summary> ```diff - // 往復遅延の概算(サーバー送信時刻 vs 受信時刻) + // 片道遅延の概算(サーバー送信時刻 vs クライアント受信時刻、クロック差含む) if (msg.serverTime) { - const rtt = Date.now() - msg.serverTime; + const oneWayDelay = Date.now() - msg.serverTime; ``` </details> <details> <summary>🤖 Prompt for AI Agents</summary> ``` Verify each finding against the current code and only fix it if needed. In `@step-07-lag/client/index.html` around lines 155 - 160, The comment is incorrect: Date.now() - msg.serverTime measures one-way delay (plus clock skew), not RTT. Update the comment/label around the code that computes rtt (the rtt variable computed from Date.now() - msg.serverTime and displayed via rttEl.innerHTML) to say "片道遅延(概算)" or add a brief note that this is one-way delay and not true RTT, and optionally mention that true RTT requires a client-side ping/pong measurement; no logic changes needed. ``` </details> </blockquote></details> <details> <summary>step-04-server-state/server/main.go-116-128 (1)</summary><blockquote> `116-128`: _⚠️ Potential issue_ | _🟡 Minor_ **ラップアラウンドの境界値が非対称です。** `X < 0` の場合は `fieldWidth` に設定されますが、これはフィールド外の座標になります(有効範囲は `0` から `fieldWidth - 1`)。一方、`X > fieldWidth` の場合は `0` に設定されます。 一貫性のため、`fieldWidth - moveSpeed` または左端の座標に設定することを検討してください。ただし、視覚的な影響は小さいです。 <details> <summary>📐 境界値の修正提案</summary> ```diff if p.X < 0 { - p.X = fieldWidth + p.X = fieldWidth - moveSpeed } - if p.X > fieldWidth { + if p.X >= fieldWidth { p.X = 0 } if p.Y < 0 { - p.Y = fieldHeight + p.Y = fieldHeight - moveSpeed } - if p.Y > fieldHeight { + if p.Y >= fieldHeight { p.Y = 0 } ``` </details> <details> <summary>🤖 Prompt for AI Agents</summary> ``` Verify each finding against the current code and only fix it if needed. In `@step-04-server-state/server/main.go` around lines 116 - 128, ラップアラウンドの境界が非対称でフィールド外(有効範囲は 0..fieldWidth-1 / 0..fieldHeight-1)を生成しているので、p.X/p.Y のラップ処理を修正してください: 現在の p.X < 0 / p.X > fieldWidth / p.Y < 0 / p.Y > fieldHeight の分岐を、範囲が 0 から fieldWidth-1(または fieldHeight-1)になるように変え、負側は fieldWidth-1 / fieldHeight-1 に設定し、超過側は >= fieldWidth / >= fieldHeight を検出して 0 に戻す(もしくは moveSpeed の影響を考慮するなら fieldWidth - moveSpeed などを代入する)—該当箇所は p.X, p.Y, fieldWidth, fieldHeight(および moveSpeed を使う場合は moveSpeed)を扱うラップアラウンドのブロックを修正してください。 ``` </details> </blockquote></details> </blockquote></details> <details> <summary>🧹 Nitpick comments (21)</summary><blockquote> <details> <summary>README.md (1)</summary><blockquote> `74-96`: **フェンスコードブロックに言語指定を追加してください。** 静的解析ツール(markdownlint)がフェンスコードブロックに言語指定がないことを警告しています。プレーンテキストの場合は `text` または `plaintext` を指定することで、マークダウンレンダリングの一貫性が向上します。 <details> <summary>📝 修正案</summary> ```diff -``` +```text Step 0〜2 → 最速で終わらせる(接続の感触をつかむ) ``` ```diff -``` +```text step-XX-name/ ├── README.md # 概念説明・コード解説・練習問題 ``` </details> <details> <summary>🤖 Prompt for AI Agents</summary>Verify each finding against the current code and only fix it if needed.
In
@README.mdaround lines 74 - 96, Update the README.md fenced code blocks to
include a language token (e.g., text or plaintext) so markdownlint stops
warning; specifically modify the two blocks that show the step descriptions and
the folder tree (the triple-backtick fences surrounding "Step 0〜2 → ..." and the
"step-XX-name/ ├── README.md ..." tree) to start withtext (orplaintext)
instead of just ```, preserving the existing content and spacing.</details> </blockquote></details> <details> <summary>step-03-input/client/index.html (1)</summary><blockquote> `90-107`: **`JSON.parse` にエラーハンドリングを追加することを検討してください。** サーバーから不正なJSONが送信された場合、`JSON.parse` が例外をスローし、`onmessage` ハンドラ全体が失敗します。教材としては現状でも問題ありませんが、堅牢性を高めるためにtry-catchの追加を検討してください。 <details> <summary>🛡️ 修正案</summary> ```diff ws.onmessage = (event) => { - const msg = JSON.parse(event.data); + let msg; + try { + msg = JSON.parse(event.data); + } catch (e) { + console.error('JSONパースエラー:', e); + return; + } if (msg.type === 'ack') { ``` </details> <details> <summary>🤖 Prompt for AI Agents</summary> ``` Verify each finding against the current code and only fix it if needed. In `@step-03-input/client/index.html` around lines 90 - 107, Wrap the JSON.parse call inside the ws.onmessage handler with try-catch to prevent a malformed server message from throwing and breaking the handler: catch the error, optionally log it (e.g., via console.error) and return early so subsequent logic (the ack/state branches that use myId, updatePlayersDisplay, myDirEl, ARROWS) only runs for valid parsed messages; ensure you still handle valid msg.type values as before. ``` </details> </blockquote></details> <details> <summary>step-07-lag/README.md (1)</summary><blockquote> `94-102`: **ファイル構成のコードブロックに言語指定を追加してください。** 静的解析ツール(markdownlint MD040)が言語指定のないフェンスコードブロックを警告しています。 <details> <summary>📝 修正案</summary> ```diff -``` +```text step-07-lag/ ├── README.md ├── server/ │ ├── main.go │ └── go.mod └── client/ └── index.html ``` ``` </details> <details> <summary>🤖 Prompt for AI Agents</summary>Verify each finding against the current code and only fix it if needed.
In
@step-07-lag/README.mdaround lines 94 - 102, The fenced code block in
README.md showing the file tree lacks a language tag which triggers markdownlint
MD040; update that block (the triple-backtick fence surrounding the tree) to
include a language identifier such as "text" (e.g., changetotext) so
the code fence is explicit and the linter warning is resolved.</details> </blockquote></details> <details> <summary>step-02-broadcast/client/index.html (1)</summary><blockquote> `80-94`: **`JSON.parse` にエラーハンドリングを追加することを検討してください。** step-03と同様に、サーバーから不正なJSONが送信された場合に備えてtry-catchの追加を検討してください。 <details> <summary>🛡️ 修正案</summary> ```diff ws.onmessage = (event) => { - // サーバーから受け取るのは JSON 形式 - const msg = JSON.parse(event.data); + // サーバーから受け取るのは JSON 形式 + let msg; + try { + msg = JSON.parse(event.data); + } catch (e) { + addLog('JSONパースエラー', 'error'); + return; + } if (msg.type === 'system') { ``` </details> <details> <summary>🤖 Prompt for AI Agents</summary> ``` Verify each finding against the current code and only fix it if needed. In `@step-02-broadcast/client/index.html` around lines 80 - 94, Wrap the ws.onmessage handler's JSON.parse in a try-catch to guard against invalid JSON: inside ws.onmessage, attempt const msg = JSON.parse(event.data) in a try block and on catch call addLog with a system/error message (e.g., "受信したメッセージの解析に失敗しました") and return early; only access msg.type, addLog(msg.content, 'chat'/'system') and update counter.textContent when parsing succeeds and msg.count is present to avoid runtime errors when msg is malformed. ``` </details> </blockquote></details> <details> <summary>step-10-reconnect/README.md (1)</summary><blockquote> `118-121`: **再接続は固定間隔より指数バックオフ+ジッター推奨です。** Line 120 の固定 3 秒リトライだと同時再接続が集中しやすいです。指数バックオフと上限を入れると実運用に近づきます。 <details> <summary>📶 改善イメージ</summary> ```diff ws.onclose = () => { - setTimeout(connect, 3000); + retryCount++; + const base = Math.min(30000, 1000 * (2 ** retryCount)); + const jitter = Math.floor(Math.random() * 500); + setTimeout(connect, base + jitter); }; ``` </details> <details> <summary>🤖 Prompt for AI Agents</summary> ``` Verify each finding against the current code and only fix it if needed. In `@step-10-reconnect/README.md` around lines 118 - 121, Replace the fixed 3s retry in the ws.onclose handler with an exponential backoff plus jitter strategy: introduce a retry state (e.g., retryDelayMs starting at something like 500ms), cap (maxRetryMs), and multiply delay on each failed connect attempt inside connect(), add randomized jitter (± some percent or random value) to avoid thundering herd, schedule reconnects using the computed delay instead of setTimeout(connect, 3000), and reset retryDelayMs back to initial value when the socket successfully opens (e.g., in ws.onopen). ``` </details> </blockquote></details> <details> <summary>step-01-echo/server/main.go (1)</summary><blockquote> `55-59`: **WebSocket メッセージの受信サイズ上限を設定すると安全です。** 接続アップグレード後、メッセージ受信ループの前に `SetReadLimit` を設定することで、過大フレームによるメモリ圧迫やDoS攻撃のリスクを低減できます。この設定により、制限を超えるメッセージを受信した場合、接続は自動的にWebSocket の close コード 1009(message too big)を送信して接続を閉じます。 <details> <summary>提案差分</summary> ```diff log.Println("新しいクライアントが接続しました") + conn.SetReadLimit(4 * 1024) // 教材用途の小さな上限例 // メッセージを受け取り続けるループ for { ``` </details> <details> <summary>🤖 Prompt for AI Agents</summary> ``` Verify each finding against the current code and only fix it if needed. In `@step-01-echo/server/main.go` around lines 55 - 59, Set a read limit on the WebSocket connection before entering the receive loop to prevent oversized frames from exhausting memory: call conn.SetReadLimit with an appropriate max size (e.g., a few kilobytes) immediately after the upgrade/successful connection and before the for { ... conn.ReadMessage() } loop in main.go so that ReadMessage will close the connection with close code 1009 when a message exceeds the limit. ``` </details> </blockquote></details> <details> <summary>step-01-echo/client/index.html (1)</summary><blockquote> `179-185`: **`ws.send()` 前に `readyState` を確認してください。** Line 184 で `ws` が存在していても、`CONNECTING` 状態なら `InvalidStateError` が発生します。また、`CLOSING` や `CLOSED` 状態ではエラーは起こりませんが、メッセージは無視されます。送信前に `WebSocket.OPEN` の状態を確認することで、これらの問題を防げます。 <details> <summary>🔧 提案差分</summary> ```diff function sendMessage() { const text = input.value.trim(); - if (!text || !ws) return; + if (!text || !ws || ws.readyState !== WebSocket.OPEN) return; // メッセージを送信 ws.send(text); addLog(`送信: ${text}`, 'send'); ``` </details> <details> <summary>🤖 Prompt for AI Agents</summary> ``` Verify each finding against the current code and only fix it if needed. In `@step-01-echo/client/index.html` around lines 179 - 185, The sendMessage function currently calls ws.send(text) without checking the socket state; before sending, check ws.readyState === WebSocket.OPEN to ensure the socket is open, and if not call addLog with a warning (e.g., "送信失敗: 接続が開いていません") or handle reconnection; update the sendMessage function (reference: sendMessage, ws.send, ws.readyState, WebSocket.OPEN, addLog) to perform this readiness check and early-return or log when the socket is not open. ``` </details> </blockquote></details> <details> <summary>step-10-reconnect/client/index.html (2)</summary><blockquote> `136-142`: **再接続ロジックは機能しますが、指数バックオフの検討を** 現在は固定3秒間隔で再接続を試みますが、サーバーがダウンしている場合、頻繁な再接続試行が続きます。教育目的であれば問題ありませんが、本番環境では指数バックオフ(例: 3秒 → 6秒 → 12秒...)が推奨されます。 <details> <summary>💡 指数バックオフの例</summary> ```javascript const BASE_DELAY = 3000; const MAX_DELAY = 30000; // onclose内で if (autoReconnect) { reconnectCount++; const delay = Math.min(BASE_DELAY * Math.pow(2, reconnectCount - 1), MAX_DELAY); addLog(`${delay/1000}秒後に再接続します(${reconnectCount}回目)`, 'info'); reconnectTimer = setTimeout(connect, delay); } ``` </details> <details> <summary>🤖 Prompt for AI Agents</summary> ``` Verify each finding against the current code and only fix it if needed. In `@step-10-reconnect/client/index.html` around lines 136 - 142, The reconnect logic currently always uses RECONNECT_DELAY causing repeated fixed-interval retries; change it to an exponential backoff by introducing constants (e.g., BASE_DELAY and MAX_DELAY), increment reconnectCount as now, compute delay = Math.min(BASE_DELAY * 2 ** (reconnectCount - 1), MAX_DELAY), use that delay for addLog and setTimeout (replacing RECONNECT_DELAY), and ensure reconnectTimer is assigned accordingly; keep existing symbols autoReconnect, reconnectCount, reconnectCountEl, reconnectTimer, and connect so the change is localized. ``` </details> --- `76-83`: **ログ要素の無制限な成長** 長時間の接続/切断サイクルでログエントリが蓄積し続けます。教育目的のデモでは問題になりにくいですが、履歴を制限する(例: 最新100件のみ保持)ことを検討してもよいでしょう。 <details> <summary>🤖 Prompt for AI Agents</summary> ``` Verify each finding against the current code and only fix it if needed. In `@step-10-reconnect/client/index.html` around lines 76 - 83, The addLog function allows unbounded growth of logEl which can exhaust memory over long reconnect cycles; modify addLog (and any code that appends to logEl) to enforce a maximum history (e.g., 100 entries): after appending the new div, if logEl.childElementCount exceeds the cap, remove the oldest children (shift out from the top) until the count is at or below the cap, then keep the existing scroll behavior. Ensure you reference addLog and the logEl element when implementing the trimming logic. ``` </details> </blockquote></details> <details> <summary>step-05-game-loop/README.md (1)</summary><blockquote> `15-17`: **コードブロックの言語指定子** 擬似コードやファイル構成を示すコードブロックに `text` の言語指定子を追加すると、Markdownリンターの警告が解消されます。 Also applies to: 26-34, 85-93 <details> <summary>🤖 Prompt for AI Agents</summary>Verify each finding against the current code and only fix it if needed.
In
@step-05-game-loop/README.mdaround lines 15 - 17, Add the Markdown language
specifier "text" to the code fences that contain pseudo-code or file-structure
snippets (for example the block containing "キー入力 → サーバーへ送信 → サーバーが状態更新 → 全員に配信"
and the other similar blocks later in the README) so the linter stops warning;
update each triple-backtick fence to start with ```text and keep the contents
unchanged.</details> </blockquote></details> <details> <summary>step-09-rooms/README.md (3)</summary><blockquote> `40-51`: **Room構造体に `running` フィールドが欠けています** 実際の実装(`step-09-rooms/server/main.go:73-80`)では `running bool` フィールドが含まれており、これはゲームループの重複起動を防ぐために重要です。ドキュメントでも「`room.running` フラグで管理し、起動済みなら起動しない」と説明しているため、構造体の例に含めると一貫性が保たれます。 <details> <summary>📝 修正案</summary> ```diff type Room struct { ID string Snakes map[string]*Snake Clients map[string]*websocket.Conn Foods []Food mu sync.RWMutex stopCh chan struct{} // ゲームループを止めるためのチャンネル + running bool // ゲームループが起動済みかどうか } ``` </details> <details> <summary>🤖 Prompt for AI Agents</summary> ``` Verify each finding against the current code and only fix it if needed. In `@step-09-rooms/README.md` around lines 40 - 51, The Room struct example is missing the running bool field used to prevent duplicated game-loop starts; update the Room declaration to include a running bool (alongside existing fields ID, Snakes, Clients, Foods, mu, stopCh) and ensure code that starts the loop (e.g., StartGame/Run loop) checks and sets room.running under the room.mu lock to avoid concurrent starts and clears it when the loop stops. ``` </details> --- `65-75`: **`findOrCreateRoom` の例にミューテックスロックが欠けています** 教育目的で簡略化されていますが、実際の実装(`step-09-rooms/server/main.go:244-280`)では `globalMu.Lock()` が必須です。後の「よくある設計ミス」セクションでロック順序の問題を説明しているため、この例でもロックの存在を示した方が学習者の理解に役立ちます。 また、`len(room.Snakes)` は実装の `room.PlayerCount()` メソッドと異なります。 <details> <summary>📝 修正案</summary> ```diff func findOrCreateRoom() *Room { + globalMu.Lock() + defer globalMu.Unlock() + for _, room := range rooms { - if len(room.Snakes) < MaxPlayersPerRoom { + if room.PlayerCount() < MaxPlayersPerRoom { return room } } // 満室なら新しい部屋を作る return createRoom() } ``` </details> <details> <summary>🤖 Prompt for AI Agents</summary> ``` Verify each finding against the current code and only fix it if needed. In `@step-09-rooms/README.md` around lines 65 - 75, The example findOrCreateRoom function is missing the global mutex and uses len(room.Snakes) instead of the room's PlayerCount() method; update findOrCreateRoom to acquire globalMu.Lock() at the start and defer globalMu.Unlock() around the scan/create block, and replace len(room.Snakes) with room.PlayerCount(); keep the call to createRoom() when no room has capacity and ensure the lock covers both the check and the create to match the real implementation. ``` </details> --- `15-27`: **コードブロックに言語指定子を追加することを検討してください** Markdownのコードブロックに言語指定子(例: `text` や `plaintext`)を追加すると、リンターの警告を解消し、一部のレンダラーでの表示も改善されます。 Also applies to: 30-34, 104-110 <details> <summary>🤖 Prompt for AI Agents</summary>Verify each finding against the current code and only fix it if needed.
In
@step-09-rooms/README.mdaround lines 15 - 27, Add language specifiers to the
Markdown code fences in the README code blocks (the fenced blocks that currently
start withand contain the three issue bullets) to silence linter warnings and improve rendering; update each fence (notably the blocks around the problem list and the other ranges called out) to use a neutral specifier like "text" or "plaintext" (i.e., changeto ```text) so the code blocks are explicitly
typed while leaving the block contents unchanged.</details> </blockquote></details> <details> <summary>step-12-advanced/README.md (1)</summary><blockquote> `15-28`: **コードブロックの言語指定子** 一部のコードブロックに `text` または `plaintext` の言語指定子を追加すると、Markdownリンターの警告が解消されます。これは必須ではありませんが、ドキュメントの一貫性向上に役立ちます。 Also applies to: 36-52, 114-124 <details> <summary>🤖 Prompt for AI Agents</summary>Verify each finding against the current code and only fix it if needed.
In
@step-12-advanced/README.mdaround lines 15 - 28, Several fenced code blocks
in the README lack a language specifier (e.g., the block showing the step list
from the diff and the other blocks at ranges 36-52 and 114-124); add a
consistent language tag liketext (orplaintext) to each of those fenced
code blocks so the Markdown linter warnings are resolved and formatting is
consistent across the README.md; update each fence around the step list and the
other mentioned blocks to include the chosen specifier.</details> </blockquote></details> <details> <summary>step-07-lag/client/index.html (1)</summary><blockquote> `196-196`: **未使用の変数 `startIdx` があります。** `startIdx` は宣言されていますが使用されていません。削除するか、意図した用途があれば実装を確認してください。 <details> <summary>🧹 削除提案</summary> ```diff - const startIdx = Math.max(0, intervals.length - MAX_HISTORY); for (let i = 0; i < intervals.length; i++) { ``` </details> <details> <summary>🤖 Prompt for AI Agents</summary> ``` Verify each finding against the current code and only fix it if needed. In `@step-07-lag/client/index.html` at line 196, The local variable startIdx is declared but never used; either remove the declaration or apply it to limit history by slicing intervals. Locate the declaration of startIdx (const startIdx = Math.max(0, intervals.length - MAX_HISTORY)) and either delete that line, or use startIdx when building the history (e.g., replace usages of intervals with intervals.slice(startIdx) where the recent MAX_HISTORY entries are intended) so intervals and MAX_HISTORY are actually applied. ``` </details> </blockquote></details> <details> <summary>step-04-server-state/server/main.go (1)</summary><blockquote> `97-99`: **`WriteMessage` のエラーを無視すると、切断済み接続がマップに残る可能性があります。** 送信に失敗した接続をクリーンアップしないと、`broadcast` が毎回失敗した接続に書き込みを試み、パフォーマンスに影響する可能性があります。教育目的のコードなので許容範囲ですが、Step 2の `broadcast` 実装のようにエラー時に接続を削除するパターンを参照してください。 <details> <summary>♻️ エラーハンドリングの追加</summary> ```diff for _, conn := range clientsCopy { - conn.WriteMessage(websocket.TextMessage, data) + if err := conn.WriteMessage(websocket.TextMessage, data); err != nil { + log.Printf("送信エラー: %v", err) + // Note: クリーンアップはhandleWebSocketのdeferで行われる + } } ``` </details> <details> <summary>🤖 Prompt for AI Agents</summary> ``` Verify each finding against the current code and only fix it if needed. In `@step-04-server-state/server/main.go` around lines 97 - 99, The loop that sends data with conn.WriteMessage ignores errors and can leave disconnected clients in the clients map; update the broadcast/send loop (the for _, conn := range clientsCopy { conn.WriteMessage(websocket.TextMessage, data) }) to check the returned error, and on error remove that client from the underlying clients map (the same pattern used in the earlier broadcast implementation) and close the connection so failed connections are cleaned up and not retried on subsequent broadcasts. ``` </details> </blockquote></details> <details> <summary>step-09-rooms/server/main.go (1)</summary><blockquote> `164-168`: **重複したコードブロックがあります。** 両方の条件分岐で同じ処理(尾の切り詰め)を行っています。条件を簡略化できます。 <details> <summary>♻️ 条件の簡略化</summary> ```diff - if !ate && len(snake.Body) > initialSnakeLen { - snake.Body = snake.Body[:len(snake.Body)-1] - } else if !ate { + if !ate { snake.Body = snake.Body[:len(snake.Body)-1] } ``` </details> <details> <summary>🤖 Prompt for AI Agents</summary> ``` Verify each finding against the current code and only fix it if needed. In `@step-09-rooms/server/main.go` around lines 164 - 168, The two branches both trim the snake tail, so simplify the logic by removing the duplicated branch and just trim when the snake did not eat: replace the if/else-if that checks !ate and len(snake.Body) > initialSnakeLen with a single check for !ate (and ensure you guard against empty body before slicing); update the code that references snake.Body, ate, and initialSnakeLen to perform a single snake.Body = snake.Body[:len(snake.Body)-1] only when !ate and length > 0. ``` </details> </blockquote></details> <details> <summary>step-11-optimization/server/main.go (2)</summary><blockquote> `203-206`: **`WriteMessage` のエラーを無視しています。** 他のステップと同様、送信エラーのログ出力を追加することを検討してください。ゲームループからの送信なので、エラー時の接続クリーンアップは `handleWebSocket` の defer で行われますが、デバッグ目的でのログ出力は有用です。 <details> <summary>🤖 Prompt for AI Agents</summary> ``` Verify each finding against the current code and only fix it if needed. In `@step-11-optimization/server/main.go` around lines 203 - 206, The loop calling conn.WriteMessage on each connection (variables cc, fullData, statsData, and websocket.TextMessage) currently ignores returned errors; update it to check the error return from each WriteMessage and log failures (including which payload—fullData or statsData—and identifying info about the connection) so send failures are visible for debugging; connection cleanup can remain handled by handleWebSocket's defer, but ensure you log the WriteMessage error with context to aid investigation. ``` </details> --- `167-206`: **軽量版データ(lightData)は計算されますが送信されていません。** `lightData` はサイズ比較のために計算されていますが、実際にクライアントに送信されるのは `fullData` のみです。これが意図的な設計(サイズ比較のデモ目的)であれば問題ありませんが、README や実装コメントで明示すると学習者の混乱を避けられます。 <details> <summary>📝 コメント追加の提案</summary> ```diff + // 注意: 軽量版はサイズ比較のために計算するが、実際には送信しない + // 教育目的でサイズ削減効果を可視化するためのデモ // 全クライアントに送信(全量版 + 統計) for _, conn := range cc { conn.WriteMessage(websocket.TextMessage, fullData) ``` </details> <details> <summary>🤖 Prompt for AI Agents</summary> ``` Verify each finding against the current code and only fix it if needed. In `@step-11-optimization/server/main.go` around lines 167 - 206, The code builds a lightweight payload (lightData from lightMsg/LightState) but only sends fullData to clients (loop over cc using conn.WriteMessage), which is confusing; either send the lightweight payload to clients or clearly document that lightData is only computed for size comparison. Fix by one of two options: 1) If you intend to deliver the light payload, add a send call alongside fullData in the client broadcast loop (use conn.WriteMessage(websocket.TextMessage, lightData) referencing cc and conn.WriteMessage), or 2) if it’s only for measurement, add a brief comment above the lightData/lightMsg construction and/or update README to state that lightData is computed only for size comparison and intentionally not transmitted. Ensure references to lightData, lightMsg/LightState, fullData, cc, and conn.WriteMessage are updated accordingly. ``` </details> </blockquote></details> <details> <summary>step-01-echo/README.md (1)</summary><blockquote> `70-77`: **フェンスドコードブロックに言語指定を追加することを検討してください。** 静的解析ツールがMD040警告を出しています。疑似コードやフロー説明でも、`text`や`plaintext`を指定するとリンターの警告が解消されます。 <details> <summary>📝 提案</summary> ```diff -``` +```text 接続の流れ: 1. クライアントが ws://localhost:8080/ws に接続を要求 ``` </details> <details> <summary>🤖 Prompt for AI Agents</summary>Verify each finding against the current code and only fix it if needed.
In
@step-01-echo/README.mdaround lines 70 - 77, Update the fenced code block
that begins with "" before the Japanese flow text "接続の流れ:" to include a language specifier (e.g.,text orplaintext) so the lint warning MD040 is resolved; edit the README.md block containing the flow steps (the triple-backtick block with the lines "1. クライアントが ws://localhost:8080/ws に接続を要求" through "5. 受け取ったメッセージをそのまま返す") and change the opening fence totext (orstep-02-broadcast/server/main.go (1)
136-141:getClientCount()とbroadcastの間で競合状態が発生する可能性があります。ログ出力(Line 136)と
getClientCount()(Line 140)の間で接続数が変わる可能性があります。教育目的のコードでは許容範囲ですが、実運用では単一のロックスコープ内で両方の操作を行うことを検討してください。🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@step-02-broadcast/server/main.go` around lines 136 - 141, The log and broadcast call can observe a different client count due to a race between log.Printf, getClientCount() and broadcast; fix by performing the client-count read and the broadcast under the same lock or atomic snapshot: acquire the same clients mutex used by getClientCount() (or add a function like broadcastWithLockedCount) and while holding it compute the count and call broadcast (or call broadcast with the precomputed count) so log.Printf, count computation (getClientCount) and the Message send occur within a single critical section; update references to getClientCount(), broadcast and Message accordingly.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 9a4ea5eb-b266-4b2b-b1fc-57cb11cf8739
📒 Files selected for processing (47)
README.mdstep-00-http-vs-websocket/README.mdstep-01-echo/README.mdstep-01-echo/client/index.htmlstep-01-echo/server/go.modstep-01-echo/server/main.gostep-02-broadcast/README.mdstep-02-broadcast/client/index.htmlstep-02-broadcast/server/go.modstep-02-broadcast/server/main.gostep-03-input/README.mdstep-03-input/client/index.htmlstep-03-input/server/go.modstep-03-input/server/main.gostep-04-server-state/README.mdstep-04-server-state/client/index.htmlstep-04-server-state/server/go.modstep-04-server-state/server/main.gostep-05-game-loop/README.mdstep-05-game-loop/client/index.htmlstep-05-game-loop/server/go.modstep-05-game-loop/server/main.gostep-06-snake/README.mdstep-06-snake/client/index.htmlstep-06-snake/server/go.modstep-06-snake/server/main.gostep-07-lag/README.mdstep-07-lag/client/index.htmlstep-07-lag/server/go.modstep-07-lag/server/main.gostep-08-interpolation/README.mdstep-08-interpolation/client/index.htmlstep-08-interpolation/server/go.modstep-08-interpolation/server/main.gostep-09-rooms/README.mdstep-09-rooms/client/index.htmlstep-09-rooms/server/go.modstep-09-rooms/server/main.gostep-10-reconnect/README.mdstep-10-reconnect/client/index.htmlstep-10-reconnect/server/go.modstep-10-reconnect/server/main.gostep-11-optimization/README.mdstep-11-optimization/client/index.htmlstep-11-optimization/server/go.modstep-11-optimization/server/main.gostep-12-advanced/README.md
| github.com/mattn/go-isatty v0.0.20 // indirect | ||
| github.com/valyala/bytebufferpool v1.0.0 // indirect | ||
| github.com/valyala/fasttemplate v1.2.2 // indirect | ||
| golang.org/x/crypto v0.17.0 // indirect |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
golang.org/x/crypto v0.17.0 vulnerability CRITICAL CVE
💡 Result:
golang.org/x/crypto v0.17.0 is affected by a critical vulnerability CVE-2024-45337 (GO-2024-3321, GHSA-v778-237x-gjrc). This is an authorization bypass in the SSH package due to misuse of ServerConfig.PublicKeyCallback. Affected versions are before v0.31.0. CVSS score: 9.1 (CRITICAL). Multiple projects and scanners flag v0.17.0 specifically as vulnerable to this CVE. No other critical CVEs are explicitly tied to exactly v0.17.0; later CVEs like CVE-2025-47914 and CVE-2025-58181 affect versions before v0.45.0 but are moderate severity. Recommendation: Update to v0.31.0 or later (latest as of 2026-03-31 is around v0.49.0).
Citations:
- 1: Critical vulnerability detected in a dependency golang-migrate/migrate#1211
- 2: https://gitlab.com/gitlab-org/fleeting/plugins/aws/-/work_items/78
- 3: golang.org/x/crypto@0.17.0 vulnerability go-gorm/postgres#300
- 4: https://advisories.gitlab.com/pkg/golang/golang.org/x/crypto/CVE-2024-45337
- 5: https://vulert.com/vuln-db/CVE-2024-45337
- 6: golang.org/x/crypto: CVE-2024-45337 aquasecurity/trivy#8100
- 7: https://cvefeed.io/vuln/detail/CVE-2024-45337
- 8: https://pkg.go.dev/vuln/GO-2024-3321
🏁 Script executed:
# Check if there are Go vulnerability databases we can query
# Let's also look at what version the code is using and common vulnerabilities
curl -s https://api.github.com/repos/golang/crypto/releases 2>/dev/null | head -100 || echo "API unavailable in sandbox"Repository: NAGA-29/WebSocket-Learn
Length of output: 346
🏁 Script executed:
# Search for known Go crypto library vulnerabilities
# Check common sources for golang.org/x/crypto issues
rg "golang\.org/x/crypto" --type go -A 2 -B 2 2>/dev/null || echo "Searching..."Repository: NAGA-29/WebSocket-Learn
Length of output: 79
golang.org/x/crypto v0.17.0 を修正済みバージョンに更新してください。
Line 16 の golang.org/x/crypto v0.17.0 は CVE-2024-45337 に該当する既知のCRITICAL脆弱性を含んでいます(CVSS 9.1)。SSHパッケージの認可バイパスに関する脆弱性であり、v0.31.0未満のすべてのバージョンが影響を受けます。v0.31.0以上(推奨:最新の v0.49.0)へ更新してください。
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@step-03-input/server/go.mod` at line 16, Update the vulnerable dependency
declaration "golang.org/x/crypto v0.17.0" in go.mod to a patched release >=
v0.31.0 (recommended v0.49.0) to address CVE-2024-45337; edit the module version
entry in go.mod (the line containing "golang.org/x/crypto") to the new version
and run module tidy (or use "go get golang.org/x/crypto@v0.49.0") to ensure
go.sum is updated and dependencies are re-resolved.
| github.com/mattn/go-isatty v0.0.20 // indirect | ||
| github.com/valyala/bytebufferpool v1.0.0 // indirect | ||
| github.com/valyala/fasttemplate v1.2.2 // indirect | ||
| golang.org/x/crypto v0.17.0 // indirect |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# 依存バージョンの現状確認(read-only)
fd -a go.mod --exec rg -n 'golang.org/x/crypto'
fd -a go.sum --exec rg -n 'golang.org/x/crypto'Repository: NAGA-29/WebSocket-Learn
Length of output: 550
🌐 Web query:
golang.org/x/crypto GO-2024-3321 GO-2025-3487 の修正済み最小バージョンは何ですか?
💡 Result:
golang.org/x/crypto の GO-2024-3321 の修正済み最小バージョンは v0.31.0、GO-2025-3487 の修正済み最小バージョンは v0.35.0 です。両方を修正するには v0.35.0 以上が必要です。
Citations:
golang.org/x/crypto v0.17.0 は GO-2024-3321 と GO-2025-3487 の既知脆弱性を含むため更新が必須です。
この脆弱性を完全に修正するには v0.35.0 以上への更新が必要です。Line 16 のバージョンを go get golang.org/x/crypto@latest && go mod tidy で最新版に更新してください。
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@step-08-interpolation/server/go.mod` at line 16, The dependency
golang.org/x/crypto is pinned at v0.17.0 and contains known vulnerabilities
(GO-2024-3321, GO-2025-3487); update the golang.org/x/crypto entry in go.mod to
a secure release (v0.35.0 or newer) and refresh module metadata (update the
dependency and run module tidy) so the go.mod/go.sum reflect the upgraded
version.
| github.com/mattn/go-isatty v0.0.20 // indirect | ||
| github.com/valyala/bytebufferpool v1.0.0 // indirect | ||
| github.com/valyala/fasttemplate v1.2.2 // indirect | ||
| golang.org/x/crypto v0.17.0 // indirect |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# 各stepの go.mod で同じ脆弱性があるか確認
echo "=== golang.org/x/crypto バージョン一覧 ==="
fd -t f 'go.mod' --exec grep -l 'golang.org/x/crypto' {} \; | xargs -I{} sh -c 'echo "File: {}"; grep "golang.org/x/crypto" {}'Repository: NAGA-29/WebSocket-Learn
Length of output: 980
🌐 Web query:
golang.org/x/crypto v0.17.0 CVE GO-2024-3321 vulnerabilities
💡 Result:
golang.org/x/crypto v0.17.0, released on December 18, 2023, is vulnerable to GO-2024-3321 (CVE-2024-45337). This vulnerability affects the ssh subpackage and enables authorization bypass due to misuse of ServerConfig.PublicKeyCallback in SSH servers. It impacts all versions before v0.31.0, released December 11, 2024. Upgrade to v0.31.0 or later to mitigate. No other CVEs directly tied to v0.17.0 were identified in the searches.
Citations:
- 1: https://pkg.go.dev/vuln/GO-2024-3321
- 2: https://osv.dev/vulnerability/GO-2024-3321
- 3: https://deps.dev/go/golang.org%2Fx%2Fcrypto/v0.0.0-20151201002508-7b85b097bf75/versions
- 4: https://pkg.go.dev/golang.org/x/crypto/ssh?tab=versions
🌐 Web query:
golang.org/x/crypto GHSA-v778-237x-gjrc CRITICAL vulnerability
💡 Result:
GHSA-v778-237x-gjrc (CVE-2024-45337, GO-2024-3321) is a critical vulnerability (CVSS 9.1) in golang.org/x/crypto/ssh affecting versions before v0.31.0. It allows authorization bypass in SSH servers misusing ServerConfig.PublicKeyCallback. The SSH protocol permits clients to query acceptability of multiple public keys before authenticating with one. The callback may be invoked for several keys, but vulnerable applications might authorize based on a queried key (e.g., key B) while the client authenticates with another (e.g., key A) they control, bypassing checks. Fix: Upgrade to golang.org/x/crypto v0.31.0 or later, which implements a 1-entry FIFO cache for the public key, ensuring the last callback key matches the authentication key. Use Permissions.Extensions for authorization. As of 2026-03-31, later versions like v0.38.0 and v0.49.0 exist and include the fix.
Citations:
- 1: GHSA-v778-237x-gjrc
- 2: https://osv.dev/vulnerability/GHSA-v778-237x-gjrc
- 3: https://pkg.go.dev/vuln/GO-2024-3321
- 4: https://osv.dev/vulnerability/GO-2024-3321
- 5: https://go.googlesource.com/crypto/%2B/refs/tags/v0.31.0
🌐 Web query:
golang.org/x/crypto v0.31.0 safe version release notes
💡 Result:
golang.org/x/crypto v0.31.0 was released on December 11, 2024. It addresses CVE-2024-45337 (GO-2024-3321), a security vulnerability in the ssh package where misuse of ServerConfig.PublicKeyCallback could lead to authorization bypass. The fix changes the public key cache to a 1-entry FIFO cache, ensuring the last key passed to PublicKeyCallback is the one used for authentication when public key auth succeeds. This is a partial mitigation; users should use the Permissions.Extensions field for authorization decisions. Commit: b4f1988a35dee11ec3e05d6bf3e90b695fbd8909 (ssh: make the public key cache a 1-entry FIFO cache, fixes golang/go#70779, CVE-2024-45337). The version is safe regarding this vulnerability and is recommended over earlier versions like v0.30.0. As of 2026-03-31, newer versions (e.g., v0.49.0) exist, but v0.31.0 resolves the key issue.
Citations:
- 1: https://pkg.go.dev/golang.org/x/crypto@v0.31.0
- 2: https://deps.dev/go/golang.org%2Fx%2Fcrypto/v0.0.0-20151201002508-7b85b097bf75/versions
- 3: https://go.googlesource.com/crypto/%2B/refs/tags/v0.31.0
- 4: https://groups.google.com/g/golang-announce/c/-nPEi39gI4Q
- 5: https://advisories.gitlab.com/pkg/golang/golang.org/x/crypto/CVE-2024-45337
- 6: bump golang.org/x/crypto to v0.31.0 hashicorp/vault#29153
- 7: CVE-2024-45337: bump golang.org/x/crypto to v0.31.0 kubernetes-csi/external-provisioner#1314
golang.org/x/crypto に重大な認可バイパス脆弱性が存在します。
golang.org/x/crypto v0.17.0 に CRITICAL 級の脆弱性 (GHSA-v778-237x-gjrc / GO-2024-3321) が存在します。SSH サーバーが ServerConfig.PublicKeyCallback を誤用する際に、認可チェックを回避されるリスクがあります。
このプロジェクトはSSH機能を直接使用していませんが、間接依存として含まれています。v0.31.0 以降へのアップグレードを強く推奨します。
修正方法
cd step-09-rooms/server
go get -u golang.org/x/crypto@v0.31.0
go mod tidyまたは go.mod を直接編集:
- golang.org/x/crypto v0.17.0 // indirect
+ golang.org/x/crypto v0.31.0 // indirect📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| golang.org/x/crypto v0.17.0 // indirect | |
| golang.org/x/crypto v0.31.0 // indirect |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@step-09-rooms/server/go.mod` at line 16, Update the indirect dependency
golang.org/x/crypto from v0.17.0 to at least v0.31.0 to address the
GHSA-v778-237x-gjrc (GO-2024-3321) vulnerability: edit the go.mod entry for
golang.org/x/crypto to v0.31.0 (or run `go get -u golang.org/x/crypto@v0.31.0`)
and then run `go mod tidy` to refresh modules and ensure the updated version is
recorded; verify the final go.mod lists golang.org/x/crypto v0.31.0 (or newer).
| github.com/mattn/go-isatty v0.0.20 // indirect | ||
| github.com/valyala/bytebufferpool v1.0.0 // indirect | ||
| github.com/valyala/fasttemplate v1.2.2 // indirect | ||
| golang.org/x/crypto v0.17.0 // indirect |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "[1/2] OSV: golang.org/x/crypto@0.17.0 の脆弱性確認"
curl -s https://api.osv.dev/v1/query \
-H 'Content-Type: application/json' \
-d '{"version":"0.17.0","package":{"ecosystem":"Go","name":"golang.org/x/crypto"}}' \
| jq -r '.vulns[] | "\(.id)\t\(.summary)"'
echo
echo "[2/2] モジュールの最新公開版確認"
curl -s https://proxy.golang.org/golang.org/x/crypto/@latestRepository: NAGA-29/WebSocket-Learn
Length of output: 1185
脆弱な golang.org/x/crypto v0.17.0 を解消してください。
Line 16 は複数の重大な脆弱性の対象バージョンです(認可回避、DoS、メモリ無制限消費など)。最新版 v0.49.0 への更新が必須です。更新後、go mod tidy を実行し、ロック内容を再生成してください。
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@step-10-reconnect/server/go.mod` at line 16, Replace the vulnerable module
version golang.org/x/crypto v0.17.0 with the fixed release golang.org/x/crypto
v0.49.0 in go.mod, then run go mod tidy to refresh the module graph and
regenerate the lockfile (go.sum); ensure any indirect references are updated and
verify builds/tests succeed after updating.
| func broadcastState() { | ||
| mu.RLock() | ||
| sc := make(map[string]*Snake, len(snakes)) | ||
| for id, s := range snakes { | ||
| cp := *s | ||
| bc := make([]Point, len(s.Body)) | ||
| copy(bc, s.Body) | ||
| cp.Body = bc | ||
| sc[id] = &cp | ||
| } | ||
| cc := make(map[string]*websocket.Conn, len(clients)) | ||
| for id, conn := range clients { | ||
| cc[id] = conn | ||
| } | ||
| mu.RUnlock() | ||
|
|
||
| if len(cc) == 0 { | ||
| return | ||
| } | ||
|
|
||
| data, _ := json.Marshal(GameState{Type: "state", Snakes: sc}) | ||
| for _, conn := range cc { | ||
| conn.WriteMessage(websocket.TextMessage, data) | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# First, let's find and examine the main.go file structure
fd -t f main.go | grep step-10-reconnect
# Then examine the file to understand the concurrent write patternsRepository: NAGA-29/WebSocket-Learn
Length of output: 99
🏁 Script executed:
# Examine the main.go file to see the full implementation
cat -n step-10-reconnect/server/main.go | head -250Repository: NAGA-29/WebSocket-Learn
Length of output: 7128
🏁 Script executed:
# Verify Gorilla WebSocket documentation on concurrent writes
# Check if there are any mutex patterns in the codebase for connection writes
rg -i "mutex|channel|lock" step-10-reconnect/server/main.go | head -20Repository: NAGA-29/WebSocket-Learn
Length of output: 222
🏁 Script executed:
# Check if there are any write synchronization patterns around WriteMessage calls
rg "WriteMessage" step-10-reconnect/server/main.go -B 2 -A 1Repository: NAGA-29/WebSocket-Learn
Length of output: 508
複数の goroutine が同じ WebSocket 接続に並行書き込みしており、フレームが壊れるリスクがあります。
broadcastState()(行 146)、初期 ack メッセージ(行 186)、ping goroutine(行 197)がそれぞれ異なる goroutine から conn.WriteMessage() を呼び出しています。Gorilla WebSocket は同一接続への並行書き込みをサポートしていないため、フレーム破損の原因になります。
解決策:
- 送信専用の goroutine を 1 つ作成し、すべての書き込みをチャネル経由で集約する
- ping については
WriteControl()と読み取り deadline を活用した方式に切り替える(現在のテキストフレームベースの ping ではなく)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@step-10-reconnect/server/main.go` around lines 124 - 147, broadcastState, the
initial ack send, and the existing ping goroutine all call conn.WriteMessage
concurrently which can corrupt frames; fix by creating a per-connection
dedicated writer goroutine and an outbound channel (e.g. outbound chan []byte)
for each websocket connection, have all places (broadcastState, ack send, other
senders) send bytes to that channel instead of calling conn.WriteMessage
directly, and let the writer goroutine serially call conn.WriteMessage for
TextMessage; change the ping logic to use conn.WriteControl with a control
timeout and rely on read deadlines for detecting dead peers rather than sending
ping frames from multiple goroutines.
step-10-reconnect/server/main.go: - Add Client struct with per-connection writeMu (sync.Mutex) to serialize all WriteMessage calls; gorilla/websocket forbids concurrent writes on the same connection - Switch ping goroutine from WriteMessage to WriteControl, which is explicitly safe to call concurrently with WriteMessage per gorilla docs - Handle json.Marshal and WriteMessage errors; remove failed connections from the clients map step-11-optimization/server/main.go: - Check json.Marshal errors instead of ignoring them with _; return early on failure to avoid sending nil/empty data - Guard the ratio log line against division by zero when fullSize == 0 - Handle WriteMessage errors in broadcastWithStats; close and remove failed connections so disconnected clients do not accumulate https://claude.ai/code/session_01Cg4waeVYhPHpatXEsQMbfB
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (4)
step-10-reconnect/server/main.go (3)
244-250: 初回 ack 送信失敗時の挙動を確認してください。
writeText(ack)が失敗した場合、クライアントはmyIdを受け取れず、サーバー側では接続が登録されたままになります。読み取りループがタイムアウトでクリーンアップするため機能的には問題ありませんが、即座に return することで不要な処理を避けられます。🔧 提案:エラー時に早期リターン
// 初回 ack を送信(writeMu で直列化) ack, err := json.Marshal(GameState{Type: "state", MyID: id, Snakes: map[string]*Snake{}}) if err != nil { log.Printf("ack: json.Marshal エラー: %v", err) + return err } else if err := client.writeText(ack); err != nil { log.Printf("ack 送信エラー: %v", err) + return err }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@step-10-reconnect/server/main.go` around lines 244 - 250, The initial ack marshal/send block currently logs errors but continues; change it so that after preparing ack (GameState with Type:"state", MyID:id) if json.Marshal fails log and return, and if client.writeText(ack) fails log and immediately return to avoid leaving the client registered; modify the ack send section (referencing GameState, id, client.writeText, and any writeMu-protected code) to perform early returns on both marshal and write failures.
256-273: ping goroutine のエラー時に接続クローズを検討してください。
WriteControlが失敗した場合(行 264-266)、goroutine は終了しますが、接続自体はクローズされません。読み取りループのタイムアウトで最終的にクリーンアップされますが、明示的にconn.Close()を呼ぶことで、より早く切断を検出できます。🔧 提案:エラー時に接続をクローズ
case <-ticker.C: // WriteControl は deadline を引数に取る deadline := time.Now().Add(5 * time.Second) if err := conn.WriteControl(websocket.PingMessage, nil, deadline); err != nil { log.Printf("ping 送信エラー: %v", err) + conn.Close() // ReadMessage をエラーにして読み取りループを終了させる return } log.Printf("ping 送信 → %s", id)🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@step-10-reconnect/server/main.go` around lines 256 - 273, The ping goroutine currently returns on WriteControl error but doesn't close the websocket, so update the goroutine handling (the anonymous goroutine that uses conn.WriteControl and pingStop) to explicitly close the connection when WriteControl fails: after logging the error from conn.WriteControl (the existing log.Printf line), call conn.Close() (and optionally signal pingStop or any cleanup channel if used) to ensure the connection is terminated promptly and readers wake up; keep the existing return after closing to stop the goroutine.
16-18:CheckOriginがすべてのオリジンを許可しています(学習用途では許容)。本番環境では、CORS オリジンを適切に検証する必要があります。学習カリキュラムとしては問題ありませんが、README にこの点を注記しておくと良いでしょう。
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@step-10-reconnect/server/main.go` around lines 16 - 18, The websocket.Upgrader currently sets CheckOrigin to allow all origins via the upgrader variable; replace this permissive CheckOrigin with a validation that checks the request Origin against an allowlist (e.g., compare r.Header.Get("Origin") to an allowedOrigins slice or a isAllowedOrigin(origin string) helper) in the upgrader.CheckOrigin function for production, and also add a clear note in the README that the current CheckOrigin is permissive for learning purposes and must be tightened before deployment.step-11-optimization/server/main.go (1)
16-18: WebSocket の Origin 検証が無効化されており、Cross-Site WebSocket Hijacking (CSWSH) の対象になります。
CheckOriginを常にtrueにすると、任意の Origin からの WebSocket 接続を許可してしまいます。gorilla/websocket のデフォルト動作(CheckOriginが nil)では、Origin ヘッダーをリクエストの Host ヘッダーと照合する安全な検証が行われます。この設定を上書きすると、その検証が完全に無効化され、クロスオリジンからの接続が可能になります。これにより悪意のあるサイトから無断で WebSocket 接続を開かれ、データの盗聴や不正なメッセージ送信のリスクが生じます。教材コードはそのまま流用されやすいため、本番環境で使用される際の脆弱性につながる可能性があります。
🔧 修正案
var upgrader = websocket.Upgrader{ - CheckOrigin: func(r *http.Request) bool { return true }, + CheckOrigin: func(r *http.Request) bool { + origin := r.Header.Get("Origin") + return origin == "http://"+r.Host || origin == "https://"+r.Host + }, }または、デモ専用であることを明確に文書化し、本番環境での使用を禁止する注釈を追加してください。
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@step-11-optimization/server/main.go` around lines 16 - 18, The Upgrader variable currently disables origin checks by setting CheckOrigin to always return true (var upgrader = websocket.Upgrader{CheckOrigin: func(r *http.Request) bool { return true }}), which enables CSWSH; fix by removing or replacing that CheckOrigin: either set CheckOrigin to nil (use the gorilla/websocket default behavior) or implement a strict validator that compares r.Header.Get("Origin") against r.Host or a configured allowed-origins list (and reference the upgrader variable and its CheckOrigin func when updating). If this is demo-only, instead add a clear comment near the upgrader declaration documenting that the permissive CheckOrigin is for demo only and must not be used in production.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@step-11-optimization/server/main.go`:
- Around line 303-307: The code currently only checks opposites when assigning
s.Direction (in the block handling msg.Type == "input" where opposites is
defined and snakes[id] is accessed), allowing unknown values like "northwest"
through and later causing moveSnake to fall back to a zero-vector; fix this by
whitelisting allowed directions (e.g., "up","down","left","right") before
assigning: after confirming msg.Type == "input" and obtaining s from snakes,
validate that msg.Direction is one of the allowed strings and also not the
opposite via opposites[s.Direction] != msg.Direction, then set s.Direction; keep
the mutex (mu.Lock) semantics and do not change moveSnake.
- Around line 227-236: You must serialize writes to each websocket connection by
introducing a per-connection write-lock wrapper and using it everywhere a
WriteMessage may be called: create a small wrapper struct (e.g., type lockedConn
struct { conn *websocket.Conn; mu sync.Mutex }) with a method WriteMessage that
locks, delegates to conn.WriteMessage, then unlocks; replace usages of raw
*websocket.Conn in the global clients map with *lockedConn (or have clients
store the wrapper alongside the conn) and update the registration code (the spot
that registers into clients after mu.Unlock and the ack WriteMessage call),
broadcastWithStats (the loop that calls conn.WriteMessage for fullData and
statsData), and gameLoop ack sends to call lockedConn.WriteMessage instead of
websocket.Conn.WriteMessage so no two goroutines write the same Conn
concurrently.
---
Nitpick comments:
In `@step-10-reconnect/server/main.go`:
- Around line 244-250: The initial ack marshal/send block currently logs errors
but continues; change it so that after preparing ack (GameState with
Type:"state", MyID:id) if json.Marshal fails log and return, and if
client.writeText(ack) fails log and immediately return to avoid leaving the
client registered; modify the ack send section (referencing GameState, id,
client.writeText, and any writeMu-protected code) to perform early returns on
both marshal and write failures.
- Around line 256-273: The ping goroutine currently returns on WriteControl
error but doesn't close the websocket, so update the goroutine handling (the
anonymous goroutine that uses conn.WriteControl and pingStop) to explicitly
close the connection when WriteControl fails: after logging the error from
conn.WriteControl (the existing log.Printf line), call conn.Close() (and
optionally signal pingStop or any cleanup channel if used) to ensure the
connection is terminated promptly and readers wake up; keep the existing return
after closing to stop the goroutine.
- Around line 16-18: The websocket.Upgrader currently sets CheckOrigin to allow
all origins via the upgrader variable; replace this permissive CheckOrigin with
a validation that checks the request Origin against an allowlist (e.g., compare
r.Header.Get("Origin") to an allowedOrigins slice or a isAllowedOrigin(origin
string) helper) in the upgrader.CheckOrigin function for production, and also
add a clear note in the README that the current CheckOrigin is permissive for
learning purposes and must be tightened before deployment.
In `@step-11-optimization/server/main.go`:
- Around line 16-18: The Upgrader variable currently disables origin checks by
setting CheckOrigin to always return true (var upgrader =
websocket.Upgrader{CheckOrigin: func(r *http.Request) bool { return true }}),
which enables CSWSH; fix by removing or replacing that CheckOrigin: either set
CheckOrigin to nil (use the gorilla/websocket default behavior) or implement a
strict validator that compares r.Header.Get("Origin") against r.Host or a
configured allowed-origins list (and reference the upgrader variable and its
CheckOrigin func when updating). If this is demo-only, instead add a clear
comment near the upgrader declaration documenting that the permissive
CheckOrigin is for demo only and must not be used in production.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 52d0e69c-1138-4002-8fff-89a4bdad2900
📒 Files selected for processing (2)
step-10-reconnect/server/main.gostep-11-optimization/server/main.go
| for id, conn := range cc { | ||
| if err := conn.WriteMessage(websocket.TextMessage, fullData); err != nil { | ||
| log.Printf("broadcastWithStats: 送信エラー(切断扱い): %s: %v", id, err) | ||
| failed = append(failed, id) | ||
| continue | ||
| } | ||
| if err := conn.WriteMessage(websocket.TextMessage, statsData); err != nil { | ||
| log.Printf("broadcastWithStats: stats 送信エラー: %s: %v", id, err) | ||
| failed = append(failed, id) | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
According to gorilla/websocket v1.5.0 documentation, are concurrent calls to WriteMessage on the same *websocket.Conn safe, and is WriteControl treated differently?
💡 Result:
No, concurrent calls to WriteMessage on the same *websocket.Conn are not safe. Applications must ensure only one goroutine calls write methods like WriteMessage concurrently. The library detects violations and panics with "concurrent write to websocket connection". WriteControl is treated differently: it can be called concurrently with other methods, including write methods.
Citations:
- 1: [question] concurrent write to websocket connection with mutex.Lock() gorilla/websocket#828
- 2: [FEATURE] Reduce support issues related to concurrent writes gorilla/websocket#957
- 3: [bug] concurrent write to websocket connection gorilla/websocket#698
- 4: https://stackoverflow.com/questions/10986008/is-websocket-send-receive-thread-safe-go-routine-safe
- 5: https://github.com/gorilla/websocket/blob/main/conn.go
🏁 Script executed:
# First, let's check the file structure and examine the critical lines
cd step-11-optimization/server && wc -l main.goRepository: NAGA-29/WebSocket-Learn
Length of output: 78
🏁 Script executed:
# Read the relevant sections to understand the race condition
sed -n '220,290p' step-11-optimization/server/main.goRepository: NAGA-29/WebSocket-Learn
Length of output: 1875
🏁 Script executed:
# Also check the gameLoop function to understand when broadcast happens
grep -n "func gameLoop" step-11-optimization/server/main.goRepository: NAGA-29/WebSocket-Learn
Length of output: 87
🏁 Script executed:
# Search for all WriteMessage calls to identify all write paths
rg "WriteMessage" step-11-optimization/server/main.go -n -B2 -A2Repository: NAGA-29/WebSocket-Learn
Length of output: 723
同一 Conn への書き込みを直列化してください。
Line 278-279 で接続を clients に登録した直後、mu.Unlock() で global lock を解放しますが、その直後 Line 283 の ack WriteMessage が実行される前に、別 goroutine で動作する gameLoop の tick が走る可能性があります。このとき Line 228 / Line 233 の broadcast が同じ Conn に対して同時 WriteMessage を呼び出してしまい、gorilla/websocket が concurrent write を検出してパニックを起こします。gorilla/websocket は 1 接続あたり同時 writer 1 本だけを前提にしているため、write mutex 付きラッパーへの変更が必須です。
🔧 修正案
+type Client struct {
+ conn *websocket.Conn
+ mu sync.Mutex
+}
+
var (
snakes = make(map[string]*FullSnake)
- clients = make(map[string]*websocket.Conn)
+ clients = make(map[string]*Client)
mu sync.RWMutex
counter int
tickCount int64
)
+
+func writeText(c *Client, payload []byte) error {
+ c.mu.Lock()
+ defer c.mu.Unlock()
+ return c.conn.WriteMessage(websocket.TextMessage, payload)
+}- cc := make(map[string]*websocket.Conn, len(clients))
- for id, conn := range clients {
- cc[id] = conn
+ cc := make(map[string]*Client, len(clients))
+ for id, client := range clients {
+ cc[id] = client
}- for id, conn := range cc {
- if err := conn.WriteMessage(websocket.TextMessage, fullData); err != nil {
+ for id, client := range cc {
+ if err := writeText(client, fullData); err != nil {
...
}
- if err := conn.WriteMessage(websocket.TextMessage, statsData); err != nil {
+ if err := writeText(client, statsData); err != nil {
...
}
}- clients[id] = conn
+ client := &Client{conn: conn}
+ clients[id] = client
mu.Unlock()
ack, err := json.Marshal(FullState{Type: "state", MyID: id, Snakes: map[string]*FullSnake{}})
if err != nil {
log.Printf("ack: json.Marshal エラー: %v", err)
- } else if err := conn.WriteMessage(websocket.TextMessage, ack); err != nil {
+ } else if err := writeText(client, ack); err != nil {
log.Printf("ack 送信エラー: %v", err)
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| for id, conn := range cc { | |
| if err := conn.WriteMessage(websocket.TextMessage, fullData); err != nil { | |
| log.Printf("broadcastWithStats: 送信エラー(切断扱い): %s: %v", id, err) | |
| failed = append(failed, id) | |
| continue | |
| } | |
| if err := conn.WriteMessage(websocket.TextMessage, statsData); err != nil { | |
| log.Printf("broadcastWithStats: stats 送信エラー: %s: %v", id, err) | |
| failed = append(failed, id) | |
| } | |
| for id, client := range cc { | |
| if err := writeText(client, fullData); err != nil { | |
| log.Printf("broadcastWithStats: 送信エラー(切断扱い): %s: %v", id, err) | |
| failed = append(failed, id) | |
| continue | |
| } | |
| if err := writeText(client, statsData); err != nil { | |
| log.Printf("broadcastWithStats: stats 送信エラー: %s: %v", id, err) | |
| failed = append(failed, id) | |
| } | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@step-11-optimization/server/main.go` around lines 227 - 236, You must
serialize writes to each websocket connection by introducing a per-connection
write-lock wrapper and using it everywhere a WriteMessage may be called: create
a small wrapper struct (e.g., type lockedConn struct { conn *websocket.Conn; mu
sync.Mutex }) with a method WriteMessage that locks, delegates to
conn.WriteMessage, then unlocks; replace usages of raw *websocket.Conn in the
global clients map with *lockedConn (or have clients store the wrapper alongside
the conn) and update the registration code (the spot that registers into clients
after mu.Unlock and the ack WriteMessage call), broadcastWithStats (the loop
that calls conn.WriteMessage for fullData and statsData), and gameLoop ack sends
to call lockedConn.WriteMessage instead of websocket.Conn.WriteMessage so no two
goroutines write the same Conn concurrently.
| if msg.Type == "input" { | ||
| opposites := map[string]string{"up": "down", "down": "up", "left": "right", "right": "left"} | ||
| mu.Lock() | ||
| if s, ok := snakes[id]; ok && opposites[s.Direction] != msg.Direction { | ||
| s.Direction = msg.Direction |
There was a problem hiding this comment.
不正な方向値を state に入れないでください。
Line 306 は「逆方向でない」だけを見ているので、"northwest" のような未知の値も通ります。その状態で次 tick の moveSnake に入ると switch がどれにも一致せず、nh のゼロ値 (0,0) にジャンプします。サーバー authoritative を保つなら、ここで許可済み方向だけを whitelist した方がいいです。
🔧 修正案
if msg.Type == "input" {
opposites := map[string]string{"up": "down", "down": "up", "left": "right", "right": "left"}
+ if _, ok := opposites[msg.Direction]; !ok {
+ continue
+ }
mu.Lock()
if s, ok := snakes[id]; ok && opposites[s.Direction] != msg.Direction {
s.Direction = msg.Direction
}
mu.Unlock()
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| if msg.Type == "input" { | |
| opposites := map[string]string{"up": "down", "down": "up", "left": "right", "right": "left"} | |
| mu.Lock() | |
| if s, ok := snakes[id]; ok && opposites[s.Direction] != msg.Direction { | |
| s.Direction = msg.Direction | |
| if msg.Type == "input" { | |
| opposites := map[string]string{"up": "down", "down": "up", "left": "right", "right": "left"} | |
| if _, ok := opposites[msg.Direction]; !ok { | |
| continue | |
| } | |
| mu.Lock() | |
| if s, ok := snakes[id]; ok && opposites[s.Direction] != msg.Direction { | |
| s.Direction = msg.Direction | |
| } | |
| mu.Unlock() | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@step-11-optimization/server/main.go` around lines 303 - 307, The code
currently only checks opposites when assigning s.Direction (in the block
handling msg.Type == "input" where opposites is defined and snakes[id] is
accessed), allowing unknown values like "northwest" through and later causing
moveSnake to fall back to a zero-vector; fix this by whitelisting allowed
directions (e.g., "up","down","left","right") before assigning: after confirming
msg.Type == "input" and obtaining s from snakes, validate that msg.Direction is
one of the allowed strings and also not the opposite via opposites[s.Direction]
!= msg.Direction, then set s.Direction; keep the mutex (mu.Lock) semantics and
do not change moveSnake.
step-11-optimization/client/index.html: - The receivedSize variable was computed but never used; now displayed in a new "実際の受信サイズ" stat card so learners can compare the client-side Blob-measured bytes against the server-reported fullSize - Guard ratio calculation with msg.fullSize > 0 to prevent Infinity/NaN when the server sends fullSize=0 step-07-lag/client/index.html: - Remove unused startIdx variable; intervals is already trimmed to MAX_HISTORY on every push, so the loop correctly starts at i=0 step-09-rooms/server/main.go: - Collapse redundant "if !ate && len > initial ... else if !ate" branches into a single "if !ate" block; both branches were identical step-01-echo/README.md: - Fix echo version in the go.mod example from v4.11.0 to v4.11.4 to match the actual server/go.mod file https://claude.ai/code/session_01Cg4waeVYhPHpatXEsQMbfB
There was a problem hiding this comment.
🧹 Nitpick comments (3)
step-01-echo/README.md (3)
70-77: フェンスドコードブロックに言語指定子を追加してください。接続の流れの説明にも言語指定子を追加することを推奨します。
♻️ 提案される修正
-``` +```text 接続の流れ: 1. クライアントが ws://localhost:8080/ws に接続を要求 2. echo がリクエストを受け取る🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@step-01-echo/README.md` around lines 70 - 77, Update the fenced code block that starts with "接続の流れ:" to include a language/info string (e.g., change ``` to ```text) so renderers treat it as plain text; also add the same language tag to any other fenced blocks in that README section to keep formatting consistent and explicit (look for the block containing the numbered steps and modify its opening fence to ```text).
51-60: フェンスドコードブロックに言語指定子を追加してください。go.modの例に適切な言語指定子を追加することで、シンタックスハイライトが有効になります。
♻️ 提案される修正
-``` +```go module step01-echo go 1.21🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@step-01-echo/README.md` around lines 51 - 60, The fenced code block containing the go.mod sample (showing "module step01-echo", "go 1.21", and the require block) needs a language specifier to enable syntax highlighting; update the opening fence from ``` to ```go so the code block is marked as Go source (e.g., change the fence that precedes "module step01-echo" to ```go).
36-44: フェンスドコードブロックに言語指定子を追加してください。マークダウンのレンダリングとアクセシビリティを向上させるため、コードブロックに言語指定子を追加することを推奨します。
♻️ 提案される修正
-``` +```text step-01-echo/ ├── README.md ├── server/🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@step-01-echo/README.md` around lines 36 - 44, Update the fenced code block in README.md that shows the directory tree for step-01-echo to include a language specifier (e.g., change ``` to ```text) so the block is rendered/accessed correctly; locate the block containing the "step-01-echo/ ├── README.md ├── server/ ..." tree and add the language token immediately after the opening backticks to preserve content but improve markdown rendering and accessibility.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@step-01-echo/README.md`:
- Around line 70-77: Update the fenced code block that starts with "接続の流れ:" to
include a language/info string (e.g., change ``` to ```text) so renderers treat
it as plain text; also add the same language tag to any other fenced blocks in
that README section to keep formatting consistent and explicit (look for the
block containing the numbered steps and modify its opening fence to ```text).
- Around line 51-60: The fenced code block containing the go.mod sample (showing
"module step01-echo", "go 1.21", and the require block) needs a language
specifier to enable syntax highlighting; update the opening fence from ``` to
```go so the code block is marked as Go source (e.g., change the fence that
precedes "module step01-echo" to ```go).
- Around line 36-44: Update the fenced code block in README.md that shows the
directory tree for step-01-echo to include a language specifier (e.g., change
``` to ```text) so the block is rendered/accessed correctly; locate the block
containing the "step-01-echo/ ├── README.md ├── server/ ..." tree and add the
language token immediately after the opening backticks to preserve content but
improve markdown rendering and accessibility.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 6a662f4e-7623-46cd-b4e1-22f22ae587b7
📒 Files selected for processing (4)
step-01-echo/README.mdstep-07-lag/client/index.htmlstep-09-rooms/server/main.gostep-11-optimization/client/index.html
✅ Files skipped from review due to trivial changes (1)
- step-07-lag/client/index.html
🚧 Files skipped from review as they are similar to previous changes (2)
- step-11-optimization/client/index.html
- step-09-rooms/server/main.go
README.md: - Split "使わないもの" into "実装では使わないもの" and "概念として 触れるもの" with a note that Redis is covered conceptually in Step 12 only; removes the contradiction between the tech stack table and the Step 12 content step-10-reconnect/server/main.go: - Add a prominent comment on CheckOrigin explaining the CSWSH risk, that this setting is development-only, and provide a minimal example of origin validation for production step-03-input/server/main.go: - Add Client struct with per-connection writeMu (sync.Mutex) to serialize WriteMessage calls; broadcast() is called from multiple handler goroutines so concurrent writes to the same conn are possible - Move idCounter increment inside mu.Lock() to fix the data race on idCounter (was modified by generatePlayerID outside any lock) - Handle json.Marshal and writeText errors instead of ignoring them step-04-server-state/server/main.go: - Same Client struct + writeMu pattern as step-03; broadcast() is called from multiple goroutines (on connect, on input, and on disconnect) so per-connection write serialization is required - Handle json.Marshal and writeText errors https://claude.ai/code/session_01Cg4waeVYhPHpatXEsQMbfB
There was a problem hiding this comment.
Actionable comments posted: 5
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@README.md`:
- Around line 78-84: フェンスドコードブロックに言語指定がなくMD040警告が出ているので、該当のバックティック囲み(例: the
triple backticks enclosing the "Step 0〜2…"/the tree example)
に言語タグを追加してlintを解消してください(例: ```text)。README.md内の両該当ブロック(先に示した箇所と「Also applies to:
93-100」のブロック)に同様の変更を適用してください。
In `@step-03-input/server/main.go`:
- Around line 117-123: After assigning/removing players you need to immediately
notify others: in the connect path where you lock mu, increment idCounter, set
clients[playerID] and playerInputs[playerID], then mu.Unlock() and call
broadcast() so joined players show up; likewise in the disconnect path(s) where
you delete from clients and playerInputs (the blocks around the regions
referenced by lines 139-145 and 172-174), perform the deletion under mu,
mu.Unlock(), and then call broadcast() to propagate the updated state. Ensure
the broadcast() calls occur after unlocking to avoid holding mu during
network/write operations.
In `@step-10-reconnect/server/main.go`:
- Around line 138-147: The switch on s.Direction assigns nh but allows unknown
values which leave nh at its zero value causing the snake to jump to (0,0);
update the handling in the same function (e.g., moveSnake / the switch that
references s.Direction and variable nh) to whitelist only "up", "down", "left",
"right" and add a default branch that returns early (or otherwise
ignores/validates the input) so invalid directions are rejected; apply the same
whitelist+default-return pattern to the other similar switch block referenced
around the 316-323 area to prevent using an uninitialized nh.
- Around line 247-258: Currently the server sends an initial GameState with
Type:"state" and empty Snakes which can overwrite a just-broadcast real state;
change the initial ack to a distinct ack message so it doesn't clobber state:
create/send a GameState (or separate struct) with Type:"ack" (or use a dedicated
Ack struct) containing MyID only, and use client.writeText to send that after
clients[id] = client; ensure functions/structs referenced are Client, writeText,
GameState (or new Ack type) and leave broadcastState() behavior unchanged.
- Around line 82-85: The writeText method lacks a write deadline causing
blocking; modify Client.writeText to set a write deadline before calling
c.conn.WriteMessage (mirror the pattern used by WriteControl) by calling
c.conn.SetWriteDeadline(time.Now().Add(writeWait)) then perform the WriteMessage
while keeping the existing c.writeMu lock and defer; ensure any error from
SetWriteDeadline/WriteMessage is returned so broadcastState/updateGame aren't
blocked by a single slow client.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: f0e9af7e-7bf6-4434-bd66-515e3652529c
📒 Files selected for processing (4)
README.mdstep-03-input/server/main.gostep-04-server-state/server/main.gostep-10-reconnect/server/main.go
🚧 Files skipped from review as they are similar to previous changes (1)
- step-04-server-state/server/main.go
| ``` | ||
| Step 0〜2 → 最速で終わらせる(接続の感触をつかむ) | ||
| Step 3〜5 → 本質理解(ここが最重要) | ||
| Step 6 → ゲームらしさを入れる | ||
| Step 7〜8 → 「リアルタイムの現実」を学ぶ | ||
| Step 9〜12 → 設計者の視点に上げる | ||
| ``` |
There was a problem hiding this comment.
フェンスドコードブロックに言語指定がなく、Markdown lint(MD040)警告になります。
コード例ブロックに言語を付けておくと、lint 警告を解消できます。
🛠 修正例
-```
+```text
Step 0〜2 → 最速で終わらせる(接続の感触をつかむ)
Step 3〜5 → 本質理解(ここが最重要)
Step 6 → ゲームらしさを入れる
Step 7〜8 → 「リアルタイムの現実」を学ぶ
Step 9〜12 → 設計者の視点に上げる@@
- +text
step-XX-name/
├── README.md # 概念説明・コード解説・練習問題
├── server/
│ └── main.go # Goサーバー
└── client/
└── index.html # フロントエンド
Also applies to: 93-100
🧰 Tools
🪛 markdownlint-cli2 (0.22.0)
[warning] 78-78: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@README.md` around lines 78 - 84,
フェンスドコードブロックに言語指定がなくMD040警告が出ているので、該当のバックティック囲み(例: the triple backticks
enclosing the "Step 0〜2…"/the tree example) に言語タグを追加してlintを解消してください(例:
```text)。README.md内の両該当ブロック(先に示した箇所と「Also applies to:
93-100」のブロック)に同様の変更を適用してください。
| // プレイヤーIDの割り当てと登録を同じロック内で行い、競合を防ぐ | ||
| mu.Lock() | ||
| idCounter++ | ||
| playerID := fmt.Sprintf("player-%d", idCounter) | ||
| clients[playerID] = client | ||
| playerInputs[playerID] = "right" // 初期方向 | ||
| mu.Unlock() |
There was a problem hiding this comment.
接続・切断イベントで状態配信していないため、他クライアントの表示状態が古く残ります。
現在は入力イベント時のみ broadcast() されるため、参加/離脱が即時反映されません。参加直後と離脱直後にも状態配信を入れてください。
🧩 最小修正案
log.Printf("プレイヤー接続: %s", playerID)
// 自分のIDをクライアントに通知
ack, err := json.Marshal(ServerMessage{
Type: "ack",
PlayerID: playerID,
})
if err != nil {
log.Printf("ack: json.Marshal エラー: %v", err)
} else if err := client.writeText(ack); err != nil {
log.Printf("ack 送信エラー: %v", err)
}
+ // 参加イベントを全員へ即時反映
+ broadcast()
// 切断時の後処理
defer func() {
mu.Lock()
delete(clients, playerID)
delete(playerInputs, playerID)
mu.Unlock()
+ // 離脱イベントを全員へ即時反映
+ broadcast()
log.Printf("プレイヤー切断: %s", playerID)
}()Also applies to: 139-145, 172-174
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@step-03-input/server/main.go` around lines 117 - 123, After
assigning/removing players you need to immediately notify others: in the connect
path where you lock mu, increment idCounter, set clients[playerID] and
playerInputs[playerID], then mu.Unlock() and call broadcast() so joined players
show up; likewise in the disconnect path(s) where you delete from clients and
playerInputs (the blocks around the regions referenced by lines 139-145 and
172-174), perform the deletion under mu, mu.Unlock(), and then call broadcast()
to propagate the updated state. Ensure the broadcast() calls occur after
unlocking to avoid holding mu during network/write operations.
| func (c *Client) writeText(data []byte) error { | ||
| c.writeMu.Lock() | ||
| defer c.writeMu.Unlock() | ||
| return c.conn.WriteMessage(websocket.TextMessage, data) |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE="step-10-reconnect/server/main.go"
echo "== gameLoop -> broadcastState -> writeText =="
sed -n '80,86p;114,121p;166,198p' "$FILE"
echo
echo "== ping write path =="
sed -n '262,276p' "$FILE"
echo
echo "== write deadline の参照 =="
rg -n 'SetWriteDeadline|WriteMessage|WriteControl' "$FILE"Repository: NAGA-29/WebSocket-Learn
Length of output: 2330
テキスト送信に write deadline がなく、遅い 1 接続で全員の tick が詰まります。
broadcastState() は game loop 上で各クライアントに同期送信しているため、WriteMessage() がブロックされると次の updateGame() 呼び出しまで進行が止まります。WriteControl() 側は deadline 付きですが、writeText() 側だけ無期限なので、ここにも書き込み期限を設定してください。
推奨修正
func (c *Client) writeText(data []byte) error {
c.writeMu.Lock()
defer c.writeMu.Unlock()
+ if err := c.conn.SetWriteDeadline(time.Now().Add(5 * time.Second)); err != nil {
+ return err
+ }
return c.conn.WriteMessage(websocket.TextMessage, data)
}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@step-10-reconnect/server/main.go` around lines 82 - 85, The writeText method
lacks a write deadline causing blocking; modify Client.writeText to set a write
deadline before calling c.conn.WriteMessage (mirror the pattern used by
WriteControl) by calling c.conn.SetWriteDeadline(time.Now().Add(writeWait)) then
perform the WriteMessage while keeping the existing c.writeMu lock and defer;
ensure any error from SetWriteDeadline/WriteMessage is returned so
broadcastState/updateGame aren't blocked by a single slow client.
| switch s.Direction { | ||
| case "up": | ||
| nh = Point{head.X, head.Y - gridSize} | ||
| case "down": | ||
| nh = Point{head.X, head.Y + gridSize} | ||
| case "left": | ||
| nh = Point{head.X - gridSize, head.Y} | ||
| case "right": | ||
| nh = Point{head.X + gridSize, head.Y} | ||
| } |
There was a problem hiding this comment.
未知の direction をそのまま保存すると、次 tick で蛇が (0,0) に飛びます。
ここは逆走だけを弾いているので、up/down/left/right 以外の値も通ります。その値が moveSnake() の switch に載らないと nh が zero value のまま使われるので、入力を whitelist したうえで default: return も入れておく方が安全です。
🛡️ 受信値のバリデーションを追加する案
switch s.Direction {
case "up":
nh = Point{head.X, head.Y - gridSize}
case "down":
nh = Point{head.X, head.Y + gridSize}
case "left":
nh = Point{head.X - gridSize, head.Y}
case "right":
nh = Point{head.X + gridSize, head.Y}
+ default:
+ return
}
@@
if msg.Type == "input" {
opposites := map[string]string{
"up": "down", "down": "up", "left": "right", "right": "left",
}
mu.Lock()
- if s, ok := snakes[id]; ok && opposites[s.Direction] != msg.Direction {
- s.Direction = msg.Direction
+ if s, ok := snakes[id]; ok {
+ if _, valid := opposites[msg.Direction]; valid && opposites[s.Direction] != msg.Direction {
+ s.Direction = msg.Direction
+ }
}
mu.Unlock()
}Also applies to: 316-323
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@step-10-reconnect/server/main.go` around lines 138 - 147, The switch on
s.Direction assigns nh but allows unknown values which leave nh at its zero
value causing the snake to jump to (0,0); update the handling in the same
function (e.g., moveSnake / the switch that references s.Direction and variable
nh) to whitelist only "up", "down", "left", "right" and add a default branch
that returns early (or otherwise ignores/validates the input) so invalid
directions are rejected; apply the same whitelist+default-return pattern to the
other similar switch block referenced around the 316-323 area to prevent using
an uninitialized nh.
| client := &Client{conn: conn} | ||
| snakes[id] = &Snake{ID: id, Body: body, Direction: "right", Color: color} | ||
| clients[id] = client | ||
| mu.Unlock() | ||
|
|
||
| log.Printf("プレイヤー接続: %s", id) | ||
|
|
||
| // 初回 ack を送信(writeMu で直列化) | ||
| ack, err := json.Marshal(GameState{Type: "state", MyID: id, Snakes: map[string]*Snake{}}) | ||
| if err != nil { | ||
| log.Printf("ack: json.Marshal エラー: %v", err) | ||
| } else if err := client.writeText(ack); err != nil { |
There was a problem hiding this comment.
初回 ack を state として空の snakes 付きで送ると、接続直後の最新 state を空で上書きできます。
clients[id] = client のあとに type: "state" を送っているので、先に broadcastState() が届いたケースでは、後着の ack が step-10-reconnect/client/index.html:114-124 の state = msg を空状態で上書きします。myId は type に依存せず読まれているので、ここは専用の ack メッセージに分けるのが安全です。
💡 サーバー側だけで収まる修正案
- ack, err := json.Marshal(GameState{Type: "state", MyID: id, Snakes: map[string]*Snake{}})
+ ack, err := json.Marshal(struct {
+ Type string `json:"type"`
+ MyID string `json:"myId"`
+ }{
+ Type: "ack",
+ MyID: id,
+ })🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@step-10-reconnect/server/main.go` around lines 247 - 258, Currently the
server sends an initial GameState with Type:"state" and empty Snakes which can
overwrite a just-broadcast real state; change the initial ack to a distinct ack
message so it doesn't clobber state: create/send a GameState (or separate
struct) with Type:"ack" (or use a dedicated Ack struct) containing MyID only,
and use client.writeText to send that after clients[id] = client; ensure
functions/structs referenced are Client, writeText, GameState (or new Ack type)
and leave broadcastState() behavior unchanged.
Introduced Client struct with per-connection writeMu sync.Mutex and writeText() helper in step-05, step-06, step-07, and step-08 servers. Prevents simultaneous WriteMessage calls from the gameLoop goroutine (broadcastState) and the handleWebSocket goroutine (initial ack send), which violates gorilla/websocket's single-writer constraint and causes a data race on connection startup. Also fixed json.Marshal errors being silently ignored (_, _) and added the⚠️ development-only comment on upgrader.CheckOrigin where missing. https://claude.ai/code/session_01Cg4waeVYhPHpatXEsQMbfB
There was a problem hiding this comment.
🧹 Nitpick comments (2)
step-05-game-loop/server/main.go (1)
136-148: ラップアラウンドの境界値が非対称です。
X < 0の場合にfieldWidth(800) に設定していますが、これはX > fieldWidthで 0 に戻る条件と組み合わせると、X=800 の位置が一瞬だけ有効になります。fieldWidth - moveSpeedに設定するか、条件を>=に統一することで対称性を保てます。教育目的のコードなので動作上の問題は軽微ですが、学習者が境界処理のベストプラクティスを学ぶ機会として修正を検討してください。
🔧 対称的なラップアラウンドの例
// ラップアラウンド(画面の端を超えたら反対側へ) if p.X < 0 { - p.X = fieldWidth + p.X = fieldWidth - moveSpeed } - if p.X > fieldWidth { + if p.X >= fieldWidth { p.X = 0 } if p.Y < 0 { - p.Y = fieldHeight + p.Y = fieldHeight - moveSpeed } - if p.Y > fieldHeight { + if p.Y >= fieldHeight { p.Y = 0 }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@step-05-game-loop/server/main.go` around lines 136 - 148, The wrap-around boundary checks for p.X and p.Y are asymmetric; update the conditions and resets in the block that manipulates p.X/p.Y so the comparisons use >= and <= consistently (or use X <= 0 / X >= fieldWidth and similarly for Y) and set the reset positions symmetrically (e.g., use fieldWidth - moveSpeed or fieldWidth-1 when wrapping left, and 0 when wrapping right) so p.X, p.Y, fieldWidth, fieldHeight and moveSpeed are handled consistently; locate and update the wrap logic around the p.X/p.Y assignments in main.go to make the comparisons and reset values symmetric.step-08-interpolation/server/main.go (1)
245-252:oppositesマップの再作成は軽微な非効率です。入力メッセージごとにマップを再作成しています。Step 6 のように
isOppositeDirectionヘルパー関数を使用するか、マップをパッケージレベルの定数として定義することで、一貫性と効率が向上します。教育目的では問題ありませんが、ステップ間でのコードの一貫性を保つために検討してください。
♻️ ヘルパー関数への統一例
+// isOppositeDirection は反対方向への転換かどうかをチェックする +func isOppositeDirection(current, next string) bool { + opposites := map[string]string{ + "up": "down", "down": "up", + "left": "right", "right": "left", + } + return opposites[current] == next +} // handleWebSocket 内 if msg.Type == "input" { - opposites := map[string]string{"up": "down", "down": "up", "left": "right", "right": "left"} mu.Lock() - if s, ok := snakes[id]; ok && opposites[s.Direction] != msg.Direction { + if s, ok := snakes[id]; ok && !isOppositeDirection(s.Direction, msg.Direction) { s.Direction = msg.Direction } mu.Unlock() }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@step-08-interpolation/server/main.go` around lines 245 - 252, The code recreates the opposites map on every input message; instead, define a package-level constant map (e.g., opposites := map[string]string{...}) or add/use an isOppositeDirection(direction, newDir string) helper and replace the inline map; update the input handling block (where msg.Type == "input", accessing snakes, mu, s.Direction) to call the helper or reference the package-level opposites map so the map is not reconstructed per message and the opposite-check remains opposites[s.Direction] != msg.Direction.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@step-05-game-loop/server/main.go`:
- Around line 136-148: The wrap-around boundary checks for p.X and p.Y are
asymmetric; update the conditions and resets in the block that manipulates
p.X/p.Y so the comparisons use >= and <= consistently (or use X <= 0 / X >=
fieldWidth and similarly for Y) and set the reset positions symmetrically (e.g.,
use fieldWidth - moveSpeed or fieldWidth-1 when wrapping left, and 0 when
wrapping right) so p.X, p.Y, fieldWidth, fieldHeight and moveSpeed are handled
consistently; locate and update the wrap logic around the p.X/p.Y assignments in
main.go to make the comparisons and reset values symmetric.
In `@step-08-interpolation/server/main.go`:
- Around line 245-252: The code recreates the opposites map on every input
message; instead, define a package-level constant map (e.g., opposites :=
map[string]string{...}) or add/use an isOppositeDirection(direction, newDir
string) helper and replace the inline map; update the input handling block
(where msg.Type == "input", accessing snakes, mu, s.Direction) to call the
helper or reference the package-level opposites map so the map is not
reconstructed per message and the opposite-check remains opposites[s.Direction]
!= msg.Direction.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 28cdeb0a-a7e7-4981-b39c-f4da92d5e598
📒 Files selected for processing (4)
step-05-game-loop/server/main.gostep-06-snake/server/main.gostep-07-lag/server/main.gostep-08-interpolation/server/main.go
✅ Files skipped from review due to trivial changes (1)
- step-07-lag/server/main.go
Summary
This PR adds a complete educational curriculum for learning WebSocket development through building a multiplayer snake game. The curriculum progresses from basic WebSocket concepts through advanced topics like room management, latency handling, interpolation, reconnection, and optimization.
Key Changes
Implementation Details
Each step includes both server and client code with detailed README documentation explaining the concepts, design decisions, and implementation patterns.
https://claude.ai/code/session_01Cg4waeVYhPHpatXEsQMbfB
Summary by CodeRabbit