Практика: Web и REST API
Задача 1: REST API CRUD для пользователей
Создайте полноценный REST API для управления пользователями.
package main
import (
"encoding/json"
"fmt"
"log"
"net/http"
"sync"
)
// Task: Определите структуры User и UserStore
type User struct {
ID int `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
Age int `json:"age"`
CreatedAt string `json:"created_at"`
}
type UserStore struct {
// Ваши поля (мьютекс, мапа)
}
// Task: Реализуйте методы UserStore
func NewUserStore() *UserStore {
return nil
}
func (s *UserStore) Create(w http.ResponseWriter, r *http.Request) {
// 1. Декодируйте JSON в User
// 2. Сгенерируйте ID
// 3. Добавьте created_at
// 4. Сохраните в мапу
// 5. Верните 201 Created
}
func (s *UserStore) GetAll(w http.ResponseWriter, r *http.Request) {
// Верните список всех пользователей
}
func (s *UserStore) Get(w http.ResponseWriter, r *http.Request) {
// Получите ID из URL
// Верните пользователя или 404
}
func (s *UserStore) Update(w http.ResponseWriter, r *http.Request) {
// Получите ID из URL
// Обновите данные
}
func (s *UserStore) Delete(w http.ResponseWriter, r *http.Request) {
// Получите ID из URL
// Удалите пользователя
}
func main() {
store := NewUserStore()
// Маршруты:
// POST /users - создать
// GET /users - все
// GET /users/:id - один
// PUT /users/:id - обновить
// DELETE /users/:id - удалить
fmt.Println("Server starting on :8080")
log.Fatal(http.ListenAndServe(":8080", nil))
}
Задача 2: Middleware логирования и аутентификации
Добавьте middleware для логирования и простой аутентификации.
package main
import (
"fmt"
"log"
"net/http"
"time"
)
// Task: Создайте middleware
func loggingMiddleware(next http.Handler) http.Handler {
// Логируйте: метод, URL, время выполнения
return nil
}
func authMiddleware(next http.Handler) http.Handler {
// Проверяйте заголовок Authorization: Bearer <token>
// Если нет токена — 401 Unauthorized
return nil
}
func main() {
mux := http.NewServeMux()
mux.HandleFunc("GET /protected", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Protected resource!"))
})
mux.HandleFunc("GET /public", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Public resource!"))
})
// Примените middleware
// handler := loggingMiddleware(mux)
// handler = authMiddleware(handler)
fmt.Println("Server starting on :8080")
log.Fatal(http.ListenAndServe(":8080", nil))
}
Задача: Паттерн Repository
Создайте абстракцию для работы с данными.
package main
import (
"database/sql"
"fmt"
"net/http"
"sync"
)
type User struct {
ID int `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
}
// Task: Определите интерфейс Repository
type Repository interface {
Create(user *User) error
GetByID(id int) (*User, error)
GetAll() ([]*User, error)
Update(user *User) error
Delete(id int) error
}
// Task: Создайте inmemory реализацию
type InMemoryRepository struct {
mu sync.RWMutex
users map[int]*User
nextID int
}
func NewInMemoryRepository() *InMemoryRepository {
return nil
}
// Task: Реализуйте все методы
// Task: HTTP handlers, использующие Repository
type UserHandler struct {
repo Repository
}
func NewUserHandler(repo Repository) *UserHandler {
return nil
}
func (h *UserHandler) Create(w http.ResponseWriter, r *http.Request) {
// Используйте h.repo
}
func (h *UserHandler) GetByID(w http.ResponseWriter, r *http.Request) {
// Используйте h.repo
}
Задача 4: Graceful Shutdown с graceful остановкой
Добавьте graceful shutdown с ожиданием завершения запросов.
package main
import (
"context"
"fmt"
"log"
"net/http"
"os"
"os/signal"
"syscall"
"time"
)
type Server struct {
srv *http.Server
activeReqs int
mu sync.Mutex
shutdownCh chan struct{}
}
func NewServer() *Server {
return &Server{
srv: &http.Server{
Addr: ":8080",
},
shutdownCh: make(chan struct{}),
}
}
func (s *Server) Handle(pattern string, handler http.Handler) {
// Оборачиваем handler для подсчёта активных запросов
wrapped := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
s.mu.Lock()
s.activeReqs++
s.mu.Unlock()
handler.ServeHTTP(w, r)
s.mu.Lock()
s.activeReqs--
if s.activeReqs == 0 {
close(s.shutdownCh)
}
s.mu.Unlock()
})
s.srv.Handler = wrapped
}
func (s *Server) Start() {
go func() {
fmt.Println("Server starting...")
if err := s.srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatal(err)
}
}()
}
func (s *Server) Shutdown(timeout time.Duration) error {
// Ожидайте SIGINT/SIGTERM
// Корректно завершите сервер
return nil
}
func main() {
server := NewServer()
server.Handle("GET /health", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("OK"))
}))
server.Handle("GET /slow", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
time.Sleep(5 * time.Second)
w.Write([]byte("Done!"))
}))
server.Start()
server.Shutdown(30 * time.Second)
}
Задача 5: Rate Limiting middleware
Создайте middleware для ограничения частоты запросов.
package main
import (
"fmt"
"net/http"
"sync"
"time"
)
// Task: Создайте RateLimitMiddleware
type RateLimitConfig struct {
RequestsPerSecond int
Burst int
}
func RateLimitMiddleware(config RateLimitConfig) func(http.Handler) http.Handler {
// Подсказки:
// 1. Используйте token bucket или leaky bucket
// 2. Храните состояние для каждого IP
// 3. Возвращайте 429 Too Many Requests при превышении
return nil
}
func main() {
mux := http.NewServeMux()
mux.HandleFunc("GET /api/data", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Data"))
})
// Примените rate limiting (10 запросов/сек, burst 20)
// handler := RateLimitMiddleware(RateLimitConfig{
// RequestsPerSecond: 10,
// Burst: 20,
// })(mux)
fmt.Println("Server starting on :8080")
http.ListenAndServe(":8080", mux)
}
Решения
Решение задачи 1
type UserStore struct {
mu sync.RWMutex
users map[int]*User
nextID int
}
func NewUserStore() *UserStore {
return &UserStore{
users: make(map[int]*User),
nextID: 1,
}
}
func (s *UserStore) Create(w http.ResponseWriter, r *http.Request) {
var user User
if err := json.NewDecoder(r.Body).Decode(&user); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
s.mu.Lock()
user.ID = s.nextID
user.CreatedAt = time.Now().Format(time.RFC3339)
s.users[user.ID] = &user
s.nextID++
s.mu.Unlock()
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(user)
}
func (s *UserStore) GetAll(w http.ResponseWriter, r *http.Request) {
s.mu.RLock()
users := make([]*User, 0, len(s.users))
for _, u := range s.users {
users = append(users, u)
}
s.mu.RUnlock()
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(users)
}
func (s *UserStore) Get(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
var userID int
fmt.Sscanf(id, "%d", &userID)
s.mu.RLock()
user, exists := s.users[userID]
s.mu.RUnlock()
if !exists {
http.Error(w, "user not found", http.StatusNotFound)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(user)
}
func (s *UserStore) Update(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
var userID int
fmt.Sscanf(id, "%d", &userID)
var update User
if err := json.NewDecoder(r.Body).Decode(&update); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
s.mu.Lock()
user, exists := s.users[userID]
if !exists {
s.mu.Unlock()
http.Error(w, "user not found", http.StatusNotFound)
return
}
user.Name = update.Name
user.Email = update.Email
user.Age = update.Age
s.mu.Unlock()
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(user)
}
func (s *UserStore) Delete(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
var userID int
fmt.Sscanf(id, "%d", &userID)
s.mu.Lock()
if _, exists := s.users[userID]; !exists {
s.mu.Unlock()
http.Error(w, "user not found", http.StatusNotFound)
return
}
delete(s.users, userID)
s.mu.Unlock()
w.WriteHeader(http.StatusNoContent)
}
Решение задачи 2
func loggingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
next.ServeHTTP(w, r)
log.Printf("%s %s %v", r.Method, r.URL.Path, time.Since(start))
})
}
func authMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
auth := r.Header.Get("Authorization")
if auth == "" {
http.Error(w, "Authorization required", http.StatusUnauthorized)
return
}
// Простая проверка (в реальном проекте используйте JWT)
if len(auth) < 7 || auth[:7] != "Bearer " {
http.Error(w, "Invalid token", http.StatusUnauthorized)
return
}
next.ServeHTTP(w, r)
})
}
Решение задачи 4
func (s *Server) Shutdown(timeout time.Duration) error {
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
log.Println("Shutting down...")
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
// Ожидаем завершения активных запросов
s.mu.Lock()
if s.activeReqs > 0 {
log.Printf("Waiting for %d active requests...", s.activeReqs)
s.mu.Unlock()
select {
case <-s.shutdownCh:
log.Println("All requests completed")
case <-ctx.Done():
log.Println("Timeout reached")
}
} else {
s.mu.Unlock()
}
return s.srv.Shutdown(ctx)
}
Решение задачи 5
type RateLimitMiddleware struct {
requests map[string][]time.Time
mu sync.Mutex
config RateLimitConfig
}
func RateLimitMiddleware(config RateLimitConfig) func(http.Handler) http.Handler {
m := &RateLimitMiddleware{
requests: make(map[string][]time.Time),
config: config,
}
// Очистка старых запросов каждую секунду
go func() {
ticker := time.NewTicker(time.Second)
for range ticker.C {
m.mu.Lock()
for ip, times := range m.requests {
cutoff := time.Now().Add(-time.Second)
var valid []time.Time
for _, t := range times {
if t.After(cutoff) {
valid = append(valid, t)
}
}
if len(valid) == 0 {
delete(m.requests, ip)
} else {
m.requests[ip] = valid
}
}
m.mu.Unlock()
}
}()
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ip := r.RemoteAddr
m.mu.Lock()
times := m.requests[ip]
now := time.Now()
// Фильтруем старые запросы
cutoff := now.Add(-time.Second)
var valid []time.Time
for _, t := range times {
if t.After(cutoff) {
valid = append(valid, t)
}
}
if len(valid) >= m.config.Burst {
m.mu.Unlock()
w.Header().Set("Retry-After", "1")
http.Error(w, "Too Many Requests", http.StatusTooManyRequests)
return
}
m.requests[ip] = append(valid, now)
m.mu.Unlock()
next.ServeHTTP(w, r)
})
}
}