Как легко и просто написать REST-сервис на языке Go (гостевой пост Владимира Солонина)
3 июня 2015
В этой заметке я опишу реализацию простого REST API для телефонной книги, созданной в прошлый раз. Должен сразу оговориться, что интерфейс к базе данных был изменен. Во-первых, прошлая реализация была направлена скорее на демонстрацию возможностей пакета database/sql, чем на решение задачи, а во-вторых, она была подвержена SQL-инъекциям.
Примечание: Другие статьи Владимира вы найдете по следующим ссылкам:
- GUI-проиложение на Go и GTK;
- Работа с goroutines на Go;
- Профилирование в языке Go;
- Go и работа с PostgreSQL;
Сначала, как и прежде, считываются параметры подключения к базе из файла конфигурации и создается таблица, если ее еще нет. Далее создается роутер, регистрируются обработчики, запускается сервер на 8080 порту:
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 на заданные в тегах:
Id int `json:"id"`
Name string `json:"name"`
Phone string `json:"phone"`
}
Первый хэндлер, помимо возврата всех записей, поддерживает параметры запроса и может вернуть записи, содержащие заданную подстроку в поле имени.
То есть, GET-запрос:
… вернет все записи, где name содержит строку «abc». В случае некорректного параметра запроса вернется ошибка 400 Bad Request. В случае ошибки базы данных или проблем с кодированием в JSON — 500 Internal Server Error. Кроме того, сервер может вернуть ошибку 404 Not Found или 405 Method Not Allowed, если запрошенный URL не существует или не поддерживает данный метод.
_ 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 проверяется на корректность:
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.
_ 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.
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.
ps httprouter.Params) {
id, ok := getID(w, ps)
if !ok {
return
}
if _, err := remove(id); err != nil {
w.WriteHeader(500)
}
w.WriteHeader(204)
}
Проверить работу приложения можно с помощью curl примерно таким образом.
Получение всех записей:
Получение всех записей имеющих в поле name подстроку «Маша»:
Получение записи с id = 3:
Создание новой записи:
-d '{"name":"Иванов Иван","phone":"9284724"}'
Редактирование записи с id = 5:
-d '{"name":"Петрова Лена","phone":"2341233"}'
Удаление записи c id = 2:
Полный листинг программы доступен по этой ссылке (зеркало).
Дополнение: Многопоточный генератор шоунотов на Go
Метки: Go.
Вы можете прислать свой комментарий мне на почту, или воспользоваться комментариями в Telegram-группе.