golang-pgx/

Многие реальные приложения, написанные на Go , используют ту или иную РСУБД. Притом, последней нередко является PostgreSQL . Для работы с постгресом в мире Go существует больше одной библиотеки, в связи с чем возникает закономерный вопрос — а какую выбрать? Неплохим и достаточно популярным вариантом является jackc/pgx , с которым мы и познакомимся.

Модуль pgx имеет интерфейс, похожий на интерфейс database/sql, про который ранее уже рассказывалось в гостевом посте Владимира Солонина . Главное отличие между модулями заключается в том, что database/sql поддерживает разные РСУБД, а pgx — только PostgreSQL. За счет этого pgx может обеспечить б о льшую производительность, и предложить больше возможностей, специфичных именно для постгреса. Если в приложении требуется поддерживать больше одной РСУБД одновременно (например, вы пишите CMS), берите database/sql. Иначе берите PostgreSQL и, соответственно, модуль pgx.

По устоявшейся традиции, писать будем телефонную книгу c REST-интерфейсом . Касаемо написания непосредственно REST на языке Go мне нечего добавить к другому гостевому посту Владимира , только вместо httprouter я решил использовать mux . Насколько мне известно, между ними нет особой разницы, только в mux хэндлеры имеют меньше аргументов. Субъективно такой код чуть красивее, только и всего. Для парсинга аргументов была использована cobra , а для парсинга файла конфигурации — viper .

На время разработки PostgreSQL удобно запускать через Docker :

# веб-страница образа: https://hub.docker.com/_/postgres
docker run -d —name postgresql -e POSTGRES_DB =restservice
-e POSTGRES_PASSWORD =s3cr3t -p 5432 : 5432 postgres: 11

При необходимости получить доступ к базе данных можно так:

docker ps | grep postgresql
docker exec -it e7042bd737f8 psql -h localhost
-U postgres restservice -W

В приложении для подключения к СУБД будем использовать пул соединений, который уже реализован в pgx:

pool , err := pgxpool . Connect ( context . Background (), dbURL )
if err != nil {
log . Fatalf ( «Unable to connection to database: %v n » , err )
}
defer pool . Close ()
log . Infof ( «Connected!» )

Пример получения соединения из пула для выполнения миграции:

conn , err := pool . Acquire ( context . Background ())
if err != nil {
log . Fatalf ( «Unable to acquire a database connection: %v n » , err )
}
migrateDatabase ( conn . Conn ())
conn . Release ()

В самом pgx миграции не реализованы, но они есть в утилите jackc/tern . Там кода буквально на 300 строк. Они с легкостью переписываются на последнюю актуальную версию v4 модуля pgx, после чего в migrateDatabase остается написать только:

func migrateDatabase ( conn * pgx . Conn ) {
migrator , err := migrate . NewMigrator ( conn , «schema_version» )
if err != nil {
log . Fatalf ( «Unable to create a migrator: %v n » , err )
}

err = migrator . LoadMigrations ( «./migrations» )
if err != nil {
log . Fatalf ( «Unable to load migrations: %v n » , err )
}

err = migrator . Migrate ()
if err != nil {
log . Fatalf ( «Unable to migrate: %v n » , err )
}

ver , err := migrator . GetCurrentVersion ()
if err != nil {
log . Fatalf ( «Unable to get current schema version: %v n » , err )
}

log . Infof ( «Migration done. Current schema version: %v n » , ver )
}

Содержимое файла миграции:

CREATE TABLE phonebook ( id SERIAL PRIMARY KEY ,
name VARCHAR ( 64 ) , phone VARCHAR ( 64 ) ) ;
—- create above / drop below —-
DROP TABLE phonebook;

Таким образом, приложение может само создавать себе схему базы данных или изменять ее при обновлении. Локи реализованы на функции pg_advisory_lock , что исключает возможность запуска двух миграций параллельно.

Далее все очень похоже на database/sql. Так, например, происходит вставка новой записи:

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

func Insert ( p * pgxpool . Pool , w http . ResponseWriter , r * http. Request ) {
var rec Record
err := json . NewDecoder ( r . Body ) . Decode ( &rec )
if err != nil { // bad request
w . WriteHeader ( 400 )
return
}

conn , err := p . Acquire ( context . Background ())
if err != nil {
log . Errorf ( «Unable to acquire a database connection: %v n » , err )
w . WriteHeader ( 500 )
return
}
defer conn . Release ()

row := conn . QueryRow ( context . Background (),
«INSERT INTO phonebook (name, phone) VALUES ($1, $2) RETURNING id» ,
rec . Name , rec . Phone )
var id uint64
err = row . Scan ( &id )
if err != nil {
log . Errorf ( «Unable to INSERT: %v n » , err )
w . WriteHeader ( 500 )
return
}

resp := make ( map [ string ] string , 1 )
resp [ «id» ] = strconv . FormatUint ( id , 10 )
w . Header () . Set ( «Content-Type» , «application/json; charset=utf-8» )
err = json . NewEncoder ( w ) . Encode ( resp )
if err != nil {
log . Errorf ( «Unable to encode json: %v n » , err )
w . WriteHeader ( 500 )
return
}
}

А так — удаление:

func Delete ( p * pgxpool . Pool , w http . ResponseWriter , r * http. Request ) {
vars := mux . Vars ( r )
id , err := strconv . ParseUint ( vars [ «id» ], 10 , 64 )
if err != nil { // bad request
w . WriteHeader ( 400 )
return
}

conn , err := p . Acquire ( context . Background ())
if err != nil {
log . Errorf ( «Unable to acquire a database connection: %v n » , err )
w . WriteHeader ( 500 )
return
}
defer conn . Release ()

ct , err := conn . Exec ( context . Background (),
«DELETE FROM phonebook WHERE id = $1» , id )
if err != nil {
log . Errorf ( «Unable to DELETE: %v n » , err )
w . WriteHeader ( 500 )
return
}

if ct . RowsAffected () == 0 {
w . WriteHeader ( 404 )
return
}

w . WriteHeader ( 200 )
}

Полную версию исходников вы найдете в этом репозитории на GitHub . Примеры запросов к сервису:

# создание:
curl -vvv -XPOST -H ‘Content-Type: application/json’
-d ‘{«name»:»Alice»,»phone»:»123″}’ localhost: 8080 / api / v1 / records

# чтение:
curl -vvv localhost: 8080 / api / v1 / records / 123

# обновление:
curl -vvv -XPUT -H ‘Content-Type: application/json’
-d ‘{«name»:»Bob»,»phone»:»456″}’ localhost: 8080 / api / v1 / records / 456

# удаление:
curl -vvv -XDELETE localhost: 8080 / api / v1 / records / 789

Заинтересованные читатели могут подумать над тем, как написать тесты к этому коду, а также реализовать API GET /api/v1/records , который сейчас показывает лишь сообщение «Under construction». API возвращает все записи, которые имеются в базе, но не более 1000 штук за один запрос. Принимаемые аргументы: limit , offset и order . Первый аргумент опционален и позволяет возвращать менее 1000 записей. Через второй аргумент можно передать id последней записи, которую вернул предыдущий запрос. Тогда текущий запрос вернет записи, идущие после offset . Наконец, order — это строковый параметр, который может быть либо asc (сортировка по возрастанию id), либо desc (по убыванию). Если order не указан, считается, что он asc .

Дополнение: Вас также могут заинтересовать посты Генерация SQL-запросов в Go с помощью squirrel и Тестирование проектов на Go с dockertest .

Дополнение: Я переписал код так, чтобы сервис был совместим с CockroachDB .

EnglishRussianUkrainian