const http = require('http') → import "net/http"
http.createServer((req, res) => {}) → http.HandleFunc("/path", handler)
express.Router() → http.NewServeMux()
res.json(data) → json.NewEncoder(w).Encode(data)
req.body → json.NewDecoder(r.Body).Decode(&v)
req.params.id → r.PathValue("id") (Go 1.22+)
req.query → r.URL.Query().Get("key")
"GET /path", "/path/{id}" (Go 1.22+)mkdir go-http-server && cd go-http-server
go mod init go-http-server
# Устанавливаем UUID для генерации ID
go get github.com/google/uuid
go mod tidy
package main
import (
"context"
"encoding/json"
"errors"
"fmt"
"log"
"net/http"
"os"
"os/signal"
"strings"
"sync"
"syscall"
"time"
"github.com/google/uuid"
)
// ╔══════════════════════════════════════════════════════════╗
// ║ 1. МОДЕЛЬ ДАННЫХ ║
// ╚══════════════════════════════════════════════════════════╝
// Note — заметка. Теги json управляют сериализацией.
// `json:"id"` — имя поля в JSON.
// `json:"-"` — поле не сериализуется.
type Note struct {
ID string `json:"id"`
Title string `json:"title"`
Content string `json:"content"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// CreateNoteRequest — DTO для создания заметки
type CreateNoteRequest struct {
Title string `json:"title"`
Content string `json:"content"`
}
// UpdateNoteRequest — DTO для обновления
type UpdateNoteRequest struct {
Title *string `json:"title"` // Указатель — чтобы отличить отсутствие от пустой строки
Content *string `json:"content"`
}
// ErrorResponse — структура ошибки для клиента
type ErrorResponse struct {
Error string `json:"error"`
Message string `json:"message,omitempty"`
Code int `json:"code"`
}
// ╔══════════════════════════════════════════════════════════╗
// ║ 2. ХРАНИЛИЩЕ ЗАМЕТОК (потокобезопасное) ║
// ╚══════════════════════════════════════════════════════════╝
// NoteStore — потокобезопасное in-memory хранилище.
// sync.RWMutex позволяет множественное чтение.
type NoteStore struct {
mu sync.RWMutex
notes map[string]Note
}
// NewNoteStore — конструктор
func NewNoteStore() *NoteStore {
return &NoteStore{
notes: make(map[string]Note),
}
}
// Create — создаёт новую заметку
func (s *NoteStore) Create(title, content string) Note {
s.mu.Lock()
defer s.mu.Unlock()
now := time.Now()
note := Note{
ID: uuid.New().String(),
Title: title,
Content: content,
CreatedAt: now,
UpdatedAt: now,
}
s.notes[note.ID] = note
return note
}
// GetAll — возвращает все заметки (слайс, отсортированный по CreatedAt)
func (s *NoteStore) GetAll() []Note {
s.mu.RLock()
defer s.mu.RUnlock()
result := make([]Note, 0, len(s.notes))
for _, n := range s.notes {
result = append(result, n)
}
// TODO: сортировка по CreatedAt
return result
}
// GetByID — возвращает заметку по ID
func (s *NoteStore) GetByID(id string) (Note, error) {
s.mu.RLock()
defer s.mu.RUnlock()
note, ok := s.notes[id]
if !ok {
return Note{}, ErrNotFound
}
return note, nil
}
// Update — обновляет заметку
func (s *NoteStore) Update(id string, req UpdateNoteRequest) (Note, error) {
s.mu.Lock()
defer s.mu.Unlock()
note, ok := s.notes[id]
if !ok {
return Note{}, ErrNotFound
}
if req.Title != nil {
note.Title = *req.Title
}
if req.Content != nil {
note.Content = *req.Content
}
note.UpdatedAt = time.Now()
s.notes[id] = note
return note, nil
}
// Delete — удаляет заметку
func (s *NoteStore) Delete(id string) error {
s.mu.Lock()
defer s.mu.Unlock()
if _, ok := s.notes[id]; !ok {
return ErrNotFound
}
delete(s.notes, id)
return nil
}
// Search — поиск по заголовку (простая реализация)
func (s *NoteStore) Search(query string) []Note {
s.mu.RLock()
defer s.mu.RUnlock()
var result []Note
lowerQuery := strings.ToLower(query)
for _, n := range s.notes {
if strings.Contains(strings.ToLower(n.Title), lowerQuery) {
result = append(result, n)
}
}
return result
}
// ╔══════════════════════════════════════════════════════════╗
// ║ 3. ОШИБКИ ХРАНИЛИЩА ║
// ╚══════════════════════════════════════════════════════════╝
var (
ErrNotFound = errors.New("note not found")
ErrEmptyTitle = errors.New("title must not be empty")
)
// ╔══════════════════════════════════════════════════════════╗
// ║ 4. HTTP-ХЕНДЛЕРЫ ║
// ╚══════════════════════════════════════════════════════════╝
// Handler — структура с зависимостями (store)
type Handler struct {
store *NoteStore
}
// NewHandler — конструктор
func NewHandler(store *NoteStore) *Handler {
return &Handler{store: store}
}
// ==========================================
// Хелперы для JSON-ответов
// ==========================================
// writeJSON — отправляет JSON-ответ
func writeJSON(w http.ResponseWriter, status int, data any) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
// json.NewEncoder пишет прямо в ResponseWriter (io.Writer)
if err := json.NewEncoder(w).Encode(data); err != nil {
log.Printf("Ошибка кодирования JSON: %v", err)
}
}
// writeError — отправляет ошибку в JSON
func writeError(w http.ResponseWriter, status int, message string) {
writeJSON(w, status, ErrorResponse{
Error: http.StatusText(status),
Message: message,
Code: status,
})
}
// decodeJSON — читает JSON из тела запроса
func decodeJSON(r *http.Request, v any) error {
// Ограничиваем размер тела (1MB)
r.Body = http.MaxBytesReader(nil, r.Body, 1<<20) // 1MB
dec := json.NewDecoder(r.Body)
// Запрещаем неизвестные поля (строгий режим)
dec.DisallowUnknownFields()
if err := dec.Decode(v); err != nil {
return fmt.Errorf("decode JSON: %w", err)
}
return nil
}
// ==========================================
// CRUD ХЕНДЛЕРЫ
// ==========================================
// CreateNote — POST /api/notes
func (h *Handler) CreateNote(w http.ResponseWriter, r *http.Request) {
var req CreateNoteRequest
if err := decodeJSON(r, &req); err != nil {
writeError(w, http.StatusBadRequest, "Invalid JSON: "+err.Error())
return
}
// Валидация
if strings.TrimSpace(req.Title) == "" {
writeError(w, http.StatusBadRequest, ErrEmptyTitle.Error())
return
}
note := h.store.Create(req.Title, req.Content)
writeJSON(w, http.StatusCreated, note)
}
// GetAllNotes — GET /api/notes
func (h *Handler) GetAllNotes(w http.ResponseWriter, r *http.Request) {
// Проверяем query-параметр ?q=search
query := r.URL.Query().Get("q")
var notes []Note
if query != "" {
notes = h.store.Search(query)
} else {
notes = h.store.GetAll()
}
// Всегда возвращаем массив (не nil) — для красивого JSON
if notes == nil {
notes = []Note{}
}
writeJSON(w, http.StatusOK, notes)
}
// GetNote — GET /api/notes/{id}
func (h *Handler) GetNote(w http.ResponseWriter, r *http.Request) {
// r.PathValue — новое в Go 1.22 (аналог req.params в Express)
id := r.PathValue("id")
if id == "" {
writeError(w, http.StatusBadRequest, "ID is required")
return
}
note, err := h.store.GetByID(id)
if err != nil {
if errors.Is(err, ErrNotFound) {
writeError(w, http.StatusNotFound, "Note not found")
return
}
writeError(w, http.StatusInternalServerError, err.Error())
return
}
writeJSON(w, http.StatusOK, note)
}
// UpdateNote — PUT /api/notes/{id}
func (h *Handler) UpdateNote(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
if id == "" {
writeError(w, http.StatusBadRequest, "ID is required")
return
}
var req UpdateNoteRequest
if err := decodeJSON(r, &req); err != nil {
writeError(w, http.StatusBadRequest, "Invalid JSON: "+err.Error())
return
}
// Проверяем, что хотя бы одно поле передано
if req.Title == nil && req.Content == nil {
writeError(w, http.StatusBadRequest, "At least one field required")
return
}
note, err := h.store.Update(id, req)
if err != nil {
if errors.Is(err, ErrNotFound) {
writeError(w, http.StatusNotFound, "Note not found")
return
}
writeError(w, http.StatusInternalServerError, err.Error())
return
}
writeJSON(w, http.StatusOK, note)
}
// DeleteNote — DELETE /api/notes/{id}
func (h *Handler) DeleteNote(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
if id == "" {
writeError(w, http.StatusBadRequest, "ID is required")
return
}
if err := h.store.Delete(id); err != nil {
if errors.Is(err, ErrNotFound) {
writeError(w, http.StatusNotFound, "Note not found")
return
}
writeError(w, http.StatusInternalServerError, err.Error())
return
}
// 204 No Content — успех без тела ответа
w.WriteHeader(http.StatusNoContent)
}
// HealthCheck — GET /health (для проверки живости сервера)
func (h *Handler) HealthCheck(w http.ResponseWriter, r *http.Request) {
writeJSON(w, http.StatusOK, map[string]any{
"status": "ok",
"time": time.Now().Format(time.RFC3339),
})
}
// ╔══════════════════════════════════════════════════════════╗
// ║ 5. НАСТРОЙКА РОУТЕРА ║
// ╚══════════════════════════════════════════════════════════╝
// setupRouter — создаёт и настраивает ServeMux
func setupRouter(h *Handler) *http.ServeMux {
// http.NewServeMux — мультиплексор (роутер)
mux := http.NewServeMux()
// В Go 1.22+ можно указывать HTTP-метод прямо в паттерне:
// "METHOD /path" — роутинг по методу
// "/path/{name}" — path-параметр
// "/path/{name}/..." — захват всего остатка пути
// Заметки
mux.HandleFunc("GET /api/notes", h.GetAllNotes)
mux.HandleFunc("POST /api/notes", h.CreateNote)
mux.HandleFunc("GET /api/notes/{id}", h.GetNote)
mux.HandleFunc("PUT /api/notes/{id}", h.UpdateNote)
mux.HandleFunc("DELETE /api/notes/{id}", h.DeleteNote)
// Health check
mux.HandleFunc("GET /health", h.HealthCheck)
// Если Go < 1.22, используем старый способ:
// mux.HandleFunc("/api/notes", func(w http.ResponseWriter, r *http.Request) {
// switch r.Method {
// case http.MethodGet: ...
// case http.MethodPost: ...
// }
// })
return mux
}
// ╔══════════════════════════════════════════════════════════╗
// ║ 6. MIDDLEWARE (логирование) ║
// ╚══════════════════════════════════════════════════════════╝
// loggingMiddleware — логирует каждый запрос
func loggingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
// Оборачиваем ResponseWriter, чтобы захватить статус-код
wrapped := &responseWriter{ResponseWriter: w, statusCode: http.StatusOK}
next.ServeHTTP(wrapped, r)
log.Printf("%s %s → %d (%v)",
r.Method,
r.URL.Path,
wrapped.statusCode,
time.Since(start),
)
})
}
// responseWriter — обёртка для захвата статус-кода
type responseWriter struct {
http.ResponseWriter
statusCode int
}
func (rw *responseWriter) WriteHeader(code int) {
rw.statusCode = code
rw.ResponseWriter.WriteHeader(code)
}
// ╔══════════════════════════════════════════════════════════╗
// ║ MAIN ║
// ╚══════════════════════════════════════════════════════════╝
func main() {
log.SetFlags(log.LstdFlags | log.Lmicroseconds)
log.Println("🚀 Запуск HTTP-сервера...")
// Инициализация зависимостей
store := NewNoteStore()
handler := NewHandler(store)
router := setupRouter(handler)
// Оборачиваем роутер в middleware
app := loggingMiddleware(router)
// Настройка HTTP-сервера с таймаутами
server := &http.Server{
Addr: ":8080",
Handler: app,
ReadTimeout: 5 * time.Second, // Максимальное время чтения запроса
WriteTimeout: 10 * time.Second, // Максимальное время записи ответа
IdleTimeout: 120 * time.Second, // Таймаут keep-alive соединений
// MaxHeaderBytes: 1 << 20, // Максимальный размер заголовков (1MB)
}
// Запуск сервера в горутине
go func() {
log.Printf("Сервер слушает на http://localhost%s", server.Addr)
log.Println("Доступные эндпоинты:")
log.Println(" GET /health")
log.Println(" GET /api/notes")
log.Println(" GET /api/notes?q=search")
log.Println(" POST /api/notes")
log.Println(" GET /api/notes/{id}")
log.Println(" PUT /api/notes/{id}")
log.Println(" DELETE /api/notes/{id}")
// ListenAndServe блокируется, пока сервер работает
if err := server.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
log.Fatalf("Ошибка сервера: %v", err)
}
}()
// ==========================================
// Graceful shutdown
// ==========================================
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit // Блокируемся до получения сигнала
log.Println("🛑 Выключение сервера...")
// Даём 10 секунд на завершение активных запросов
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
if err := server.Shutdown(ctx); err != nil {
log.Fatalf("Ошибка при остановке сервера: %v", err)
}
log.Println("✅ Сервер остановлен")
}
# Запуск сервера
go run main.go
# В другом терминале:
# Проверка здоровья
curl http://localhost:8080/health
# Создание заметки
curl -X POST http://localhost:8080/api/notes \
-H "Content-Type: application/json" \
-d '{"title":"Первая заметка","content":"Содержание заметки"}'
# Получение всех заметок
curl http://localhost:8080/api/notes
# Получение всех с поиском
curl "http://localhost:8080/api/notes?q=первая"
# Получение одной заметки (замените ID на реальный)
curl http://localhost:8080/api/notes/ВАШ_ID
# Обновление заметки
curl -X PUT http://localhost:8080/api/notes/ВАШ_ID \
-H "Content-Type: application/json" \
-d '{"title":"Обновлённый заголовок"}'
# Удаление заметки
curl -X DELETE http://localhost:8080/api/notes/ВАШ_ID
# Проверка ошибки: пустой заголовок
curl -X POST http://localhost:8080/api/notes \
-H "Content-Type: application/json" \
-d '{"title":"","content":"test"}'
# Проверка ошибки: несуществующий ID
curl http://localhost:8080/api/notes/nonexistent
| Компонент | Go | Node.js (Express) |
|---|---|---|
| Метод | r.Method | req.method |
| Путь | r.URL.Path | req.path |
| Query-параметры | r.URL.Query().Get("q") | req.query.q |
| Path-параметры | r.PathValue("id") (1.22+) | req.params.id |
| Заголовки | r.Header.Get("Content-Type") | req.headers['content-type'] |
| Тело запроса | io.ReadAll(r.Body) | req.body |
| Ответ JSON | json.NewEncoder(w).Encode(v) | res.json(v) |
| Статус ответа | w.WriteHeader(200) | res.status(200) |
# Запуск
go run main.go
# Сборка
go build -o http-server main.go
./http-server
# Запуск с race-детектором
go run -race main.go
💡 Best practices от сеньоров:
"GET /path", "POST /path/{id}" — чистый и понятный роутинг.💡 Для Node.js разработчика: