Язык Go RESTful JSON API создание

Go API

Разработать 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, модификаторы, процессоры и т. д.
  • Тестирование, так что, вы не можете забыть об этом. Мы не делали никаких тестов здесь. Для производственных систем тестирование является обязательным.

исходный код

GitHub.com/Кори Блю Оу/…

Суммировать

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

Ссылка на ссылку