net/http помогает решать многие задачи: создавать HTTP-сервер, отправлять запросы GET, POST, PUT, DELETE. Но на практике функций этого пакета хватает не всегда. net/http реализовать легко:resp, err := http.Get("https://yandex.ru/")
resp, err := http.Post("http://yandex-example.ru/upload", "image/png", &buf)
resp, err := http.PostForm("http://yandex-example.com/form",
url.Values{"key": {"Value"}, "id": {"123"}}) package main
import (
"fmt"
"io"
"log"
"net/http"
)
var bearer = "Bearer <Token>"
func main() {
// создаём новый запрос
req, err := http.NewRequest("GET", "https://yandex.ru", nil)
if err != nil {
log.Println(err)
return
}
// добавляем авторизацию
req.Header.Add("Authorization", bearer)
// создаём клиент
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
log.Println("Error on response.\n[ERROR] -", err)
return
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
log.Println("Bad status code on response: ", resp.StatusCode)
return
}
body, err := io.ReadAll(resp.Body)
// продолжаем работу
fmt.Println(body)
} | Название | Описание |
|---|---|
| gentleman | Полнофункциональный, управляемый плагинами, ориентированный на middleware пакет для создания универсальных и композитных HTTP-клиентов |
| grequests | «Клон» библиотеки Requests, ориентированный на Python-разработчиков |
| heimdall | Пакет, ориентированный на высоконагруженные проекты |
| pester | Пакет, ориентированный на клиентские вызовы HTTP-запросов с повторными попытками, backoff-стратегиями и параллелизмом |
| request | Пакет, ориентированный на разработчиков, знакомых с Axios или Requests |
| resty | Пакет, ориентированный на построение REST-приложений на Go |
| rq | Обёртка для пакета net/http, которая предлагает более красивый интерфейс |
| sling | Пакет, ориентированный на взаимодействие со сторонними API |
grequests. Для высоконагруженных проектов подойдёт heimdall, а если вы начинающий разработчик, выбирайте resty.resty. Она поддерживает много дополнительных функций:GET, POST, PUT, PATCH, OPTIONS;SetError() для ошибок, SetResult() для результата;package main
import (
"fmt"
"github.com/go-resty/resty/v2"
)
func main() {
// создаём новый клиент
client := resty.New()
resp, err := client.R().
SetAuthToken("Bearer <TOKEN>").
Get("https://my-example-site.ru")
fmt.Println("Исследуем объект Response:")
fmt.Println("Error :", err)
fmt.Println("Status Code:", resp.StatusCode())
fmt.Println("Status :", resp.Status())
fmt.Println("Time :", resp.Time())
fmt.Println("Received At:", resp.ReceivedAt())
fmt.Println("Body :\n", resp)
fmt.Println("----")
} SetError() и SetResult(). Например, так:package main
import (
"fmt"
"github.com/go-resty/resty/v2"
"time"
)
// MyApiError — описание ошибки при неверном запросе.
type MyApiError struct {
Code int `json:"code"`
Message string `json:"message"`
Timestamp time.Time `json:"timestamp"`
}
// Post — модель, описание основного объекта.
type Post struct {
UserID int `json:"userId"`
ID int `json:"id"`
Title string `json:"title"`
Text string `json:"text"`
}
func main() {
client := resty.New()
var responseErr MyApiError
var post Post
_, err := client.R().
SetError(&responseErr).
SetResult(&post).
Get("https://jsonplaceholder.typicode.com/posts/1")
if err != nil {
fmt.Println(responseErr)
panic(err)
return
}
fmt.Println(post)
} package main
import (
"fmt"
"github.com/go-resty/resty/v2"
)
func main() {
client := resty.New()
resp, err := client.R().SetPathParams(map[string]string{
"postID": "1",
}).Get("https://jsonplaceholder.typicode.com/posts/{postID}")
if err != nil {
panic(err)
}
fmt.Println(resp)
} POST-запросы разными способами:package main
import (
"fmt"
"github.com/go-resty/resty/v2"
"time"
)
func main() {
client := resty.New()
client.
// устанавливаем количество повторений
SetRetryCount(3).
// длительность ожидания между попытками
SetRetryWaitTime(30 * time.Second).
// длительность максимального ожидания
SetRetryMaxWaitTime(90 * time.Second)
resp, err := client.R().
SetHeader("Content-Type", "application/json").
SetBody(`{"title":"foo", "body":"bar", "userId": 7}`).
Post("https://jsonplaceholder.typicode.com/posts")
if err != nil {
panic(err)
}
fmt.Println(resp)
// другой вариант POST-запроса
// если передаётся map, то по умолчанию используется JSON
resp, err = client.R().
SetBody(map[string]interface{}{"title":"My title", "body":"Content", "userId": 7}).
Post("https://jsonplaceholder.typicode.com/posts")
if err != nil {
panic(err)
}
fmt.Println(resp)
} cmd/skill/main_test.go на новый код:package main
import (
"net/http"
"net/http/httptest"
"testing"
"github.com/go-resty/resty/v2"
"github.com/stretchr/testify/assert"
)
func TestWebhook(t *testing.T) {
// тип http.HandlerFunc реализует интерфейс http.Handler
// это поможет передать хендлер тестовому серверу
handler := http.HandlerFunc(webhook)
// запускаем тестовый сервер, будет выбран первый свободный порт
srv := httptest.NewServer(handler)
// останавливаем сервер после завершения теста
defer srv.Close()
// ожидаемое содержимое тела ответа при успешном запросе
successBody := `{
"response": {
"text": "Извините, я пока ничего не умею"
},
"version": "1.0"
}`
// описываем набор данных: метод запроса, ожидаемый код ответа, ожидаемое тело
testCases := []struct {
method string
expectedCode int
expectedBody string
}{
{method: http.MethodGet, expectedCode: http.StatusMethodNotAllowed, expectedBody: ""},
{method: http.MethodPut, expectedCode: http.StatusMethodNotAllowed, expectedBody: ""},
{method: http.MethodDelete, expectedCode: http.StatusMethodNotAllowed, expectedBody: ""},
{method: http.MethodPost, expectedCode: http.StatusOK, expectedBody: successBody},
}
for _, tc := range testCases {
t.Run(tc.method, func(t *testing.T) {
// делаем запрос с помощью библиотеки resty к адресу запущенного сервера,
// который хранится в поле URL соответствующей структуры
req := resty.New().R()
req.Method = tc.method
req.URL = srv.URL
resp, err := req.Send()
assert.NoError(t, err, "error making HTTP request")
assert.Equal(t, tc.expectedCode, resp.StatusCode(), "Response code didn't match expected")
// проверяем корректность полученного тела ответа, если мы его ожидаем
if tc.expectedBody != "" {
assert.JSONEq(t, tc.expectedBody, string(resp.Body()))
}
})
}
} srv := httptest.NewServer(handler): мы запускаем тестовый сервер с хендлером в качестве обработчика запросов. Это настоящий HTTP-сервер, запрос к которому можно отправить с помощью любого HTTP-клиента, что мы и делаем в обновлённом тесте.net/http в одном файле:package main
import (
"io"
"net/http"
"strings"
)
var cars = map[string]string{
"id1": "Renault Logan",
"id2": "Renault Duster",
"id3": "BMW X6",
"id4": "BMW M5",
"id5": "VW Passat",
"id6": "VW Jetta",
"id7": "Audi A4",
"id8": "Audi Q7",
}
// carsListFunc — вспомогательная функция для вывода всех машин.
func carsListFunc() []string {
var list []string
for _, c := range cars {
list = append(list, c)
}
return list
}
// carFunc — вспомогательная функция для вывода определённой машины.
func carFunc(id string) string {
if c, ok := cars[id]; ok {
return c
}
return "unknown identifier " + id
}
func carsHandle(rw http.ResponseWriter, r *http.Request) {
carsList := carsListFunc()
io.WriteString(rw, strings.Join(carsList, ", "))
}
func carHandle(rw http.ResponseWriter, r *http.Request) {
carID := r.URL.Query().Get("id")
if carID == "" {
http.Error(rw, "carID param is missed", http.StatusBadRequest)
return
}
rw.Write([]byte(carFunc(carID)))
}
func main() {
// определяем хендлер, который выводит все машины
http.HandleFunc("/cars", carsHandle)
// определяем хендлер, который выводит определённую машину
http.HandleFunc("/car", carHandle)
log.Fatal(http.ListenAndServe(":8080", nil))
} /car?id=123 красивый URL /car/lada-priora-black-2021;GET, POST, PUT, DELETE;CORS,OPTIONS,net/http? Да. Но нужно ли изобретать велосипед и отлавливать ошибки?| Название | Описание | Совместим ли с пакетом net/http | Для чего используется |
|---|---|---|---|
| gin | Веб-фреймворк на Go. Реализован свой API. Высокая производительность благодаря httprouter | да | REST API, бэкенд-сервис |
| chi | Лёгкий композитный пакет для создания HTTP-сервисов. Помогает писать крупные сервисы REST API, которые поддерживаются по мере развития проекта | да | REST API, бэкенд-сервис |
| echo | Высокопроизводительный и минималистичный веб-фреймворк | да | REST API, бэкенд-сервис |
| fasthttp | Высокопроизводительная библиотека для обработки огромного количества запросов: например, 200 тысяч запросов в секунду | нет | highload-сервис |
| beego | Полноценный веб-фреймворк, использующий модель MVC (Model–View–Controller). Помогает, когда нужно быстро разработать проект | нет | веб-сайт |
| buffalo | Экосистема, которая включает роутинг net/http, работу с темплейтами, ORM и вспомогательные инструменты | нет | веб-сайт |
buffalo, для бэкенд-микросервиса — gin или chi, для решения проблем highload — fasthttp (хотя часто проблема RPS — в базах данных или алгоритмах).chi — чтобы разобраться с его базовыми возможностями.chi:net/http;docgen;package main
import (
"fmt"
"io"
"net/http"
"github.com/go-chi/chi/v5"
)
func main() {
r := chi.NewRouter()
r.Get("/", func(rw http.ResponseWriter, r *http.Request) {
rw.Write([]byte("chi"))
})
r.Get("/item/{id}", func(rw http.ResponseWriter, r *http.Request) {
// получаем значение URL-параметра id
id := chi.URLParam(r, "id")
io.WriteString(rw, fmt.Sprintf("item = %s", id))
})
// r передаётся как http.Handler
http.ListenAndServe(":8080", r)
} chi написанные ранее хендлеры — они будут полностью совместимы.chi есть дополнительные возможности для настройки маршрутизации запросов, то есть для роутинга. Route(). Например, так:r.Get("/cars", carsHandle) // GET /cars
r.Get("/cars/{brand}", brandHandle) // GET /cars/renault
r.Get("/cars/{brand}/{model}", modelHandle) // GET /cars/renault/duster
// то же самое можно описать, используя Route
r.Route("/cars", func(r chi.Router) {
r.Get("/", carsHandle) // GET /cars
// Route можно вкладывать один в другой
r.Route("/{brand}", func(r chi.Router) {
r.Get("/", brandHandle) // GET /cars/renault
r.Get("/{model}", modelHandle) // GET /cars/renault/duster
})
}) Route() позволяет группировать разные HTTP-методы для одного и того же запроса:r.Post("/car", newCar) // POST /car
r.Get("/car/{id}", getCar) // GET /car/1234
r.Put("/car/{id}", updateCar) // PUT /car/1234
r.Delete("/car/{id}", deleteCar) // DELETE /car/1234
// то же самое, используя Router
r.Route("/car", func(r chi.Router) {
r.Post("/", newCar) // POST /car
r.Route("/{id}", func(r chi.Router) {
r.Get("/", getCar) // GET /car/1234
r.Put("/", updateCar) // PUT /car/1234
r.Delete("/", deleteCar) // DELETE /car/1234
})
}) middleware. Напомним, что middleware — это функции-фильтры для обработки запросов. Они совершают дополнительные действия (аутентификация, логирование, сжатие и другие) и не мешают выполнению основного обработчика. Например, они помогают проверять авторизацию пользователя: если посетитель не авторизован, middleware перенаправляют его на страницу авторизации.chi есть встроенные middleware — можно использовать их или написать свои. middleware:AllowContentType — допускает запросы только c определёнными заголовками Content-Type.BasicAuth — реализует базовую (Basic) схему аутентификации.Compress — сжимает тело ответа в соответствии с заголовком запроса Accept-Encoding.Logger — отвечает за логирование запросов.RealIP — устанавливает RemoteAddr запроса в соответствии с заголовком X-Forwarded-For или X-Real-IP.Recoverer — восстанавливается после panic, регистрирует panic и обратную трассировку, возвращает статус HTTP 500 (внутренняя ошибка сервера), если это возможно.middleware можно функцией Use(middlewares ...func(http.Handler) http.Handler). Если подключается несколько функций, то они будут выполняться в том же порядке, в каком были добавлены.import (
// ...
"github.com/go-chi/chi/v5/middleware"
"github.com/go-chi/chi/v5"
)
func main() {
r := chi.NewRouter()
r.Use(middleware.RealIP)
r.Use(middleware.Logger)
r.Use(middleware.Recoverer)
// или
// r.Use(middleware.RealIP, middleware.Logger, middleware.Recoverer)
// ...
} middleware-функция, которая фиксирует время выполнения запросов:func TimerTrace(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// перед началом выполнения функции сохраняем текущее время
start := time.Now()
// вызываем следующий обработчик
next.ServeHTTP(w, r)
// после завершения замеряем время выполнения запроса
duration := time.Since(start)
// сохраняем или сразу обрабатываем полученный результат
// ...
})
} chi. Для простоты проверим только один обработчик на поиск машины по бренду и модели: /cars/{brand}/{model}. package main
import (
"net/http"
"github.com/go-chi/chi/v5"
)
var cars = map[string]string{
"id1": "Renault Logan",
"id2": "Renault Duster",
"id3": "BMW X6",
"id4": "BMW M5",
"id5": "VW Passat",
"id6": "VW Jetta",
"id7": "Audi A4",
"id8": "Audi Q7",
}
func modelHandle(rw http.ResponseWriter, r *http.Request) {
car := strings.ToLower(chi.URLParam(r, "brand") + ` ` +
chi.URLParam(r, "model"))
for _, c := range cars {
if strings.ToLower(c) == car {
io.WriteString(rw, c)
return
}
}
http.Error(rw, "unknown model: "+car, http.StatusNotFound)
}
func CarRouter() chi.Router {
r := chi.NewRouter()
r.Get("/cars/{brand}/{model}", modelHandle) // GET /cars/renault/duster
return r
}
func main() {
http.ListenAndServe(":8080", CarRouter())
} httptest.NewServer и передать ей CarRouter. testRequest:// model_test.go
package main
import (
"io"
"net/http"
"net/http/httptest"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func testRequest(t *testing.T, ts *httptest.Server, method,
path string) (*http.Response, string) {
req, err := http.NewRequest(method, ts.URL+path, nil)
require.NoError(t, err)
resp, err := ts.Client().Do(req)
require.NoError(t, err)
defer resp.Body.Close()
respBody, err := io.ReadAll(resp.Body)
require.NoError(t, err)
return resp, string(respBody)
}
func TestRouter(t *testing.T) {
ts := httptest.NewServer(CarRouter())
defer ts.Close()
// ниже добавим тестовые запросы
// ...
} func TestRouter(t *testing.T) {
ts := httptest.NewServer(CarRouter())
defer ts.Close()
var testTable = []struct {
url string
want string
status int
}{
{"/cars/renault/Logan", "Renault Logan", http.StatusOK},
{"/cars/audi/a4", "Audi A4", http.StatusOK},
// проверим на ошибочный запрос
{"/cars/audi/a6", "unknown model: audi a6\n", http.StatusNotFound},
{"/cars/BMW/M5", "BMW M5", http.StatusOK},
{"/cars/bmw/X6", "BMW X6", http.StatusOK},
{"/cars/Vw/Passat", "VW Passat", http.StatusOK},
}
for _, v := range testTable {
resp, get := testRequest(t, ts, "GET", v.url)
assert.Equal(t, v.status, resp.StatusCode)
assert.Equal(t, v.want, get)
}
} chi принципиально не отличается от тестирования с net/http. resty и научились применять её для отправки HTTP-запросов. Также вы разобрали примеры использования серверной библиотеки chi, которая расширяет возможности маршрутизации при разработке веб-сервера.resty.chi.middleware для роутера chi.