Интерфейс прикладного программирования (API) — это программный шлюз, который позволяет различным программным компонентам взаимодействовать друг с другом. API-интерфейсы помогают раскрыть возможности приложения внешнему миру, обеспечивая программный доступ к их данным.
Рассмотрим случай приложения, предоставляющего биржевую или погодную информацию. Создание и предоставление API для таких систем позволит другим программно извлекать данные, предлагаемые этими системами, например, прогнозы погоды для указанного местоположения. Тот же пример можно распространить на различные другие варианты использования. Широко используемые системы, такие как YouTube, Reddit, Google Maps и другие, предоставляют API-интерфейсы, позволяющие авторизованным клиентам получать доступ к ресурсам, предоставляемым этими системами.
С годами API развивались и приобретали некоторые функции, делающие их еще более эффективными и полезными. Современные API соответствуют стандартам HTTP, что делает их удобными для разработчиков и простыми в использовании.
Передача репрезентативного состояния (REST) — это стиль веб-архитектуры, предложенный в 2000 году для решения проблемы экспоненциальной масштабируемости сети. REST требует, чтобы сервер выполнял запрос клиента, предоставляя представление ресурса, которое содержит ссылки для изменения состояния системы и получения представления новых ресурсов. Архитектурный стиль REST основан на ограничениях, таких как взаимодействие клиент-сервер, возможность кэширования, безгражданство и другие, и составляет основу современного Интернета.
API, основанные на архитектуре REST, удачно называются RESTful API. Они обычно используют HTTP в качестве базового протокола с методами HTTP (GET, POST, PUT, DELETE) для управления ресурсами.
Создание RESTful API в Go
В Educative у нас есть разнообразный каталог интерактивных и практических курсов по API, и тема этого блога — как создать RESTful API.
Мы будем использовать Go в качестве языка программирования. Однако концепции, представленные в этом блоге, можно применять и к другим языкам программирования.
Код намеренно представлен в упрощенном виде, чтобы помочь учащимся легко понять концепции. Мы не обрабатывали ошибки и не пытались оптимизировать код для облегчения чтения и понимания.
Мы рассмотрим пример создания API для управления курсами. Мы будем следовать пошаговому подходу, и к концу этого блога у нас будет полностью функциональный API в Go. В видео ниже показано, как можно запустить API на платформе Educative, следуя пошаговому подходу, и к концу этого блога у нас будет полностью функциональный API в Go. Видео ниже показывает, как можно запустить API на платформе Educative. форма
Как показано на видео выше, мы можем запускать сервер и делать GETзапросы с помощью браузера. А как насчет других HTTPзапросов, таких как POSTи DELETE? Мы можем использовать один из многофункциональных виджетов, доступных на платформе Educative, виджет API. Его можно использовать для выполнения запросов API с различными HTTPметодами и параметрами. Мы представим демонстрацию в конце этого блога.
Шаг 1. Конечные точки API
Прежде чем перейти непосредственно к коду, было бы полезно ознакомиться с тем, что мы пытаемся создать, и с тем, как структурировать наш API. Мы создаем каталог курсов в памяти и должны иметь возможность выполнять операции CRUD с курсами. Нам нужно сопоставить эти операции CRUD с конечными точками API при разработке API.
В двунаправленной связи конечная точка представляет собой один конец связи, по существу определяя, как получить доступ к API с помощью URL-адреса. Клиент может отправлять запросы конечной точке для получения ресурсов или выполнения операций. На приведенном выше рисунке показано, что конечные точки состоят из двух частей: базового URL-адреса и имени конечной точки.
Присвоение имен конечным точкам важно, и соблюдение рекомендаций по именованию конечных точек поможет другим использовать API. Как правило, имена должны быть существительными (в основном во множественном числе), основанными на содержимом ресурса, а не глаголом, определяющим действие, выполняемое с ресурсом. Для нашего API одно из возможных соглашений об именах выглядит следующим образом:
HTTP- запрос | Имя конечной точки | Описание |
GET | /courses | Возвращает список всех курсов. |
GET | /courses/:id | Извлекает информацию о конкретном курсе, указанном в параметре ID. |
POST | /courses | Добавляет новый курс в коллекцию. Мы можем заметить, что имя конечной точки остается прежним. |
PUT | /courses/:id | Обновляет курс с указанным идентификатором. Мы также можем использовать PATCH для частичного обновления курса. |
DELETE | /courses/:id | Удаляет определенный курс, предоставляя его идентификатор. |
Мы назвали нашу конечную точку как /coursesсуществительное во множественном числе, основанное на содержимом ресурса, доступного в конечной точке. Мы также использовали косую черту /для представления иерархии и /courses/:idпредставления определенного курса вместо коллекции. Кроме того, мы используем HTTPметоды для указания действий, выполняемых над ресурсом. Наконец, мы также можем заметить, что некоторые действия выполняются с отдельным ресурсом, а не со всей коллекцией.
Веб-приложение в Go
Прежде чем двигаться дальше, давайте кратко вспомним о написании веб-приложений на Go. Go предоставляет net/httpпакет. Приведенный ниже код предназначен для веб-приложения «hello-world» в Go.
package mainimport («fmt»«net/http»)//the handler functionfunc getHandler(w http.ResponseWriter, r *http.Request) {fmt.Fprintf(w, «Hello from Educative!»)}func main() {http.HandleFunc(«/», getHandler)http.ListenAndServe(«:8080», nil)}
Поскольку мы создаем отдельную программу на Go, нам нужно поместить наш код в package main. Внутри пакета mainначнем с mainфункции, точки входа в наш код. Сначала функция определяет функцию-дескриптор с помощью http.HandleFunc, которая указывает, что все запросы должны обрабатываться функцией с именем getHandler. mainЗатем функция использует для http.ListenAndServeзапуска HTTPсервера, прослушивающего указанный порт, 8080.
Это чисто, просто и мощно. С этими строками наш многопоточный HTTP сервер запущен и работает, прослушивая HTTPзапросы и направляя их нашему обработчику. Далее, давайте сосредоточимся на нашей функции дескриптора, которая определена непосредственно перед основной функцией. Сигнатура функции показывает, что она принимает http.ResponseWriterи http.Requestв качестве аргументов. Их легче понять. Переменная http.Requestпредставляет запрос клиента во время записи для http.ResponseWriterотправки HTTPответа клиенту.
Внутри функции мы просто пишем строку клиенту.
Шаг 2: GETвсе курсы
Теперь мы готовы реализовать нашу первую конечную точку. Сначала нам нужна структура данных для хранения информации о курсах. В нашем случае на данном этапе будет достаточно структуры (с полями IDи ). TitleКонечно, при необходимости мы можем добавить в структуру другие поля.
//other fields can be addedtype course struct {ID string `json:»id»`Title string `json:»title»`}//courses is a slice of course typevar courses = []course{{«100»,»Grokking Modern System Design «},{«101″,»CloudLab: WebSockets-based Chat Application using API Gateway»},}
Структура данных для хранения информации о курсах
Такие теги, как json:»id«помогают нам сериализовать содержимое структуры в JSON, используя имена полей в нижнем регистре, что является распространенным стилем для JSON. Мы также создаем экземпляр слайса courses, содержащего несколько примеров курсов.
Итак, у нас есть контент, готовый к использованию, и нам нужен способ доставки его клиентам, запрашивающим его через HTTP. Мы можем обновить наш обработчик из приведенного выше кода «hello-world», чтобы отправлять фрагмент coursesвместо строки.
//we are now sending courses instead of a stringfunc getHandler(w http.ResponseWriter, r *http.Request) {fmt.Fprintf(w, «<h1>%s</h1>», courses)}func main() {http.HandleFunc(«/courses/», getHandler)log.Fatal(http.ListenAndServe(«:8080», nil))}
Обновлен обработчик для отправки фрагмента курсов
Это было очень просто, но, к сожалению, мы все еще не там. Хотя наш код работает как надо, ответ предоставляется клиенту в формате HTML, что удобно для рендеринга в браузере, но не для потребления. API-интерфейсы RESTful обычно предоставляют ответ в формате JSON, и мы можем легко маршалировать данные с помощью encoding/jsonпакета. Обновленная функция выглядит следующим образом.
//Marshaling data and setting content-type headerfunc getHandler(w http.ResponseWriter, r *http.Request) {jsonData, _ := json.Marshal(courses)w.Header().Set(«Content-Type», «application/json»)fmt.Fprintf(w, «%s», jsonData)}
Маршалинг данных с помощью пакета encoding/json
На этом мы закончили с нашей первой GETконечной точкой. Если мы запустим сервер и получим доступ к /coursesконечной точке на порту 8080из нашего браузера, мы увидим ответ JSON. Мы также можем заметить, что возвращаемый вывод содержит имена полей в нижнем регистре, которые указаны как теги с определением структуры.
Представляем маршрутизатор
Хотя мы можем продолжить создание нашего API с помощью net/httpпакета, обработка связанных случаев может оказаться сложной. Например, нам нужно использовать одну и ту же конечную точку /coursesс запросами GETи POST. Согласно нашему коду выше, для обоих запросов будет вызываться один и тот же обработчик. Один из способов обойти это может заключаться в том, что внутри обработчика мы используем что-то вроде if r.Method == http.MethodGet {обработки разных методов. Чтобы упростить разработку, мы можем использовать сторонний пакет, такой как github.com/gorilla/mux. Затем с помощью gorillaмы можем обновить код следующим образом:
var mux *http.ServeMux = http.NewServeMux()// register handlers to muxmux.HandleFunc(«/courses», getHandler).Methods(«GET»)mux.HandleFunc(«/courses», postHandler).Methods(«POST»)
Использование пакета github.com/gorilla/mux
Пакет gorilla/muxшироко используется на протяжении многих лет. Однако github.com/gorilla/muxрепозиторий был заархивирован владельцем в декабре 2022 года. Поэтому нам нужна (лучшая) альтернатива, и мы ginможем нам помочь. Ginпредставляет собой веб-фреймворк на основе Go для создания веб-приложений; он также упрощает и улучшает многие связанные аспекты.
В оставшейся части этого блога мы отойдем от стандартного net/httpпакета и будем использовать gin. Однако мы продолжим сравнение, ginчтобы net/httpподчеркнуть, насколько это упрощает процесс разработки.
Шаг 2а: GETвсе курсы с использованиемgin
Прежде чем двигаться дальше, давайте обсудим, как мы можем добиться той же функциональности, что и на шаге 2, используя файлы gin. Обновленный код, за исключением структуры данных и coursesсреза для краткости, выглядит следующим образом:
package mainimport («net/http»«github.com/gin-gonic/gin»)/* removed struct declaration and courses instantiation for brevity */func main() {grouter := gin.Default()grouter.GET(«/courses», getcourses)grouter.Run() //listens on 8080, by default}func getcourses(c *gin.Context) {c.IndentedJSON(http.StatusOK, courses)}
ПОЛУЧИТЬ все курсы, используя джин
Мы будем восхищаться возможностями по ginмере продвижения и построения других маршрутов. Однако с самого начала мы можем заметить, насколько чистый наш код по сравнению с более ранней версией. В основной функции выше мы инициализируем маршрутизатор и указываем функцию-обработчик. Функция-обработчик использует переменную-указатель типа gin.Context, представляющего сведения о запросе и другую связанную информацию.
Вызов c.IndentedJSON(http.StatusOK, courses)неявно сериализует нашу структуру в JSON и записывает версию с отступом обратно клиенту вместе с кодом состояния 200 OK. В качестве альтернативы мы можем использовать c.JSONдля более компактного ответа JSON. Теперь мы готовы реализовать и другие конечные точки.
Шаг 3: GETдетали конкретного курса
Наша функция-обработчик getcoursesв настоящее время возвращает клиенту полный список курсов. Что, если мы хотим получить информацию о конкретном курсе, указав его идентификатор? Хотя у нас есть некоторая идея, основанная на том, что было сказано выше, о том, как это можно сделать, давайте сначала вернемся к части того, что мы планировали при определении конечных точек.
HTTP- запрос | Имя конечной точки | Описание |
GET | /courses/:id | Параметр ID можно использовать для получения информации о конкретном курсе. |
Имя конечной точки /courses/:idвключает idпараметр. URL-адрес для доступа к этой конечной точке будет отличаться от того, который используется для доступа к /coursesконечной точке. Как вы, наверное, догадались, сначала нам нужно обновить нашу маршрутизацию в функции main и ввести новый маршрут для этого случая.
func main() {grouter := gin.Default()grouter.GET(«/courses», getcourses)//we have added a new route belowgrouter.GET(«/courses/:id», getSpecificCourse)grouter.Run() //listens on 8080, by default}
Новое определение маршрута в основной функции
Из приведенного выше кода видно, что мы добавили новое определение маршрута. Как только пользователь получает доступ к конечной точке для определенного курса, он перенаправляется к getSpecificCourseфункции. Здесь мы перебираем coursesструктуру, используя rangeключевое слово, и отправляем ответ, если курс IDсоответствует курсу, отправленному в запросе, как показано ниже:
func getSpecificCourse(c *gin.Context) {//ID passed as a path parameter can then be retrievedrequestedId := c.Param(«id»)for _, course := range courses {if course.ID == requestedId {c.IndentedJSON(http.StatusOK, course)return}}}
ПОЛУЧИТЬ подробную информацию о конкретном курсе
Обратите внимание, что мы используем специальный синтаксис для указания этого маршрута, /courses/:idи любой idпереданный в качестве параметра пути можно получить с помощью c.Param(«id«).
Шаг 4. Обработка POSTдобавления нового курса
Чтобы добавить новый курс в нашу коллекцию, мы будем использовать ту же /coursesконечную точку. На этот раз мы будем использовать POSTметод с этой конечной точкой. Давайте на минутку разберемся, как это можно сделать. Это не сложно, и вы, должно быть, уже разобрались с процессом, которому нужно следовать. Первым шагом является добавление нового определения маршрута в mainфункцию:
func main() {grouter := gin.Default()grouter.GET(«/courses», getcourses)grouter.GET(«/courses/:id», getSpecificCourse)//we have added a new route belowgrouter.POST(«/courses», addCourse)grouter.Run()}
Новое определение маршрута в основной функции
В приведенном выше коде мы обновляем нашу mainфункцию для обработки POSTзапросов. Мы можем заметить добавление метода POSTс обработчиком маршрута. Далее нам нужно обработать запрос, как только он достигнет функции обработчика, addCourse.
Функция обработчика, опять же, относительно проста. Основная часть — это вызов функции BindJSON, которая десериализует информацию о курсе, отправленную с запросом, в формате JSON в переменную структуры. Конечно, процесс может завершиться ошибкой из-за неправильно сформированных запросов; мы уже рассмотрели и этот случай. Полный код функции addCourse показан ниже:
func addCourse(c *gin.Context) {var courseToAdd courseerr := c.BindJSON(&courseToAdd)if err != nil {c.IndentedJSON(http.StatusBadRequest, gin.H{«message»: «malformed request!»})}courses = append(courses, courseToAdd)c.IndentedJSON(http.StatusOK, courses)}
Обработка POST для добавления нового курса
Прежде чем мы двинемся дальше, подумайте о том, как это можно сделать с помощью стандартного net/httpпакета и как это ginупрощает и облегчает процесс.
Шаг 5: PUTобновленная версия
Мы можем использовать HTTP PUTметод для обновления ресурса (и PATCHдля его частичного обновления). Как это может быть сделано? См. приведенный ниже код, чтобы понять, что нужно сделать. Если вы все еще не уверены, краткое описание следующей задачи поможет вам полностью ее понять.
func updateCourse(c *gin.Context) {courseIdtoUpdate := c.Param(«id»)var updatedCourse coursec.BindJSON(&updatedCourse)for index, course := range courses {if course.ID == courseIdtoUpdate {courses[index] = updatedCoursec.IndentedJSON(http.StatusOK, courses)return}}c.IndentedJSON(http.StatusNotFound, gin.H{«message»: «course not found!»})}
ПОСТАВЬТЕ обновленную версию
Шаг 6: DELETEконкретный курс
Давайте завершим этот блог обсуждением того, как удалить курс. Использование gin делает это простым. Мы указываем новый обработчик в DELETEкачестве HTTPметода и используем idс динамическим маршрутом. Затем мы анализируем его внутри обработчика и удаляем из среза.
Вот полный код:
package main//we are using net/http and ginimport («net/http»«github.com/gin-gonic/gin»)//data structure to hold course informationtype course struct {ID string `json:»id»`Title string `json:»title»`}//initializing some coursesvar courses = []course{{«100», «Grokking Modern System Design «},{«101», «CloudLab: WebSockets-based Chat Application using API Gateway»},}//main function contains routes for performing different operationsfunc main() {grouter := gin.Default()grouter.GET(«/courses», getcourses)grouter.GET(«/courses/:id», getSpecificCourse)grouter.POST(«/courses», addCourse)grouter.PUT(«/courses/:id», updateCourse)grouter.DELETE(«/courses/:id», deleteCourse)grouter.Run()}//GET all courses using ginfunc getcourses(c *gin.Context) {c.IndentedJSON(http.StatusOK, courses)}//GET details of a specific coursefunc getSpecificCourse(c *gin.Context) {requestedId := c.Param(«id»)for _, course := range courses {if course.ID == requestedId {c.IndentedJSON(http.StatusOK, course)return}}}//Handling POST to add a new coursefunc addCourse(c *gin.Context) {var courseToAdd courseerr := c.BindJSON(&courseToAdd)if err != nil {c.IndentedJSON(http.StatusBadRequest, gin.H{«message»: «malformed request!»})}courses = append(courses, courseToAdd)c.IndentedJSON(http.StatusOK, courses)}func inefficientRemoval(srcSlice []course, index int) []course {return append(srcSlice[:index], srcSlice[index+1:]…)}//DELETE a specific coursefunc deleteCourse(c *gin.Context) {requestedId := c.Param(«id»)courseID := -1for index, course := range courses {if course.ID == requestedId {courseID = indexbreak}}if courseID != -1 {courses = inefficientRemoval(courses, courseID)c.IndentedJSON(http.StatusOK, courses)return}c.IndentedJSON(http.StatusNotFound, gin.H{«message»: «course not found!»})}//PUT an updated versionfunc updateCourse(c *gin.Context) {courseIdtoUpdate := c.Param(«id»)var updatedCourse coursec.BindJSON(&updatedCourse)for index, course := range courses {if course.ID == courseIdtoUpdate {courses[index] = updatedCoursec.IndentedJSON(http.StatusOK, courses)return}}c.IndentedJSON(http.StatusNotFound, gin.H{«message»: «course not found!»})}
Заключение
В этом блоге мы предоставили пошаговое руководство по созданию RESTful API. Мы рассмотрели пример создания API для управления курсами и использовали Go в качестве языка программирования. Однако концепции, представленные в этом блоге, могут быть применены к другим языкам программирования и сопоставлены с другими вариантами использования.
Этот блог посвящен RESTful API; однако другие стили архитектуры API, включая GraphQL и gRPC, имеют свои сильные стороны и варианты использования. Мы рекомендуем вам изучить их подробнее.