← На главную

Go и работа с базами данных, в частности с PostgreSQL (гостевой пост Владимира Солонина)

Пожалуй, основной областью применения Go на сегодня является написание серверных приложений. А их трудно представить в отрыве от какой-нибудь базы данных. В этой заметке я опишу создание простой телефонной книги с интерфейсом командной строки и PostgeSQL в качестве хранилища.

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

Стандартным интерфейсом доступа к реляционным СУБД для Go является пакет database/sql. Он используется в связке с драйвером выбранной базы данных. Таким образом, полагаясь на арсенал, предоставляемый database/sql, мы получаем возможность миграции на любую другую РСУБД с минимальными изменениями в коде.

База данных в пакете database/sql представлена объектом sql.DB, который содержит пул соединений и может безопасно применяться для параллельной работы с БД из разных горутин. Он создается функцией sql.Open(), принимающей в качестве параметров название драйвера и строку с информацией о подключении. Последняя является драйвероспецифичной и обычно состоит из адреса сервера БД, номера порта, имени базы данных, имени пользователя и пароля. Объект sql.DB задуман, как долгоживущий. Правильно будет создать его перед первым использованием и закрыть, когда доступ к базе больше не нужен. Вопреки названию, функция sql.Open() не устанавливает соединение с БД. Она также не проверяет возможность подключения и правильность полученных параметров. Все это будет сделано при первом обращении к базе. Если необходима предварительная проверка, используйте метод (*DB)Ping().

func main() { db, err := sql.Open("postgres", params()) chk(err) defer db.Close()

Здесь функция params() возвращает параметры БД, взятые из обычного INI-файла конфигурации $HOME/.phonebookrc. В качестве парсера конфига я использовал пакет github.com/FogCreek/mini.

Вызов функции закрытия базы данных сделан с помощью оператора defer. Он используется, когда нужно отложить выполнение некоторой функции до выхода из текущей. Преимущество отложенных с помощью defer функций в том, что они выполняются в любом случае, даже если окружающая их функция завершилась ошибкой или паникой.

Отложенные функции подчиняются трем правилам:

  1. Их аргументы вычисляются там, где находится вызов;
  2. Вызов нескольких отложенных функций происходит в обратном порядке, LIFO, как в стеке;
  3. Отложенные функции могут читать и влиять на именованные возвращаемые значения после их возврата внешней функцией.

Таким образом, закрытие нашей базы данных будет выполнено сразу после выхода из функции main.

Далее проверяем наличие в базе таблицы phonebook и создаем, если ее там нет:

_, err = db.Exec("CREATE TABLE IF NOT EXISTS " + `phonebook("id" SERIAL PRIMARY KEY,` + `"name" varchar(50), "phone" varchar(100))`) chk(err)

Метод db.Exec() применяется, когда нужно сделать однократное обращение к базе, не требующее возврата данных. Помимо ошибки он возвращает sql.Result, который дает возможность узнать последний введенный ID и количество затронутых запросом строк. Сразу после выполнения (*DB)Exec() возвращает соединение обратно в пул.

Теперь разбор и проверка аргументов командной строки:

switch os.Args[1] { 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():

// удаляет выбранные по id записи 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(), как только строки будут не нужны, чтобы не занимать соединение до выполнения отложенной функции.

// возвращает все записи где name содержит заданную подстроку 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.