Русский
Русский
English
Статистика
Реклама

Rest сервер

Перевод Разработка REST-серверов на Go. Часть 1 стандартная библиотека

28.05.2021 16:10:14 | Автор: admin
Это первый материал из серии статей, посвящённой разработке REST-серверов на Go. В этих статьях я планирую описать реализацию простого REST-сервера с использованием нескольких различных подходов. В результате эти подходы можно будет сравнить друг с другом, можно будет понять их относительные преимущества друг перед другом.

Первый вопрос разработчиков, которые только начинают применять Go, часто выглядит так: Какой фреймворк стоит использовать для решения задачи X. И хотя это совершенно нормальный вопрос, если задавать его, имея в виду веб-приложения и серверы, написанные на многих других языках, в случае с Go при ответе на этот вопрос нужно принять во внимание множество тонкостей. Существуют серьёзные аргументы как за, так и против использования фреймворков в Go-проектах. Я, работая над статьями из этой серии, вижу своей целью объективное разностороннее исследование этого вопроса.

Задача


Для начала хочу сказать, что тут я исхожу из предположения о том, что читателю знакомо понятие REST-сервер. Если вам нужно освежить знания взгляните на этот хороший материал (но есть и много других подобных статей). Дальше я буду считать, что вы поймёте, что я имею в виду, когда я буду использовать понятия путь, HTTP-заголовок, код ответа и прочие подобные.

В нашем случае сервер представляет собой простую бэкенд-систему для приложения, реализующего функционал управления задачами (вроде Google Keep, Todoist и прочих подобных). Сервер предоставляет клиентам следующий REST API:

POST  /task/       : создаёт задачу и возвращает её IDGET  /task/<taskid>   : возвращает одну задачу по её IDGET  /task/       : возвращает все задачиDELETE /task/<taskid>   : удаляет задачу по IDGET  /tag/<tagname>   : возвращает список задач с заданным тегомGET  /due/<yy>/<mm>/<dd> : возвращает список задач, запланированных на указанную дату

Обратите внимание на то, что этот API создан специально для нашего примера. В следующих материалах этой серии статей мы поговорим о более структурированном и стандартизированном подходе к проектированию API.

Наш сервер поддерживает GET-, POST- и DELETE-запросы, некоторые из них с возможностью использования нескольких путей. То, что в описании API приведено в угловых скобках (<...>), обозначает параметры, которые клиент предоставляет серверу в виде части запроса. Например, запрос GET /task/42 направлен на получение с сервера задачи с ID 42. ID это уникальные идентификаторы задач.

Для кодирования данных используется формат JSON. При выполнении запроса POST /task/ клиент отправляет серверу JSON-представление задачи, которую нужно создать. И, аналогично, в ответах на те запросы, в описании которых сказано, что они что-то возвращают, содержатся JSON-данные. В частности, они размещаются в теле HTTP-ответов.

Код


Далее мы будем заниматься поэтапным написанием кода сервера на Go. Его полный вариант можно найти здесь. Это самодостаточный Go-модуль, в котором не используются зависимости. После клонирования или копирования директории проекта на компьютер сервер можно тут же, ничего дополнительно не устанавливая, запустить:

$ SERVERPORT=4112 go run .

Обратите внимание на то, что в качестве SERVERPORT можно использовать любой порт, который будет прослушивать локальный сервер в ожидании подключений. После того, как сервер будет запущен, с ним, используя отдельное окно терминала, можно поработать, воспользовавшись, например, утилитой curl. Взаимодействовать с ним можно и с помощью каких-нибудь других подобных программ. Примеры команд, использующихся для отправки запросов к серверу, можно найти в этом скрипте. В директории, содержащей этот скрипт, есть средства для автоматизированного тестирования сервера.

Модель


Начнём с обсуждения модели (или уровня данных) для нашего сервера. Найти её можно в пакете taskstore (internal/taskstore в директории проекта). Это простая абстракция, представляющая базу данных, в которой хранятся задачи. Вот её API:

func New() *TaskStore// CreateTask создаёт новую задачу в хранилище.func (ts *TaskStore) CreateTask(text string, tags []string, due time.Time) int// GetTask получает задачу из хранилища по ID. Если ID не существует -// будет возвращена ошибка.func (ts *TaskStore) GetTask(id int) (Task, error)// DeleteTask удаляет задачу с заданным ID. Если ID не существует -// будет возвращена ошибка.func (ts *TaskStore) DeleteTask(id int) error// DeleteAllTasks удаляет из хранилища все задачи.func (ts *TaskStore) DeleteAllTasks() error// GetAllTasks возвращает из хранилища все задачи в произвольном порядке.func (ts *TaskStore) GetAllTasks() []Task// GetTasksByTag возвращает, в произвольном порядке, все задачи// с заданным тегом.func (ts *TaskStore) GetTasksByTag(tag string) []Task// GetTasksByDueDate возвращает, в произвольном порядке, все задачи, которые// запланированы на указанную дату.func (ts *TaskStore) GetTasksByDueDate(year int, month time.Month, day int) []Task

Вот объявление типа Task:

type Task struct {Id  int    `json:"id"`Text string  `json:"text"`Tags []string `json:"tags"`Due time.Time `json:"due"`}

В пакете taskstore этот API реализован с использованием простого словаря map[int]Task, данные при этом хранятся в памяти. Но несложно представить себе реализацию этого API, основанную на базе данных. В реальном приложении TaskStore, вероятнее всего, будет интерфейсом, реализовать который могут разные бэкенды. Но для нашего простого примера достаточно и такого API. Если вы хотите поупражняться реализуйте TaskStore с использованием чего-то вроде MongoDB.

Подготовка сервера к работе


Функция main нашего сервера устроена довольно просто:

func main() {mux := http.NewServeMux()server := NewTaskServer()mux.HandleFunc("/task/", server.taskHandler)mux.HandleFunc("/tag/", server.tagHandler)mux.HandleFunc("/due/", server.dueHandler)log.Fatal(http.ListenAndServe("localhost:"+os.Getenv("SERVERPORT"), mux))}

Уделим немного времени команде NewTaskServer, а потом поговорим о маршрутизаторе и об обработчиках путей.

NewTaskServer это конструктор для нашего сервера, имеющего тип taskServer. Сервер включает в себя TaskStore, что безопасно с точки зрения конкурентного доступа к данным.

type taskServer struct {store *taskstore.TaskStore}func NewTaskServer() *taskServer {store := taskstore.New()return &taskServer{store: store}}

Маршрутизация и обработчики путей


Теперь вернёмся к маршрутизации. Тут используются стандартный HTTP-мультиплексор, входящий в состав пакета net/http:

mux.HandleFunc("/task/", server.taskHandler)mux.HandleFunc("/tag/", server.tagHandler)mux.HandleFunc("/due/", server.dueHandler)

Стандартный мультиплексор обладает достаточно скромными возможностями. В этом и его сила, и его слабость. Его сильная сторона заключается в том, что с ним очень легко разобраться, так как в его работе нет ничего сложного. А слабость стандартного мультиплексора выражается в том, что иногда его применение делает решение задачи сопоставления запросов с имеющимися в системе путями достаточно утомительным. То, что, по логике вещей, хорошо было бы расположить в каком-то одном месте, приходится размещать в разных местах. Скоро мы поговорим об этом подробнее.

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

Изучим обработчик путей taskHandler:

func (ts *taskServer) taskHandler(w http.ResponseWriter, req *http.Request) {if req.URL.Path == "/task/" {// Запрос направлен к "/task/", без идущего в конце ID.if req.Method == http.MethodPost {ts.createTaskHandler(w, req)} else if req.Method == http.MethodGet {ts.getAllTasksHandler(w, req)} else if req.Method == http.MethodDelete {ts.deleteAllTasksHandler(w, req)} else {http.Error(w, fmt.Sprintf("expect method GET, DELETE or POST at /task/, got %v", req.Method), http.StatusMethodNotAllowed)return}

Мы начинаем работу с проверки на точное совпадение пути с /task/ (это означает, что в конце нет <taskid>). Тут нам нужно понять то, какой HTTP-метод используется, и вызвать соответствующий метод сервера. Большинство обработчиков путей это достаточно простые обёртки для API TaskStore. Посмотрим на один из таких обработчиков:

func (ts *taskServer) getAllTasksHandler(w http.ResponseWriter, req *http.Request) {log.Printf("handling get all tasks at %s\n", req.URL.Path)allTasks := ts.store.GetAllTasks()js, err := json.Marshal(allTasks)if err != nil {http.Error(w, err.Error(), http.StatusInternalServerError)return}w.Header().Set("Content-Type", "application/json")w.Write(js)}

Он решает две основные задачи:

  1. Получает данные из модели (TaskStore).
  2. Формирует HTTP-ответ для клиента.

Обе эти задачи достаточно просты и понятны, но если исследовать код других обработчиков путей, можно обратить внимание на то, что вторая задача имеет свойство повторяться она заключается в маршалинге JSON-данных, в подготовке корректного HTTP-заголовка ответа и в выполнении других подобных действий. Мы ещё раз поднимем этот вопрос позже.

Вернёмся теперь к taskHandler. Пока мы видели только то, как он обрабатывает запросы, в которых имеется точное совпадение с путём /task/. А как насчёт пути /task/<taskid>? Именно тут в дело вступает вторая часть функции:

} else {// В запросе есть ID, выглядит он как "/task/<id>".path := strings.Trim(req.URL.Path, "/")pathParts := strings.Split(path, "/")if len(pathParts) < 2 {http.Error(w, "expect /task/<id> in task handler", http.StatusBadRequest)return}id, err := strconv.Atoi(pathParts[1])if err != nil {http.Error(w, err.Error(), http.StatusBadRequest)return}if req.Method == http.MethodDelete {ts.deleteTaskHandler(w, req, int(id))} else if req.Method == http.MethodGet {ts.getTaskHandler(w, req, int(id))} else {http.Error(w, fmt.Sprintf("expect method GET or DELETE at /task/<id>, got %v", req.Method), http.StatusMethodNotAllowed)return}}

Когда запрос не в точности соответствует пути /task/, мы ожидаем, что за косой чертой будет идти числовой ID задачи. Вышеприведённый код анализирует этот ID и вызывает соответствующий обработчик (основываясь на методе HTTP-запроса).

Остальной код более или менее похож на тот, что мы уже рассмотрели, понять его должно быть несложно.

Улучшение сервера


Теперь, когда у нас есть базовая работающая версия сервера, пришло время подумать о возможных проблемах, которые могут с ним возникнуть, и о том, как его улучшить.

Одной из используемых нами программных конструкций, которая, очевидно, нуждается в улучшении, и о которой мы уже говорили, является повторяющийся код подготовки JSON-данных при формировании HTTP-ответов. Я создал отдельную версию сервера, stdlib-factorjson, в которой эта проблема решена. Я выделил эту реализацию сервера в отдельную папку для того чтобы её было легче сравнить с первоначальным кодом сервера и проанализировать изменения. Главное новшество этого кода представлено следующей функцией:

// renderJSON преобразует 'v' в формат JSON и записывает результат, в виде ответа, в w.func renderJSON(w http.ResponseWriter, v interface{}) {js, err := json.Marshal(v)if err != nil {http.Error(w, err.Error(), http.StatusInternalServerError)return}w.Header().Set("Content-Type", "application/json")w.Write(js)}

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

func (ts *taskServer) getAllTasksHandler(w http.ResponseWriter, req *http.Request) {log.Printf("handling get all tasks at %s\n", req.URL.Path)allTasks := ts.store.GetAllTasks()renderJSON(w, allTasks)}

Более фундаментальное улучшение заключалось бы в том, чтобы сделать код сопоставления запросов и путей чище, и в том, чтобы, по возможности, собрать этот код в одном месте. Хотя текущий подход по сопоставлению запросов и путей упрощает отладку, соответствующий код трудно понять с первого взгляда, так как он разбросан по нескольким функциям. Например, предположим, что мы пытаемся разобраться с тем, как обрабатывается запрос DELETE, который направлен к /task/<taskid>. Для этого выполняются следующие действия:

  1. Во-первых мы находим в main мультиплексор и узнаём, что корневой путь /task/ обрабатывается в taskHandler.
  2. Далее, в taskHandler, нам надо найти выражение else, которое отвечает за обработку путей, не точно совпадающих с /task/. Там нам надо прочитать код преобразования <taskid> в целое число.
  3. И наконец мы смотрим на выражение if, в котором перечислены различные методы, применяемые при обработке запросов, соответствующих этому пути, и выясняем, что метод DELETE обрабатывается в deleteTaskHandler.

Можно поместить весь этот код в одно место. Так работать с ним будет гораздо проще и удобнее. Именно на решение этой задачи направлены HTTP-маршрутизаторы сторонних разработчиков. О них мы поговорим во второй части этой серии статей.

Это первая часть из серии статей, посвящённой разработке серверов на Go. Посмотреть список статей можно в начале оригинала этого материала.


Подробнее..

Перевод Разработка REST-серверов на Go. Часть 2 применение маршрутизатора gorillamux

06.06.2021 14:21:06 | Автор: admin
Перед вами второй материал из серии статей, посвящённой разработке REST-серверов на Go. В первом материале этой серии мы создали простой сервер, пользуясь стандартными средствами Go, а после этого отрефакторили код формирования JSON-данных, вынеся его во вспомогательную функцию. Это позволило нам выйти на достаточно компактный код обработчиков маршрутов.

Там мы говорили об одной проблеме нашего сервера, которая заключается в том, что логика маршрутизации разбросана по нескольким местам нашей программы.



Это проблема, с которой сталкиваются все, кто пишет HTTP-сервера, не используя зависимости. Если только сервер, принимая во внимание систему его маршрутов, не является до крайности минималистичной конструкцией (например это некоторые специализированные серверы, имеющие лишь один-два маршрута), то оказывается, что размеры и сложность организации кода маршрутизатора это нечто такое, на что очень быстро обращают внимание опытные программисты.

Улучшенная система маршрутизации


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

Перед переходом к практическому примеру вспомним о том, как устроен API нашего сервера:

POST  /task/       : создаёт задачу и возвращает её IDGET  /task/<taskid>   : возвращает одну задачу по её IDGET  /task/       : возвращает все задачиDELETE /task/<taskid>   : удаляет задачу по IDGET  /tag/<tagname>   : возвращает список задач с заданным тегомGET  /due/<yy>/<mm>/<dd> : возвращает список задач, запланированных на указанную дату

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

  1. Можно создать механизм, позволяющий задавать отдельные обработчики для разных методов одного и того же маршрута. Например запрос POST /task/ должен обрабатываться одним обработчиком, а запрос GET /task/ другим.
  2. Можно сделать так, чтобы обработчик маршрута выбирался бы на основе более глубокого, чем сейчас, анализа запросов. То есть, например, у нас при таком подходе должна быть возможность указать, что один обработчик обрабатывает запрос к /task/, а другой обработчик обрабатывает запрос к /task/<taskid> с числовым ID.
  3. При этом система обработки маршрутов должна просто извлекать числовой ID из /task/<taskid> и передавать его обработчику каким-нибудь удобным для нас способом.

Написание собственного маршрутизатора на Go это очень просто. Это так из-за того, что организовывать работу с HTTP-обработчиками можно, используя компоновку. Но тут я не стану потакать своему желанию написать всё самому. Вместо этого предлагаю поговорить о том, как организовать систему маршрутизации с использованием одного из самых популярных маршрутизаторов, который называется gorilla/mux.

Сервер приложения для управления задачами, использующий gorilla/mux


Пакет gorilla/mux представляет собой один из самых старых и самых популярных HTTP-маршрутизаторов для Go. Слово mux, в соответствии с документацией к пакету, расшифровывается как HTTP request multiplexer (Мультиплексор HTTP-запросов) (такое же значение mux имеет и в стандартной библиотеке).

Так как это пакет, нацеленный на решение единственной узкоспециализированной задачи, пользоваться им очень просто. Вариант нашего сервера, в котором для маршрутизации используется gorilla/mux, можно найти здесь. Вот код определения маршрутов:

router := mux.NewRouter()router.StrictSlash(true)server := NewTaskServer()router.HandleFunc("/task/", server.createTaskHandler).Methods("POST")router.HandleFunc("/task/", server.getAllTasksHandler).Methods("GET")router.HandleFunc("/task/", server.deleteAllTasksHandler).Methods("DELETE")router.HandleFunc("/task/{id:[0-9]+}/", server.getTaskHandler).Methods("GET")router.HandleFunc("/task/{id:[0-9]+}/", server.deleteTaskHandler).Methods("DELETE")router.HandleFunc("/tag/{tag}/", server.tagHandler).Methods("GET")router.HandleFunc("/due/{year:[0-9]+}/{month:[0-9]+}/{day:[0-9]+}/", server.dueHandler).Methods("GET")

Обратите внимание на то, что одни только эти определения тут же закрывают первые два пункта вышеприведённого списка задач, которые надо решить для повышения удобства работы с маршрутами. Благодаря тому, что в описании маршрутов используются вызовы Methods, мы можем с лёгкостью назначать в одном маршруте разные методы для разных обработчиков. Поиск совпадений с шаблонами (с использованием регулярных выражений) в путях позволяет нам легко различать /task/ и /task/<taskid> на самом верхнем уровне описания маршрутов.

Для того чтобы разобраться с задачей, которая имеется в третьем пункте нашего списка, посмотрим на использование getTaskHandler:

func (ts *taskServer) getTaskHandler(w http.ResponseWriter, req *http.Request) {log.Printf("handling get task at %s\n", req.URL.Path)// Тут и в других местах мы не проверяем ошибку Atoi, так как маршрутизатор// принимает лишь данные, проверенные регулярным выражением [0-9]+.id, _ := strconv.Atoi(mux.Vars(req)["id"])ts.Lock()task, err := ts.store.GetTask(id)ts.Unlock()if err != nil {http.Error(w, err.Error(), http.StatusNotFound)return}renderJSON(w, task)}

В определении маршрутов маршрут /task/{id:[0-9]+}/ описывает регулярное выражение, используемое для разбора пути и назначает идентификатор переменной id. К этой переменной можно обратиться, вызвав функцию mux.Vars с передачей ей req (эту переменную gorilla/mux хранит в контексте каждого запроса, а mux.Vars представляет собой удобную вспомогательную функцию для работы с ней).

Сравнение различных подходов к организации маршрутизации


Вот как выглядит последовательность чтения кода, применяемая в исходном варианте сервера тем, кто хочет разобраться в том, как обрабатывается маршрут GET /task/<taskid>.


А вот что нужно прочитать тому, кто хочет понять код, в котором применяется gorilla/mux:


При использовании gorilla/mux придётся не только меньше прыгать по тексту программы. Тут, кроме того, читать придётся гораздо меньший объём кода. По моему скромному мнению это очень хорошо с точки зрения улучшения читабельности кода. Описание путей при использовании gorilla/mux это простая задача, при решении которой нужно написать лишь небольшой объём кода. И тому, кто читает этот код, сразу понятно то, как этот код работает. Ещё одно преимущество такого подхода заключается в том, что все маршруты можно увидеть буквально раз взглянув на код, расположенный в одном месте. И, на самом деле, код настройки маршрутов выглядит теперь очень похожим на описание нашего REST API, выполненное в произвольной форме.

Мне нравится пользоваться такими пакетами, как gorilla/mux, из-за того, что подобные пакеты представляют собой узкоспециализированные инструменты. Они решают одну единственную задачу и решают её хорошо. Они не забираются в каждый уголок программного кода проекта, а значит, их, при необходимости можно легко убрать или заменить чем-то другим. Если вы посмотрите полный код того варианта сервера, о котором мы говорим в этой статье, то сможете увидеть, что область использования механизмов gorilla/mux ограничена несколькими строками кода. Если, по мере развития проекта, в пакете gorilla/mux будет обнаружено какое-то ограничение, несовместимое с особенностями этого проекта, задача замены gorilla/mux на другой маршрутизатор стороннего разработчика (или на собственный маршрутизатор) должна решаться достаточно быстро и просто.

Какой маршрутизатор вы использовали бы при разработке REST-сервера на Go?


Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru