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 — нет.
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))
}
Генерация 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"
Параллельное выполнение задач через горутины
Стандартный паттерн запуска нескольких горутин и ожидания завершения всех через 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()
}
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 и т.д.
}
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)
Построчное чтение файла
Потоковое чтение текстового файла построчно без загрузки всего файла в память.
Как это работает: 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()
}
Создание простого 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))
}
Другие подборки сниппетов
Контактная информация
Оставьте заявку или напишите напрямую в телеграм. Я отвечу в течение 24 часов.