Разработать RESTFul JSON API на языке Go
RESTful API широко используется при разработке веб-проектов, в этой статье объясняется, как шаг за шагом реализовать RESTful JSON API на языке Go, а также затрагивается тема RESTful-дизайна.
Может быть, мы использовали все виды API раньше, и когда мы столкнулись с плохо разработанным API, это просто ломается. Я надеюсь, что после прочтения этой статьи я могу иметь предварительное понимание хорошо продуманного восстановленного API.
Что такое JSON API?
До JSON многие веб-сайты использовали XML для обмена данными. Если вы поработаете с XML, а потом соприкоснетесь с JSON, то, несомненно, почувствуете, насколько прекрасен мир. Здесь нет подробного введения в JSON API, если вам интересно, вы можете обратиться к нему.jsonapi.
базовый веб-сервер
По сути, службы RESTful — это в первую очередь веб-службы. Итак, мы можем сначала посмотреть, как реализован базовый веб-сервер в Go. В следующем примере реализован простой веб-сервер. На любой запрос сервер отвечает на запрошенный URL-адрес.
package main
import (
"fmt"
"html"
"log"
"net/http"
)
func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello, %q", html.EscapeString(r.URL.Path))
})
log.Fatal(http.ListenAndServe(":8080", nil))
}
Приведенный выше базовый веб-сервер использует две основные функции стандартной библиотеки Go, HandleFunc и ListenAndServe.
func HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
DefaultServeMux.HandleFunc(pattern, handler)
}
func ListenAndServe(addr string, handler Handler) error {
server := &Server{Addr: addr, Handler: handler}
return server.ListenAndServe()
}
Запустите базовый веб-сервис выше, вы можете получить к нему доступ напрямую через браузерhttp://localhost:8080 посетить.
> go run basic_server.go
добавить маршрут
Хотя стандартная библиотека включает маршрутизатор, я обнаружил, что многие люди не понимают, как он работает. Я использовал различные сторонние библиотеки маршрутизаторов в своих проектах. Наиболее примечательным является мультиплексорный маршрутизатор Gorilla Web ToolKit.
Еще одним популярным маршрутизатором является пакет httprouter от Julien Schmidt.
package main
import (
"fmt"
"html"
"log"
"net/http"
"github.com/gorilla/mux"
)
func main() {
router := mux.NewRouter().StrictSlash(true)
router.HandleFunc("/", Index)
log.Fatal(http.ListenAndServe(":8080", router))
}
func Index(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello, %q", html.EscapeString(r.URL.Path))
}
Чтобы запустить приведенный выше код, сначала получите исходный код мультиплексора, используя go get:
> go get github.com/gorilla/mux
Приведенный выше код создает базовый маршрутизатор и назначает обработчик индекса запросу «/», когда клиент запрашиваетhttp://localhost:8080/, будет выполнен обработчик Index.
Если вы достаточно внимательны, вы найдете базовый доступ к веб-сервису из предыдущегоhttp://localhost:8080/abc может нормально ответить: "Привет, "/abc"", но после добавления маршрута вы можете получить доступ толькоhttp://localhost:8080 сейчас. Причина очень проста, потому что мы добавили только парсинг "/", остальные маршруты недействительны, поэтому все они 404.
Создайте несколько основных маршрутов
Теперь, когда мы добавили маршруты, мы можем добавить больше маршрутов.
Предположим, мы хотим создать базовое приложение ToDo, поэтому наш код выглядит следующим образом:
package main
import (
"fmt"
"log"
"net/http"
"github.com/gorilla/mux"
)
func main() {
router := mux.NewRouter().StrictSlash(true)
router.HandleFunc("/", Index)
router.HandleFunc("/todos", TodoIndex)
router.HandleFunc("/todos/{todoId}", TodoShow)
log.Fatal(http.ListenAndServe(":8080", router))
}
func Index(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "Welcome!")
}
func TodoIndex(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "Todo Index!")
}
func TodoShow(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
todoId := vars["todoId"]
fmt.Fprintln(w, "Todo Show:", todoId)
}
Здесь мы добавляем еще два маршрута: todos и todos/{todoId}.
Здесь начинается проектирование RESTful API.
Обратите внимание, что в последнем маршруте мы добавили переменную с именем todoId в конец маршрута.
Это позволяет нам передавать идентификатор маршруту и иметь возможность отвечать на запрос определенной записью.
базовая модель
Теперь маршруты готовы, пришло время создать модель, которую можно использовать для отправки и получения данных. В языке Go модели могут быть реализованы с помощью структур, тогда как модели в других языках обычно реализуются с помощью классов.
package main
import (
"time"
)
type Todo struct {
Name string
Completed bool
Due time.Time
}
type Todos []Todo
Выше мы определили структуру Todo для представления элементов списка дел. Кроме того, мы также определяем тип Todos, который представляет список дел, массив или срез.
Позже вы увидите, что это может быть очень полезно.
вернуть некоторый JSON
Теперь, когда у нас есть базовая модель, мы можем смоделировать некоторые реальные реакции. Мы можем смоделировать некоторые статические списки данных для TodoIndex.
package main
import (
"encoding/json"
"fmt"
"log"
"net/http"
"github.com/gorilla/mux"
)
// ...
func TodoIndex(w http.ResponseWriter, r *http.Request) {
todos := Todos{
Todo{Name: "Write presentation"},
Todo{Name: "Host meetup"},
}
json.NewEncoder(w).Encode(todos)
}
// ...
Теперь мы создаем статический сегмент Todos для ответа на запросы клиентов. Обратите внимание, что если вы запроситеhttp://localhost:8080/todos, вы получите следующий ответ:
[
{
"Name": "Write presentation",
"Completed": false,
"Due": "0001-01-01T00:00:00Z"
},
{
"Name": "Host meetup",
"Completed": false,
"Due": "0001-01-01T00:00:00Z"
}
]
лучшая модель
Для опытных ветеранов вы, возможно, заметили проблему. Каждый ключ в JSON-ответе пишется с первой буквы.Хотя это и кажется тривиальным, не принято писать первую букву ключа в JSON-ответе с заглавной буквы. Итак, вот как решить эту проблему:
type Todo struct {
Name string `json:"name"`
Completed bool `json:"completed"`
Due time.Time `json:"due"`
}
На самом деле очень просто добавить атрибуты тега в структуру, чтобы вы могли полностью контролировать, как структура маршалируется в JSON.
сплит-код
Пока что весь наш код находится в одном файле. Кажется загроможденным, пришло время разделить код. Мы можем разделить код на следующие несколько файлов по функциям.
Мы собираемся создать следующие файлы и переместить соответствующий код в определенные файлы кода:
- main.go: файл входа в программу.
- handlers.go: обработчики, связанные с маршрутизацией.
- route.go: маршруты.
- todo.go: код, связанный с todo.
package main
import (
"encoding/json"
"fmt"
"net/http"
"github.com/gorilla/mux"
)
func Index(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "Welcome!")
}
func TodoIndex(w http.ResponseWriter, r *http.Request) {
todos := Todos{
Todo{Name: "Write presentation"},
Todo{Name: "Host meetup"},
}
if err := json.NewEncoder(w).Encode(todos); err != nil {
panic(err)
}
}
func TodoShow(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
todoId := vars["todoId"]
fmt.Fprintln(w, "Todo show:", todoId)
}
package main
import (
"net/http"
"github.com/gorilla/mux"
)
type Route struct {
Name string
Method string
Pattern string
HandlerFunc http.HandlerFunc
}
type Routes []Route
func NewRouter() *mux.Router {
router := mux.NewRouter().StrictSlash(true)
for _, route := range routes {
router.
Methods(route.Method).
Path(route.Pattern).
Name(route.Name).
Handler(route.HandlerFunc)
}
return router
}
var routes = Routes{
Route{
"Index",
"GET",
"/",
Index,
},
Route{
"TodoIndex",
"GET",
"/todos",
TodoIndex,
},
Route{
"TodoShow",
"GET",
"/todos/{todoId}",
TodoShow,
},
}
package main
import "time"
type Todo struct {
Name string `json:"name"`
Completed bool `json:"completed"`
Due time.Time `json:"due"`
}
type Todos []Todo
package main
import (
"log"
"net/http"
)
func main() {
router := NewRouter()
log.Fatal(http.ListenAndServe(":8080", router))
}
Лучшая маршрутизация
В ходе рефакторинга мы создали более функциональный файл маршрутов. Этот новый файл использует структуру, содержащую некоторую информацию о маршрутизации. Обратите внимание, что здесь мы можем указать тип запроса, например GET, POST, DELETE и т. д.
вывод веб-журнала
В разделенный файл маршрутизации я также включил неспроста. Как вы увидите позже, обработчик http можно легко украсить дополнительными функциями после разделения.
Во-первых, нам нужна возможность регистрировать веб-запросы, как это делают многие популярные веб-серверы. В стандартной библиотеке Go нет пакета веб-журналов или функций, поэтому нам нужно создать его самостоятельно.
package logger
import (
"log"
"net/http"
"time"
)
func Logger(inner http.Handler, name string) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
inner.ServeHTTP(w, r)
log.Printf(
"%s\t%s\t%s\t%s",
r.Method,
r.RequestURI,
name,
time.Since(start),
)
})
}
Выше мы определили функцию Logger, которая может оборачивать и украшать обработчик.
Это довольно стандартный идиоматический способ в Go. Фактически, это также идиоматический способ функционального программирования. Очень эффективно, нам нужно только передать обработчик в эту функцию, а затем она обернет входящий обработчик и добавит веб-журналы и трудоемкие функции статистики.
Примените декоратор Logger
Для применения модификатора Logger мы можем создать роутер, просто заворачиваем в него все наши текущие маршруты, а функцию NewRouter модифицируем следующим образом:
func NewRouter() *mux.Router {
router := mux.NewRouter().StrictSlash(true)
for _, route := range routes {
var handler http.Handler
handler = route.HandlerFunc
handler = Logger(handler, route.Name)
router.
Methods(route.Method).
Path(route.Pattern).
Name(route.Name).
Handler(handler)
}
return router
}
Теперь снова запустив нашу программу, мы увидим, что журнал выглядит так:
2014/11/19 12:41:39 GET /todos TodoIndex 148.324us
Этот файл маршрутизации сумасшедший... давайте рефакторим его
Файл маршрутов теперь немного больше, поэтому давайте разобьем его на несколько файлов:
- routes.go
- router.go
package main
import "net/http"
type Route struct {
Name string
Method string
Pattern string
HandlerFunc http.HandlerFunc
}
type Routes []Route
var routes = Routes{
Route{
"Index",
"GET",
"/",
Index,
},
Route{
"TodoIndex",
"GET",
"/todos",
TodoIndex,
},
Route{
"TodoShow",
"GET",
"/todos/{todoId}",
TodoShow,
},
}
package main
import (
"net/http"
"github.com/gorilla/mux"
)
func NewRouter() *mux.Router {
router := mux.NewRouter().StrictSlash(true)
for _, route := range routes {
var handler http.Handler
handler = route.HandlerFunc
handler = Logger(handler, route.Name)
router.
Methods(route.Method).
Path(route.Pattern).
Name(route.Name).
Handler(handler)
}
return router
}
Кроме того, взять на себя некоторую ответственность
Теперь, когда у нас есть довольно хороший шаблон, пришло время вернуться к нашим процессорам. Нам нужно немного больше ответственности. Сначала измените TodoIndex и добавьте следующие две строки кода:
func TodoIndex(w http.ResponseWriter, r *http.Request) {
todos := Todos{
Todo{Name: "Write presentation"},
Todo{Name: "Host meetup"},
}
w.Header().Set("Content-Type", "application/json; charset=UTF-8")
w.WriteHeader(http.StatusOK)
if err := json.NewEncoder(w).Encode(todos); err != nil {
panic(err)
}
}
Здесь происходят две вещи. Во-первых, мы устанавливаем тип ответа и говорим клиенту ожидать JSON. Во-вторых, мы явно устанавливаем код состояния ответа.
Сервер Go net/http попытается угадать для нас тип выходного контента (не всегда точный), но, поскольку мы уже знаем точный тип ответа, мы всегда должны устанавливать его сами.
Минуточку, а где наша база данных?
Очевидно, что если мы собираемся создать RESTful API, нам нужно какое-то место для хранения и извлечения данных. Однако это выходит за рамки данной статьи, поэтому мы просто создадим очень рудиментарную фиктивную базу данных (не потокобезопасную).
Мы создаем файл repo.go со следующим содержимым:
package main
import "fmt"
var currentId int
var todos Todos
// Give us some seed data
func init() {
RepoCreateTodo(Todo{Name: "Write presentation"})
RepoCreateTodo(Todo{Name: "Host meetup"})
}
func RepoFindTodo(id int) Todo {
for _, t := range todos {
if t.Id == id {
return t
}
}
// return empty Todo if not found
return Todo{}
}
func RepoCreateTodo(t Todo) Todo {
currentId += 1
t.Id = currentId
todos = append(todos, t)
return t
}
func RepoDestroyTodo(id int) error {
for i, t := range todos {
if t.Id == id {
todos = append(todos[:i], todos[i+1:]...)
return nil
}
}
return fmt.Errorf("Could not find Todo with id of %d to delete", id)
}
Добавить ID в Todo
Мы создали фиктивную базу данных, мы использовали и назначили идентификатор, поэтому нам нужно соответствующим образом обновить нашу структуру Todo.
package main
import "time"
type Todo struct {
Id int `json:"id"`
Name string `json:"name"`
Completed bool `json:"completed"`
Due time.Time `json:"due"`
}
type Todos []Todo
Обновите наш TodoIndex
Чтобы использовать базу данных, нам нужно получить данные в TodoIndex. Измените код следующим образом:
func TodoIndex(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=UTF-8")
w.WriteHeader(http.StatusOK)
if err := json.NewEncoder(w).Encode(todos); err != nil {
panic(err)
}
}
POST JSON
До сих пор мы только выводили JSON, теперь пришло время сохранить JSON.
Добавьте следующие маршруты в файл route.go:
Route{
"TodoCreate",
"POST",
"/todos",
TodoCreate,
},
Создать маршрут
func TodoCreate(w http.ResponseWriter, r *http.Request) {
var todo Todo
body, err := ioutil.ReadAll(io.LimitReader(r.Body, 1048576))
if err != nil {
panic(err)
}
if err := r.Body.Close(); err != nil {
panic(err)
}
if err := json.Unmarshal(body, &todo); err != nil {
w.Header().Set("Content-Type", "application/json; charset=UTF-8")
w.WriteHeader(422) // unprocessable entity
if err := json.NewEncoder(w).Encode(err); err != nil {
panic(err)
}
}
t := RepoCreateTodo(todo)
w.Header().Set("Content-Type", "application/json; charset=UTF-8")
w.WriteHeader(http.StatusCreated)
if err := json.NewEncoder(w).Encode(t); err != nil {
panic(err)
}
}
Сначала мы открываем тело запроса. Обратите внимание, что мы используем io.LimitReader. Это отличный способ защитить ваш сервер от вредоносных атак. Что если кто-то захочет отправить 500 ГБ JSON на ваш сервер?
После прочтения тела мы деконструируем структуру Todo. Если это не удается, мы отвечаем правильно, с соответствующим кодом ответа 422, но мы все еще отвечаем в ответ json. Это позволяет клиенту понять, что что-то пошло не так, и у него есть способ узнать, что пошло не так.
Наконец, если все проходит успешно, мы отвечаем кодом состояния 201, указывающим, что объект, запрошенный для создания, был успешно создан. Мы также отвечаем json, представляющим созданную нами сущность, которая будет содержать идентификатор, который может понадобиться клиенту в следующий раз.
ОТПРАВИТЬ немного JSON
Теперь у нас есть поддельное репо и маршрут создания, поэтому нам нужно опубликовать некоторые данные. Для этого мы используем curl с помощью следующей команды:
curl -H "Content-Type: application/json" -d '{"name": "New Todo"}' http://localhost:8080/todos
если ты снова пройдёшьhttp://localhost:8080/todos access, вы, вероятно, получите следующий ответ:
[
{
"id": 1,
"name": "Write presentation",
"completed": false,
"due": "0001-01-01T00:00:00Z"
},
{
"id": 2,
"name": "Host meetup",
"completed": false,
"due": "0001-01-01T00:00:00Z"
},
{
"id": 3,
"name": "New Todo",
"completed": false,
"due": "0001-01-01T00:00:00Z"
}
]
вещи, которые мы не сделали
Несмотря на то, что у нас отличное начало, еще многое предстоит сделать:
- Версионирование: что, если нам нужно изменить API, а результат полностью изменится, может быть, нам нужно добавить /v1/prefix в начало наших маршрутов?
- Авторизация: если это не общедоступные/бесплатные API, нам также может потребоваться авторизация. Рекомендуется узнать кое-что о веб-токенах JSON.
eTag — если вы создаете что-то, что необходимо расширить, вы можете реализовать eTag.
что еще?
Как и во всех проектах, он начинается с малого, но быстро выходит из-под контроля. Но если мы хотим поднять его на новый уровень и подготовить к производству, нужно сделать еще несколько вещей:
- Много рефакторинга.
- Создайте несколько пакетов для этих файлов, таких как некоторые помощники JSON, модификаторы, процессоры и т. д.
- Тестирование, так что, вы не можете забыть об этом. Мы не делали никаких тестов здесь. Для производственных систем тестирование является обязательным.
исходный код
Суммировать
Для меня важнее всего помнить, что мы хотим создать ответственный API. Отправка соответствующих кодов состояния, заголовков и т. д. является ключом к широкому внедрению API. Я надеюсь, что эта статья поможет вам как можно скорее приступить к работе с собственным API.