🐹 Go

Trabalhando com rede e API HTTP em Go (Golang): um guia detalhado

Что это такое и зачем? (аналогия из реальной жизни)

Представьте, что вы сидите в уютном кафе и хотите заказать пиццу. Вы не идёте на кухню сами, не лезете в печь и не трогаете тесто. Вместо этого вы подзываете официанта, говорите: «Принесите, пожалуйста, «Маргариту»», — и через некоторое время получаете готовое блюдо.

В мире программирования работа с сетью и HTTP API — это ровно то же самое. Ваша программа (клиент) — это посетитель кафе. Сервер — это кухня, где готовят данные. А HTTP-запрос — это официант, который передаёт ваш заказ и приносит ответ.

API (Application Programming Interface) — это меню ресторана. В нём написано, какие блюда (данные) можно заказать и как именно их нужно называть. Например, если вы хотите узнать погоду, вы «заказываете» у сервера погоды данные по адресу https://api.weather.com/current.

Зачем это нужно в программировании? Представьте, что вы пишете приложение для отслеживания курса доллара. Вы не храните курсы у себя в телефоне — они меняются каждую секунду. Вместо этого ваша программа раз в минуту «звонит» на сервер банка, спрашивает: «Какой сейчас курс?» — и получает свежий ответ. Без сети любая программа была бы изолированным островом, который умеет только то, что в него заранее загрузили.

💡 Важно: HTTP — это не единственный протокол, но самый популярный. Именно по нему работает весь интернет: сайты, мобильные приложения, умные колонки и даже некоторые холодильники.

Теория: как это работает изнутри

Давайте заглянем «под капот» и разберёмся, что происходит, когда ваша программа делает HTTP-запрос.

1. Клиент и сервер

В любой сетевой коммуникации есть две стороны:

  • Клиент — тот, кто просит (ваша программа на Go).
  • Сервер — тот, кто отвечает (удалённый компьютер с данными).

Клиент и сервер общаются через протокол — набор правил, как упаковывать и распаковывать сообщения. HTTP (HyperText Transfer Protocol) — самый распространённый протокол для передачи данных.

2. Структура HTTP-запроса

Когда клиент «стучится» к серверу, он отправляет сообщение, которое состоит из четырёх частей:

  • Метод (HTTP-глагол): что именно мы хотим сделать. Самые популярные:
    • GET — получить данные (прочитать меню).
    • POST — создать новые данные (добавить новое блюдо в меню).
    • PUT — полностью заменить данные (переписать рецепт).
    • PATCH — частично изменить данные (добавить соли в рецепт).
    • DELETE — удалить данные (убрать блюдо из меню).
  • URL (адрес): куда идти. Например, https://api.example.com/users/42.
  • Заголовки (Headers): дополнительная информация — какой формат данных мы принимаем, есть ли у нас пароль (токен) и т.д.
  • Тело (Body): сами данные (обычно в формате JSON). Есть только у POST, PUT, PATCH.

3. Структура HTTP-ответа

Сервер отвечает клиенту своим сообщением:

  • Статус-код: трёхзначное число, которое говорит, всё ли хорошо. Основные коды:
    • 200 OK — успех, вот ваши данные.
    • 201 Created — успешно создано.
    • 400 Bad Request — вы что-то не так спросили.
    • 401 Unauthorized — у вас нет прав (нужен токен).
    • 404 Not Found — такого адреса не существует.
    • 500 Internal Server Error — у сервера проблемы.
  • Заголовки: мета-информация от сервера.
  • Тело: ответные данные (обычно JSON).

4. JSON — язык общения

Большинство современных API общаются на языке JSON (JavaScript Object Notation). Это просто текст, который выглядит так:

{
    "name": "Иван",
    "age": 25,
    "isStudent": false
}

Go умеет превращать (маршалить) свои структуры в JSON и обратно (анмаршалить).

🔍 Интересный факт: HTTP — это «глупый» протокол. Он не помнит, кто вы и что делали раньше. Каждый запрос — это как первая встреча. Поэтому для «запоминания» используют токены или куки.

Синтаксис в Go (Golang): все формы и варианты

В Go работа с HTTP API построена вокруг стандартной библиотеки net/http. Это мощный инструмент, который не требует установки дополнительных пакетов для базовых задач.

Основные типы и функции

Тип/Функция Описание Пример использования
http.Client Клиент, который отправляет запросы. Можно настроить таймауты и прокси. client := &http.Client{Timeout: 10 * time.Second}
http.NewRequest() Создаёт новый запрос с указанием метода, URL и тела. req, _ := http.NewRequest("GET", url, nil)
client.Do() Отправляет запрос и возвращает ответ. resp, err := client.Do(req)
http.Get() Упрощённая функция для GET-запроса (без тонкой настройки). resp, err := http.Get(url)
http.Post() Упрощённая функция для POST-запроса. resp, err := http.Post(url, "application/json", body)
ioutil.ReadAll() Читает всё тело ответа в виде байтов. body, _ := ioutil.ReadAll(resp.Body)
json.Unmarshal() Преобразует JSON (байты) в структуру Go. json.Unmarshal(body, &myStruct)
json.Marshal() Преобразует структуру Go в JSON (байты). jsonData, _ := json.Marshal(myStruct)

Обработка ошибок — обязательное правило

В Go нет исключений (try-catch). Каждая функция, которая может завершиться ошибкой, возвращает её как второе значение. Игнорировать ошибки — самая частая ошибка новичков.

resp, err := http.Get("https://api.example.com")
if err != nil {
    // Обязательно обрабатываем ошибку
    fmt.Println("Ошибка при запросе:", err)
    return
}
defer resp.Body.Close() // Важно: закрываем тело ответа!

Работа с JSON в Go

Чтобы Go понимал, как превратить JSON в структуру, нужно использовать теги — специальные метки в коде:

type User struct {
    Name  string `json:"name"`  // Поле Name будет связано с ключом "name" в JSON
    Age   int    `json:"age"`
    Email string `json:"email,omitempty"` // omitempty — если поле пустое, не включать в JSON
}

Примеры кода — от простого к сложному

Пример 1: Простой GET-запрос (узнаём курс валют)

package main

import (
    "fmt"
    "io/ioutil"
    "net/http"
)

func main() {
    // 1. Делаем GET-запрос к бесплатному API курса валют
    url := "https://api.exchangerate-api.com/v4/latest/USD"
    resp, err := http.Get(url)
    if err != nil {
        fmt.Println("Ошибка при запросе:", err)
        return
    }
    defer resp.Body.Close() // Закрываем тело ответа, когда закончим

    // 2. Читаем тело ответа
    body, err := ioutil.ReadAll(resp.Body)
    if err != nil {
        fmt.Println("Ошибка при чтении ответа:", err)
        return
    }

    // 3. Выводим результат (это сырой JSON)
    fmt.Println("Курсы валют (сырой JSON):")
    fmt.Println(string(body))
}

Объяснение: Мы просто «стучимся» на сервер, получаем JSON и печатаем его. Это база, с которой начинается любая работа с API.

Пример 2: GET-запрос с парсингом JSON

package main

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

// Структура для парсинга ответа
type ExchangeRates struct {
    Base  string             `json:"base"`
    Rates map[string]float64 `json:"rates"`
}

func main() {
    url := "https://api.exchangerate-api.com/v4/latest/USD"
    resp, err := http.Get(url)
    if err != nil {
        fmt.Println("Ошибка:", err)
        return
    }
    defer resp.Body.Close()

    body, _ := ioutil.ReadAll(resp.Body)

    // Парсим JSON в структуру
    var rates ExchangeRates
    err = json.Unmarshal(body, &rates)
    if err != nil {
        fmt.Println("Ошибка парсинга JSON:", err)
        return
    }

    // Теперь можно работать с данными как с обычными переменными Go
    fmt.Printf("Базовая валюта: %s
", rates.Base)
    fmt.Printf("Курс USD к RUB: %.2f
", rates.Rates["RUB"])
    fmt.Printf("Курс USD к EUR: %.2f
", rates.Rates["EUR"])
}

Объяснение: Мы не просто получили JSON, а превратили его в удобную структуру. Теперь мы можем обращаться к полям через точку: rates.Rates["RUB"].

Пример 3: POST-запрос с отправкой данных

package main

import (
    "bytes"
    "encoding/json"
    "fmt"
    "net/http"
)

// Структура для отправки
type NewUser struct {
    Name  string `json:"name"`
    Email string `json:"email"`
}

func main() {
    // 1. Создаём данные для отправки
    user := NewUser{
        Name:  "Анна Петрова",
        Email: "anna@example.com",
    }

    // 2. Превращаем структуру в JSON (байты)
    jsonData, err := json.Marshal(user)
    if err != nil {
        fmt.Println("Ошибка маршалинга:", err)
        return
    }

    // 3. Отправляем POST-запрос
    url := "https://jsonplaceholder.typicode.com/users"
    resp, err := http.Post(url, "application/json", bytes.NewBuffer(jsonData))
    if err != nil {
        fmt.Println("Ошибка при POST:", err)
        return
    }
    defer resp.Body.Close()

    // 4. Проверяем статус-код
    fmt.Println("Статус:", resp.StatusCode) // Должно быть 201 Created
}

Объяснение: Мы создали нового пользователя и отправили его на сервер. Обратите внимание: мы указали тип содержимого "application/json" — это обязательно, чтобы сервер понял, что мы шлём JSON.

Пример 4: Запрос с заголовками (авторизация)

package main

import (
    "fmt"
    "io/ioutil"
    "net/http"
)

func main() {
    // 1. Создаём клиента с таймаутом
    client := &http.Client{}

    // 2. Создаём запрос вручную (чтобы добавить заголовки)
    url := "https://api.github.com/user"
    req, err := http.NewRequest("GET", url, nil)
    if err != nil {
        fmt.Println("Ошибка создания запроса:", err)
        return
    }

    // 3. Добавляем заголовки
    req.Header.Add("Authorization", "token ВАШ_ТОКЕН_ЗДЕСЬ")
    req.Header.Add("Accept", "application/vnd.github.v3+json")

    // 4. Отправляем запрос через клиента
    resp, err := client.Do(req)
    if err != nil {
        fmt.Println("Ошибка при запросе:", err)
        return
    }
    defer resp.Body.Close()

    body, _ := ioutil.ReadAll(resp.Body)
    fmt.Println(string(body))
}

Объяснение: Многие API требуют авторизации. Токен — это как пропуск. Мы создаём запрос через http.NewRequest, чтобы иметь доступ к заголовкам, и добавляем туда токен.

Пример 5: Обработка разных статус-кодов

package main

import (
    "fmt"
    "net/http"
)

func main() {
    url := "https://api.github.com/users/несуществующий_пользователь"
    resp, err := http.Get(url)
    if err != nil {
        fmt.Println("Сетевая ошибка:", err)
        return
    }
    defer resp.Body.Close()

    // Проверяем статус-код
    switch {
    case resp.StatusCode == 200:
        fmt.Println("Успех! Пользователь найден.")
    case resp.StatusCode == 404:
        fmt.Println("Ошибка 404: пользователь не найден.")
    case resp.StatusCode >= 500:
        fmt.Println("Ошибка сервера:", resp.StatusCode)
    default:
        fmt.Println("Неожиданный статус:", resp.StatusCode)
    }
}

Объяснение: Сервер может ответить по-разному. Хорошая программа проверяет статус-код и реагирует соответственно. Нельзя просто считать, что ответ всегда успешен.

Пример 6: GET-запрос с параметрами в URL

package main

import (
    "fmt"
    "io/ioutil"
    "net/http"
    "net/url"
)

func main() {
    // 1. Создаём базовый URL
    baseURL, _ := url.Parse("https://api.openweathermap.org/data/2.5/weather")

    // 2. Добавляем параметры запроса
    params := url.Values{}
    params.Add("q", "Moscow")
    params.Add("appid", "ВАШ_КЛЮЧ")
    params.Add("units", "metric")
    baseURL.RawQuery = params.Encode() // Превращаем параметры в строку

    // 3. Делаем запрос
    resp, err := http.Get(baseURL.String())
    if err != nil {
        fmt.Println("Ошибка:", err)
        return
    }
    defer resp.Body.Close()

    body, _ := ioutil.ReadAll(resp.Body)
    fmt.Println(string(body))
}

Объяснение: Параметры в URL — это как уточнение заказа. Мы говорим: «Дай погоду для Москвы, в метрической системе, вот мой ключ». Использование url.Values — правильный способ, он сам экранирует спецсимволы.

Пример 7: Полноценный клиент с обработкой ошибок и таймаутом

package main

import (
    "encoding/json"
    "fmt"
    "io/ioutil"
    "net/http"
    "time"
)

type Todo struct {
    UserID    int    `json:"userId"`
    ID        int    `json:"id"`
    Title     string `json:"title"`
    Completed bool   `json:"completed"`
}

func main() {
    // Создаём клиента с таймаутом 5 секунд
    client := &http.Client{
        Timeout: 5 * time.Second,
    }

    url := "https://jsonplaceholder.typicode.com/todos/1"
    req, _ := http.NewRequest("GET", url, nil)

    resp, err := client.Do(req)
    if err != nil {
        fmt.Println("Ошибка соединения или таймаут:", err)
        return
    }
    defer resp.Body.Close()

    if resp.StatusCode != 200 {
        fmt.Printf("Сервер вернул ошибку: %d
", resp.StatusCode)
        return
    }

    body, _ := ioutil.ReadAll(resp.Body)

    var todo Todo
    err = json.Unmarshal(body, &todo)
    if err != nil {
        fmt.Println("Ошибка парсинга JSON:", err)
        return
    }

    fmt.Printf("Задача: %s
", todo.Title)
    fmt.Printf("Выполнена: %t
", todo.Completed)
}

Объяснение: Это профессиональный подход: таймаут, проверка статуса, обработка всех возможных ошибок. Такой код не «упадёт» в самый неподходящий момент.

Сравнение с Python: чем похоже и чем отличается

Аспект Go Python
Основная библиотека net/http (встроенная) requests (сторонняя, нужно устанавливать)
Простота GET-запроса http.Get(url) requests.get(url)
Работа с JSON Нужно вручную маршалить/анмаршалить response.json() — автоматически
Обработка ошибок Явная проверка err != nil Исключения (try-except)
Таймауты Настраиваются через http.Client requests.get(timeout=5)
Производительность Очень высокая, горутины Ниже, но для большинства задач хватает
Типизация Строгая, структуры обязательны Динамическая, можно работать со словарями

Главное отличие: Python проще для быстрого прототипирования — написал 3 строки и пошло. Go требует больше кода, но он надёжнее, быстрее и легче поддерживается в больших проектах. В Go вы всегда точно знаете, какие данные придут, потому что описали структуру. В Python данные могут быть «сюрпризом».

Типичные ошибки и подводные камни

  1. Забыли закрыть тело ответа. Каждый resp.Body нужно закрывать через defer resp.Body.Close(). Иначе будет утечка памяти.
  2. Игнорирование ошибок. resp, _ := http.Get(url) — это бомба замедленного действия. Всегда проверяйте err.
  3. Не проверяете статус-код. Сервер может вернуть 404 или 500, а вы будете пытаться парсить тело ответа, которое содержит сообщение об ошибке, а не данные.
  4. Нет таймаута. Если сервер завис, ваша программа зависнет навсегда. Всегда устанавливайте таймаут через http.Client.
  5. Путаница с JSON-тегами. Если в JSON ключ "user_name", а в структуре поле UserName без тега, Go не сможет его распарсить.
  6. Отправка данных без указания Content-Type. Сервер может не понять, что вы шлёте JSON, и вернуть ошибку.
  7. Чтение всего тела в память без ограничения. Если сервер пришлёт 2 ГБ данных, ваша программа упадёт. Используйте io.LimitReader для больших ответов.
⚠️ Золотое правило: Любая сетевая операция может завершиться ошибкой. Сеть — это ненадёжная среда. Ваш код должен быть готов к тому, что сервер не ответит, ответит не тем или ответит слишком поздно.

Лучшие практики и советы профессионалов

  1. Всегда используйте http.Client с таймаутом. Не используйте http.Get() в production — он использует клиент без таймаута по умолчанию.
  2. Создавайте один клиент для всего приложения. http.Client можно переиспользовать. Не создавайте новый клиент на каждый запрос.
  3. Используйте Retry (повторные попытки). Сеть нестабильна. Если запрос упал с 500-ой ошибкой, попробуйте ещё раз через 1-2 секунды.
  4. Логируйте запросы. В production всегда включайте логирование: какой URL запросили, какой статус получили, сколько времени заняло.
  5. Используйте контексты (context). Для отмены долгих запросов и передачи мета-информации.
  6. Не доверяйте данным от API. Всегда проверяйте, что пришло: не пустые ли поля, правильного ли типа.
  7. Для сложных API используйте готовые клиенты. Например, для работы с GitHub, Telegram, OpenAI есть готовые Go-библиотеки — не изобретайте велосипед.

Вопросы для самопроверки

  1. Вопрос: Что такое HTTP-метод GET и для чего он используется?
    Ответ: GET — это запрос на получение данных. Он не должен изменять данные на сервере. Используется для чтения информации (например, получить список пользователей).
  2. Вопрос: Чем отличается POST от PUT?
    Ответ: POST создаёт новый ресурс (например, нового пользователя). PUT полностью заменяет существующий ресурс по известному ID. POST неидемпотентен (повторный вызов создаст ещё одного пользователя), PUT идемпотентен (повторный вызов с теми же данными не изменит результат).
  3. Вопрос: Что такое статус-код 404?
    Ответ: 404 Not Found — сервер не нашёл ресурс по указанному URL. Это ошибка клиента (неправильный адрес).
  4. Вопрос: Зачем нужен defer resp.Body.Close()?
    Ответ: Чтобы гарантированно закрыть соединение с сервером после обработки ответа. Если не закрыть, будет утечка ресурсов (программа будет держать открытые соединения, пока не закончится память).
  5. Вопрос: Что такое JSON-теги в Go и зачем они нужны?
    Ответ: Теги — это аннотации к полям структуры, которые указывают, какому ключу в JSON соответствует поле. Например, Name string `json:"name"` означает, что поле Name будет связано с ключом "name" в JSON.
  6. Вопрос: Как установить таймаут для HTTP-запроса в Go?
    Ответ: Создать экземпляр http.Client с полем Timeout: client := &http.Client{Timeout: 10 * time.Second}.
  7. Вопрос: Что произойдёт, если не проверить err после http.Get()?
    Ответ: Если произошла ошибка (например, сервер недоступен), переменная resp будет равна nil. Любая попытка обратиться к resp.Body вызовет панику (аварийное завершение программы).
  8. Вопрос: Как передать параметры в URL (например, ?q=Moscow&units=metric)?
    Ответ: Использовать пакет net/url: создать url.Values, добавить параметры через .Add(), затем установить baseURL.RawQuery = params.Encode().
  9. Вопрос: В чём преимущество Go перед Python для работы с HTTP API?
    Ответ: Go быстрее, имеет встроенную поддержку HTTP без сторонних библиотек, строгая типизация делает код надёжнее, горутины позволяют легко делать тысячи параллельных запросов.
  10. Вопрос: Что такое «идемпотентность» HTTP-метода?
    Ответ: Идемпотентность — это свойство метода, при котором повторный запрос с теми же параметрами даёт тот же результат, что и первый. GET, PUT, DELETE — идемпотентны. POST — нет.

Итог и что изучить дальше

Мы прошли большой путь: от простой аналогии с кафе до полноценного HTTP-клиента на Go с обработкой ошибок, JSON и таймаутами. Теперь вы знаете:

  • Как работает протокол HTTP: запросы, ответы, статус-коды.
  • Как отправлять GET и POST запросы в Go.
  • Как парсить JSON в структуры и обратно.
  • Как добавлять заголовки и параметры.
  • Как избегать типичных ошибок.

Что изучить дальше:

  1. Создание HTTP-сервера на Go. Вы научились быть клиентом — теперь научитесь быть сервером. Пакет net/http позволяет легко создавать свои API.
  2. Работа с контекстами (context). Для отмены запросов, таймаутов и передачи мета-информации.
  3. Маршрутизация (роутинг). Стандартный http.ServeMux — это база. Для сложных проектов изучите gorilla/mux или chi.
  4. Middleware (промежуточное ПО). Как добавлять логирование, авторизацию и сжатие ко всем запросам сразу.
  5. gRPC. Современный протокол от Google, который быстрее HTTP для микросервисов.
  6. WebSockets. Для реального времени (чаты, уведомления).
🚀 Совет на будущее: Лучший способ закрепить теорию — написать свой проект. Попробуйте создать простого Telegram-бота или приложение для отслеживания погоды. Вы удивитесь, как много вы уже умеете!