Go и работа с базами данных, в частности с PostgreSQL (гостевой пост Владимира Солонина)
11 мая 2015
Пожалуй, основной областью применения Go на сегодня является написание серверных приложений. А их трудно представить в отрыве от какой-нибудь базы данных. В этой заметке я опишу создание простой телефонной книги с интерфейсом командной строки и PostgeSQL в качестве хранилища.
Примечание: Другие статьи Владимира вы найдете по следующим ссылкам:
Стандартным интерфейсом доступа к реляционным СУБД для Go является пакет database/sql. Он используется в связке с драйвером выбранной базы данных. Таким образом, полагаясь на арсенал, предоставляемый database/sql, мы получаем возможность миграции на любую другую РСУБД с минимальными изменениями в коде.
База данных в пакете database/sql представлена объектом sql.DB, который содержит пул соединений и может безопасно применяться для параллельной работы с БД из разных горутин. Он создается функцией sql.Open(), принимающей в качестве параметров название драйвера и строку с информацией о подключении. Последняя является драйвероспецифичной и обычно состоит из адреса сервера БД, номера порта, имени базы данных, имени пользователя и пароля. Объект sql.DB задуман, как долгоживущий. Правильно будет создать его перед первым использованием и закрыть, когда доступ к базе больше не нужен. Вопреки названию, функция sql.Open() не устанавливает соединение с БД. Она также не проверяет возможность подключения и правильность полученных параметров. Все это будет сделано при первом обращении к базе. Если необходима предварительная проверка, используйте метод (*DB)Ping().
db, err := sql.Open("postgres", params())
chk(err)
defer db.Close()
Здесь функция params() возвращает параметры БД, взятые из обычного INI-файла конфигурации $HOME/.phonebookrc. В качестве парсера конфига я использовал пакет github.com/FogCreek/mini.
Вызов функции закрытия базы данных сделан с помощью оператора defer. Он используется, когда нужно отложить выполнение некоторой функции до выхода из текущей. Преимущество отложенных с помощью defer функций в том, что они выполняются в любом случае, даже если окружающая их функция завершилась ошибкой или паникой.
Отложенные функции подчиняются трем правилам:
- Их аргументы вычисляются там, где находится вызов;
- Вызов нескольких отложенных функций происходит в обратном порядке, LIFO, как в стеке;
- Отложенные функции могут читать и влиять на именованные возвращаемые значения после их возврата внешней функцией.
Таким образом, закрытие нашей базы данных будет выполнено сразу после выхода из функции main.
Далее проверяем наличие в базе таблицы phonebook и создаем, если ее там нет:
`phonebook("id" SERIAL PRIMARY KEY,` +
`"name" varchar(50), "phone" varchar(100))`)
chk(err)
Метод db.Exec() применяется, когда нужно сделать однократное обращение к базе, не требующее возврата данных. Помимо ошибки он возвращает sql.Result, который дает возможность узнать последний введенный ID и количество затронутых запросом строк. Сразу после выполнения (*DB)Exec() возвращает соединение обратно в пул.
Теперь разбор и проверка аргументов командной строки:
case "add":
if len(os.Args) != 4 {
fatal("Usage: phonebook add NAME PHONE")
}
num, err := insert(db, os.Args[2], os.Args[3])
chk(err)
fmt.Println(num, "rows affected")
case "del":
if len(os.Args) < 3 {
fatal("Usage: phonebook del ID...")
}
err = remove(db, os.Args[2:])
chk(err)
case "edit":
if len(os.Args) != 5 {
fatal("Usage: phonebook edit ID NAME PHONE")
}
err = update(db, os.Args[2], os.Args[3], os.Args[4])
chk(err)
case "show":
if len(os.Args) > 3 {
fatal("Usage: phonebook show [SUBSTRING]")
}
var s string
if len(os.Args) == 3 {
s = os.Args[2]
}
res, err := show(db, s)
chk(err)
format(res)
case "help":
fmt.Println(help)
default:
fatal("Invalid command: " + os.Args[1])
}
По результатам которых вызывается одна из следующих функций:
// затронутых строк(всегда одну, очевидно)
func insert(db *sql.DB, name, phone string) (int64, error) {
res, err := db.Exec("INSERT INTO phonebook VALUES (default, $1, $2)",
name, phone)
if err != nil {
return 0, err
}
return res.RowsAffected()
}
Для многократного выполнения однотипных действий имеет смысл использовать связку из (*DB)Prepare() и (*Stmt)Exec():
func remove(db *sql.DB, ids []string) error {
stmt, err := db.Prepare("DELETE FROM phonebook WHERE id=$1")
if err != nil {
return err
}
defer stmt.Close()
for _, v := range ids {
_, err := stmt.Exec(v)
if err != nil {
return err
}
}
return nil
}
Использование транзакций:
func update(db *sql.DB, id, name, phone string) error {
tx, err := db.Begin()
if err != nil {
return err
}
defer tx.Rollback()
_, err = tx.Exec("UPDATE phonebook SET name = $1, " +
"phone = $2 WHERE id=$3",
name, phone, id)
if err != nil {
return err
}
return tx.Commit()
}
Транзакция всегда резервирует для себя одно соединение, в котором происходит все взаимодействие с базой. Она начинается с вызова (*DB)Begin() и заканчивается вызовом (*Tx)Commit() или (*Tx)Rollback(). После этого соединение возвращается в пул свободных соединений и все попытки использовать данную транзакцию оканчиваются ошибкой ErrTxDone. Таким образом, отложенный tx.Rollback() после выхода из функции update проверит, закрыта ли транзакция, и если да, ничего больше делать не будет, а просто вернет ошибку (ее мы не проверяем). Если же коммит не произошел и транзакция еще открыта, Rollback() произведет отмену всех изменений сделанных в рамках транзакции и закроет ее.
В функции show() используется еще один метод db.Query(). В отличие от db.Exec, его первое возвращаемое значение sql.Rows не должно игнорироваться. Дело в том, что возвращаемые sql.Rows резервируют соединение до вызова (*Rows)Close(), даже если результат запроса пуст или проигнорирован. И хотя в конце концов сборщик мусора закроет это соединение, лучше на это не полагаться. В случае вызова (*DB)Query() из долгоживущей функции рекомендуется выполнить (*Rows)Close(), как только строки будут не нужны, чтобы не занимать соединение до выполнения отложенной функции.
func show(db *sql.DB, arg string) ([]record, error) {
var s string
if len(arg) != 0 {
s = "WHERE name LIKE '%" + arg + "%'"
}
rows, err := db.Query("SELECT * FROM phonebook " + s +
" ORDER BY id")
if err != nil {
return nil, err
}
defer rows.Close()
var rs = make([]record, 0)
var rec record
for rows.Next() {
err = rows.Scan(&rec.id, &rec.name, &rec.phone)
if err != nil {
return nil, err
}
rs = append(rs, rec)
}
err = rows.Err()
if err != nil {
return nil, err
}
return rs, nil
}
Для итерации по строкам используется метод (*Rows)Next() в паре с for, а для считывания данных из строки — (*Rows)Scan(). Последняя, к сожалению, не может работать напрямую со структурами, но эта возможность есть в сторонних библиотеках. (*Rows)Scan() помимо прочего может самостоятельно конвертировать значение из базы к заданному типу. Так, например, если в базе колонка с типом VARCHAR(20) используется для записи целых чисел, передача указателя на int в качестве аргумента (*Rows)Scan() автоматически вызовет функцию strconv.ParseInt() и не надо будет делать это вручную.
Полная версия программы находится здесь. Подробнее о тонкостях работы с базами данных в Go вы можете узнать из этого руководства. Как всегда, если у вас есть вопросы, не стесняйтесь задавать их в комментариях.
Дополнение: Еще вас может заинтересовать пост Работа с PostgreSQL в языке Go при помощи pgx, а также Как легко и просто написать REST-сервис на Go.
Метки: Go, PostgreSQL, СУБД.
Вы можете прислать свой комментарий мне на почту, или воспользоваться комментариями в Telegram-группе.