Multiplayer game lobby server built with Go. Supports HTTP API, WebSocket real-time events, PostgreSQL persistence, and Redis pub/sub for horizontal scaling.
┌─────────────┐ ┌──────────────┐ ┌────────────┐
│ Client │────▶│ HTTP/WS │────▶│ Service │
│ (HTTP/WS) │◀────│ (net/http) │◀────│ Layer │
└─────────────┘ └──────┬───────┘ └─────┬──────┘
│ │
│ ┌─────▼──────┐
│ │ Store │
│ │ (PostgreSQL│
│ │ /Memory) │
│ └─────┬──────┘
│ │
┌──────▼───────┐ │
│ WebSocket │ │
│ Hub │ │
│ (Redis Pub/ │ │
│ Sub) │ │
└──────────────┘ │
│
┌──────────────┐ │
│ Redis │◀───────────┘
│ (Pub/Sub) │
└──────────────┘
| Layer | Path | Responsibility |
|---|---|---|
| Entry point | cmd/game-server/main.go |
Config loading, DI wiring, server start |
| Config | internal/config/ |
Env-based configuration |
| HTTP API | internal/server/api/http/ |
Route handlers, request/response serialization |
| WebSocket | internal/server/api/ws/ |
Real-time connections, room broadcast |
| Middleware | internal/server/middleware/ |
Panic recovery, request logging |
| Service | internal/service/ |
Business logic, validation, event publishing |
| Store | internal/store/ |
Storage interface + PostgreSQL / in-memory implementations |
| Domain | internal/server/domain/ |
Shared types (Player, Room, Event) |
| Database | internal/database/ |
PostgreSQL connection pool + auto-migration |
| Redis | internal/redis/ |
Redis client wrapper for pub/sub |
| Error handling | internal/error_handling/ |
Standardized errors and JSON responses |
- Go 1.25 with
net/http - PostgreSQL 16 via
jackc/pgx/v5(connection pooling, prepared statements) - Redis 7 via
redis/go-redis/v9(pub/sub for real-time events) - WebSocket via
gorilla/websocket - Swagger via
swaggo/http-swagger - UUID via
google/uuid(player IDs only; room IDs use 4-digit codes viacrypto/rand)
Requires PostgreSQL and Redis running locally.
# Terminal 1: start dependencies
docker compose up postgres redis -d
# Terminal 2: start app
go run ./cmd/game-serverdocker compose up --buildThe app is available at http://localhost:8080.
All via environment variables:
| Variable | Default | Description |
|---|---|---|
SERVER_PORT |
8080 |
HTTP server port |
POSTGRES_DSN |
postgres://postgres:postgres@localhost:5432/game_server?sslmode=disable |
PostgreSQL connection string |
REDIS_ADDR |
localhost:6379 |
Redis address |
REDIS_PASS |
"" |
Redis password |
If Redis is unavailable, the app starts with a warning and real-time features are disabled.
All endpoints return JSON:
Success:
{
"success": true,
"data": { ... }
}Error:
{
"success": false,
"error": {
"code": "NOT_FOUND",
"message": "Player not found: abc-123"
}
}| Code | HTTP Status | Description |
|---|---|---|
NOT_FOUND |
404 | Resource not found |
BAD_REQUEST |
400 | Invalid input |
VALIDATION_ERROR |
400 | Validation failed |
CONFLICT |
409 | Duplicate or capacity exceeded |
INTERNAL_ERROR |
500 | Server-side failure |
METHOD_NOT_ALLOWED |
405 | Wrong HTTP method |
Create a new player.
curl -X POST http://localhost:8080/create-player \
-H "Content-Type: application/json" \
-d '{"nickname":"alice"}'Response: 201 Created
{
"success": true,
"data": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"nickname": "alice",
"hp": 100
}
}Get player details.
curl "http://localhost:8080/get-player?playerId=550e8400-e29b-41d4-a716-446655440000"Response: 200 OK
{
"success": true,
"data": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"nickname": "alice",
"hp": 100
}
}Create a new room.
curl -X POST http://localhost:8080/create-room \
-H "Content-Type: application/json" \
-d '{"name":"lobby","max-players":10}'Response: 201 Created
{
"success": true,
"data": {
"id": "660e8400-e29b-41d4-a716-446655440001",
"name": "lobby",
"playerIds": [],
"max-players": 10
}
}Get room state including players.
curl "http://localhost:8080/get-room?roomId=7429"Response: 200 OK
{
"success": true,
"data": {
"id": "7429",
"name": "lobby",
"playerIds": ["550e8400-e29b-41d4-a716-446655440000"],
"max-players": 10
}
}Join a player to a room.
curl -X POST http://localhost:8080/join-room \
-H "Content-Type: application/json" \
-d '{"roomId":"7429","playerId":"550e8400-..."}'Response: 200 OK
{
"success": true,
"data": {
"message": "Player joined the room"
}
}Error cases: 400 (missing fields), 404 (room not found), 409 (already in room / room full).
const ws = new WebSocket("ws://localhost:8080/ws?roomId=7429");
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
console.log(data.type, data);
};The server pushes events to all WebSocket clients connected to a room:
| Event type | Direction | Description |
|---|---|---|
room_created |
Server → Client | A new room was created |
player_joined |
Server → Client | A player joined, payload includes nickname |
player_left |
Server → Client | A player disconnected |
player_moved |
Server → Client | A player moved, payload {x, y} |
coin_collected |
Server → Client | A coin was collected, payload {coinId} |
room_state |
Server → Client | Current positions and nicknames of all players (sent on join) |
error |
Server → Client | An error occurred, payload {message} |
Client → Server messages:
| Message type | Description |
|---|---|
{"type":"move","x":100,"y":200} |
Update player position |
{"type":"collect","coinId":"c01"} |
Collect a coin |
Events are published via Redis pub/sub, so the system scales horizontally.
A web-based 2D multiplayer game lives in client/.
- Vue 3 + TypeScript + Vite
- Canvas 2D rendering
- WebSocket real-time communication
# Terminal 1 — start the Go server
go run ./cmd/game-server
# Terminal 2 — start the Vue dev server
cd client && npm run devThen open http://localhost:5173 in two browser tabs.
- Enter a nickname
- Leave Room ID empty to create a new room, or enter an existing room ID to join a friend
- Click Play
- Move your circle with WASD or arrow keys
- Collect golden ★ coins to earn points
- Watch other players move in real-time
| Feature | Description |
|---|---|
| Real-time movement | Position synced via WebSocket, ~20 updates/sec |
| Coins | 30 coins spawn across the map, respawn after 5s |
| Score | +10 points per coin, shown in HUD |
| Minimap | Top-right corner shows full map with player dots |
| Particles | Dust particles when walking, burst on coin collect |
| Player colors | Each player gets a deterministic color from their ID |
| Nicknames | Shown above each player circle |
| Camera | Follows your player smoothly |
| Walls | Brown border walls prevent leaving the map |
| Trees | Decorative obstacles at the corners |
| Eyes | Your character has cute eyes 👀 |
client/
├── index.html
├── package.json
├── vite.config.ts
├── tsconfig.json
├── src/
│ ├── main.ts
│ ├── App.vue ← screen router (lobby / game)
│ ├── style.css
│ ├── components/
│ │ ├── Lobby.vue ← nickname & room input
│ │ └── GameCanvas.vue ← canvas rendering & game loop
│ └── game/
│ ├── types.ts ← TypeScript interfaces
│ └── websocket.ts ← WebSocket client
- Map size: 2000 × 2000 px
- Camera: follows your player (viewport fills the window)
- Boundaries: brown walls, 24px thick
- 30 coins at fixed positions, golden with glow effect
- 8 decorative trees at map corners and edges
Swagger UI is available at:
http://localhost:8080/swagger/index.html
To regenerate docs:
swag init -g cmd/game-server/main.go# All tests
go test ./...
# With race detection
go test -race ./...
# Benchmarks
go test -bench=. ./...
# Verbose
go test -v ./...| Test file | Type | What it covers |
|---|---|---|
internal/error_handling/errors_test.go |
Unit | Error constructors, unwrapping, HTTP status codes |
internal/store/memory_test.go |
Unit | Store CRUD, concurrent access safety |
internal/service/player_test.go |
Service | Player creation, retrieval, validation |
internal/service/room_test.go |
Service | Room CRUD, join logic, capacity, duplicates |
internal/server/api/http/handler_test.go |
Integration | Full HTTP call chain via httptest |
internal/server/api/http/ws_test.go |
Integration | WebSocket connect, room isolation, broadcast |
internal/server/api/http/benchmark_test.go |
Benchmark/Stress | Per-endpoint throughput, concurrent requests |
cmd/game-server/main.go
internal/
├── config/config.go
├── database/postgres.go
├── error_handling/
│ ├── codes.go
│ ├── errors.go
│ └── response.go
├── redis/client.go
├── server/
│ ├── StartServer.go
│ ├── api/
│ │ ├── http/
│ │ │ ├── routes.go
│ │ │ ├── players.go
│ │ │ ├── rooms.go
│ │ │ ├── handler_test.go
│ │ │ ├── ws_test.go
│ │ │ └── benchmark_test.go
│ │ └── ws/
│ │ ├── hub.go
│ │ ├── client.go
│ │ └── handler.go
│ ├── domain/
│ │ ├── PlayerTypes.go
│ │ ├── RoomTypes.go
│ │ └── EventTypes.go
│ ├── id/GenerateUniqueID.go
│ └── middleware/middleware.go
├── service/
│ ├── service.go
│ ├── player.go
│ ├── player_test.go
│ ├── room.go
│ └── room_test.go
└── store/
├── interface.go
├── memory.go
├── memory_test.go
└── postgres.go
client/
├── package.json
├── vite.config.ts
├── index.html
└── src/
├── main.ts
├── App.vue
├── style.css
├── components/
│ ├── Lobby.vue
│ └── GameCanvas.vue
└── game/
├── types.ts
└── websocket.ts
Dockerfile
docker-compose.yml
