13 декабря 2024

🦫 Самоучитель по Go для начинающих. Часть 18. Протокол HTTP. Создание HTTP-сервера и клиента. Пакет net/http

Энтузиаст-разработчик, автор статей по программированию.
В этой части самоучителя мы рассмотрим основные принципы работы с протоколом HTTP в контексте разработки веб-приложений. Изучим структуру HTTP-запросов и ответов, а также основные способы взаимодействия клиента и сервера. Особое внимание уделим пакету net/http, который предоставляет удобные инструменты для работы с HTTP-протоколом. В процессе обучения вы узнаете, как отправлять запросы, получать ответы и настраивать параметры HTTP-сервера.
🦫 Самоучитель по Go для начинающих. Часть 18. Протокол HTTP. Создание HTTP-сервера и клиента. Пакет net/http

HTTP

Протокол передачи гипертекста (HTTP) является неотъемлемой частью интернета. Он работает на прикладном уровне и представляет собой основу для обмена данными между клиентом и сервером. Когда пользователь вводит URL в браузере, отправляется HTTP-запрос к серверу, который затем возвращает соответствующий ответ. Этот процесс охватывает широкий спектр ресурсов, от простых HTML-документов до сложных JavaScript-файлов, изображений и стилей.

Процесс коммуникации в протоколе HTTP начинается с того, что клиент (чаще всего это веб-браузер) отправляет запрос на сервер, указывая адрес ресурса в формате URL. Сервер в ответ отсылает соответствующие данные, которые затем отображаются пользователю. Примером такого взаимодействия является ситуация, когда пользователь вводит адрес "www.google.com" в адресной строке браузера. В ответ сервер Google отправляет главную страницу поисковика, которая затем появляется на экране пользователя.

URL

URL (Uniform Resource Locator) представляет собой адрес ресурса, который используется для поиска веб-сервера и указания запрашиваемого контента. URL состоит из нескольких компонентов, каждый из которых имеет свою роль. Схема (например, http или https) определяет протокол, по которому будет происходить передача данных. Авторизация, хотя и является необязательной частью URL, может включать в себя имя пользователя и пароль для доступа к защищенным ресурсам. Хостнейм и порт указывают на сервер и его порт, а путь сообщает, где именно на сервере находится нужный ресурс. Запрос, который начинается с символа ?, включает параметры, передаваемые серверу, а фрагмент, начинающийся с #, указывает на конкретную часть документа или ресурса.

Пример URL: https://username:password@www.example.com:8080/path?query=value#section

HTTP-запросы

HTTP-запросы — это сообщения, которое клиент отправляет серверу с просьбой предоставить запрашиваемый ресурс. Структура запроса состоит из метода, целевого ресурса, заголовков и тела. Метод HTTP-запроса указывает, что нужно сделать с целевым ресурсом. Например, метод GET сообщает серверу, что клиент хочет получить данные, связанные с ресурсом, в то время как метод POST указывает на необходимость отправки данных на сервер.

Заголовки запроса содержат метаданные, такие как размер тела запроса или его формат. Например, если клиент отправляет изображение на сервер, то в теле запроса будет содержаться изображение, а в заголовке Content-Length – размер передаваемых данных.

В языке Go для работы с HTTP-запросами используется пакет net/http, который поддерживает самые распространенные HTTP-методы, определенные в стандартах RFC 7231 и RFC 5789. Среди них можно выделить такие методы, как GET для получения ресурса, POST для отправки данных, PUT для их обновления и другие.

Статья по теме

HTTP-ответы

Когда сервер завершает обработку запроса, он отправляет клиенту HTTP-ответ, содержащий статус выполнения запроса, данные и дополнительные метаданные. Ответы могут быть классифицированы по статусным кодам, которые дают представление о результате обработки запроса.

Ответ состоит из нескольких частей: стартовой строки, заголовков и тела. Стартовая строка включает в себя HTTP-версию, статусный код и текстовое описание результата. Например, строка HTTP/1.1 200 OK означает, что запрос был успешно обработан, и сервер вернул запрашиваемые данные.

Заголовки ответа предоставляют метаданные о содержимом ответа, такие как тип данных или время кэширования. Например, заголовок Content-Type может указывать, что ответ содержит HTML-документ или JSON-данные. Тело ответа содержит сам контент, например, HTML-страницу или файл изображения.

Пример HTTP-ответа:

        HTTP/1.1 200 OK
Date: Wed, 10 Nov 2024 10:00:00 GMT
Server: Apache/2.4.41 (Ubuntu)
Content-Type: text/html; charset=UTF-8
Content-Length: 1254
Connection: keep-alive


    

Статусные коды HTTP-ответов

Статусные коды HTTP-ответов классифицируются на пять основных категорий, каждая из которых обозначает определенный тип результата:

  1. Информационные ответы (100-199) информируют клиента о том, что запрос был принят, и сервер ожидает дальнейших действий.
  2. Успешные ответы (200-299) подтверждают успешное выполнение запроса. Например, код 200 OK означает, что сервер успешно обработал запрос.
  3. Перенаправления (300-399) просят клиента сделать другой запрос, как правило, по другому адресу URI.
  4. Ошибки клиента (400-499) указывают на ошибки со стороны клиента. Например, запрос несуществующей страницы или попытка
  5. Ошибки сервера (500-599) указывают на внутренние проблемы серверной стороны, такие как сбой в работе или временная недоступность ресурса.
🦫 Библиотека Go разработчика
Больше полезных материалов вы найдете на нашем телеграм-канале «Библиотека Go разработчика»
🦫🎓 Библиотека Go для собеса
Подтянуть свои знания по Go вы можете на нашем телеграм-канале «Библиотека Go для собеса»
🦫🧩 Библиотека задач по Go
Интересные задачи по Go для практики можно найти на нашем телеграм-канале «Библиотека задач по Go»

Клиент-серверное взаимодействие

HTTP-клиент

Пакет net/http предоставляет структуру http.Client для создания и настройки HTTP-клиента, который используется для отправки HTTP-запросов и получения ответов от веб-серверов:

        type Client struct {
	Transport RoundTripper
	CheckRedirect func(req *Request, via []*Request) error
	Jar CookieJar
	Timeout time.Duration
}


    

Детально рассмотрим каждое из полей структуры http.Client:

  1. Transport определяет механизм выполнения запросов. В большинстве случаев используется стандартный объект http.DefaultTransport, который обрабатывает все части HTTP-запроса, включая установление соединения, отправку запроса и получение ответа. Однако допускается создавать собственные реализации интерфейса RoundTripper для более тонкой настройки поведения клиента, например, для использования прокси-сервера.
  2. CheckRedirect управляет поведением клиента при перенаправлении (например, при 301 или 302 статусах). Вы можете настроить ее так, чтобы определенные перенаправления были разрешены или отклонены.
  3. Jar используется для управления cookies, которые клиент может получать от серверов и отправлять обратно в последующих запросах. По умолчанию используется http.CookieJar, который управляет cookies в автоматическом режиме. Это позволяет клиенту сохранять состояние между запросами, что важно при работе с сессиями.
  4. Timeout задает максимальное время, которое клиент будет ожидать получения ответа от сервера. Если сервер не отвечает в течение этого времени, запрос будет отменен с ошибкой. Такое поведения позволяет избежать зависания клиента при плохих сетевых соединениях.

Запросы GET и POST

Самый простой способ отправить GET-запрос средствами Go – это указать URL запрашиваемого ресурса в качестве аргумента функции http.Get:

        func main() {
	response, err := http.Get("https://httpbin.org/get?proglib=cool")
	if err != nil {
		log.Fatal(err)
	}
	defer response.Body.Close()

	body, err := io.ReadAll(response.Body)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(response.Status, string(body))
}


    

В результате сервер вернет ответ, который можно прочитать с помощью функции io.ReadAll и вывести на экран:

        200 OK {
  "args": {
    "proglib": "cool"
  },
  "headers": {
    "Accept-Encoding": "gzip",
    "Host": "httpbin.org",
    "User-Agent": "Go-http-client/2.0",
    "X-Amzn-Trace-Id": "Root=1-673b84cf-0d7e53aa14f60ac80478a841"
  },
  "origin": "80.85.241.77",
  "url": "https://httpbin.org/get?proglib=cool"
}


    

Ответ от сервера задается структурой http.Response, которая содержит множество полей (статус, код статуса, протокол, заголовок и тд).

Тело ответа (response.Body) представляет собой структуру io.ReadCloser, поэтому с ним можно взаимодействовать при помощи функций пакета io. При этом важно вызывать отложенное закрытие тела ответа с помощью defer для избежания утечек ресурсов.

Встроенные методы http.Get и http.Post довольны просты в использовании и справляются с базовыми задачами, но иногда их возможностей недостаточно. Для более комплексных задач, таких как добавление пользовательских заголовков, настройка прокси, установка времени ожидания и других используются два объекта: структура http.Client и функция http.NewRequest, на которых основаны методы http.Get и http.Post. Чтобы в этом убедиться, посмотрим реализацию функции http.Get в пакете net/http:

        var DefaultClient = &Client{}

func Get(url string) (resp *Response, err error) {
	return DefaultClient.Get(url)
}

func (c *Client) Get(url string) (resp *Response, err error) {
	req, err := NewRequest("GET", url, nil)
	if err != nil {
		return nil, err
	}
	return c.Do(req)
}


    

Как можно видеть, метод http.Get() создает новый объект http.Request с помощью функции http.NewRequest(), а затем использует метод http.Client.Do для его отправки.

Давайте напишем POST-запрос с использованием http.Client и функции http.NewRequest:

        func main() {
	client := http.Client{}

	requestData := []byte(`{"somedata":"sometext"}`)

	request, err := http.NewRequest(http.MethodPost, "https://httpbin.org/post", bytes.NewBuffer(requestData))
	if err != nil {
		log.Fatal(err)
	}
	request.Header.Set("Content-Type", "application/json")

	response, err := client.Do(request)
	if err != nil {
		log.Fatal(err)
	}
	defer response.Body.Close()

	body, err := io.ReadAll(response.Body)
	if err != nil {
		log.Fatal(err)
	}

	fmt.Println(string(body))
}


    

Для начала мы подготавливаем наш запрос, создавая структуру клиента и тело запроса в виде JSON. Далее происходит создание объекта http.Request с помощью функции http.NewRequest со следующими аргументами: HTTP-метод, URL, тело запроса типа io.Reader. Обратите внимание, что вместо названий HTTP-методов в виде строк (например, "GET", "POST") можно использовать соответствующие им константы пакета net/http.

Следующий шаг – с помощью конструкции request.Header.Set указать в заголовке тип контента, который мы хотим отослать. В данном случае ограничимся заданием объекта JSON в виде слайса байт, но для полноценной работы с JSON предпочтительнее воспользоваться пакетом encoding/json, который детально рассматривается в статье Эффективная работа с JSON в Go.

Далее отправляем запрос серверу с помощью вызова у клиента метода Do и записываем ответ (http.Response) в переменную response, не забыв обработать ошибку и вызвать отложенное закрытие тела ответа.

В завершение считываем содержимое ответа с помощью io.ReadAll и выводим его на экран.

Отправка запросов

Поле Transport в структуре http.Client позволяет настроить механизм, используемый для выполнения HTTP-запросов. Поле представляет собой интерфейс RoundTripper, который содержит метод RoundTrip(*http.Request) (*http.Response, error). Этот интерфейс отвечает за фактическое выполнение запроса, установление соединений, обработку прокси, настройку времени ожидания и прочие аспекты сетевого взаимодействия.

Для изменения поведения HTTP-запросов, например, добавления логирования, собственных заголовков или прокси, нужно реализовать интерфейс RoundTripper:

        // CustomTransport - наша собственная реализация RoundTripper
type CustomTransport struct {
	Transport http.RoundTripper
}

func (c *CustomTransport) RoundTrip(req *http.Request) (*http.Response, error) {
	// Логирование каждого запроса
	fmt.Println("Отправляется запрос на URL:", req.URL)

	// Дополнительная логика (например, добавление заголовков)
	req.Header.Add("X-Custom-Header", "CustomValue")

	return c.Transport.RoundTrip(req)
}

func main() {
	client := &http.Client{
		Transport: &CustomTransport{Transport: http.DefaultTransport}, // Используем кастомный транспорт
	}

	// Отправляем GET запрос
	resp, err := client.Get("https://httpbin.org/get")
	if err != nil {
		fmt.Println("Ошибка:", err)
		return
	}
	defer resp.Body.Close()

	fmt.Println("Статус ответа:", resp.Status)
}


    

В этом примере мы создали собственную реализацию CustomTransport, которая логирует каждый запрос перед его отправкой и добавляет кастомный заголовок X-Custom-Header в запрос.

Одной из частых задач является настройка прокси-сервера для HTTP-запросов. Для этого можно использовать структуру http.ProxyURL, которая позволяет указать URL прокси-сервера:

        func main() {
	// Вместо адреса ниже стоит указать адрес существующего прокси-сервера
	proxyURL, err := url.Parse("http://proxyserver:8080")
	if err != nil {
		fmt.Println("Ошибка при парсинге прокси:", err)
		return
	}

	// Структура клиента
	client := &http.Client{
		Transport: &http.Transport{
			Proxy: http.ProxyURL(proxyURL), // Настройка прокси
		},
	}

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

	fmt.Println("Статус ответа:", resp.Status)
}


    

Перенаправление запросов

Поле CheckRedirect в структуре http.Client используется для управления поведением клиента при получении ответа на запрос с перенаправлением (например, HTTP-статусы 3xx). Оно представляет собой функцию, которая вызывается каждый раз, когда происходит перенаправление. Эта функция принимает два аргумента: запрос для выполнения (*http.Request) и срез ответов ([]*http.Response), которые уже были получены в процессе перенаправлений.

Самый простой сценарий использования поля CheckRedirect — это ограничение числа перенаправлений для избежания зацикливания или бесконечного выполнения запросов:

        client := http.Client{
    CheckRedirect: func(req *http.Request, via []*http.Request) error {
        if len(via) >= 5 {
            return http.ErrUseLastResponse // Вернуть последний запрос, не интерпретируя его как ошибку
        }
        return nil // Выполнить перенаправление
    },
}


    

Таким образом, при достижении 5 перенаправлений клиент вернет последний полученный ответ, не следуя за новым перенаправлением. Это полезно, чтобы избежать потенциальных бесконечных циклов перенаправлений.

Таймауты

Поле Timeout в структуре http.Client определяет максимальное время ожидания выполнения всего HTTP-запроса, включая установление соединения, чтение и запись данных. Это полезно, в тех случаях, когда необходимо ограничить время, которое клиент будет тратить на обработку запроса, особенно если сетевое соединение нестабильно или сервер работает медленно.

Рассмотрим простой пример использования поля Timeout для установки тайм-аута на 5 секунд:

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

	// Отправляем GET-запрос
	resp, err := client.Get("https://httpbin.org/get")
	if err != nil {
		log.Fatal(err)
	}
	defer resp.Body.Close()

	// Читаем тело ответа
	fmt.Println("Статус ответа:", resp.Status)
}


    

Если значение поля Timeout превысит заданное значение, то запрос будет прерван, и вернется ошибка context.DeadlineExceeded.

HTTP-сервер

Классическая схема обработки HTTP-запроса

Переходим к изучению и реализации второго компонента клиент-серверной архитектуры – HTTP-сервера.

Для начала рассмотрим классическую схему обработки запроса клиента:

  1. Клиент посылает запросы на сервер
  2. Сервер распределяет (мультиплексирует) запросы на обработчики
  3. Запрос проходит через промежуточный слой (middleware)
  4. После прохождения промежуточного слоя запрос попадает конкретному обработчику, который отслылает клиенту ответ.

Можно сказать, что процесс обработки запросов в Go основывается на двух основных компонентах:

  1. Обработчики (handlers): они отвечают за логику приложения и формирование ответа клиенту. Обработчики в Go могут быть как простыми функциями, так и сложными структурами, реализующими интерфейс http.Handler.
  2. Серверные мультиплексоры (servemuxes): это компоненты, которые сопоставляют URL-пути с соответствующими обработчиками. Мультиплексоры позволяют серверу понимать, какой обработчик следует использовать для каждого конкретного запроса.

Продемонстрируем пример простого HTTP-сервера с одним мультиплексором и обработчиком:

        func main() {
	// Создаем и возвращаем новый экземпляр структуры ServeMux
	mux := http.NewServeMux()

	// Создаем обработчик, перенаправляющий запросы по указанному URL с HTTP-кодом 308
	redirectHandler := http.RedirectHandler("http://google.com", http.StatusPermanentRedirect)

	// Регистрируем ранее созданный servemux по указанному URL
	mux.Handle("/redirect", redirectHandler)

	log.Print("Сервер запущен...")

	// Запуск сервера на порту 8080
	http.ListenAndServe(":8080", mux)
}


    

Чтобы протестировать работу сервера, достаточно запустить код и отправить запрос по адресу http://localhost:8080/redirect. Сделать это можно с помощью популярной программы Postman или утилиты curl с дополнительными флагами:

        curl -i http://localhost:8080/redirect


    

В результате получим следующий подробный вывод, содержащий HTTP-заголовки, информацию о подключении и другую диагностическую информацию:

        *   Trying 127.0.0.1:8080...
* Connected to localhost (127.0.0.1) port 8080 (#0)
> GET /redirect HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/8.1.2
> Accept: */*
>
< HTTP/1.1 308 Permanent Redirect
< Content-Type: text/html; charset=utf-8
< Location: http://google.com
< Date: Wed, 20 Nov 2024 17:46:38 GMT
< Content-Length: 53
<
<a href="http://google.com">Permanent Redirect</a>.

* Connection #0 to host localhost left intact


    

Пользовательские обработчики

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

Любой пользовательский обработчик должен реализовывать интерфейс http.Handler с единственным методом ServeHTTP:

        type Handler interface {
    ServeHTTP(ResponseWriter, *Request)
}

    

Напишем собственный обработчик, который будет отсылать ответ с предоставленной информацией о пользователе:

        // Структура, содержащая данные о пользователе
type userHandler struct {
	name  string
	age   int
}

// Реализация ServeHTTP для userHandler
func (uh userHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	// Формируем строку с данными о пользователе
	message := fmt.Sprintf("User: %s, Age: %d", uh.name, uh.age)

	// Один из способов записи данных в соединение
	w.Write([]byte(message))
}

func main() {
	mux := http.NewServeMux()

	// Инициализация userHandler с данными о пользователе
	uh := userHandler{name: "Гоша", age: 30}

	// Регистрация обработчика для маршрута "/user"
	mux.Handle("/user", uh)

	log.Print("Сервер запущен...")
	http.ListenAndServe(":8080", mux)
}


    

Обработка запроса на стороне сервера происходит следующим образом: сервер получает HTTP-запрос и передает его мультиплексору, который указан вторым аргументом функции http.ListenAndServe. Следующим этапом мультиплексор ищет подходящий обработчик по указанному маршруту, в данном случае – "/user". Далее мультиплексор вызывает у подобранного обработчика метод ServeHTTP, который записывает данные в HTTP-соединение и отсылает ответ клиенту.

Объект http.ServeMux можно воспринимать как особый обработчик, который вместо предоставления ответа передает запрос другому обработчику, формируя таким образом цепочку. Стоит отметить, что подобный механизм находит применение в специальных промежуточных обработчиках – middleware, с которыми мы познакомимся в дальнейшем.

Отметим, что в качестве обработчиков могут выступать обычные функции с сигнатурой func(http.ResponseWriter, *http.Request), обернутые в специальный тип http.HandlerFunc, который реализует интерфейс Handler:

        type HandlerFunc func(ResponseWriter, *Request)

func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
	f(w, r)
}


    

Для демонстрации создадим обработчик infoHandler, конвертируем его в тип HandlerFunc и передадим в мультиплексор:

        func infoHandler(w http.ResponseWriter, r *http.Request) {
	log.Printf("Информация о запросе: %s %s от %s", r.Method, r.URL.Path, r.RemoteAddr)
	fmt.Fprintf(w, "Запрос обработан\\\\n")
}

func main() {
	mux := http.NewServeMux()

	ih := http.HandlerFunc(infoHandler)

	mux.Handle("/info", ih)

	log.Print("Сервер запущен...")
	http.ListenAndServe(":8080", mux)
}


    

Так как конвертация функции в тип http.HandlerFunc и добавление её в мультиплексор является довольно частой задачей, для нее придумали отдельный метод mux.HandleFunc. Поэтому код

        ih := http.HandlerFunc(infoHandler)
mux.Handle("/info", ih)


    

можно заменить единственной строкой:

        mux.HandleFunc("/info", infoHandler)


    

Middleware

В контексте веб-разработки middleware выполняет роль промежуточного звена между клиентом и сервером, обрабатывая HTTP-запросы до того, как они будут переданы в конечные обработчики. Middleware широко используется в Go-фреймворках Gin, Echo и Chi.

Middleware может выполнять разные задачи, например:

  1. Авторизация и аутентификация
  2. Логирование
  3. Валидация входных данных
  4. Ограничение времени медленных запросов
  5. Сокрытие конфиденциальных данных

Middleware работает по принципу цепочки: когда HTTP-запрос поступает в сервер, он сначала проходит через серию middleware-компонентов, и только потом передается в основной обработчик. Каждый middleware может либо завершить обработку запроса, отправив ответ клиенту, либо передать управление следующему middleware в цепочке.

В языке Go создание middleware происходит в два этапа:

  1. Написать функцию middleware, чтобы она удовлетворяла интерфейсу http.Handler
  2. Выстроить цепочку из middleware и обработчиков

Начнем с написания стандартного шаблона middleware:

        func sampleMiddleware(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		// ...
		next.ServeHTTP(w, r)
	})
}

    

Функция sampleMiddleware принимает в качестве аргумента обработчик next типа http.Handler и возвращает новый обработчик того же типа. Внутри функции создаётся анонимный обработчик с использованием конструктора http.HandlerFunc, который преобразует функцию в объект, соответствующий интерфейсу http.Handler. Этот анонимный обработчик выполняет основную логику, после чего вызывается метод ServeHTTP у next, который передаёт запрос и ответ следующему обработчику в цепочке.

Рассмотренный способ задания функции позволяет создавать цепочки middleware с вложенными обработчиками и наглядно регистрировать их в мультиплексорах по следующему примеру:

        mux := http.NewServeMux()
mux.Handle("/", middlewareOne(middlewareTwo(mainHandler)))

    

Руководствуясь полученными знаниями, напишем HTTP-сервер, иллюстрирующий передачу запроса по цепочке из двух middleware:

        package main

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

// middlewareOne измеряет время выполнения запроса и логирует информацию о начале и конце обработки
func middlewareOne(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		start := time.Now()
		log.Printf("middlewareOne: начало обработки запроса в %s", start.Format("15:04:05"))

		// Передаём управление следующему обработчику
		next.ServeHTTP(w, r)

		// Рассчитываем и выводим время выполнения в наносекундах
		duration := time.Since(start).Nanoseconds()
		log.Printf("middlewareOne: запрос обработан за %d наносекунд", duration)
	})
}

// middlewareTwo логирует информацию о маршруте и подтверждает успешную обработку
func middlewareTwo(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		log.Printf("middlewareTwo: обработка маршрута %s", r.URL.Path)

		// Передаём управление следующему обработчику
		next.ServeHTTP(w, r)

		log.Print("middlewareTwo: запрос успешно обработан")
	})
}

// mainHandler — основной обработчик
func mainHandler(w http.ResponseWriter, r *http.Request) {
	log.Print("mainHandler: обработка запроса")
	w.Write([]byte("Запрос выполнен успешно\\\\n"))
}

func main() {
	mux := http.NewServeMux()

	mh := http.HandlerFunc(mainHandler)

	// Формируем цепочку из middleware с основным запросом в конце
	mux.Handle("/", middlewareOne(middlewareTwo(mh)))

	log.Print("Сервер запущен...")
	err := http.ListenAndServe(":8080", mux)
	log.Fatal(err)
}


    

После запуска приведенного кода и отправки запроса curl -i http://localhost:8080 получим следующий вывод:

        HTTP/1.1 200 OK
Date: Wed, 20 Nov 2024 19:13:25 GMT
Content-Length: 45
Content-Type: text/plain; charset=utf-8

Запрос выполнен успешно


    

В терминале, где был запущен код, выведутся логи, по которым можно отследить процесс передачи управления от одного middleware к другому сначала в порядке их вложенности, а затем – в обратном:

        2024/11/20 22:13:23 Сервер запущен...
2024/11/20 22:13:25 middlewareOne: начало обработки запроса в 22:13:25
2024/11/20 22:13:25 middlewareTwo: обработка маршрута /
2024/11/20 22:13:25 mainHandler: обработка запроса
2024/11/20 22:13:25 middlewareTwo: запрос успешно обработан
2024/11/20 22:13:25 middlewareOne: запрос обработан за 85632 наносекунд


    

Заключение

В 18 части самоучителя мы изучили основы работы с HTTP-протоколом, разобрались в структуре запросов и ответов, а также познакомились с методами взаимодействия между клиентом и сервером. Мы подробно рассмотрели возможности пакета net/http в Go для реализации веб-приложений и закрепили полученные знания с помощью практических примеров.

Эта глава завершает наш самоучитель по языку Go, в котором мы подробно разобрали основные подходы программирования на этом мощном и современном языке. Мы искренне благодарим вас за то, что прошли этот путь вместе с нами, и надеемся, что изученные материалы станут надежной основой для вашего дальнейшего профессионального роста.

Не бойтесь применять полученные знания на практике, развивайте свои навыки, исследуйте новые области и вдохновляйтесь успехами других. Помните, что каждая строка кода — это шаг к воплощению ваших идей и решению реальных задач. Желаем вам успехов, творческого вдохновения и удовольствия от программирования на Go!

***

Содержание самоучителя

  1. Особенности и сфера применения Go, установка, настройка
  2. Ресурсы для изучения Go с нуля
  3. Организация кода. Пакеты, импорты, модули. Ввод-вывод текста.
  4. Переменные. Типы данных и их преобразования. Основные операторы
  5. Условные конструкции if-else и switch-case. Цикл for. Вложенные и бесконечные циклы
  6. Функции и аргументы. Области видимости. Рекурсия. Defer
  7. Массивы и слайсы. Append и сopy. Пакет slices
  8. Строки, руны, байты. Пакет strings. Хеш-таблица (map)
  9. Структуры и методы. Интерфейсы. Указатели. Основы ООП
  10. Наследование, абстракция, полиморфизм, инкапсуляция
  11. Обработка ошибок. Паника. Восстановление. Логирование
  12. Обобщенное программирование. Дженерики
  13. Работа с датой и временем. Пакет time
  14. Интерфейсы ввода-вывода. Буферизация. Работа с файлами. Пакеты io, bufio, os
  15. Конкурентность. Горутины. Каналы
  16. Тестирование кода и его виды. Table-driven подход. Параллельные тесты
  17. Основы сетевого программирования. Стек TCP/IP. Сокеты. Пакет net
  18. Протокол HTTP. Создание HTTP-сервера и клиента. Пакет net/http

МЕРОПРИЯТИЯ

Комментарии

ВАКАНСИИ

Добавить вакансию
Разработчик C++
Москва, по итогам собеседования

ЛУЧШИЕ СТАТЬИ ПО ТЕМЕ