Home » Как создать HTTP-сервер на Go
Как создать HTTP-сервер на Go

Как создать HTTP-сервер на Go

Сколько раз мы разработчики сталкиваемся с ситуацией, когда нужно быстро поднять HTTP-сервер? Может быть, для тестирования API, прототипирования или развёртывания микросервиса в production. Go — это один из самых удобных языков для создания веб-серверов, и сегодня мы разберём, как написать эффективный HTTP-сервер на Go с нуля. Я покажу не только базовые примеры, но и продвинутые техники, которые пригодятся в реальных проектах.

Почему именно Go для HTTP-серверов?

Go был создан в Google специально для системного программирования и сетевых приложений. Встроенная поддержка HTTP, горутины для конкурентности и минималистичный синтаксис делают его идеальным выбором для веб-разработки. По статистике Stack Overflow Developer Survey 2023, Go занимает 4-е место по популярности среди языков для backend-разработки.

Основные преимущества Go для HTTP-серверов:

  • Встроенный пакет net/http без внешних зависимостей
  • Высокая производительность (сравнимая с C++)
  • Простота развёртывания — один исполняемый файл
  • Отличная поддержка конкурентности
  • Быстрая компиляция

Базовый HTTP-сервер за 5 минут

Давайте начнём с самого простого примера. Создадим файл main.go:

package main

import (
    "fmt"
    "net/http"
    "log"
)

func main() {
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        fmt.Fprintf(w, "Hello, World! Method: %s, URL: %s", r.Method, r.URL.Path)
    })
    
    log.Println("Server starting on :8080")
    log.Fatal(http.ListenAndServe(":8080", nil))
}

Запускаем сервер:

go run main.go

Всё! Сервер работает на localhost:8080. Но это только начало.

Создание полноценного REST API

Теперь создадим более практичный пример — REST API для управления пользователями:

package main

import (
    "encoding/json"
    "fmt"
    "log"
    "net/http"
    "strconv"
    "strings"
)

type User struct {
    ID   int    `json:"id"`
    Name string `json:"name"`
    Email string `json:"email"`
}

var users = []User{
    {ID: 1, Name: "John Doe", Email: "john@example.com"},
    {ID: 2, Name: "Jane Smith", Email: "jane@example.com"},
}

func getUsersHandler(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(users)
}

func getUserHandler(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "application/json")
    
    // Извлекаем ID из URL
    path := strings.TrimPrefix(r.URL.Path, "/users/")
    id, err := strconv.Atoi(path)
    if err != nil {
        http.Error(w, "Invalid user ID", http.StatusBadRequest)
        return
    }
    
    // Ищем пользователя
    for _, user := range users {
        if user.ID == id {
            json.NewEncoder(w).Encode(user)
            return
        }
    }
    
    http.Error(w, "User not found", http.StatusNotFound)
}

func createUserHandler(w http.ResponseWriter, r *http.Request) {
    if r.Method != http.MethodPost {
        http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
        return
    }
    
    var newUser User
    if err := json.NewDecoder(r.Body).Decode(&newUser); err != nil {
        http.Error(w, "Invalid JSON", http.StatusBadRequest)
        return
    }
    
    // Генерируем новый ID
    newUser.ID = len(users) + 1
    users = append(users, newUser)
    
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(http.StatusCreated)
    json.NewEncoder(w).Encode(newUser)
}

func usersHandler(w http.ResponseWriter, r *http.Request) {
    switch r.Method {
    case http.MethodGet:
        if r.URL.Path == "/users" {
            getUsersHandler(w, r)
        } else {
            getUserHandler(w, r)
        }
    case http.MethodPost:
        createUserHandler(w, r)
    default:
        http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
    }
}

func main() {
    http.HandleFunc("/users", usersHandler)
    http.HandleFunc("/users/", usersHandler)
    
    log.Println("Server starting on :8080")
    log.Fatal(http.ListenAndServe(":8080", nil))
}

Тестируем API:

# Получить всех пользователей
curl http://localhost:8080/users

# Получить пользователя по ID
curl http://localhost:8080/users/1

# Создать нового пользователя
curl -X POST http://localhost:8080/users \
  -H "Content-Type: application/json" \
  -d '{"name":"Bob Wilson","email":"bob@example.com"}'

Продвинутые возможности: middleware и роутинг

Для production-приложений нужны middleware для логирования, аутентификации и обработки ошибок. Вот пример с кастомным роутером:

package main

import (
    "encoding/json"
    "log"
    "net/http"
    "time"
)

// Middleware для логирования
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))
    })
}

// Middleware для CORS
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)
    })
}

// Простой роутер
type Router struct {
    routes map[string]map[string]http.HandlerFunc
}

func NewRouter() *Router {
    return &Router{
        routes: make(map[string]map[string]http.HandlerFunc),
    }
}

func (r *Router) Handle(method, path string, handler http.HandlerFunc) {
    if r.routes[path] == nil {
        r.routes[path] = make(map[string]http.HandlerFunc)
    }
    r.routes[path][method] = handler
}

func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) {
    if methods, exists := r.routes[req.URL.Path]; exists {
        if handler, exists := methods[req.Method]; exists {
            handler(w, req)
            return
        }
    }
    http.NotFound(w, req)
}

func healthHandler(w http.ResponseWriter, r *http.Request) {
    response := map[string]string{
        "status": "ok",
        "time":   time.Now().Format(time.RFC3339),
    }
    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(response)
}

func main() {
    router := NewRouter()
    router.Handle("GET", "/health", healthHandler)
    router.Handle("GET", "/users", getUsersHandler)
    
    // Применяем middleware
    handler := loggingMiddleware(corsMiddleware(router))
    
    server := &http.Server{
        Addr:         ":8080",
        Handler:      handler,
        ReadTimeout:  15 * time.Second,
        WriteTimeout: 15 * time.Second,
        IdleTimeout:  60 * time.Second,
    }
    
    log.Println("Server starting on :8080")
    log.Fatal(server.ListenAndServe())
}

Работа с базой данных

Реальные приложения требуют персистентности данных. Вот пример с PostgreSQL:

package main

import (
    "database/sql"
    "encoding/json"
    "log"
    "net/http"
    "strconv"
    
    _ "github.com/lib/pq"
)

type User struct {
    ID    int    `json:"id"`
    Name  string `json:"name"`
    Email string `json:"email"`
}

var db *sql.DB

func initDB() {
    var err error
    db, err = sql.Open("postgres", "postgres://user:password@localhost/dbname?sslmode=disable")
    if err != nil {
        log.Fatal(err)
    }
    
    // Создаём таблицу
    _, err = db.Exec(`
        CREATE TABLE IF NOT EXISTS users (
            id SERIAL PRIMARY KEY,
            name VARCHAR(100) NOT NULL,
            email VARCHAR(100) UNIQUE NOT NULL
        )
    `)
    if err != nil {
        log.Fatal(err)
    }
}

func getUsersFromDB(w http.ResponseWriter, r *http.Request) {
    rows, err := db.Query("SELECT id, name, email FROM users")
    if err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }
    defer rows.Close()
    
    var users []User
    for rows.Next() {
        var u User
        if err := rows.Scan(&u.ID, &u.Name, &u.Email); err != nil {
            http.Error(w, err.Error(), http.StatusInternalServerError)
            return
        }
        users = append(users, u)
    }
    
    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(users)
}

func createUserInDB(w http.ResponseWriter, r *http.Request) {
    var user User
    if err := json.NewDecoder(r.Body).Decode(&user); err != nil {
        http.Error(w, "Invalid JSON", http.StatusBadRequest)
        return
    }
    
    err := db.QueryRow(
        "INSERT INTO users (name, email) VALUES ($1, $2) RETURNING id",
        user.Name, user.Email,
    ).Scan(&user.ID)
    
    if err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }
    
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(http.StatusCreated)
    json.NewEncoder(w).Encode(user)
}

func main() {
    initDB()
    defer db.Close()
    
    http.HandleFunc("/users", func(w http.ResponseWriter, r *http.Request) {
        switch r.Method {
        case http.MethodGet:
            getUsersFromDB(w, r)
        case http.MethodPost:
            createUserInDB(w, r)
        default:
            http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
        }
    })
    
    log.Println("Server starting on :8080")
    log.Fatal(http.ListenAndServe(":8080", nil))
}

Для установки драйвера PostgreSQL:

go mod init myserver
go get github.com/lib/pq

Конфигурация для production

Для production-среды нужна правильная конфигурация. Создадим файл config.go:

package main

import (
    "context"
    "log"
    "net/http"
    "os"
    "os/signal"
    "syscall"
    "time"
)

type Config struct {
    Port         string
    ReadTimeout  time.Duration
    WriteTimeout time.Duration
    IdleTimeout  time.Duration
    DBUrl        string
}

func getConfig() *Config {
    return &Config{
        Port:         getEnv("PORT", "8080"),
        ReadTimeout:  30 * time.Second,
        WriteTimeout: 30 * time.Second,
        IdleTimeout:  120 * time.Second,
        DBUrl:        getEnv("DATABASE_URL", "postgres://localhost/mydb?sslmode=disable"),
    }
}

func getEnv(key, defaultValue string) string {
    if value, exists := os.LookupEnv(key); exists {
        return value
    }
    return defaultValue
}

func main() {
    config := getConfig()
    
    // Инициализируем роутер
    router := NewRouter()
    router.Handle("GET", "/health", healthHandler)
    
    server := &http.Server{
        Addr:         ":" + config.Port,
        Handler:      loggingMiddleware(corsMiddleware(router)),
        ReadTimeout:  config.ReadTimeout,
        WriteTimeout: config.WriteTimeout,
        IdleTimeout:  config.IdleTimeout,
    }
    
    // Graceful shutdown
    go func() {
        log.Printf("Server starting on port %s", config.Port)
        if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
            log.Fatalf("Server failed to start: %v", err)
        }
    }()
    
    // Ожидаем сигнал завершения
    quit := make(chan os.Signal, 1)
    signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
    <-quit
    
    log.Println("Server shutting down...")
    
    ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
    defer cancel()
    
    if err := server.Shutdown(ctx); err != nil {
        log.Fatal("Server forced to shutdown:", err)
    }
    
    log.Println("Server exited")
}

Сравнение с другими решениями

Решение Производительность Простота Экосистема Память
Go net/http Очень высокая Простая Средняя Низкое потребление
Node.js Express Высокая Простая Огромная Среднее потребление
Python Flask Средняя Очень простая Большая Высокое потребление
Java Spring Boot Высокая Сложная Огромная Очень высокое потребление

Популярные Go-фреймворки

Хотя стандартная библиотека Go мощная, иногда нужны дополнительные возможности:

  • Gin — самый популярный, быстрый и минималистичный
  • Echo — высокопроизводительный с отличной документацией
  • Fiber — вдохновлён Express.js, очень быстрый
  • Gorilla Mux — мощный роутер для стандартной библиотеки

Пример с Gin:

package main

import (
    "github.com/gin-gonic/gin"
    "net/http"
)

func main() {
    r := gin.Default()
    
    r.GET("/ping", func(c *gin.Context) {
        c.JSON(http.StatusOK, gin.H{
            "message": "pong",
        })
    })
    
    r.Run() // По умолчанию на :8080
}

Развёртывание на сервере

Для развёртывания в production понадобится VPS или выделенный сервер. Вот пример развёртывания с systemd:

Создаём systemd service файл /etc/systemd/system/myserver.service:

[Unit]
Description=My Go HTTP Server
After=network.target

[Service]
Type=simple
User=www-data
WorkingDirectory=/opt/myserver
ExecStart=/opt/myserver/myserver
Restart=always
RestartSec=5
Environment=PORT=8080
Environment=DATABASE_URL=postgres://user:pass@localhost/dbname

[Install]
WantedBy=multi-user.target

Команды для управления:

# Компилируем приложение
go build -o myserver main.go

# Копируем на сервер
sudo cp myserver /opt/myserver/

# Запускаем сервис
sudo systemctl enable myserver
sudo systemctl start myserver
sudo systemctl status myserver

Nginx как reverse proxy

Конфигурация Nginx для проксирования запросов:

server {
    listen 80;
    server_name example.com;
    
    location / {
        proxy_pass http://localhost:8080;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

Мониторинг и метрики

Добавим endpoint для метрик Prometheus:

package main

import (
    "github.com/prometheus/client_golang/prometheus"
    "github.com/prometheus/client_golang/prometheus/promhttp"
    "net/http"
    "time"
)

var (
    requestsTotal = prometheus.NewCounterVec(
        prometheus.CounterOpts{
            Name: "http_requests_total",
            Help: "Total number of HTTP requests",
        },
        []string{"method", "path", "status"},
    )
    
    requestDuration = prometheus.NewHistogramVec(
        prometheus.HistogramOpts{
            Name: "http_request_duration_seconds",
            Help: "HTTP request duration in seconds",
        },
        []string{"method", "path"},
    )
)

func init() {
    prometheus.MustRegister(requestsTotal)
    prometheus.MustRegister(requestDuration)
}

func metricsMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        start := time.Now()
        
        // Обёртка для захвата status code
        ww := &responseWriter{ResponseWriter: w, statusCode: 200}
        
        next.ServeHTTP(ww, r)
        
        duration := time.Since(start).Seconds()
        requestsTotal.WithLabelValues(r.Method, r.URL.Path, string(ww.statusCode)).Inc()
        requestDuration.WithLabelValues(r.Method, r.URL.Path).Observe(duration)
    })
}

type responseWriter struct {
    http.ResponseWriter
    statusCode int
}

func (rw *responseWriter) WriteHeader(code int) {
    rw.statusCode = code
    rw.ResponseWriter.WriteHeader(code)
}

func main() {
    router := NewRouter()
    router.Handle("GET", "/health", healthHandler)
    
    // Endpoint для метрик
    http.Handle("/metrics", promhttp.Handler())
    
    handler := metricsMiddleware(loggingMiddleware(router))
    
    log.Println("Server starting on :8080")
    log.Fatal(http.ListenAndServe(":8080", handler))
}

Тестирование HTTP-сервера

Пример unit-тестов:

package main

import (
    "bytes"
    "encoding/json"
    "net/http"
    "net/http/httptest"
    "testing"
)

func TestGetUsersHandler(t *testing.T) {
    req, err := http.NewRequest("GET", "/users", nil)
    if err != nil {
        t.Fatal(err)
    }
    
    rr := httptest.NewRecorder()
    handler := http.HandlerFunc(getUsersHandler)
    
    handler.ServeHTTP(rr, req)
    
    if status := rr.Code; status != http.StatusOK {
        t.Errorf("handler returned wrong status code: got %v want %v",
            status, http.StatusOK)
    }
    
    var users []User
    if err := json.Unmarshal(rr.Body.Bytes(), &users); err != nil {
        t.Errorf("could not parse response: %v", err)
    }
    
    if len(users) == 0 {
        t.Error("expected users, got none")
    }
}

func TestCreateUserHandler(t *testing.T) {
    user := User{Name: "Test User", Email: "test@example.com"}
    jsonUser, _ := json.Marshal(user)
    
    req, err := http.NewRequest("POST", "/users", bytes.NewBuffer(jsonUser))
    if err != nil {
        t.Fatal(err)
    }
    req.Header.Set("Content-Type", "application/json")
    
    rr := httptest.NewRecorder()
    handler := http.HandlerFunc(createUserHandler)
    
    handler.ServeHTTP(rr, req)
    
    if status := rr.Code; status != http.StatusCreated {
        t.Errorf("handler returned wrong status code: got %v want %v",
            status, http.StatusCreated)
    }
}

Запуск тестов:

go test -v
go test -cover

Интересные факты и нестандартные применения

Go HTTP-серверы можно использовать не только для веб-API:

  • Файловый серверhttp.FileServer(http.Dir("./static"))
  • WebSocket сервер — для real-time приложений
  • gRPC-HTTP gateway — мост между gRPC и REST
  • Прокси-сервер — используя httputil.ReverseProxy
  • Webhook обработчик — для GitHub, GitLab и других сервисов

Пример WebSocket сервера:

package main

import (
    "github.com/gorilla/websocket"
    "net/http"
    "log"
)

var upgrader = websocket.Upgrader{
    CheckOrigin: func(r *http.Request) bool {
        return true
    },
}

func websocketHandler(w http.ResponseWriter, r *http.Request) {
    conn, err := upgrader.Upgrade(w, r, nil)
    if err != nil {
        log.Println(err)
        return
    }
    defer conn.Close()
    
    for {
        messageType, p, err := conn.ReadMessage()
        if err != nil {
            log.Println(err)
            break
        }
        
        // Эхо-сервер
        if err := conn.WriteMessage(messageType, p); err != nil {
            log.Println(err)
            break
        }
    }
}

func main() {
    http.HandleFunc("/ws", websocketHandler)
    log.Fatal(http.ListenAndServe(":8080", nil))
}

Автоматизация и CI/CD

Пример GitHub Actions для автоматического деплоя:

name: Deploy Go Server

on:
  push:
    branches: [ main ]

jobs:
  deploy:
    runs-on: ubuntu-latest
    
    steps:
    - uses: actions/checkout@v2
    
    - name: Set up Go
      uses: actions/setup-go@v2
      with:
        go-version: 1.21
    
    - name: Build
      run: go build -o myserver main.go
    
    - name: Deploy to server
      uses: appleboy/ssh-action@v0.1.5
      with:
        host: ${{ secrets.HOST }}
        username: ${{ secrets.USERNAME }}
        key: ${{ secrets.SSH_KEY }}
        script: |
          sudo systemctl stop myserver
          sudo cp myserver /opt/myserver/
          sudo systemctl start myserver

Заключение и рекомендации

Go предоставляет мощные и простые инструменты для создания HTTP-серверов. Основные рекомендации:

  • Для простых API — используйте стандартную библиотеку net/http
  • Для сложных приложений — рассмотрите Gin или Echo
  • Всегда используйте middleware для логирования, CORS и аутентификации
  • Настройте graceful shutdown для production
  • Добавьте метрики для мониторинга
  • Пишите тесты с самого начала

Go HTTP-серверы отлично подходят для микросервисов, API Gateway, и высоконагруженных приложений. Благодаря простоте развёртывания и высокой производительности, Go стал стандартом де-факто для backend-разработки во многих компаниях.

Полезные ссылки:


В этой статье собрана информация и материалы из различных интернет-источников. Мы признаем и ценим работу всех оригинальных авторов, издателей и веб-сайтов. Несмотря на то, что были приложены все усилия для надлежащего указания исходного материала, любая непреднамеренная оплошность или упущение не являются нарушением авторских прав. Все упомянутые товарные знаки, логотипы и изображения являются собственностью соответствующих владельцев. Если вы считаете, что какой-либо контент, использованный в этой статье, нарушает ваши авторские права, немедленно свяжитесь с нами для рассмотрения и принятия оперативных мер.

Данная статья предназначена исключительно для ознакомительных и образовательных целей и не ущемляет права правообладателей. Если какой-либо материал, защищенный авторским правом, был использован без должного упоминания или с нарушением законов об авторском праве, это непреднамеренно, и мы исправим это незамедлительно после уведомления. Обратите внимание, что переиздание, распространение или воспроизведение части или всего содержимого в любой форме запрещено без письменного разрешения автора и владельца веб-сайта. Для получения разрешений или дополнительных запросов, пожалуйста, свяжитесь с нами.

Leave a reply

Your email address will not be published. Required fields are marked