В этой заметке я опишу реализацию простого 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