HTTP-сервер и клиент
Пакет net/http -- одна из жемчужин стандартной библиотеки Go. Он предоставляет production-ready HTTP-сервер и клиент без внешних зависимостей. С Go 1.22 роутер стал ещё мощнее, добавив поддержку HTTP-методов и path-параметров.
Простейший сервер
package main
import (
"fmt"
"log"
"net/http"
)
func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello, %s!", r.URL.Path[1:])
})
log.Println("Server starting on :8080")
log.Fatal(http.ListenAndServe(":8080", nil))
}
http.Handler -- ключевой интерфейс
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}
Весь HTTP в Go строится вокруг этого интерфейса. Любой тип, реализующий ServeHTTP, может обрабатывать HTTP-запросы.
// Custom handler type
type apiHandler struct {
db *sql.DB
}
func (h *apiHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// Handle request with access to database
users, err := h.db.Query("SELECT name FROM users")
if err != nil {
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}
defer users.Close()
// ...
}
func main() {
db, _ := sql.Open("postgres", "...")
handler := &apiHandler{db: db}
http.ListenAndServe(":8080", handler)
}
http.HandlerFunc -- адаптер
HandlerFunc -- тип-адаптер, превращающий обычную функцию в http.Handler:
type HandlerFunc func(ResponseWriter, *Request)
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
f(w, r)
}
Это позволяет использовать функции напрямую:
func healthCheck(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte(`{"status":"ok"}`))
}
// Both are equivalent:
http.Handle("/health", http.HandlerFunc(healthCheck))
http.HandleFunc("/health", healthCheck) // Shortcut
http.ServeMux -- роутер
Стандартный ServeMux (до Go 1.22)
mux := http.NewServeMux()
mux.HandleFunc("/", homeHandler)
mux.HandleFunc("/users", usersHandler)
mux.HandleFunc("/users/", userHandler) // Trailing slash = subtree match
http.ListenAndServe(":8080", mux)
Улучшенный ServeMux (Go 1.22+) -- рекомендуется!
Go 1.22 добавил поддержку HTTP-методов и path-параметров прямо в стандартный роутер:
mux := http.NewServeMux()
// Method-specific routing
mux.HandleFunc("GET /api/users", listUsers)
mux.HandleFunc("POST /api/users", createUser)
mux.HandleFunc("GET /api/users/{id}", getUser)
mux.HandleFunc("PUT /api/users/{id}", updateUser)
mux.HandleFunc("DELETE /api/users/{id}", deleteUser)
// Path parameters with {name}
mux.HandleFunc("GET /api/posts/{postID}/comments/{commentID}", getComment)
// Exact match (no subtree) with {$}
mux.HandleFunc("GET /{$}", homeHandler) // Only matches exactly "/"
// Wildcard: match rest of path
mux.HandleFunc("GET /files/{path...}", serveFiles)
http.ListenAndServe(":8080", mux)
Path-параметры (Go 1.22+)
func getUser(w http.ResponseWriter, r *http.Request) {
// Extract path parameter
id := r.PathValue("id")
user, err := findUser(id)
if err != nil {
http.Error(w, "User not found", http.StatusNotFound)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(user)
}
func getComment(w http.ResponseWriter, r *http.Request) {
postID := r.PathValue("postID")
commentID := r.PathValue("commentID")
// ...
}
func serveFiles(w http.ResponseWriter, r *http.Request) {
// {path...} captures the rest of the URL
filePath := r.PathValue("path") // e.g., "docs/readme.md"
// ...
}
http.Request -- входящий запрос
func handler(w http.ResponseWriter, r *http.Request) {
// Method
method := r.Method // "GET", "POST", etc.
// URL components
path := r.URL.Path // "/api/users"
query := r.URL.Query() // url.Values (map[string][]string)
page := query.Get("page") // "1" or ""
// Headers
contentType := r.Header.Get("Content-Type")
auth := r.Header.Get("Authorization")
// Body (for POST, PUT, PATCH)
defer r.Body.Close()
body, err := io.ReadAll(r.Body)
// Context (carries deadlines, cancellation, values)
ctx := r.Context()
// Remote address
remoteAddr := r.RemoteAddr // "192.168.1.1:12345"
// Form data
r.ParseForm()
name := r.FormValue("name")
// Multipart form (file uploads)
r.ParseMultipartForm(10 << 20) // 10 MB max
file, header, err := r.FormFile("avatar")
}
http.ResponseWriter -- формирование ответа
func handler(w http.ResponseWriter, r *http.Request) {
// Set headers BEFORE WriteHeader or Write
w.Header().Set("Content-Type", "application/json")
w.Header().Set("X-Request-ID", "abc-123")
w.Header().Set("Cache-Control", "no-cache")
// Set status code (default 200 if not called)
w.WriteHeader(http.StatusCreated) // 201
// Write body
w.Write([]byte(`{"id": "123", "name": "Alice"}`))
}
Важно: Порядок имеет значение! Заголовки нужно устанавливать до
WriteHeader()илиWrite(). После первогоWrite()заголовки уже отправлены.
JSON-ответы
type ErrorResponse struct {
Error string `json:"error"`
Code int `json:"code"`
Details string `json:"details,omitempty"`
}
func respondJSON(w http.ResponseWriter, status int, data any) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
if err := json.NewEncoder(w).Encode(data); err != nil {
log.Printf("failed to encode response: %v", err)
}
}
func respondError(w http.ResponseWriter, status int, message string) {
respondJSON(w, status, ErrorResponse{
Error: message,
Code: status,
})
}
Middleware-паттерн
Middleware в Go -- это функция, принимающая http.Handler и возвращающая http.Handler:
func middleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Before handler
// ...
next.ServeHTTP(w, r) // Call next handler
// After handler
// ...
})
}
Logging middleware
func loggingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
// Wrap ResponseWriter to capture status code
wrapped := &statusRecorder{ResponseWriter: w, statusCode: http.StatusOK}
next.ServeHTTP(wrapped, r)
log.Printf("%s %s %d %s",
r.Method,
r.URL.Path,
wrapped.statusCode,
time.Since(start),
)
})
}
type statusRecorder struct {
http.ResponseWriter
statusCode int
}
func (r *statusRecorder) WriteHeader(code int) {
r.statusCode = code
r.ResponseWriter.WriteHeader(code)
}
Auth middleware
func authMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
token := r.Header.Get("Authorization")
if token == "" {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
// Validate token
userID, err := validateToken(strings.TrimPrefix(token, "Bearer "))
if err != nil {
http.Error(w, "Invalid token", http.StatusUnauthorized)
return
}
// Add user ID to context
ctx := context.WithValue(r.Context(), userIDKey, userID)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
CORS middleware
func corsMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
if r.Method == http.MethodOptions {
w.WriteHeader(http.StatusOK)
return
}
next.ServeHTTP(w, r)
})
}
Recovery middleware
func recoveryMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if rec := recover(); rec != nil {
log.Printf("PANIC: %v\n%s", rec, debug.Stack())
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
}
}()
next.ServeHTTP(w, r)
})
}
Цепочка middleware
func chain(handler http.Handler, middlewares ...func(http.Handler) http.Handler) http.Handler {
// Apply in reverse order so first middleware is outermost
for i := len(middlewares) - 1; i >= 0; i-- {
handler = middlewares[i](handler)
}
return handler
}
func main() {
mux := http.NewServeMux()
mux.HandleFunc("GET /api/users", listUsers)
mux.HandleFunc("POST /api/users", createUser)
// Apply middleware chain
handler := chain(mux,
recoveryMiddleware,
loggingMiddleware,
corsMiddleware,
)
http.ListenAndServe(":8080", handler)
}
http.Client -- HTTP-клиент
Базовое использование
// Default client (no timeout -- DON'T use in production!)
resp, err := http.Get("https://api.example.com/users")
// ALWAYS create custom client with timeout
client := &http.Client{
Timeout: 10 * time.Second,
}
resp, err := client.Get("https://api.example.com/users")
if err != nil {
return fmt.Errorf("fetching users: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return fmt.Errorf("unexpected status %d: %s", resp.StatusCode, body)
}
var users []User
if err := json.NewDecoder(resp.Body).Decode(&users); err != nil {
return fmt.Errorf("decoding response: %w", err)
}
Полноценный клиент с настройками
client := &http.Client{
Timeout: 30 * time.Second,
Transport: &http.Transport{
MaxIdleConns: 100,
MaxIdleConnsPerHost: 10,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
},
}
// Custom request with headers
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, body)
if err != nil {
return fmt.Errorf("creating request: %w", err)
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("User-Agent", "MyApp/1.0")
resp, err := client.Do(req)
if err != nil {
return fmt.Errorf("sending request: %w", err)
}
defer resp.Body.Close()
POST-запрос с JSON
func createUser(ctx context.Context, client *http.Client, user User) (*User, error) {
body, err := json.Marshal(user)
if err != nil {
return nil, fmt.Errorf("marshaling user: %w", err)
}
req, err := http.NewRequestWithContext(ctx, http.MethodPost,
"https://api.example.com/users",
bytes.NewReader(body),
)
if err != nil {
return nil, fmt.Errorf("creating request: %w", err)
}
req.Header.Set("Content-Type", "application/json")
resp, err := client.Do(req)
if err != nil {
return nil, fmt.Errorf("sending request: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusCreated {
body, _ := io.ReadAll(resp.Body)
return nil, fmt.Errorf("unexpected status %d: %s", resp.StatusCode, body)
}
var created User
if err := json.NewDecoder(resp.Body).Decode(&created); err != nil {
return nil, fmt.Errorf("decoding response: %w", err)
}
return &created, nil
}
JSON API: полный пример
type UserHandler struct {
store UserStore
}
func (h *UserHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
h.list(w, r)
case http.MethodPost:
h.create(w, r)
default:
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
}
}
func (h *UserHandler) create(w http.ResponseWriter, r *http.Request) {
// Limit request body size
r.Body = http.MaxBytesReader(w, r.Body, 1<<20) // 1 MB
var input struct {
Name string `json:"name"`
Email string `json:"email"`
}
dec := json.NewDecoder(r.Body)
dec.DisallowUnknownFields() // Strict parsing
if err := dec.Decode(&input); err != nil {
respondError(w, http.StatusBadRequest, "Invalid JSON: "+err.Error())
return
}
// Validate
if input.Name == "" {
respondError(w, http.StatusBadRequest, "Name is required")
return
}
user, err := h.store.Create(r.Context(), input.Name, input.Email)
if err != nil {
if errors.Is(err, ErrDuplicate) {
respondError(w, http.StatusConflict, "User already exists")
return
}
log.Printf("creating user: %v", err)
respondError(w, http.StatusInternalServerError, "Internal error")
return
}
respondJSON(w, http.StatusCreated, user)
}
File Server
// Serve static files from directory
fs := http.FileServer(http.Dir("./static"))
http.Handle("/static/", http.StripPrefix("/static/", fs))
// Serve embedded files
//go:embed static
var staticFiles embed.FS
sub, _ := fs.Sub(staticFiles, "static")
http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.FS(sub))))
// Serve single file
http.HandleFunc("/favicon.ico", func(w http.ResponseWriter, r *http.Request) {
http.ServeFile(w, r, "static/favicon.ico")
})
Graceful Shutdown
Production-серверы должны завершаться корректно -- дождаться завершения текущих запросов:
func main() {
mux := http.NewServeMux()
mux.HandleFunc("GET /health", healthHandler)
mux.HandleFunc("GET /api/users", listUsersHandler)
server := &http.Server{
Addr: ":8080",
Handler: mux,
ReadTimeout: 5 * time.Second,
WriteTimeout: 10 * time.Second,
IdleTimeout: 120 * time.Second,
}
// Start server in goroutine
go func() {
log.Printf("Server listening on %s", server.Addr)
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("Server error: %v", err)
}
}()
// Wait for interrupt signal
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
log.Println("Shutting down server...")
// Give outstanding requests 30 seconds to complete
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if err := server.Shutdown(ctx); err != nil {
log.Fatalf("Server forced to shutdown: %v", err)
}
log.Println("Server stopped gracefully")
}
http.MaxBytesReader -- защита от переполнения
func uploadHandler(w http.ResponseWriter, r *http.Request) {
// Limit to 10 MB
r.Body = http.MaxBytesReader(w, r.Body, 10<<20)
if err := r.ParseMultipartForm(10 << 20); err != nil {
var maxBytesErr *http.MaxBytesError
if errors.As(err, &maxBytesErr) {
respondError(w, http.StatusRequestEntityTooLarge, "File too large (max 10 MB)")
return
}
respondError(w, http.StatusBadRequest, "Invalid form data")
return
}
file, header, err := r.FormFile("file")
if err != nil {
respondError(w, http.StatusBadRequest, "Missing file")
return
}
defer file.Close()
fmt.Printf("Uploaded: %s (%d bytes)\n", header.Filename, header.Size)
}