Avatar ·

Как правильно выполнить GET-запрос и собрать QueryString в Go?

Всем привет! Я новичок в Go и пытаюсь выполнить GET-запрос к API, который требует параметры в строке запроса (query string). Например, мне нужно отправить запрос на `https://api.example.com/search?q=golang&page=1&limit=10`. Я пробовал делать это вручную, просто склеивая строку, но это выглядит ужасно и, кажется, неправильно: ```go package main import ( "fmt" "io/ioutil" "net/http" ) func main() { url := "https://api.example.com/search?q=" + "golang" + "&page=" + "1" + "&limit="10" resp, err := http.Get(url) if err != nil { panic(err) } defer resp.Body.Close() body, _ := ioutil.ReadAll(resp.Body) fmt.Println(string(body)) } ``` Во-первых, это небезопасно: если в параметре `q` будут пробелы или специальные символы (например, `golang & go`), URL сломается. Во-вторых, код выглядит грязно и неудобно поддерживать, особенно когда параметров много. Я знаю, что в других языках (например, в Python есть `urllib.parse.urlencode` или `requests.params`) есть встроенные способы для сборки query string. А как правильно делать это в Go? Есть ли стандартный способ, или нужно использовать сторонние библиотеки? Буду благодарен за примеры с пояснениями! <<>>

Правильный способ: использование net/url пакета

В Go есть встроенный пакет net/url, который предоставляет все необходимые инструменты для безопасной и удобной сборки QueryString. Основная идея — создать объект url.URL, задать его поля (схему, хост, путь), а затем использовать метод Query() для получения объекта url.Values, который представляет собой карту (map) параметров. После добавления параметров нужно закодировать их в строку запроса через метод Encode().

Пошаговый пример

package main

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

func main() {
    // 1. Создаём базовый URL
    baseURL, _ := url.Parse("https://api.example.com/search")

    // 2. Получаем объект для параметров query string
    params := url.Values{}
    params.Add("q", "golang & go") // специальные символы будут экранированы
    params.Add("page", "1")
    params.Add("limit", "10")

    // 3. Кодируем параметры в строку запроса
    baseURL.RawQuery = params.Encode()

    // 4. Выполняем GET-запрос
    resp, err := http.Get(baseURL.String())
    if err != nil {
        panic(err)
    }
    defer resp.Body.Close()

    body, _ := ioutil.ReadAll(resp.Body)
    fmt.Println(string(body))
    // URL будет: https://api.example.com/search?limit=10&page=1&q=golang+%26+go
}

Что здесь происходит?

  • url.Parse() — разбирает строку URL на структуру url.URL. Это удобно, если у вас уже есть базовый URL.
  • url.Values{} — это аналог map[string][]string. Каждый ключ может иметь несколько значений (например, для массива параметров).
  • params.Add(key, value) — добавляет параметр. Если ключ уже существует, значение добавляется в список (не перезаписывается).
  • params.Set(key, value) — если нужно заменить все значения ключа на новое.
  • params.Encode() — кодирует все параметры в строку запроса с правильным экранированием. Пробелы становятся + или %20, специальные символы — %XX.
  • baseURL.RawQuery — присваивает закодированную строку запроса URL.

Более лаконичный вариант (без url.Parse)

Если вам нужно просто собрать URL с нуля, можно использовать url.URL напрямую:

func main() {
    params := url.Values{}
    params.Add("q", "golang & go")
    params.Add("page", "1")

    u := &url.URL{
        Scheme:   "https",
        Host:     "api.example.com",
        Path:     "/search",
        RawQuery: params.Encode(),
    }

    resp, _ := http.Get(u.String())
    // ...
}

Что делать с ответом? (Совет для новичков)

Не забывайте проверять статус ответа и закрывать тело:

if resp.StatusCode != http.StatusOK {
    body, _ := ioutil.ReadAll(resp.Body)
    log.Fatalf("API error: %s", string(body))
}

Также в Go 1.16+ рекомендуется использовать io.ReadAll вместо ioutil.ReadAll, но ioutil пока работает.

Итог

Никогда не склеивайте URL вручную — это источник багов и уязвимостей. Используйте net/url — он стандартный, легковесный и полностью покрывает все потребности.

<<>>

Альтернативный подход: использование сторонней библиотеки go-querystring

Если вы работаете с большим количеством запросов и хотите автоматически преобразовывать структуры Go в QueryString, можно использовать библиотеку github.com/google/go-querystring/query. Она позволяет определить параметры запроса как поля структуры с тегами url.

package main

import (
    "fmt"
    "net/http"
    "net/url"

    "github.com/google/go-querystring/query"
)

type SearchParams struct {
    Query string `url:"q"`
    Page  int    `url:"page"`
    Limit int    `url:"limit"`
}

func main() {
    params := SearchParams{
        Query: "golang & go",
        Page:  1,
        Limit: 10,
    }

    v, _ := query.Values(params)
    baseURL, _ := url.Parse("https://api.example.com/search")
    baseURL.RawQuery = v.Encode()

    resp, _ := http.Get(baseURL.String())
    defer resp.Body.Close()

    fmt.Println("URL:", baseURL.String())
    // Результат: https://api.example.com/search?limit=10&page=1&q=golang+%26+go
}

Плюсы и минусы

  • Плюсы: код становится чище, если параметров много; теги позволяют задавать имена полей, отличные от Go-переменных; автоматическая сериализация типов (int, bool, slice).
  • Минусы: нужно подключать внешнюю зависимость; для простых запросов избыточно.

Рекомендую для проектов, где много разных API-запросов. Для единичных случаев используйте стандартный net/url из первого ответа.