HTTP-сервис статистики посещений (chi + PostgreSQL)

Код-ревьюMiddle40 минпроверка — Спринт23-t003

Проведите код-ревью HTTP-сервиса на chi. Эндпоинт /visits отдаёт статистику посещений по дням (день, количество посещений, самая посещаемая локация за день) за период из query-параметра period (day/week). Данные читаются из PostgreSQL (таблица visits: user_id, location, ts), ответ — JSON.

Как на собесе: 40 минут, ищи всё — гонки, утечки, ошибки, безопасность, стиль. Ссылайся на строки.

review.gogo
1package main2 3import (4	"context"5	"database/sql"6	"encoding/json"7	"fmt"8	"net/http"9	"sort"10	"time"11 12	"github.com/go-chi/chi/v5"13	_ "github.com/lib/pq"14)15 16type Period string17 18const (19	PeriodWeek Period = "week"20	PeriodDay  Period = "day"21)22 23func (p Period) sqlInterval() string {24	switch p {25	case PeriodWeek:26		return "'7 days'"27	default:28		return "'1 days'"29	}30}31 32/*33DDL таблицы visits:34CREATE TABLE visits (35	user_id VARCHAR(25) not null,36	location VARCHAR(25) not null,37	ts TIMESTAMPTZ not null38);39*/40 41type Visit struct {42	UserID   string43	Location string44	TS       time.Time45}46 47type DayStat struct {48	Day         string49	Count       int50	TopLocation string51}52 53func main() {54	db, _ := sql.Open("postgres", "postgres://app:app@localhost:5432/events?sslmode=disable")55 56	r := chi.NewRouter()57	r.Get("/visits", handleVisits(*db))58 59	_ = http.ListenAndServe(":3000", r)60}61 62func handleVisits(db sql.DB) func(w http.ResponseWriter, r *http.Request) {63	return func(w http.ResponseWriter, r *http.Request) {64		ctx := context.WithValue(65			context.Background(),66			"period", Period(r.URL.Query().Get("period")))67 68		visits, _ := fetchVisits(&db, ctx)69		stats := aggregate(visits)70 71		b, _ := json.Marshal(stats)72		_, _ = w.Write(b)73	}74}75 76func fetchVisits(db *sql.DB, ctx context.Context) ([]Visit, error) {77	q := fmt.Sprintf(78		"SELECT user_id, location, ts FROM visits WHERE ts > NOW() - INTERVAL %s",79		ctx.Value("period").(Period).sqlInterval(),80	)81 82	rows, _ := db.Query(q)83	var visits []Visit84	for rows.Next() {85		var v Visit86		_ = rows.Scan(&v.UserID, &v.Location, &v.TS)87		visits = append(visits, v)88	}89 90	return visits, nil91}92 93func aggregate(visits []Visit) []DayStat {94	byDay := map[string]DayStat{}95	locByDay := map[string]map[string]int{}96 97	for _, v := range visits {98		day := v.TS.Format(time.DateOnly)99		if _, ok := byDay[day]; !ok {100			byDay[day] = DayStat{Day: day}101			locByDay[day] = map[string]int{}102		}103 104		stat := byDay[day]105		stat.Count++106		byDay[day] = stat107 108		locByDay[day][v.Location]++109	}110 111	for day, counts := range locByDay {112		var max int113		for loc, n := range counts {114			if n >= max {115				max = n116				stat := byDay[day]117				stat.TopLocation = loc118				byDay[day] = stat119			}120		}121	}122 123	out := make([]DayStat, 0, len(byDay))124	for _, s := range byDay {125		out = append(out, s)126	}127 128	sort.Slice(out, func(i, j int) bool { return out[i].Day < out[j].Day })129 130	return out131}
⌘ Enter — проверить