go-databases/

Пожалуй, основной областью применения 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 .

EnglishRussianUkrainian