Как легко и просто написать REST-сервис на языке Go (гостевой пост Владимира Солонина)

3 июня 2015

В этой заметке я опишу реализацию простого REST API для телефонной книги, созданной в прошлый раз. Должен сразу оговориться, что интерфейс к базе данных был изменен. Во-первых, прошлая реализация была направлена скорее на демонстрацию возможностей пакета database/sql, чем на решение задачи, а во-вторых, она была подвержена SQL-инъекциям.

Примечание: Другие статьи Владимира вы найдете по следующим ссылкам:

Сначала, как и прежде, считываются параметры подключения к базе из файла конфигурации и создается таблица, если ее еще нет. Далее создается роутер, регистрируются обработчики, запускается сервер на 8080 порту:

router := httprouter.New()
router.GET("/api/v1/records", getRecords)
router.GET("/api/v1/records/:id", getRecord)
router.POST("/api/v1/records", addRecord)
router.PUT("/api/v1/records/:id", updateRecord)
router.DELETE("/api/v1/records/:id", deleteRecord)
http.ListenAndServe(":8080", router)

Вместо стандартного http.ServeMux в качестве роутера я выбрал httprouter. Он проще в использовании и славится высокой производительностью. Есть, впрочем, у него и недостатки. В гибкости настройки он уступает многим другим роутерам. И он несовместим напрямую со стандартным интерфейсом http.Handler. Но для данной задачи это не проблема.

Для кодирования/декодирования JSON используется пакет из стандартной библиотеки encoding/json. Его особенность в том, что он может работать только с экспортируемыми полями структур. А так как в языке Go экспортируемые идентификаторы всегда начинаются с заглавной буквы, они будут такими же и после кодирования в JSON. Изменить это поведение можно с помощью тегов.

Тег — это строковый литерал, состоящий, по соглашению, из одной и более пар ключ:"значение", разделенных пробелом. Теги могут быть присвоены полям структуры в качестве атрибутов и становятся в таком случае частью типа структуры. Программно теги могут быть получены с помощью рефлексии времени выполнения (пакет reflect). Так пакет encoding/json при разборе структуры Record считает теги и заменит имена ключей в JSON на заданные в тегах:

type Record struct {
  Id    int    `json:"id"`
  Name  string `json:"name"`
  Phone string `json:"phone"`
}

Первый хэндлер, помимо возврата всех записей, поддерживает параметры запроса и может вернуть записи, содержащие заданную подстроку в поле имени.

То есть, GET-запрос:

http://localhost:8080/api/v1/records?name=abc

… вернет все записи, где name содержит строку «abc». В случае некорректного параметра запроса вернется ошибка 400 Bad Request. В случае ошибки базы данных или проблем с кодированием в JSON — 500 Internal Server Error. Кроме того, сервер может вернуть ошибку 404 Not Found или 405 Method Not Allowed, если запрошенный URL не существует или не поддерживает данный метод.

func getRecords(w http.ResponseWriter, r *http.Request,
                _ httprouter.Params) {
  var str string
  if len(r.URL.RawQuery) > 0 {
    str = r.URL.Query().Get("name")
    if str == "" {
      w.WriteHeader(400)
      return
    }
  }
  recs, err := read(str)
  if err != nil {
    w.WriteHeader(500)
    return
  }
  w.Header().Set("Content-Type", "application/json; charset=utf-8")
  if err = json.NewEncoder(w).Encode(recs); err != nil {
    w.WriteHeader(500)
  }
}

Второй хэндлер возвращает запись с заданным id или ошибку 404 Not Found, если такого id нет в базе. Но сначала id проверяется на корректность:

func getID(w http.ResponseWriter, ps httprouter.Params) (int, bool) {
  id, err := strconv.Atoi(ps.ByName("id"))
  if err != nil {
    w.WriteHeader(400)
    return 0, false
  }
  return id, true
}

func getRecord(w http.ResponseWriter, r *http.Request,
               ps httprouter.Params) {
  id, ok := getID(w, ps)
  if !ok {
    return
  }
  rec, err := readOne(id)
  if err != nil {
    if err == sql.ErrNoRows {
      w.WriteHeader(404)
      return
    }
    w.WriteHeader(500)
    return
  }
  w.Header().Set("Content-Type", "application/json; charset=utf-8")
  if err = json.NewEncoder(w).Encode(rec); err != nil {
    w.WriteHeader(500)
  }
}

Третий хэндлер добавляет запись в базу и возвращает код 201 Created в случае успеха. Если во время декодирования JSON возникла ошибка, возвращается код 400 Bad Request.

func addRecord(w http.ResponseWriter, r *http.Request,
               _ httprouter.Params) {
  var rec Record
  err := json.NewDecoder(r.Body).Decode(&rec)
  if err != nil || rec.Name == "" || rec.Phone == "" {
    w.WriteHeader(400)
    return
  }
  if _, err := insert(rec.Name, rec.Phone); err != nil {
    w.WriteHeader(500)
    return
  }
  w.WriteHeader(201)
}

Четвертый хэндлер изменяет запись с данным id. В случае успеха возвращается 204 No Content. Если такого id нет в базе, возвращается ошибка 404 Not Found.

func updateRecord(w http.ResponseWriter, r *http.Request,
                  ps httprouter.Params) {
  id, ok := getID(w, ps)
  if !ok {
    return
  }
  var rec Record
  err := json.NewDecoder(r.Body).Decode(&rec)
  if err != nil || rec.Name == "" || rec.Phone == "" {
    w.WriteHeader(400)
    return
  }
  res, err := update(id, rec.Name, rec.Phone)
  if err != nil {
    w.WriteHeader(500)
    return
  }
  n, _ := res.RowsAffected()
  if n == 0 {
    w.WriteHeader(404)
    return
  }
  w.WriteHeader(204)
}

Пятый хэндлер удаляет запись с данным id. В случае успеха возвращается 204 No Content.

func deleteRecord(w http.ResponseWriter, r *http.Request,
                  ps httprouter.Params) {
  id, ok := getID(w, ps)
  if !ok {
    return
  }
  if _, err := remove(id); err != nil {
    w.WriteHeader(500)
  }
  w.WriteHeader(204)
}

Проверить работу приложения можно с помощью curl примерно таким образом.

Получение всех записей:

curl -i http://localhost:8080/api/v1/records

Получение всех записей имеющих в поле name подстроку «Маша»:

curl -i http://localhost:8080/api/v1/records?name=Маша

Получение записи с id = 3:

curl -i http://localhost:8080/api/v1/records/3

Создание новой записи:

curl -i http://localhost:8080/api/v1/records \
  -d '{"name":"Иванов Иван","phone":"9284724"}'

Редактирование записи с id = 5:

curl -i http://localhost:8080/api/v1/records/5 -XPUT \
  -d '{"name":"Петрова Лена","phone":"2341233"}'

Удаление записи c id = 2:

curl -i http://localhost:8080/api/v1/records/2 -XDELETE

Полный листинг программы доступен по этой ссылке (зеркало).

Дополнение: Многопоточный генератор шоунотов на Go

Метки: .


Вы можете прислать свой комментарий мне на почту, или воспользоваться комментариями в Telegram-группе.