No.1 文檔概要
在Golang中使用SQL或類似SQL的數(shù)據(jù)庫的慣用方法是通過 database/sql 包操作蝗罗。它為面向行的數(shù)據(jù)庫提供了輕量級(jí)的接口舆绎。這篇文章是關(guān)于如何使用它,最常見的參考捞稿。
為什么需要這個(gè)且警?包文檔告訴你每件事情都做了什么橘荠,但它并沒有告訴你如何使用這個(gè)包。我們很多人都希望自己能快速參考和入門的方法矾削,而不是講故事壤玫。歡迎捐款;請(qǐng)?jiān)谶@里發(fā)送請(qǐng)求哼凯。
在Golang中你用sql.DB訪問數(shù)據(jù)庫欲间。你可以使用此類型創(chuàng)建語句和事務(wù),執(zhí)行查詢断部,并獲取結(jié)果猎贴。下面的代碼列出了sql.DB是一個(gè)結(jié)構(gòu)體,點(diǎn)擊 database/sql/sql.go 查看官方源碼蝴光。
首先你應(yīng)該知道一個(gè)sql.DB不是一個(gè)數(shù)據(jù)庫的連接她渴。它也沒有映射到任何特點(diǎn)數(shù)據(jù)庫軟件的“數(shù)據(jù)庫”或“模式”的概念。它是數(shù)據(jù)庫的接口和數(shù)據(jù)庫的抽象蔑祟,它可能與本地文件不同趁耗,可以通過網(wǎng)絡(luò)連接訪問,也可以在內(nèi)存和進(jìn)程中訪問疆虚。
sql.DB為你在幕后執(zhí)行一些重要的任務(wù):
? 通過驅(qū)動(dòng)程序打開和關(guān)閉實(shí)際的底層數(shù)據(jù)庫的連接苛败。
? 它根據(jù)需要管理一個(gè)連接池满葛,這可能是如上所述的各種各樣的事情。
sql.DB抽象旨在讓你不必?fù)?dān)心如何管理對(duì)基礎(chǔ)數(shù)據(jù)存儲(chǔ)的并發(fā)訪問罢屈。一個(gè)連接在使用它執(zhí)行任務(wù)時(shí)被標(biāo)記為可用嘀韧,然后當(dāng)它不在使用時(shí)返回到可用的池中。這樣的后果之一是缠捌,如果你無法將連接釋放到池中乳蛾,則可能導(dǎo)致db.SQL打開大量連接,可能會(huì)耗盡資源(連接太多鄙币,打開的文件句柄太多肃叶,缺少可用網(wǎng)絡(luò)端口等)。稍后我們將進(jìn)一步討論這個(gè)問題十嘿。
在創(chuàng)建sql.DB之后因惭,你可以用它來查詢它所代表的數(shù)據(jù)庫,以及創(chuàng)建語句和事務(wù)绩衷。
No.2 導(dǎo)入數(shù)據(jù)庫驅(qū)動(dòng)
要使用 database/sql蹦魔,你需要 database/sql 自身,以及需要使用的特定的數(shù)據(jù)庫驅(qū)動(dòng)咳燕。
你通常不應(yīng)該直接使用驅(qū)動(dòng)包勿决,盡管有些驅(qū)動(dòng)鼓勵(lì)你這樣做。(在我們看來招盲,這通常是個(gè)壞主意低缩。) 相反的,如果可能曹货,你的代碼應(yīng)該僅引用 database/sql 中定義的類型咆繁。這有助于避免使你的代碼依賴于驅(qū)動(dòng),從而可以通過最少的代碼來更改底層驅(qū)動(dòng)(因此訪問的數(shù)據(jù)庫)顶籽。它還強(qiáng)制你使用Golang習(xí)慣用法玩般,而不是特定驅(qū)動(dòng)作者可能提供的特定的習(xí)慣用法。
在本文檔中礼饱,我們將使用@julienschmidt 和 @arnehormann中優(yōu)秀的MySql驅(qū)動(dòng)坏为。
將以下內(nèi)容添加到Go源文件的頂部(也就是package name下面):
import (
"database/sql"
_ "github.com/go-sql-driver/mysql"
)
注意我們正在加載的驅(qū)動(dòng)是匿名的,將其限定符別名為_镊绪,因此我們的代碼中沒有一個(gè)導(dǎo)出的名稱可見匀伏。在引擎下,驅(qū)動(dòng)將自身注冊(cè)為可用于 database/sql 包镰吆,但一般來說沒有其他情況發(fā)生帘撰。
現(xiàn)在你已經(jīng)準(zhǔn)備好訪問數(shù)據(jù)庫了。
No.3 訪問數(shù)據(jù)庫
現(xiàn)在你已經(jīng)加載了驅(qū)動(dòng)包万皿,就可以創(chuàng)建一個(gè)數(shù)據(jù)庫對(duì)象sql.DB摧找。創(chuàng)建一個(gè)sql.DB你可以使用sql.Open()核行。Open返回一個(gè)*sql.DB。
func main() {
db, err := sql.Open("mysql",
"user:password@tcp(127.0.0.1:3306)/hello")
if err != nil {
log.Fatal(err)
}
defer db.Close()
}
在示例中蹬耘,我們演示了幾件事:
sql.Open的第一個(gè)參數(shù)是驅(qū)動(dòng)名稱芝雪。這是驅(qū)動(dòng)用來注冊(cè)database/sql的字符串,并且通常與包名相同以避免混淆综苔。例如惩系,它是github.com/go-sql-driver/mysql的MySql驅(qū)動(dòng)(作者:jmhodges)。某些驅(qū)動(dòng)不遵守公約的名稱如筛,例如github.com/mattn/go-sqlite3的sqlite3(作者:matte)和github.com/lib/pq的postgres(作者:mjibson)堡牡。
第二個(gè)參數(shù)是一個(gè)驅(qū)動(dòng)特定的語法,它告訴驅(qū)動(dòng)如何訪問底層數(shù)據(jù)存儲(chǔ)杨刨。在本例中晤柄,我們將連接本地的MySql服務(wù)器實(shí)例中的“hello”數(shù)據(jù)庫。
你應(yīng)該(幾乎)總是檢查并處理從所有database/sql操作返回的錯(cuò)誤妖胀。有一些特殊情況芥颈,我們稍后將討論這樣做事沒有意義的。
如果sql.DB不應(yīng)該超出該函數(shù)的作用范圍赚抡,則延遲函數(shù)defer db.Close()是慣用的爬坑。
也許是反直覺的,sql.Open()不建立與數(shù)據(jù)庫的任何連接涂臣,也不會(huì)驗(yàn)證驅(qū)動(dòng)連接參數(shù)钧椰。相反砌们,它只是準(zhǔn)備數(shù)據(jù)庫抽象以供以后使用幅狮。首次真正的連接底層數(shù)據(jù)存儲(chǔ)區(qū)將在第一次需要時(shí)懶惰地建立睹欲。如果你想立即檢查數(shù)據(jù)庫是否可用(例如灼舍,檢查是否可以建立網(wǎng)絡(luò)連接并登陸)吼和,請(qǐng)使用db.Ping()來執(zhí)行此操作,記得檢查錯(cuò)誤:
err = db.Ping()
if err != nil {
// do something here
}
雖然在完成數(shù)據(jù)庫之后Close()數(shù)據(jù)庫是慣用的骑素,但是sql.DB對(duì)象被設(shè)計(jì)為長(zhǎng)連接炫乓。不要經(jīng)常Open()和Close()數(shù)據(jù)庫。相反献丑,為你需要訪問的每個(gè)不同的數(shù)據(jù)存儲(chǔ)創(chuàng)建一個(gè)sql.DB對(duì)象末捣,并保留它,直到程序訪問數(shù)據(jù)存儲(chǔ)完畢创橄。在需要時(shí)傳遞它箩做,或在全局范圍內(nèi)使其可用,但要保持開放妥畏。并且不要從短暫的函數(shù)中Open()和Close()邦邦。相反安吁,通過sql.DB作為參數(shù)傳遞給該短暫的函數(shù)。
如果你不把sql.DB視為長(zhǎng)期存在的對(duì)象燃辖,則可能會(huì)遇到諸如重復(fù)使用和連接共享不足鬼店,耗盡可用的網(wǎng)絡(luò)資源以及由于TIME_WAIT中剩余大量TCP連接而導(dǎo)致的零星故障的狀態(tài)。這些問題表明你沒有像設(shè)計(jì)的那樣使用database/sql的跡象黔龟。
現(xiàn)在是時(shí)候使用你的sql.DB對(duì)象了妇智。
No.4 檢索結(jié)果集
有幾個(gè)慣用的操作來從數(shù)據(jù)存儲(chǔ)中檢索結(jié)果。
執(zhí)行返回行的查詢氏身。
準(zhǔn)備重復(fù)使用的語句巍棱,多次執(zhí)行并銷毀它。
以一次關(guān)閉的方式執(zhí)行語句蛋欣,不準(zhǔn)備重復(fù)使用拉盾。
執(zhí)行一個(gè)返回單行的查詢。這種特殊情況有一個(gè)捷徑豁状。
Golang的database/sql函數(shù)名非常重要捉偏。如果一個(gè)函數(shù)名包含查詢Query(),它被設(shè)計(jì)為詢問數(shù)據(jù)庫的問題泻红,并返回一組行夭禽,即使它是空的。不返回行的語句不應(yīng)該使用Query()函數(shù)谊路;他們應(yīng)該使用Exec()讹躯。
從數(shù)據(jù)庫獲取數(shù)據(jù)
讓我們來看一下如何查詢數(shù)據(jù)庫,使用Query的例子缠劝。我們將向用戶表查詢id為1的用戶潮梯,并打印出用戶的id和name。我們將使用rows.Scan()將結(jié)果分配給變量惨恭,一次一行秉馏。
var (
id int
name string
)
rows, err := db.Query("select id, name from users where id = ?", 1)
if err != nil {
log.Fatal(err)
}
defer rows.Close()
for rows.Next() {
err := rows.Scan(&id, &name)
if err != nil {
log.Fatal(err)
}
log.Println(id, name)
}
err = rows.Err()
if err != nil {
log.Fatal(err)
}
下面是上面代碼中正在發(fā)生的事情:
我們使用db.Query()將查詢發(fā)送到數(shù)據(jù)庫。我們像往常一樣檢查錯(cuò)誤脱羡。
我們用defer內(nèi)置函數(shù)推遲了rows.Close()的執(zhí)行萝究。這個(gè)非常重要。
我們用rows.Next()遍歷了數(shù)據(jù)行锉罐。
我們用rows.Scan()讀取每行中的列變量帆竹。
我們完成遍歷行之后檢查錯(cuò)誤。
這幾乎是Golang中唯一的辦法脓规。例如栽连,你不能將一行作為映射來獲取。這是因?yàn)樗袞|西都是強(qiáng)類型的侨舆。你需要?jiǎng)?chuàng)建正確類型的變量并將指針傳遞給它們秒紧,如圖所示舷暮。
其中的幾個(gè)部分很容易出錯(cuò),可能會(huì)產(chǎn)生不良后果噩茄。
? 你應(yīng)該總是檢查rows.Next()循環(huán)結(jié)尾處的錯(cuò)誤下面。如果循環(huán)中出現(xiàn)錯(cuò)誤,則需要了解它绩聘。不要僅僅假設(shè)循環(huán)遍歷沥割,直到你已經(jīng)處理了所有的行。
? 第二凿菩,只要有一個(gè)打開的結(jié)果集(由行代表)机杜,底層連接就很忙,不能用于任何其他查詢衅谷。這意味著它在連接池中不可用椒拗。如果你使用rows.Next()遍歷所有行,最終將讀取最后一行获黔,rows.Next()將遇到內(nèi)部EOF錯(cuò)誤蚀苛,并為你調(diào)用rows.Close()。但是玷氏,如果由于某種原因退出該循環(huán)-提前返回堵未,那么行不會(huì)關(guān)閉,并且連接保持打開狀態(tài)盏触。(如果rows.Next()由于錯(cuò)誤而返回false渗蟹,則會(huì)自動(dòng)關(guān)閉)。這是一種簡(jiǎn)單耗盡資源的方法赞辩。
? rows.Close()是一種無害的操作雌芽,如果它已經(jīng)關(guān)閉,所以你可以多次調(diào)用它辨嗽。但是請(qǐng)注意世落,我們首先檢查錯(cuò)誤,如果沒有錯(cuò)誤召庞,則調(diào)用rows.Close()岛心,以避免運(yùn)行時(shí)的panic。
? 你應(yīng)該總是用延遲語句defer推遲rows.Close()篮灼,即使你也在循環(huán)結(jié)束時(shí)調(diào)用rows.Close(),這不是一個(gè)壞主意徘禁。
? 不要在循環(huán)中用defer推遲诅诱。延遲語句在函數(shù)退出之前不會(huì)執(zhí)行,所以長(zhǎng)時(shí)間運(yùn)行的函數(shù)不應(yīng)該使用它送朱。如果你這樣做娘荡,你會(huì)慢慢積累記憶干旁。如果你在循環(huán)中反復(fù)查詢和使用結(jié)果集,則在完成每個(gè)結(jié)果后應(yīng)顯示的調(diào)用rows.Close()炮沐,而不用延遲語句defer争群。
Scan()如何工作
當(dāng)你遍歷行并將其掃描到目標(biāo)變量中時(shí),Golang會(huì)在幕后為你執(zhí)行數(shù)據(jù)類型轉(zhuǎn)換大年。它基于目標(biāo)變量的類型换薄。意識(shí)到這一點(diǎn)可以干凈你的代碼,并幫助避免重復(fù)工作翔试。
例如轻要,假設(shè)你從表中選擇了一些行,這是用字符串列定義的垦缅。如varchar(45)或類似的列冲泥。然而,你碰巧知道表格總是包含數(shù)字壁涎。如果傳遞指向字符串的指針凡恍,Golang會(huì)將字節(jié)復(fù)制到字符串中。現(xiàn)在可以使用strconv.ParseInt()或類似的方式將值轉(zhuǎn)換為數(shù)字怔球。你必須檢查SQL操作中的錯(cuò)誤以及解析整數(shù)的錯(cuò)誤咳焚。這又亂又糟糕。
或者庞溜,你可以通過Scan()指向一個(gè)整數(shù)即可革半。Golang會(huì)檢測(cè)到并為你調(diào)用strconv.ParseInt()。如果有轉(zhuǎn)換錯(cuò)誤流码,則調(diào)用Scan()將返回它又官。你的代碼現(xiàn)在更小更整潔。這是推薦使用database/sql的方法漫试。
準(zhǔn)備查詢
一般來說六敬,你應(yīng)該總是準(zhǔn)備多次使用查詢。準(zhǔn)備查詢的結(jié)果是一個(gè)準(zhǔn)備語句驾荣,可以為執(zhí)行語句時(shí)提供的參數(shù)外构,提供占位符(a.k.a bind值)。這比連接字符串更好播掷,出于所有通常的理由(例如避免SQL注入攻擊)审编。
在MySql中,參數(shù)占位符為歧匈?垒酬,在PostgreSql中為$N,其中N為數(shù)字。SQLite接受這兩者之一。在Oracle中占位符以冒號(hào)開始勘究,并命名為:param1矮湘。本文檔中我們使用?占位符口糕,因?yàn)槲覀兪褂肕ySql作為示例缅阳。
stmt, err := db.Prepare("select id, name from users where id = ?")
if err != nil {
log.Fatal(err)
}
defer stmt.Close()
rows, err := stmt.Query(1)
if err != nil {
log.Fatal(err)
}
defer rows.Close()
for rows.Next() {
// ...
}
if err = rows.Err(); err != nil {
log.Fatal(err)
}
在引擎下,db.Query()實(shí)際上準(zhǔn)備景描,執(zhí)行和關(guān)閉一個(gè)準(zhǔn)備好的語句十办。這是數(shù)據(jù)庫的三次往返。如果你不小心伏伯,可以使應(yīng)用程序的數(shù)據(jù)庫交互數(shù)量增加三倍橘洞!有些驅(qū)動(dòng)可以在特定情況下避免這種情況,但并非所有驅(qū)動(dòng)都可以這樣做说搅。點(diǎn)擊prepared statements查看更多聲明炸枣。
單行查詢
如果一個(gè)查詢返回最多一行,可以使用一些快速的樣板代碼:
var name string
err = db.QueryRow("select name from users where id = ?", 1).Scan(&name)
if err != nil {
log.Fatal(err)
}
fmt.Println(name)
來自查詢的錯(cuò)誤將被推遲到Scan()弄唧,然后返回适肠。你也可以在準(zhǔn)備的語句中調(diào)用QueryRow():
stmt, err := db.Prepare("select name from users where id = ?")
if err != nil {
log.Fatal(err)
}
var name string
err = stmt.QueryRow(1).Scan(&name)
if err != nil {
log.Fatal(err)
}
fmt.Println(name)
No.5 修改數(shù)據(jù)和使用事務(wù)
現(xiàn)在我們已經(jīng)準(zhǔn)備好了如何修改數(shù)據(jù)和處理事務(wù)。如果你習(xí)慣于使用“statement”對(duì)象來獲取行并更新數(shù)據(jù)候引,那么這種區(qū)別可能視乎是認(rèn)為的侯养,但是在Golang中有一個(gè)重要的原因。
修改數(shù)據(jù)的statements
使用Exec()澄干,最好用一個(gè)準(zhǔn)備好的statement來完成INSERT,UPDATE,DELETE或者其他不返回行的語句逛揩。下面的示例演示如何插入行并檢查有關(guān)操作的元數(shù)據(jù):
stmt, err := db.Prepare("INSERT INTO users(name) VALUES(?)")
if err != nil {
log.Fatal(err)
}
res, err := stmt.Exec("Dolly")
if err != nil {
log.Fatal(err)
}
lastId, err := res.LastInsertId()
if err != nil {
log.Fatal(err)
}
rowCnt, err := res.RowsAffected()
if err != nil {
log.Fatal(err)
}
log.Printf("ID = %d, affected = %d\n", lastId, rowCnt)
執(zhí)行該語句將生成一個(gè)sql.Result,該語句提供對(duì)statement元數(shù)據(jù)的訪問:最后插入的ID和行數(shù)受到影響麸俘。
如果你不在乎結(jié)果怎么辦辩稽?如果你只想執(zhí)行一個(gè)語句并檢查是否有錯(cuò)誤,但忽略結(jié)果該怎么辦从媚?下面兩個(gè)語句不會(huì)做同樣的事情嗎逞泄?
_, err := db.Exec("DELETE FROM users") // OK
_, err := db.Query("DELETE FROM users") // BAD
答案是否定的。他們不做同樣的事情拜效,你不應(yīng)該使用Query()喷众。Query()將返回一個(gè)sql.Rows,它保留數(shù)據(jù)庫連接紧憾,直到sql.Rows關(guān)閉到千。由于可能有未讀數(shù)據(jù)(例如更多的數(shù)據(jù)行),所以不能使用連接稻励。在上面的示例中父阻,連接將永遠(yuǎn)不會(huì)被釋放愈涩。垃圾回收器最終會(huì)關(guān)閉底層的net.Conn望抽,但這可能需要很長(zhǎng)時(shí)間加矛。此外,database/sql包將繼續(xù)跟蹤池中的連接煤篙,希望在某個(gè)時(shí)候釋放它斟览,以便可以再次使用連接。因此辑奈,這種反模式是耗盡資源的好方法(例如連接數(shù)太多)苛茂。
事務(wù)處理
在Golang中,事務(wù)本質(zhì)上是保留與數(shù)據(jù)存儲(chǔ)的連接的對(duì)象鸠窗。它允許你執(zhí)行我們迄今為止所看到的所有操作妓羊,但保證它們將在同一連接上執(zhí)行。
你可以通過調(diào)用db.Begin()開始一個(gè)事務(wù)稍计,并在結(jié)果Tx變量上用Commit()或Rollback()方法關(guān)閉它躁绸。在封面下,Tx從池中獲取連接臣嚣,并保留它僅用于該事務(wù)净刮。Tx上的方法一對(duì)一到可以調(diào)用數(shù)據(jù)本本身的方法,例如Query()等等硅则。
在事務(wù)中創(chuàng)建的Prepare語句僅限于該事務(wù)淹父。點(diǎn)擊prepared statements查看更多準(zhǔn)備的聲明。
你不應(yīng)該在SQL代碼中混合BEGIN和COMMIT相關(guān)的函數(shù)(如Begin()和Commit()的SQL語句)怎虫,可能會(huì)導(dǎo)致悲勈钊稀:
? Tx對(duì)象可以保持打開狀態(tài),從池中保留連接而不返回大审。
? 數(shù)據(jù)庫的狀態(tài)可能與代表它的Golang變量的狀態(tài)不同步蘸际。
? 你可能會(huì)認(rèn)為你是在事務(wù)內(nèi)部的單個(gè)連接上執(zhí)行查詢,實(shí)際上Golang已經(jīng)為你創(chuàng)建了幾個(gè)連接饥努,而且一些語句不是事務(wù)的一部分捡鱼。
當(dāng)你在事務(wù)中工作時(shí),你應(yīng)該注意不要對(duì)Db變量進(jìn)行調(diào)用酷愧。應(yīng)當(dāng)使用db.Begin()創(chuàng)建的Tx變量進(jìn)行所有調(diào)用驾诈。Db不在一個(gè)事務(wù)中,只有Tx是溶浴。如果你進(jìn)一步調(diào)用db.Exec()或類似的函數(shù)乍迄,那么這些調(diào)用將發(fā)生在事務(wù)范圍之外,是在其他的連接上士败。
如果你需要處理修改連接狀態(tài)的多個(gè)語句闯两,即使你不希望事務(wù)本身褥伴,也需要一個(gè)Tx。例如:
? 創(chuàng)建僅在一個(gè)連接中可見的臨時(shí)表漾狼。
? 設(shè)置變量重慢,如MySql's SET @var := somevalue語法。
? 更改連接選項(xiàng)逊躁,如字符集或超時(shí)似踱。
如果你需要執(zhí)行任何這些操作,則需要把你的作業(yè)(也可以說Tx操作語句)綁定到單個(gè)連接稽煤,而在Golang中執(zhí)行此操作的唯一方法是使用Tx核芽。
No.6 使用預(yù)處理語句
準(zhǔn)備語句(db.Prepare()或者tx.Prepare())在Golang中具有所有常見的優(yōu)點(diǎn):安全性,效率酵熙,方便性轧简。但是他們的實(shí)現(xiàn)方式與你習(xí)慣的方式可能有所不同,特別是關(guān)于它們?nèi)绾闻cdatabase/sql的一些內(nèi)部組件進(jìn)行交互的方式匾二。
準(zhǔn)備語句和連接
在數(shù)據(jù)庫級(jí)別哮独,將準(zhǔn)備好的語句綁定到單個(gè)數(shù)據(jù)庫連接。典型的流程是:客戶端向服務(wù)器發(fā)送帶有占位符的SQL語句以進(jìn)行準(zhǔn)備假勿,服務(wù)器使用語句ID進(jìn)行響應(yīng)借嗽,然后客戶端通過發(fā)送其ID和參數(shù)來執(zhí)行該語句。
然而在Golang中转培,連接不會(huì)直接暴露給database/sql包的用戶恶导。你不準(zhǔn)備連接上語句。你準(zhǔn)備好在一個(gè)db或tx浸须。并且database/sql具有一些便捷的行為惨寿,如自動(dòng)重試。由于這些原因删窒,準(zhǔn)備好的語句和連接(存在于驅(qū)動(dòng)級(jí)別)之間的潛在關(guān)聯(lián)被隱藏在代碼中裂垦。
下面是它的工作原理:
準(zhǔn)備一個(gè)語句時(shí),它會(huì)在池中的連接上準(zhǔn)備好肌索。
Stmt對(duì)象記住使用哪個(gè)連接蕉拢。
當(dāng)你執(zhí)行Stmt時(shí),它試圖使用Stmt對(duì)象記住的那個(gè)連接(后面我們將這里的連接稱為原始連接)诚亚。如果它不可用晕换,因?yàn)樗P(guān)閉或忙于做其他事情,它從池中獲取另一個(gè)連接站宗,并在另一個(gè)連接上重新準(zhǔn)備與數(shù)據(jù)庫的語句闸准。
因?yàn)樵谠歼B接繁忙時(shí),會(huì)根據(jù)需要重新準(zhǔn)備語句梢灭,因此數(shù)據(jù)庫的高并發(fā)使用可能會(huì)導(dǎo)致大量連接繁忙夷家,從而創(chuàng)建大量的準(zhǔn)備語句蒸其。這會(huì)導(dǎo)致語句的明顯泄露,正在準(zhǔn)備和重新準(zhǔn)備的語句比你想象的更多库快,甚至?xí)绊懙椒?wù)器端對(duì)語句數(shù)量的限制摸袁。
避免準(zhǔn)備好的語句
Golang將為你在封面下創(chuàng)建準(zhǔn)備好的聲明。例如缺谴,一個(gè)簡(jiǎn)單的db.Query(sql,param1,param2)通過準(zhǔn)備sql但惶,然后使用參數(shù)執(zhí)行它耳鸯,最后關(guān)閉語句湿蛔。
有時(shí),準(zhǔn)備好的語句并不是你想要的县爬。這可能有幾個(gè)原因阳啥。
數(shù)據(jù)庫不支持準(zhǔn)備好的語句。例如财喳,當(dāng)使用MySql驅(qū)動(dòng)時(shí)察迟,你可以連接到MemSql和Sphinx,因?yàn)樗鼈冎С諱ySql線路協(xié)議耳高。但是它們不支持包含準(zhǔn)備語句的“二進(jìn)制”協(xié)議扎瓶,因此它們會(huì)以混亂的方式失敗。
這些語句沒有重用到足以使它們變得有價(jià)值泌枪,而安全問題則以其他方式處理概荷,因此性能開銷是不需要的。這方面點(diǎn)擊VividCortex博客可以看到一個(gè)例子碌燕。
如果不想使用預(yù)處理語句误证,則需要使用fmt.Sprint()或類似的方法來組合SQL,并將其作為db.Query()或db.QueryRow()的唯一參數(shù)傳遞修壕。你的驅(qū)動(dòng)需要支持明文查詢執(zhí)行愈捅,這是通過執(zhí)行器(Execer是一個(gè)結(jié)構(gòu)體)和查詢器(Queryer是一個(gè)結(jié)構(gòu)體)接口在Golang 1.1中添加的,在此記錄慈鸠。
事務(wù)中的準(zhǔn)備語句
在Tx中創(chuàng)建的準(zhǔn)備語句僅限于它蓝谨,因此早期關(guān)于重新準(zhǔn)備的注意事項(xiàng)不適用。當(dāng)你對(duì)Tx對(duì)象進(jìn)行操作時(shí)青团,你的操作直接映射到它下面唯一的一個(gè)連接上譬巫。
這也意味著在Tx內(nèi)創(chuàng)建的準(zhǔn)備語句不能與之分開使用。同樣壶冒,在DB中創(chuàng)建的準(zhǔn)備語句不能再事務(wù)中使用缕题,因?yàn)樗鼈儗⒈唤壎ǖ讲煌倪B接。
要在Tx中使用事務(wù)外的預(yù)處理語句胖腾,可以使用Tx.Stmt()烟零,它將從事務(wù)外部準(zhǔn)備一個(gè)新的特定于事務(wù)的語句瘪松。它通過采用現(xiàn)有的預(yù)處理語句,設(shè)置與事務(wù)的連接锨阿,并在執(zhí)行時(shí)重新準(zhǔn)備所有語句宵睦。這個(gè)行為及其實(shí)現(xiàn)是不可取的,甚至在databse/sql源代碼中有一個(gè)TODO來改進(jìn)它墅诡;我們建議不要使用這個(gè)壳嚎。
在處理事務(wù)中的預(yù)處理語句時(shí),必須小心謹(jǐn)慎末早。請(qǐng)考慮下面的示例:
tx, err := db.Begin()
if err != nil {
log.Fatal(err)
}
defer tx.Rollback()
stmt, err := tx.Prepare("INSERT INTO foo VALUES (?)")
if err != nil {
log.Fatal(err)
}
defer stmt.Close() // danger!
for i := 0; i < 10; i++ {
_, err = stmt.Exec(i)
if err != nil {
log.Fatal(err)
}
}
err = tx.Commit()
if err != nil {
log.Fatal(err)
}
// stmt.Close() runs here!
之前Golang1.4關(guān)閉*sql.Tx將與之關(guān)聯(lián)的連接返還到池中烟馅,但是,在預(yù)處理語句結(jié)束后然磷,延遲調(diào)用時(shí)在那之后發(fā)生的郑趁,這可能導(dǎo)致并發(fā)訪問底層的連接,使連接狀態(tài)不一致姿搜。如果使用Golang1.4或更高的版本寡润,則應(yīng)確保在提交事務(wù)或回滾之前聲明始終關(guān)閉。點(diǎn)擊查看這個(gè)issues舅柜。
參數(shù)占位符語法
預(yù)處理語句中的占位符參數(shù)的語法是特定于數(shù)據(jù)庫的梭纹。例如,比較MySql,PostgreSQL,Oracle:
MySQL PostgreSQL Oracle
===== ========== ======
WHERE col = ? WHERE col = $1 WHERE col = :col
VALUES(?, ?, ?) VALUES($1, $2, $3) VALUES(:val1, :val2, :val3)
No.7 錯(cuò)誤處理
幾乎所有使用database/sql類型的操作都會(huì)返回一個(gè)錯(cuò)誤作為最后一個(gè)值致份。你應(yīng)該總是檢查這些錯(cuò)誤变抽,千萬不要忽視它們。有幾個(gè)地方錯(cuò)誤行為是特殊情況知举,還有一些額外的東西可能需要知道瞬沦。
遍歷結(jié)果集的錯(cuò)誤
請(qǐng)思考下面的代碼:
for rows.Next() {
// ...
}
if err = rows.Err(); err != nil {
// handle the error here
}
來自rows.Err()的錯(cuò)誤可能是rows.Next()循環(huán)中各種錯(cuò)誤的結(jié)果。除了正常完成循環(huán)之外雇锡,循環(huán)可能會(huì)退出逛钻,因此你總是需要檢查循環(huán)是否正常終止。異常終止自動(dòng)調(diào)用rows.Close()锰提,盡管多次調(diào)用它是無害的曙痘。
關(guān)閉結(jié)果集的錯(cuò)誤
如上所述,如果你過早的退出循環(huán)立肘,則應(yīng)該總是顯式的關(guān)閉sql.Rows边坤。如果循環(huán)正常退出或通過錯(cuò)誤,它會(huì)自動(dòng)關(guān)閉谅年,但你可能會(huì)錯(cuò)誤的執(zhí)行此操作:
for rows.Next() {
// ...
break; // whoops, rows is not closed! memory leak...
}
// do the usual "if err = rows.Err()" [omitted here]...
// it's always safe to [re?]close here:
if err = rows.Close(); err != nil {
// but what should we do if there's an error?
log.Println(err)
}
rows.Close()返回的錯(cuò)誤是一般規(guī)則的唯一例外茧痒,最好是捕獲并檢查所有數(shù)據(jù)庫操作中的錯(cuò)誤。如果rows.Close()返回錯(cuò)誤融蹂,那么你應(yīng)該怎么做旺订。記錄錯(cuò)誤信息或panic可能是唯一明智的事情弄企,如果這不明智,那么也許你應(yīng)該忽略錯(cuò)誤区拳。
QueryRow()的錯(cuò)誤
思考下面的代碼來獲取一行數(shù)據(jù):
var name string
err = db.QueryRow("select name from users where id = ?", 1).Scan(&name)
if err != nil {
log.Fatal(err)
}
fmt.Println(name)
如果沒有id = 1的用戶怎么辦拘领?那么結(jié)果中不會(huì)有行,而.Scan()不會(huì)將值掃描到name中樱调。那會(huì)怎么樣约素?
Golang定義了一個(gè)特殊的錯(cuò)誤常量,稱為sql.ErrNoRows笆凌,當(dāng)結(jié)果為空時(shí)圣猎,它將從QueryRow()返回。這在大多數(shù)情況下需要作為特殊情況來處理菩颖⊙幔空的結(jié)果通常不被應(yīng)用程序代碼認(rèn)為是錯(cuò)誤的,如果不檢查錯(cuò)誤是不是這個(gè)特殊常量晦闰,那么會(huì)導(dǎo)致你意想不到的應(yīng)用程序代碼錯(cuò)誤。
來自查詢的錯(cuò)誤被推遲到調(diào)用Scan()鳍怨,然后從中返回呻右。上面的代碼可以更好地寫成這樣:
var name string
err = db.QueryRow("select name from users where id = ?", 1).Scan(&name)
if err != nil {
if err == sql.ErrNoRows {
// there were no rows, but otherwise no error occurred
} else {
log.Fatal(err)
}
}
fmt.Println(name)
有人可能會(huì)問為什么一個(gè)空的結(jié)果集被認(rèn)為是一個(gè)錯(cuò)誤⌒空集沒有什么錯(cuò)誤声滥。原因是QueryRow()方法需要使用這種特殊情況才能讓調(diào)用者區(qū)分是否QueryRow()實(shí)際上找到一行;沒有它侦香,Scan(0不會(huì)做任何事情落塑,你可能不會(huì)意識(shí)到你的變量畢竟沒有從數(shù)據(jù)庫中獲取任何值。
當(dāng)你使用QueryRow()時(shí)罐韩,你應(yīng)該只會(huì)遇到此錯(cuò)誤憾赁。如果你在別處遇到這個(gè)錯(cuò)誤,你就做錯(cuò)了什么散吵。
識(shí)別特定的數(shù)據(jù)庫錯(cuò)誤
像下面這樣編寫代碼是很有誘惑力的:
rows, err := db.Query("SELECT someval FROM sometable")
// err contains:
// ERROR 1045 (28000): Access denied for user 'foo'@'::1' (using password: NO)
if strings.Contains(err.Error(), "Access denied") {
// Handle the permission-denied error
}
這不是最好的方法龙考。例如,字符串值可能會(huì)取決于服務(wù)器使用什么語言發(fā)送錯(cuò)誤消息矾睦。比較錯(cuò)誤編號(hào)以確定具體錯(cuò)誤是啥要好得多晦款。
但是,驅(qū)動(dòng)的機(jī)制不同枚冗,因?yàn)檫@不是database/sql本身的一部分缓溅。在本教程重點(diǎn)介紹的MySql驅(qū)動(dòng)中,你可以編寫以下代碼:
if driverErr, ok := err.(*mysql.MySQLError); ok { // Now the error number is accessible directly
if driverErr.Number == 1045 {
// Handle the permission-denied error
}
}
再次赁温,這里的MySQLError類型由此特定驅(qū)動(dòng)程序提供坛怪,并且驅(qū)動(dòng)程序之間的.Number字段可能不同州藕。然而,該值是從MySql的錯(cuò)誤消息中提取的酝陈,因此是特定于數(shù)據(jù)庫的床玻,而不是特定于驅(qū)動(dòng)的。
這段代碼還是很丑相對(duì)于1045沉帮,一個(gè)魔術(shù)數(shù)字是一種代碼氣味锈死。一些驅(qū)動(dòng)(雖然不是MySql的驅(qū)動(dòng)程序,因?yàn)檫@里的主題的原因)提供錯(cuò)誤標(biāo)識(shí)符的列表穆壕。例如Postgres pg驅(qū)動(dòng)程序在error.go中待牵。還有一個(gè)由VividCortex維護(hù)的MySql錯(cuò)誤號(hào)的外部包。使用這樣的列表喇勋,上面的代碼寫的更漂亮:
if driverErr, ok := err.(*mysql.MySQLError); ok {
if driverErr.Number == mysqlerr.ER_ACCESS_DENIED_ERROR {
// Handle the permission-denied error
}
}
處理連接錯(cuò)誤
如果與數(shù)據(jù)庫的連接被丟棄缨该,殺死或發(fā)生錯(cuò)誤該怎么辦?
當(dāng)發(fā)生這種情況時(shí)川背,你不需要實(shí)現(xiàn)任何邏輯來重試失敗的語句贰拿。作為database/sql連接池的一部分,處理失敗的連接是內(nèi)置的熄云。如果你執(zhí)行查詢或其他語句膨更,底層連接失敗,則Golang將重新打開一個(gè)新的連接(或從連接池中獲取另一個(gè)連接)缴允,并重試10次荚守。
然而,可能會(huì)產(chǎn)生一些意想不到的后果练般。當(dāng)某些類型錯(cuò)誤可能會(huì)發(fā)生其他錯(cuò)誤條件矗漾。這也可能是驅(qū)動(dòng)程序特定的。MySql驅(qū)動(dòng)程序發(fā)生的一個(gè)例子是使用KILL取消不需要的語句(例如長(zhǎng)時(shí)間運(yùn)行的查詢)會(huì)導(dǎo)致語句被重試10次薄料。
No.8 使用空值
可以為空的字段是令人煩惱的敞贡,并導(dǎo)致很多丑陋的代碼。如果可以都办,避開它們嫡锌。如果沒有,那么你需要使用database/sql包中的特殊類型來處理它們琳钉,或者定義你自己的類型势木。
有可以空的布爾值,字符串歌懒,整數(shù)和浮點(diǎn)數(shù)的類型啦桌。下面是你使用它們的方法:
for rows.Next() {
var s sql.NullString
err := rows.Scan(&s)
// check err
if s.Valid {
// use s.String
} else {
// NULL value
}
}
可以空的類型的限制和避免的理由的情況下你需要更有說服力的可以為空的列:
沒有sql.NullUint64或sql.NullYourFavoriteType。你需要為這個(gè)定義你自己的。
可空性可能會(huì)非常棘手甫男,并不是未來的證明且改。如果你認(rèn)為某些內(nèi)容不會(huì)為空,但是你錯(cuò)了板驳,你的程序?qū)?huì)崩潰又跛,也許很少會(huì)發(fā)生錯(cuò)誤。
Golang的好處之一是為每個(gè)變量設(shè)置一個(gè)有用的默認(rèn)零值若治。這不是空的工作方式慨蓝。
如果你需要定義自己的類型來處理NULLS,則可以復(fù)制sql.NullString的設(shè)計(jì)來實(shí)現(xiàn)端幼。
如果你不能避免在你的數(shù)據(jù)庫中具有空值礼烈,周圍有多數(shù)數(shù)據(jù)庫系統(tǒng)支持的另一項(xiàng)工作是COALESCE()。像下面這樣的東西可能是可以使用的婆跑,而不需要引入大量的sql.Null*類型
rows, err := db.Query(`
SELECT
name,
COALESCE(other_field, '') as other_field
WHERE id = ?
`, 42)
for rows.Next() {
err := rows.Scan(&name, &otherField)
// ..
// If `other_field` was NULL, `otherField` is now an empty string. This works with other data types as well.
}
No.9 使用未知列
Scan()函數(shù)要求你準(zhǔn)確傳遞正確數(shù)目的目標(biāo)變量此熬。如果你不知道查詢將返回什么呢?
如果你不知道查詢將返回多少列滑进,則可以使用Columns()來查詢列名稱列表犀忱。你可以檢查此列表的長(zhǎng)度以查看有多少列,并且可以將切片傳遞給具有正確數(shù)值的Scan()郊供。列如峡碉,MySql的某些fork為SHOW PROCESSLIST命令返回不同的列,因此你必須為此準(zhǔn)備好驮审,否則將導(dǎo)致錯(cuò)誤,這是一種方法吉执;還有其他的方法:
cols, err := rows.Columns()
if err != nil {
// handle the error
} else {
dest := []interface{}{ // Standard MySQL columns
new(uint64), // id
new(string), // host
new(string), // user
new(string), // db
new(string), // command
new(uint32), // time
new(string), // state
new(string), // info
}
if len(cols) == 11 {
// Percona Server
} else if len(cols) > 8 {
// Handle this case
}
err = rows.Scan(dest...)
// Work with the values in dest
}
如果你不知道這些列或者它們的類型疯淫,你應(yīng)該使用sql.RawBytes。
cols, err := rows.Columns() // Remember to check err afterwards
vals := make([]interface{}, len(cols))
for i, _ := range cols {
vals[i] = new(sql.RawBytes)
}
for rows.Next() {
err = rows.Scan(vals...)
// Now you can check each element of vals for nil-ness,
// and you can use type introspection and type assertions
// to fetch the column into a typed variable.
}
No.10 連接池
database/sql包中有一個(gè)基本的連接池戳玫。沒有很多的控制或檢查能力熙掺,但這里有一些你可能會(huì)發(fā)現(xiàn)有用的知識(shí):
? 連接池意味著在單個(gè)數(shù)據(jù)庫上執(zhí)行兩個(gè)連續(xù)的語句可能會(huì)打開兩個(gè)鏈接并單獨(dú)執(zhí)行它們。對(duì)于程序員來說咕宿,為什么它們的代碼行為不當(dāng)币绩,這是相當(dāng)普遍的。例如府阀,后面跟著INSERT的LOCK TABLES可能會(huì)被阻塞缆镣,因?yàn)镮NSERT位于不具有表鎖定的連接上。
? 連接是在需要時(shí)創(chuàng)建的试浙,池中沒有空閑連接董瞻。
? 默認(rèn)情況下,連接數(shù)量沒有限制。如果你嘗試同時(shí)執(zhí)行很多操作钠糊,可以創(chuàng)建任意數(shù)量的連接挟秤。這可能導(dǎo)致數(shù)據(jù)庫返回錯(cuò)誤,例如“連接太多”抄伍。
? 在Golang1.1或更新版本中艘刚,你可以使用db.SetMaxIdleConns(N)來限制池中的空閑連接數(shù)。這并不限制池的大小截珍。
? 在Golang1.2.1或更新版本中攀甚,可以使用db.SetMaxOpenConns(N)來限制于數(shù)據(jù)庫的總打開連接數(shù)。不幸的是笛臣,一個(gè)死鎖bug(修復(fù))阻止db.SetMaxOpenConns(N)在1.2中安全使用云稚。
? 連接回收相當(dāng)快。使用db.SetMaxIdleConns(N)設(shè)置大量空閑連接可以減少此流失沈堡,并有助于保持連接以重新使用静陈。
? 長(zhǎng)期保持連接空閑可能會(huì)導(dǎo)致問題(例如在微軟azure上的這個(gè)問題)。嘗試db.SetMaxIdleConns(0)如果你連接超時(shí)诞丽,因?yàn)檫B接空閑時(shí)間太長(zhǎng)鲸拥。
No.11 驚喜,反模式和限制
雖然database/sql很簡(jiǎn)單僧免,但一旦你習(xí)慣了它刑赶,你可能會(huì)對(duì)它支持的用例的微妙之處感到驚訝。這是Golang的核心庫通用的懂衩。
資源枯竭
如本網(wǎng)站所述撞叨,如果你不按預(yù)期使用database/sql,你一定會(huì)為自己造成麻煩浊洞,通常是通過消耗一些資源或阻止它們被有效的重用:
? 打開和關(guān)閉數(shù)據(jù)庫可能會(huì)導(dǎo)致資源耗盡牵敷。
? 沒有讀取所有行或使用rows.Close()保留來自池的連接。
? 對(duì)于不返回行的語句法希,使用Query()將從池中預(yù)留一個(gè)連接枷餐。
? 沒有意識(shí)到預(yù)處理語句如何工作會(huì)導(dǎo)致大量額外的數(shù)據(jù)庫活動(dòng)。
巨大的uint64值
這里有一個(gè)令人吃驚的錯(cuò)誤苫亦。如果設(shè)置了高位毛肋,就不能將大的無符號(hào)整數(shù)作為參數(shù)傳遞給語句:
_, err := db.Exec("INSERT INTO users(id) VALUES", math.MaxUint64) // Error
這將拋出一個(gè)錯(cuò)誤。如果你使用uint64值要小心屋剑,因?yàn)樗鼈兛赡荛_始小而且無錯(cuò)誤的工作润匙,但會(huì)隨著時(shí)間的推移而增加,并開始拋出錯(cuò)誤饼丘。
連接狀態(tài)不匹配
有些事情可以改變連接狀態(tài)趁桃,這可能導(dǎo)致的問題有兩個(gè)原因:
某些連接狀態(tài),比如你是否處于事務(wù)中,應(yīng)該通過Golang類型來處理卫病。
你可能假設(shè)你的查詢?cè)趩蝹€(gè)連接上運(yùn)行油啤。
例如,使用USE語句設(shè)置當(dāng)前數(shù)據(jù)庫對(duì)于很多人來說是一個(gè)典型的事情蟀苛。但是在Golang中益咬,它只會(huì)影響你運(yùn)行的連接。除非你處于事務(wù)中帜平,否則你認(rèn)為在該連接上執(zhí)行的其他語句實(shí)際上可能在從池中獲取的不同的連接上運(yùn)行幽告,因此它們不會(huì)看到這些更改的影響。
此外,在更改連接后,它將返回到池数尿,并可能會(huì)污染其他代碼的狀態(tài)壳猜。這就是為什么你不應(yīng)該直接將BEGIN或COMMIT語句作為SQL命令發(fā)出的原因之一爱榕。
數(shù)據(jù)庫特定的語法
database/sql API提供了面向行的數(shù)據(jù)庫抽象,但是具體的數(shù)據(jù)庫和驅(qū)動(dòng)程序可能會(huì)在行為或語法上有差異,例如預(yù)處理語句占位符。
多個(gè)結(jié)果集
Golang驅(qū)動(dòng)程序不以任何方式支持單個(gè)查詢中的多個(gè)結(jié)果集叨叙,盡管有一個(gè)支持大容量操作(如批量復(fù)制)的功能請(qǐng)求似乎沒有任何計(jì)劃。
這意味著堪澎,除了別的以外擂错,返回多個(gè)結(jié)果集的存儲(chǔ)過程將無法正常工作。
調(diào)用存儲(chǔ)過程
調(diào)用存儲(chǔ)過程是特定于驅(qū)動(dòng)程序的樱蛤,但在MySql驅(qū)動(dòng)程序中钮呀,目前無法完成∽蚍玻看來你可以調(diào)用一個(gè)簡(jiǎn)單的過程來返回一個(gè)單一的結(jié)果集行楞,通過執(zhí)行如下的操作:
err := db.QueryRow("CALL mydb.myprocedure").Scan(&result) // Error
事實(shí)上這行不通。你將收到以下錯(cuò)誤1312:PROCEDURE mydb.myprocedure無法返回給定上下文中的結(jié)果集土匀。這是因?yàn)镸ySql希望將連接設(shè)置為多語句模式,即使單個(gè)結(jié)果形用,并且驅(qū)動(dòng)程序當(dāng)前沒有執(zhí)行此操作(盡管看到這個(gè)問題)就轧。
多個(gè)聲明支持
database/sql沒有顯式的擁有多個(gè)語句支持,這意味著這個(gè)行為是后端依賴的:
_, err := db.Exec("DELETE FROM tbl1; DELETE FROM tbl2") // Error/unpredictable result
服務(wù)器可以解釋它想要的田度,它可以包括返回的錯(cuò)誤妒御,只執(zhí)行第一個(gè)語句,或執(zhí)行兩者镇饺。
同樣乎莉,在事務(wù)中沒有辦法批處理語句。事務(wù)中的每個(gè)語句必須連續(xù)執(zhí)行,并且結(jié)果中的資源(如行或行)必須被掃描或關(guān)閉惋啃,以便底層連接可供下一個(gè)語句使用哼鬓。這與通常不在事務(wù)中工作時(shí)的行為不同。在這種情況下边灭,完全可以執(zhí)行查詢异希,循環(huán)遍歷行,并在循環(huán)中對(duì)數(shù)據(jù)庫進(jìn)行查詢(這將發(fā)生在一個(gè)新的連接上):
rows, err := db.Query("select * from tbl1") // Uses connection 1
for rows.Next() {
err = rows.Scan(&myvariable)
// The following line will NOT use connection 1, which is already in-use
db.Query("select * from tbl2 where id = ?", myvariable)
}
但是事務(wù)只綁定到一個(gè)連接绒瘦,所以事務(wù)不可能做到這一點(diǎn):
tx, err := db.Begin()
rows, err := tx.Query("select * from tbl1") // Uses tx's connection
for rows.Next() {
err = rows.Scan(&myvariable)
// ERROR! tx's connection is already busy!
tx.Query("select * from tbl2 where id = ?", myvariable)
}
不過称簿,Golang不會(huì)阻止你去嘗試。因此惰帽,如果你試圖在第一個(gè)釋放資源并自行清理之前嘗試執(zhí)行另一個(gè)語句憨降,可能會(huì)導(dǎo)致一個(gè)損壞的連接。這也意味著事務(wù)中的每個(gè)語句都會(huì)產(chǎn)生一組單獨(dú)的網(wǎng)絡(luò)往返數(shù)據(jù)庫该酗。
No.12 相關(guān)資料
以下是我們發(fā)現(xiàn)有幫助的一些外部信息來源授药。
? 官方database/sql源碼可能需要vpn打開
? MySql驅(qū)動(dòng)作者jmoiron關(guān)于驅(qū)動(dòng)的說明
? pregresql驅(qū)動(dòng)作者VividCortex博客透明加密
我們希望本教程是有幫助的。如果你有任何改進(jìn)意見垂涯,請(qǐng)?jiān)?a target="_blank" rel="nofollow">https://github.com/VividCortex/go-database-sql-tutorial.database-sql-tutorial)的ISSUE中提出烁焙。