1. 程式人生 > >Go database/sql文件

Go database/sql文件

Go database/sql文件

 

No.1 文件概要

在Golang中使用SQL或類似SQL的資料庫的慣用方法是通過 database/sql 包操作。它為面向行的資料庫提供了輕量級的介面。這篇文章是關於如何使用它,最常見的參考。

為什麼需要這個?包文件告訴你每件事情都做了什麼,但它並沒有告訴你如何使用這個包。我們很多人都希望自己能快速參考和入門的方法,而不是講故事。歡迎捐款;請在這裡傳送請求。

在Golang中你用sql.DB訪問資料庫。你可以使用此型別建立語句和事務,執行查詢,並獲取結果。下面的程式碼列出了sql.DB是一個結構體,點選 

database/sql/sql.go 檢視官方原始碼。

首先你應該知道一個sql.DB不是一個數據庫的連線。它也沒有對映到任何特點資料庫軟體的“資料庫”或“模式”的概念。它是資料庫的介面和資料庫的抽象,它可能與本地檔案不同,可以通過網路連線訪問,也可以在記憶體和程序中訪問。

sql.DB為你在幕後執行一些重要的任務:

• 通過驅動程式開啟和關閉實際的底層資料庫的連線。
• 它根據需要管理一個連線池,這可能是如上所述的各種各樣的事情。

sql.DB抽象旨在讓你不必擔心如何管理對基礎資料儲存的併發訪問。一個連線在使用它執行任務時被標記為可用,然後當它不在使用時返回到可用的池中。這樣的後果之一是,如果你無法將連線釋放到池中,則可能導致db.SQL開啟大量連線,可能會耗盡資源(連線太多,開啟的檔案控制代碼太多,缺少可用網路埠等)。稍後我們將進一步討論這個問題。

在建立sql.DB之後,你可以用它來查詢它所代表的資料庫,以及建立語句和事務。

No.2 匯入資料庫驅動

要使用 database/sql,你需要 database/sql 自身,以及需要使用的特定的資料庫驅動。

你通常不應該直接使用驅動包,儘管有些驅動鼓勵你這樣做。(在我們看來,這通常是個壞主意。) 相反的,如果可能,你的程式碼應該僅引用 database/sql 中定義的型別。這有助於避免使你的程式碼依賴於驅動,從而可以通過最少的程式碼來更改底層驅動(因此訪問的資料庫)。它還強制你使用Golang習慣用法,而不是特定驅動作者可能提供的特定的習慣用法。

在本文件中,我們將使用@julienschmidt 和 @arnehormann中優秀的MySql驅動。

將以下內容新增到Go原始檔的頂部(也就是package name下面):

import (
    "database/sql"
    _ "github.com/go-sql-driver/mysql"
)

注意我們正在載入的驅動是匿名的,將其限定符別名為_,因此我們的程式碼中沒有一個匯出的名稱可見。在引擎下,驅動將自身註冊為可用於 database/sql 包,但一般來說沒有其他情況發生。

現在你已經準備好訪問資料庫了。

No.3 訪問資料庫

現在你已經載入了驅動包,就可以建立一個數據庫物件sql.DB。建立一個sql.DB你可以使用sql.Open()。Open返回一個*sql.DB。

func main() {
    db, err := sql.Open("mysql",
        "user:[email protected](127.0.0.1:3306)/hello")
    if err != nil {
        log.Fatal(err)
    }
    defer db.Close()
}

在示例中,我們演示了幾件事:

  1. sql.Open的第一個引數是驅動名稱。這是驅動用來註冊database/sql的字串,並且通常與包名相同以避免混淆。例如,它是github.com/go-sql-driver/mysql的MySql驅動(作者:jmhodges)。某些驅動不遵守公約的名稱,例如github.com/mattn/go-sqlite3的sqlite3(作者:matte)和github.com/lib/pq的postgres(作者:mjibson)。

  2. 第二個引數是一個驅動特定的語法,它告訴驅動如何訪問底層資料儲存。在本例中,我們將連線本地的MySql伺服器例項中的“hello”資料庫。

  3. 你應該(幾乎)總是檢查並處理從所有database/sql操作返回的錯誤。有一些特殊情況,我們稍後將討論這樣做事沒有意義的。

  4. 如果sql.DB不應該超出該函式的作用範圍,則延遲函式defer db.Close()是慣用的。

也許是反直覺的,sql.Open()不建立與資料庫的任何連線,也不會驗證驅動連線引數。相反,它只是準備資料庫抽象以供以後使用。首次真正的連線底層資料儲存區將在第一次需要時懶惰地建立。如果你想立即檢查資料庫是否可用(例如,檢查是否可以建立網路連線並登陸),請使用db.Ping()來執行此操作,記得檢查錯誤:

err = db.Ping()
if err != nil {
    // do something here
}

雖然在完成資料庫之後Close()資料庫是慣用的,但是sql.DB物件被設計為長連線。不要經常Open()和Close()資料庫。相反,為你需要訪問的每個不同的資料儲存建立一個sql.DB物件,並保留它,直到程式訪問資料儲存完畢。在需要時傳遞它,或在全域性範圍內使其可用,但要保持開放。並且不要從短暫的函式中Open()和Close()。相反,通過sql.DB作為引數傳遞給該短暫的函式。

如果你不把sql.DB視為長期存在的物件,則可能會遇到諸如重複使用和連線共享不足,耗盡可用的網路資源以及由於TIME_WAIT中剩餘大量TCP連線而導致的零星故障的狀態。這些問題表明你沒有像設計的那樣使用database/sql的跡象。

現在是時候使用你的sql.DB物件了。

No.4 檢索結果集

有幾個慣用的操作來從資料儲存中檢索結果。

  1. 執行返回行的查詢。

  2. 準備重複使用的語句,多次執行並銷燬它。

  3. 以一次關閉的方式執行語句,不準備重複使用。

  4. 執行一個返回單行的查詢。這種特殊情況有一個捷徑。

Golang的database/sql函式名非常重要。如果一個函式名包含查詢Query(),它被設計為詢問資料庫的問題,並返回一組行,即使它是空的。不返回行的語句不應該使用Query()函式;他們應該使用Exec()。

從資料庫獲取資料

讓我們來看一下如何查詢資料庫,使用Query的例子。我們將向用戶表查詢id為1的使用者,並打印出使用者的id和name。我們將使用rows.Scan()將結果分配給變數,一次一行。

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)
}

下面是上面程式碼中正在發生的事情:

  1. 我們使用db.Query()將查詢傳送到資料庫。我們像往常一樣檢查錯誤。

  2. 我們用defer內建函式推遲了rows.Close()的執行。這個非常重要。

  3. 我們用rows.Next()遍歷了資料行。

  4. 我們用rows.Scan()讀取每行中的列變數。

  5. 我們完成遍歷行之後檢查錯誤。

這幾乎是Golang中唯一的辦法。例如,你不能將一行作為對映來獲取。這是因為所有東西都是強型別的。你需要建立正確型別的變數並將指標傳遞給它們,如圖所示。

其中的幾個部分很容易出錯,可能會產生不良後果。

• 你應該總是檢查rows.Next()迴圈結尾處的錯誤。如果迴圈中出現錯誤,則需要了解它。不要僅僅假設迴圈遍歷,直到你已經處理了所有的行。

• 第二,只要有一個開啟的結果集(由行代表),底層連線就很忙,不能用於任何其他查詢。這意味著它在連線池中不可用。如果你使用rows.Next()遍歷所有行,最終將讀取最後一行,rows.Next()將遇到內部EOF錯誤,併為你呼叫rows.Close()。但是,如果由於某種原因退出該迴圈-提前返回,那麼行不會關閉,並且連線保持開啟狀態。(如果rows.Next()由於錯誤而返回false,則會自動關閉)。這是一種簡單耗盡資源的方法。

• rows.Close()是一種無害的操作,如果它已經關閉,所以你可以多次呼叫它。但是請注意,我們首先檢查錯誤,如果沒有錯誤,則呼叫rows.Close(),以避免執行時的panic。

• 你應該總是用延遲語句defer推遲rows.Close(),即使你也在迴圈結束時呼叫rows.Close(),這不是一個壞主意。

• 不要在迴圈中用defer推遲。延遲語句在函式退出之前不會執行,所以長時間執行的函式不應該使用它。如果你這樣做,你會慢慢積累記憶。如果你在迴圈中反覆查詢和使用結果集,則在完成每個結果後應顯示的呼叫rows.Close(),而不用延遲語句defer。

Scan()如何工作

當你遍歷行並將其掃描到目標變數中時,Golang會在幕後為你執行資料型別轉換。它基於目標變數的型別。意識到這一點可以乾淨你的程式碼,並幫助避免重複工作。

例如,假設你從表中選擇了一些行,這是用字串列定義的。如varchar(45)或類似的列。然而,你碰巧知道表格總是包含數字。如果傳遞指向字串的指標,Golang會將位元組複製到字串中。現在可以使用strconv.ParseInt()或類似的方式將值轉換為數字。你必須檢查SQL操作中的錯誤以及解析整數的錯誤。這又亂又糟糕。

或者,你可以通過Scan()指向一個整數即可。Golang會檢測到併為你呼叫strconv.ParseInt()。如果有轉換錯誤,則呼叫Scan()將返回它。你的程式碼現在更小更整潔。這是推薦使用database/sql的方法。

準備查詢

一般來說,你應該總是準備多次使用查詢。準備查詢的結果是一個準備語句,可以為執行語句時提供的引數,提供佔位符(a.k.a bind值)。這比連線字串更好,出於所有通常的理由(例如避免SQL注入攻擊)。

在MySql中,引數佔位符為?,在PostgreSql中為$N,其中N為數字。SQLite接受這兩者之一。在Oracle中佔位符以冒號開始,並命名為:param1。本文件中我們使用?佔位符,因為我們使用MySql作為示例。

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()實際上準備,執行和關閉一個準備好的語句。這是資料庫的三次往返。如果你不小心,可以使應用程式的資料庫互動數量增加三倍!有些驅動可以在特定情況下避免這種情況,但並非所有驅動都可以這樣做。點選prepared statements檢視更多宣告。

單行查詢

如果一個查詢返回最多一行,可以使用一些快速的樣板程式碼:

var name string
err = db.QueryRow("select name from users where id = ?", 1).Scan(&name)
if err != nil {
    log.Fatal(err)
}
fmt.Println(name)

來自查詢的錯誤將被推遲到Scan(),然後返回。你也可以在準備的語句中呼叫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 修改資料和使用事務

現在我們已經準備好了如何修改資料和處理事務。如果你習慣於使用“statement”物件來獲取行並更新資料,那麼這種區別可能視乎是認為的,但是在Golang中有一個重要的原因。

修改資料的statements

使用Exec(),最好用一個準備好的statement來完成INSERT,UPDATE,DELETE或者其他不返回行的語句。下面的示例演示如何插入行並檢查有關操作的元資料:

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)

執行該語句將生成一個sql.Result,該語句提供對statement元資料的訪問:最後插入的ID和行數受到影響。

如果你不在乎結果怎麼辦?如果你只想執行一個語句並檢查是否有錯誤,但忽略結果該怎麼辦?下面兩個語句不會做同樣的事情嗎?

_, err := db.Exec("DELETE FROM users")  // OK
_, err := db.Query("DELETE FROM users") // BAD

答案是否定的。他們不做同樣的事情,你不應該使用Query()。Query()將返回一個sql.Rows,它保留資料庫連線,直到sql.Rows關閉。由於可能有未讀資料(例如更多的資料行),所以不能使用連線。在上面的示例中,連線將永遠不會被釋放。垃圾回收器最終會關閉底層的net.Conn,但這可能需要很長時間。此外,database/sql包將繼續跟蹤池中的連線,希望在某個時候釋放它,以便可以再次使用連線。因此,這種反模式是耗盡資源的好方法(例如連線數太多)。

事務處理

在Golang中,事務本質上是保留與資料儲存的連線的物件。它允許你執行我們迄今為止所看到的所有操作,但保證它們將在同一連線上執行。

你可以通過呼叫db.Begin()開始一個事務,並在結果Tx變數上用Commit()或Rollback()方法關閉它。在封面下,Tx從池中獲取連線,並保留它僅用於該事務。Tx上的方法一對一到可以呼叫資料本本身的方法,例如Query()等等。

在事務中建立的Prepare語句僅限於該事務。點選prepared statements檢視更多準備的宣告。

你不應該在SQL程式碼中混合BEGIN和COMMIT相關的函式(如Begin()和Commit()的SQL語句),可能會導致悲劇:

• Tx物件可以保持開啟狀態,從池中保留連線而不返回。
• 資料庫的狀態可能與代表它的Golang變數的狀態不同步。
• 你可能會認為你是在事務內部的單個連線上執行查詢,實際上Golang已經為你建立了幾個連線,而且一些語句不是事務的一部分。

當你在事務中工作時,你應該注意不要對Db變數進行呼叫。應當使用db.Begin()建立的Tx變數進行所有呼叫。Db不在一個事務中,只有Tx是。如果你進一步呼叫db.Exec()或類似的函式,那麼這些呼叫將發生在事務範圍之外,是在其他的連線上。

如果你需要處理修改連線狀態的多個語句,即使你不希望事務本身,也需要一個Tx。例如:

• 建立僅在一個連線中可見的臨時表。

• 設定變數,如MySql's SET @var := somevalue語法。

• 更改連線選項,如字符集或超時。

如果你需要執行任何這些操作,則需要把你的作業(也可以說Tx操作語句)繫結到單個連線,而在Golang中執行此操作的唯一方法是使用Tx。

No.6 使用預處理語句

準備語句(db.Prepare()或者tx.Prepare())在Golang中具有所有常見的優點:安全性,效率,方便性。但是他們的實現方式與你習慣的方式可能有所不同,特別是關於它們如何與database/sql的一些內部元件進行互動的方式。

準備語句和連線

在資料庫級別,將準備好的語句繫結到單個數據庫連線。典型的流程是:客戶端向伺服器傳送帶有佔位符的SQL語句以進行準備,伺服器使用語句ID進行響應,然後客戶端通過傳送其ID和引數來執行該語句。

然而在Golang中,連線不會直接暴露給database/sql包的使用者。你不準備連線上語句。你準備好在一個db或tx。並且database/sql具有一些便捷的行為,如自動重試。由於這些原因,準備好的語句和連線(存在於驅動級別)之間的潛在關聯被隱藏在程式碼中。

下面是它的工作原理:

  1. 準備一個語句時,它會在池中的連線上準備好。

  2. Stmt物件記住使用哪個連線。

  3. 當你執行Stmt時,它試圖使用Stmt物件記住的那個連線(後面我們將這裡的連線稱為原始連線)。如果它不可用,因為它關閉或忙於做其他事情,它從池中獲取另一個連線,並在另一個連線上重新準備與資料庫的語句。

因為在原始連線繁忙時,會根據需要重新準備語句,因此資料庫的高併發使用可能會導致大量連線繁忙,從而建立大量的準備語句。這會導致語句的明顯洩露,正在準備和重新準備的語句比你想象的更多,甚至會影響到伺服器端對語句數量的限制。

避免準備好的語句

Golang將為你在封面下建立準備好的宣告。例如,一個簡單的db.Query(sql,param1,param2)通過準備sql,然後使用引數執行它,最後關閉語句。

有時,準備好的語句並不是你想要的。這可能有幾個原因。

  1. 資料庫不支援準備好的語句。例如,當使用MySql驅動時,你可以連線到MemSql和Sphinx,因為它們支援MySql線路協議。但是它們不支援包含準備語句的“二進位制”協議,因此它們會以混亂的方式失敗。

  2. 這些語句沒有重用到足以使它們變得有價值,而安全問題則以其他方式處理,因此效能開銷是不需要的。這方面點選VividCortex部落格可以看到一個例子。

如果不想使用預處理語句,則需要使用fmt.Sprint()或類似的方法來組合SQL,並將其作為db.Query()或db.QueryRow()的唯一引數傳遞。你的驅動需要支援明文查詢執行,這是通過執行器(Execer是一個結構體)和查詢器(Queryer是一個結構體)介面在Golang 1.1中新增的,在此記錄。

事務中的準備語句

在Tx中建立的準備語句僅限於它,因此早期關於重新準備的注意事項不適用。當你對Tx物件進行操作時,你的操作直接對映到它下面唯一的一個連線上。

這也意味著在Tx內建立的準備語句不能與之分開使用。同樣,在DB中建立的準備語句不能再事務中使用,因為它們將被繫結到不同的連線。

要在Tx中使用事務外的預處理語句,可以使用Tx.Stmt(),它將從事務外部準備一個新的特定於事務的語句。它通過採用現有的預處理語句,設定與事務的連線,並在執行時重新準備所有語句。這個行為及其實現是不可取的,甚至在databse/sql原始碼中有一個TODO來改進它;我們建議不要使用這個。

在處理事務中的預處理語句時,必須小心謹慎。請考慮下面的示例:

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關閉*sql.Tx將與之關聯的連線返還到池中,但是,在預處理語句結束後,延遲呼叫時在那之後發生的,這可能導致併發訪問底層的連線,使連線狀態不一致。如果使用Golang1.4或更高的版本,則應確保在提交事務或回滾之前宣告始終關閉。點選檢視這個issues

引數佔位符語法

預處理語句中的佔位符引數的語法是特定於資料庫的。例如,比較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 錯誤處理

幾乎所有使用database/sql型別的操作都會返回一個錯誤作為最後一個值。你應該總是檢查這些錯誤,千萬不要忽視它們。有幾個地方錯誤行為是特殊情況,還有一些額外的東西可能需要知道。

遍歷結果集的錯誤

請思考下面的程式碼:

for rows.Next() {
    // ...
}
if err = rows.Err(); err != nil {
    // handle the error here
}

來自rows.Err()的錯誤可能是rows.Next()迴圈中各種錯誤的結果。除了正常完成迴圈之外,迴圈可能會退出,因此你總是需要檢查迴圈是否正常終止。異常終止自動呼叫rows.Close(),儘管多次呼叫它是無害的。

關閉結果集的錯誤

如上所述,如果你過早的退出迴圈,則應該總是顯式的關閉sql.Rows。如果迴圈正常退出或通過錯誤,它會自動關閉,但你可能會錯誤的執行此操作:

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()返回的錯誤是一般規則的唯一例外,最好是捕獲並檢查所有資料庫操作中的錯誤。如果rows.Close()返回錯誤,那麼你應該怎麼做。記錄錯誤資訊或panic可能是唯一明智的事情,如果這不明智,那麼也許你應該忽略錯誤。

QueryRow()的錯誤

思考下面的程式碼來獲取一行資料:

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的使用者怎麼辦?那麼結果中不會有行,而.Scan()不會將值掃描到name中。那會怎麼樣?

Golang定義了一個特殊的錯誤常量,稱為sql.ErrNoRows,當結果為空時,它將從QueryRow()返回。這在大多數情況下需要作為特殊情況來處理。空的結果通常不被應用程式程式碼認為是錯誤的,如果不檢查錯誤是不是這個特殊常量,那麼會導致你意想不到的應用程式程式碼錯誤。

來自查詢的錯誤被推遲到呼叫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)

有人可能會問為什麼一個空的結果集被認為是一個錯誤。空集沒有什麼錯誤。原因是QueryRow()方法需要使用這種特殊情況才能讓呼叫者區分是否QueryRow()實際上找到一行;沒有它,Scan(0不會做任何事情,你可能不會意識到你的變數畢竟沒有從資料庫中獲取任何值。

當你使用QueryRow()時,你應該只會遇到此錯誤。如果你在別處遇到這個錯誤,你就做錯了什麼。

識別特定的資料庫錯誤

像下面這樣編寫程式碼是很有誘惑力的:

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
}

這不是最好的方法。例如,字串值可能會取決於伺服器使用什麼語言傳送錯誤訊息。比較錯誤編號以確定具體錯誤是啥要好得多。

但是,驅動的機制不同,因為這不是database/sql本身的一部分。在本教程重點介紹的MySql驅動中,你可以編寫以下程式碼:

if driverErr, ok := err.(*mysql.MySQLError); ok { // Now the error number is accessible directly
    if driverErr.Number == 1045 {
        // Handle the permission-denied error
    }
}

再次,這裡的MySQLError型別由此特定驅動程式提供,並且驅動程式之間的.Number欄位可能不同。然而,該值是從MySql的錯誤訊息中提取的,因此是特定於資料庫的,而不是特定於驅動的。

這段程式碼還是很醜相對於1045,一個魔術數字是一種程式碼氣味。一些驅動(雖然不是MySql的驅動程式,因為這裡的主題的原因)提供錯誤識別符號的列表。例如Postgres pg驅動程式在error.go中。還有一個由VividCortex維護的MySql錯誤號的外部包。使用這樣的列表,上面的程式碼寫的更漂亮:

if driverErr, ok := err.(*mysql.MySQLError); ok {
    if driverErr.Number == mysqlerr.ER_ACCESS_DENIED_ERROR {
        // Handle the permission-denied error
    }
}

處理連線錯誤

如果與資料庫的連線被丟棄,殺死或發生錯誤該怎麼辦?

當發生這種情況時,你不需要實現任何邏輯來重試失敗的語句。作為database/sql連線池的一部分,處理失敗的連線是內建的。如果你執行查詢或其他語句,底層連線失敗,則Golang將重新開啟一個新的連線(或從連線池中獲取另一個連線),並重試10次。

然而,可能會產生一些意想不到的後果。當某些型別錯誤可能會發生其他錯誤條件。這也可能是驅動程式特定的。MySql驅動程式發生的一個例子是使用KILL取消不需要的語句(例如長時間執行的查詢)會導致語句被重試10次。

No.8 使用空值

可以為空的欄位是令人煩惱的,並導致很多醜陋的程式碼。如果可以,避開它們。如果沒有,那麼你需要使用database/sql包中的特殊型別來處理它們,或者定義你自己的型別。

有可以空的布林值,字串,整數和浮點數的型別。下面是你使用它們的方法:

for rows.Next() {
    var s sql.NullString
    err := rows.Scan(&s)
    // check err
    if s.Valid {
       // use s.String
    } else {
       // NULL value
    }
}

可以空的型別的限制和避免的理由的情況下你需要更有說服力的可以為空的列:

  1. 沒有sql.NullUint64或sql.NullYourFavoriteType。你需要為這個定義你自己的。

  2. 可空性可能會非常棘手,並不是未來的證明。如果你認為某些內容不會為空,但是你錯了,你的程式將會崩潰,也許很少會發生錯誤。

  3. Golang的好處之一是為每個變數設定一個有用的預設零值。這不是空的工作方式。

如果你需要定義自己的型別來處理NULLS,則可以複製sql.NullString的設計來實現。

如果你不能避免在你的資料庫中具有空值,周圍有多數資料庫系統支援的另一項工作是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()函式要求你準確傳遞正確數目的目標變數。如果你不知道查詢將返回什麼呢?
如果你不知道查詢將返回多少列,則可以使用Columns()來查詢列名稱列表。你可以檢查此列表的長度以檢視有多少列,並且可以將切片傳遞給具有正確數值的Scan()。列如,MySql的某些fork為SHOW PROCESSLIST命令返回不同的列,因此你必須為此準備好,否則將導致錯誤,這是一種方法;還有其他的方法:

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
}

如果你不知道這些列或者它們的型別,你應該使用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包中有一個基本的連線池。沒有很多的控制或檢查能力,但這裡有一些你可能會發現有用的知識:
• 連線池意味著在單個數據庫上執行兩個連續的語句可能會開啟兩個連結並單獨執行它們。對於程式設計師來說,為什麼它們的程式碼行為不當,這是相當普遍的。例如,後面跟著INSERT的LOCK TABLES可能會被阻塞,因為INSERT位於不具有表鎖定的連線上。

• 連線是在需要時建立的,池中沒有空閒連線。

• 預設情況下,連線數量沒有限制。如果你嘗試同時執行很多操作,可以建立任意數量的連線。這可能導致資料庫返回錯誤,例如“連線太多”。

• 在Golang1.1或更新版本中,你可以使用db.SetMaxIdleConns(N)來限制池中的空閒連線數。這並不限制池的大小。

• 在Golang1.2.1或更新版本中,可以使用db.SetMaxOpenConns(N)來限制於資料庫的總開啟連線數。不幸的是,一個死鎖bug(修復)阻止db.SetMaxOpenConns(N)在1.2中安全使用。

• 連接回收相當快。使用db.SetMaxIdleConns(N)設定大量空閒連線可以減少此流失,並有助於保持連線以重新使用。

• 長期保持連線空閒可能會導致問題(例如在微軟azure上的這個問題)。嘗試db.SetMaxIdleConns(0)如果你連線超時,因為連線空閒時間太長。

No.11 驚喜,反模式和限制

雖然database/sql很簡單,但一旦你習慣了它,你可能會對它支援的用例的微妙之處感到驚訝。這是Golang的核心庫通用的。

資源枯竭

如本網站所述,如果你不按預期使用database/sql,你一定會為自己造成麻煩,通常是通過消耗一些資源或阻止它們被有效的重用:
• 開啟和關閉資料庫可能會導致資源耗盡。

• 沒有讀取所有行或使用rows.Close()保留來自池的連線。

• 對於不返回行的語句,使用Query()將從池中預留一個連線。

• 沒有意識到預處理語句如何工作會導致大量額外的資料庫活動。

巨大的uint64值

這裡有一個令人吃驚的錯誤。如果設定了高位,就不能將大的無符號整數作為引數傳遞給語句:

_, err := db.Exec("INSERT INTO users(id) VALUES", math.MaxUint64) // Error

這將丟擲一個錯誤。如果你使用uint64值要小心,因為它們可能開始小而且無錯誤的工作,但會隨著時間的推移而增加,並開始丟擲錯誤。

連線狀態不匹配

有些事情可以改變連線狀態,這可能導致的問題有兩個原因:

  1. 某些連線狀態,比如你是否處於事務中,應該通過Golang型別來處理。

  2. 你可能假設你的查詢在單個連線上執行。

例如,使用USE語句設定當前資料庫對於很多人來說是一個典型的事情。但是在Golang中,它只會影響你執行的連線。除非你處於事務中,否則你認為在該連線上執行的其他語句實際上可能在從池中獲取的不同的連線上執行,因此它們不會看到這些更改的影響。

此外,在更改連線後,它將返回到池,並可能會汙染其他程式碼的狀態。這就是為什麼你不應該直接將BEGIN或COMMIT語句作為SQL命令發出的原因之一。

資料庫特定的語法

database/sql API提供了面向行的資料庫抽象,但是具體的資料庫和驅動程式可能會在行為或語法上有差異,例如預處理語句佔位符。

多個結果集

Golang驅動程式不以任何方式支援單個查詢中的多個結果集,儘管有一個支援大容量操作(如批量複製)的功能請求似乎沒有任何計劃。

這意味著,除了別的以外,返回多個結果集的儲存過程將無法正常工作。

呼叫儲存過程

呼叫儲存過程是特定於驅動程式的,但在MySql驅動程式中,目前無法完成。看來你可以呼叫一個簡單的過程來返回一個單一的結果集,通過執行如下的操作:

err := db.QueryRow("CALL mydb.myprocedure").Scan(&result) // Error

事實上這行不通。你將收到以下錯誤1312:PROCEDURE mydb.myprocedure無法返回給定上下文中的結果集。這是因為MySql希望將連線設定為多語句模式,即使單個結果,並且驅動程式當前沒有執行此操作(儘管看到這個問題)。

多個宣告支援

database/sql沒有顯式的擁有多個語句支援,這意味著這個行為是後端依賴的:

_, err := db.Exec("DELETE FROM tbl1; DELETE FROM tbl2") // Error/unpredictable result

伺服器可以解釋它想要的,它可以包括返回的錯誤,只執行第一個語句,或執行兩者。

同樣,在事務中沒有辦法批處理語句。事務中的每個語句必須連續執行,並且結果中的資源(如行或行)必須被掃描或關閉,以便底層連線可供下一個語句使用。這與通常不在事務中工作時的行為不同。在這種情況下,完全可以執行查詢,迴圈遍歷行,並在迴圈中對資料庫進行查詢(這將發生在一個新的連線上):

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)
}

但是事務只繫結到一個連線,所以事務不可能做到這一點:

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不會阻止你去嘗試。因此,如果你試圖在第一個釋放資源並自行清理之前嘗試執行另一個語句,可能會導致一個損壞的連線。這也意味著事務中的每個語句都會產生一組單獨的網路往返資料庫。