Skip to content
← К сниппетам
Go

Go: HTTP-серверы, context и конкурентность

Семь сниппетов на Go про то, как собирать сервис средствами стандартной библиотеки: минимальный HTTP-сервер, graceful shutdown, таймауты запроса через context, конкурентная работа с goroutine и WaitGroup, генерация UUID и построчное чтение файла.

Стандартная библиотека Go покрывает веб-сервис полнее, чем у большинства языков, — поэтому эти сниппеты почти ничего не импортируют. Интересен здесь не синтаксис, а три-четыре решения, которые библиотека заставляет принять явно.

У сервера по умолчанию нет таймаутов

http.ListenAndServe(":8080", nil) поднимает сервер, у которого ReadTimeout, WriteTimeout и IdleTimeout равны нулю, то есть не ограничены. Клиент, открывший соединение и ничего не отправивший, бессрочно удерживает горутину и файловый дескриптор; при достаточном количестве таких клиентов процесс исчерпает и то, и другое. Явное создание http.Server, чтобы выставить таймауты, — не улучшение, а разница между сервисом, переживающим контакт с открытым интернетом, и не переживающим.

Graceful shutdown — то, ради чего существует context

Когда оркестратор присылает SIGTERM, наивная реакция — выйти, обрывая все запросы в полёте. Server.Shutdown вместо этого перестаёт принимать новые соединения и дожидается завершения активных, ограничивая ожидание дедлайном контекста, чтобы зависший обработчик не задержал остановку навсегда.

Тот же контекст проходит сквозь всё остальное. Исходящий HTTP-вызов, созданный через http.NewRequestWithContext, наследует дедлайн породившего его запроса: когда клиент отключился, нижележащий вызов отменяется, а не продолжает жечь соединение ради ответа, который никто не прочитает. Context — это не мешок значений на время запроса, а дерево отмены, и использование его в первом качестве — самое частое злоупотребление.

WaitGroup отвечает на «когда», а не на «что случилось»

sync.WaitGroup сообщает, когда набор горутин завершился. Он не собирает их результаты и не поднимает их ошибки — а горутина с паникой роняет весь процесс независимо от того, насколько аккуратно её ждали. Результатам нужен канал или слайс с индексацией по позиции, ошибкам нужно место, куда попасть. Сниппет здесь делает и то, и другое явно, потому что версия, игнорирующая ошибки, — как раз та, которую копируют, после чего сбои начинают теряться.

Статья в блоге Go про context остаётся самым внятным объяснением того, как должна распространяться отмена.

Чтение файлов без загрузки в память

bufio.Scanner читает построчно фиксированным буфером, поэтому расход памяти не зависит от размера файла. У него есть одна острая грань: буфер по умолчанию — 64 КБ, и строка длиннее останавливает сканирование ошибкой, а не продолжает чтение. Для разбора логов этого лимита обычно хватает; для чего-либо со встроенным JSON — нет.

Go

HTTP запрос с таймаутом через Context (Go)

Создание HTTP-запроса на Go с использованием context.WithTimeout. Гарантирует прерывание сетевых запросов при превышении заданного времени.

Шаблон для выполнения внешних HTTP-запросов в Go с использованием пакета context. Задание лимита времени на выполнение запроса предотвращает «зависание» горутин при сетевых сбоях или медленных ответах от удаленного API.

package main

import (
    "context"
    "io"
    "log"
    "net/http"
    "time"
)

func fetchExternalAPI() ([]byte, error) {
    // 1. Создаем контекст с таймаутом в 3 секунды
    ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
    defer cancel() // Всегда вызываем cancel для очистки ресурсов контекста

    // 2. Создаем HTTP-запрос, привязанный к созданному контексту
    req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://api.github.com/users/octocat", nil)
    if err != nil {
        return nil, err
    }

    client := &http.Client{}

    // 3. Выполняем HTTP-запрос
    resp, err := client.Do(req)
    if err != nil {
        // Если время ожидания вышло, Do вернет ошибку с контекстом
        if ctx.Err() == context.DeadlineExceeded {
            log.Println("HTTP request timed out after 3 seconds")
        }
        return nil, err
    }
    defer resp.Body.Close()

    // 4. Считываем полученные данные
    body, err := io.ReadAll(resp.Body)
    if err != nil {
        return nil, err
    }

    return body, nil
}

func main() {
    data, err := fetchExternalAPI()
    if err != nil {
        log.Fatalf("Failed to fetch API: %v", err)
    }
    log.Printf("Successfully fetched %d bytes", len(data))
}
Go

Генерация UUID

Создаёт случайный UUID v4, используя только crypto/rand из стандартной библиотеки — без внешнего пакета uuid.

Как это работает: из crypto/rand считываются 16 случайных байт, затем биты версии (4) и варианта устанавливаются по RFC 4122, после чего байты форматируются в стандартные hex-группы 8-4-4-4-12.

package main

import (
    "crypto/rand"
    "fmt"
)

func generateUUID() (string, error) {
    b := make([]byte, 16)
    if _, err := rand.Read(b); err != nil {
        return "", err
    }

    b[6] = (b[6] & 0x0f) | 0x40
    b[8] = (b[8] & 0x3f) | 0x80

    return fmt.Sprintf("%x-%x-%x-%x-%x", b[0:4], b[4:6], b[6:8], b[8:10], b[10:16]), nil
}

// Example usage
id, _ := generateUUID() // "3f9a1c2e-8b3d-4c1a-9f2e-6d8b1a4c7e9f"
Go

Параллельное выполнение задач через горутины

Стандартный паттерн запуска нескольких горутин и ожидания завершения всех через sync.WaitGroup.

Как это работает: wg.Add(1) перед запуском каждой горутины и defer wg.Done() внутри неё держат счётчик сбалансированным; передача url как параметра u (вместо захвата переменной цикла напрямую через замыкание) избегает классической ошибки Go, когда все горутины видят одно и то же, последнее значение переменной цикла.

package main

import (
    "fmt"
    "sync"
)

func processURLs(urls []string) {
    var wg sync.WaitGroup

    for _, url := range urls {
        wg.Add(1)
        go func(u string) {
            defer wg.Done()
            fmt.Println("fetched:", u)
        }(url)
    }

    wg.Wait() // blocks until every goroutine has called wg.Done()
}
Go

HTTP Server с Graceful Shutdown (Go)

Шаблон HTTP-сервера на Go с корректным завершением работы при получении сигналов SIGINT/SIGTERM. Завершает активные соединения и закрывает БД.

Пример создания продакшн-ready HTTP-сервера на Go, который отслеживает сигналы прерывания от ОС (Ctrl+C или сигналы контейнеризации Docker/Kubernetes) и корректно останавливается, давая до 10 секунд на обработку текущих запросов перед выходом.

package main

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

func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
        w.WriteHeader(http.StatusOK)
        w.Write([]byte("OK"))
    })

    server := &http.Server{
        Addr:    ":8080",
        Handler: mux,
    }

    // Канал для отслеживания системных сигналов
    shutdownChan := make(chan os.Signal, 1)
    signal.Notify(shutdownChan, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)

    // Запуск HTTP-сервера в отдельной горутине
    go func() {
        log.Printf("Starting server on %s", server.Addr)
        if err := server.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
            log.Fatalf("ListenAndServe error: %v", err)
        }
    }()

    // Блокируем поток до получения сигнала
    sig := <-shutdownChan
    log.Printf("Received signal: %v. Initiating graceful shutdown...", sig)

    // Таймаут на завершение текущих соединений (10 секунд)
    ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
    defer cancel()

    // Корректное завершение работы сервера
    if err := server.Shutdown(ctx); err != nil {
        log.Fatalf("Server forced to shutdown: %v", err)
    }

    log.Println("Server stopped cleanly. Releasing database resources...")
    // Здесь можно закрыть соединения с БД, Redis и т.д.
}
Go

HTTP GET-запрос

Способ получить и разобрать JSON от API средствами стандартной библиотеки — без стороннего HTTP-клиента.

Как это работает: http.Get выполняет запрос, io.ReadAll считывает тело ответа в срез байт, а json.Unmarshal декодирует его прямо в типизированную структуру, переданную по указателю.

package main

import (
    "encoding/json"
    "io"
    "net/http"
)

func httpGetJSON(url string, target interface{}) error {
    resp, err := http.Get(url)
    if err != nil {
        return err
    }
    defer resp.Body.Close()

    body, err := io.ReadAll(resp.Body)
    if err != nil {
        return err
    }

    return json.Unmarshal(body, target)
}

// Example usage
type User struct {
    Name string `json:"name"`
}

var user User
err := httpGetJSON("https://api.example.com/users/1", &user)
Go

Построчное чтение файла

Потоковое чтение текстового файла построчно без загрузки всего файла в память.

Как это работает: bufio.Scanner читает файл блоками и отдаёт по одной строке за раз через Scan()/Text(), поэтому потребление памяти остаётся постоянным независимо от размера файла.

package main

import (
    "bufio"
    "fmt"
    "os"
)

func readLines(path string) error {
    file, err := os.Open(path)
    if err != nil {
        return err
    }
    defer file.Close()

    scanner := bufio.NewScanner(file)
    for scanner.Scan() {
        fmt.Println(scanner.Text())
    }
    return scanner.Err()
}
Go

Создание простого HTTP-сервера

Способ обслуживать HTTP-роуты средствами стандартной библиотеки Go — фреймворк не нужен для небольшого API.

Как это работает: http.HandleFunc регистрирует обработчик для пути, а http.ListenAndServe запускает сервер и блокирует выполнение — передача nil вторым аргументом указывает использовать стандартный мультиплексор запросов, на котором HandleFunc зарегистрировал роуты.

package main

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

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

func main() {
    http.HandleFunc("/health", healthHandler)
    log.Println("Listening on :8080")
    log.Fatal(http.ListenAndServe(":8080", nil))
}
// CONNECT

Контактная информация

Оставьте заявку или напишите напрямую в телеграм. Я отвечу в течение 24 часов.

Укажите email или Telegram, чтобы я мог вам ответить.