Go database/sql 教程

4 人贊了文章

閱讀原文請點擊

摘要: Go使用SQL與類SQL資料庫的慣例是通過標準庫[database/sql](golang.org/pkg/database)。這是一個對關係型資料庫的通用抽象,它提供了標準的、輕量的、面向行的介面。不過`database/sql`的包文檔只講它做了什麼,卻對如何使用隻字未提。快速指南遠比堆砌事實有用,本文講述了`database/sql`的使用方法及其注意事項。

Go使用SQL與類SQL資料庫的慣例是通過標準庫database/sql。這是一個對關係型資料庫的通用抽象,它提供了標準的、輕量的、面向行的介面。不過database/sql的包文檔只講它做了什麼,卻對如何使用隻字未提。快速指南遠比堆砌事實有用,本文講述了database/sql的使用方法及其注意事項。

1. 頂層抽象

在Go中訪問資料庫需要用到sql.DB介面:它可以創建語句(statement)和事務(transaction),執行查詢,獲取結果。

sql.DB並不是資料庫連接,也並未在概念上映射到特定的資料庫(Database)或模式(schema)。它只是一個抽象的介面,不同的具體驅動有著不同的實現方式。通常而言,sql.DB會處理一些重要而麻煩的事情,例如操作具體的驅動打開/關閉實際底層資料庫的連接,按需管理連接池。

sql.DB這一抽象讓用戶不必考慮如何管理並發訪問底層資料庫的問題。當一個連接在執行任務時會被標記為正在使用。用完之後會放回連接池中。不過用戶如果用完連接後忘記釋放,就會產生大量的連接,極可能導致資源耗盡(建立太多連接,打開太多文件,缺少可用網路埠)。

2. 導入驅動

使用資料庫時,除了database/sql包本身,還需要引入想使用的特定資料庫驅動。

儘管有時候一些資料庫特有的功能必需通過驅動的Ad Hoc介面來實現,但通常只要有可能,還是應當盡量只用database/sql中定義的類型。這可以減小用戶代碼與驅動的耦合,使切換驅動時代碼改動最小化,也儘可能地使用戶遵循Go的慣用法。本文使用PostgreSQL為例,PostgreSQL的著名的驅動有:

  • github.com/lib/pq
  • github.com/go-pg/pg
  • github.com/jackc/pgx。

這裡以pgx為例,它性能表現不俗,並對PostgreSQL諸多特性與類型有著良好的支持。既可使用Ad-Hoc API,也提供了標準資料庫介面的實現:github.com/jackc/pgx/st

import (

"database/sql" _ "github.com/jackx/pgx/st")

使用_別名來匿名導入驅動,驅動的導出名字不會出現在當前作用域中。導入時,驅動的初始化函數會調用sql.Register將自己註冊在database/sql包的全局變數sql.drivers中,以便以後通過sql.Open訪問。

3. 訪問數據

載入驅動包後,需要使用sql.Open()來創建sql.DB:

func main() {

db, err := sql.Open("pgx","postgres://localhost:5432/postgres") if err != nil {

log.Fatal(err)

} defer db.Close()}

sql.Open有兩個參數:

  • 第一個參數是驅動名稱,字元串類型。為避免混淆,一般與包名相同,這裡是pgx。
  • 第二個參數也是字元串,內容依賴於特定驅動的語法。通常是URL的形式,例如postgres://localhost:5432。
  • 絕大多數情況下都應當檢查database/sql操作所返回的錯誤。
  • 一般而言,程序需要在退出時通過sql.DB的Close()方法釋放資料庫連接資源。如果其生命周期不超過函數的範圍,則應當使用defer db.Close()

執行sql.Open()並未實際建立起到資料庫的連接,也不會驗證驅動參數。第一個實際的連接會惰性求值,延遲到第一次需要時建立。用戶應該通過db.Ping()來檢查資料庫是否實際可用。

if err = db.Ping(); err != nil {

// do something about db error}

sql.DB對象是為了長連接而設計的,不要頻繁Open()和Close()資料庫。而應該為每個待訪問的資料庫創建**一個**sql.DB實例,並在用完前一直保留它。需要時可將其作為參數傳遞,或註冊為全局對象。

如果沒有按照database/sql設計的意圖,不把sql.DB當成長期對象來用而頻繁開關啟停,就可能遭遇各式各樣的錯誤:無法復用和共享連接,耗盡網路資源,由於TCP連接保持在TIME_WAIT狀態而間斷性的失敗等……

4. 獲取結果

有了sql.DB實例之後就可以開始執行查詢語句了。

Go將資料庫操作分為兩類:Query與Exec。兩者的區別在於前者會返回結果,而後者不會。

  • Query表示查詢,它會從資料庫獲取查詢結果(一系列行,可能為空)。
  • Exec表示執行語句,它不會返回行。

此外還有兩種常見的資料庫操作模式:

  • QueryRow表示只返回一行的查詢,作為Query的一個常見特例。
  • Prepare表示準備一個需要多次使用的語句,供後續執行用。

4.1 獲取數據

讓我們看一個如何查詢資料庫並且處理結果的例子:利用資料庫計算從1到10的自然數之和。

func example() {

var sum, n int32 // invoke query rows, err := db.Query("SELECT generate_series(1,$1)", 10) // handle query error if err != nil { fmt.Println(err) }

// defer close result set

defer rows.Close() // Iter results for rows.Next() { if err = rows.Scan(&n); err != nil { fmt.Println(err) // Handle scan error } sum += n // Use result } // check iteration error if rows.Err() != nil { fmt.Println(err)

}

fmt.Println(sum)}

整體工作流程如下:

  1. 使用db.Query()來發送查詢到資料庫,獲取結果集Rows,並檢查錯誤。
  2. 使用rows.Next()作為循環條件,迭代讀取結果集。
  3. 使用rows.Scan從結果集中獲取一行結果。
  4. 使用rows.Err()在退出迭代後檢查錯誤。
  5. 使用rows.Close()關閉結果集,釋放連接。

一些需要詳細說明的地方:

  1. db.Query會返回結果集*Rows和錯誤。每個驅動返回的錯誤都不一樣,用錯誤字元串來判斷錯誤類型並不是明智的做法,更好的方法是對抽象的錯誤做Type Assertion,利用驅動提供的更具體的信息來處理錯誤。當然類型斷言也可能產生錯誤,這也是需要處理的。

if err.(pgx.PgError).Code == "0A000" {

// Do something with that type or error}
  1. rows.Next()會指明是否還有未讀取的數據記錄,通常用於迭代結果集。迭代中的錯誤會導致rows.Next()返回false。
  2. rows.Scan()用於在迭代中獲取一行結果。資料庫會使用wire protocal通過TCP/UnixSocket傳輸數據,對Pg而言,每一行實際上對應一條DataRow消息。Scan接受變數地址,解析DataRow消息並填入相應變數中。因為Go語言是強類型的,所以用戶需要創建相應類型的變數並在rows.Scan中傳入其指針,Scan函數會根據目標變數的類型執行相應轉換。

例如某查詢返回一個單列string結果集,用戶可以傳入[]byte或string類型變數的地址,Go會將原始二進位數據或其字元串形式填入其中。但如果用戶知道這一列始終存儲著數字字面值,那麼相比傳入string地址後手動使用strconv.ParseInt()解析,更推薦的做法是直接傳入一個整型變數的地址(如上面所示),Go會替用戶完成解析工作。如果解析出錯,Scan會返回相應的錯誤。

  1. rows.Err()用於在退出迭代後檢查錯誤。正常情況下迭代退出是因為內部產生的EOF錯誤,使得下一次rows.Next() == false,從而終止循環;在迭代結束後要檢查錯誤,以確保迭代是因為數據讀取完畢,而非其他「真正」錯誤而結束的。遍歷結果集的過程實際上是網路IO的過程,可能出現各種錯誤。健壯的程序應當考慮這些可能,而不能總是假設一切正常。
  2. rows.Close()用於關閉結果集。結果集引用了資料庫連接,並會從中讀取結果。讀取完之後必須關閉它才能避免資源泄露。只要結果集仍然打開著,相應的底層連接就處於忙碌狀態,不能被其他查詢使用。

因錯誤(包括EOF)導致的迭代退出會自動調用rows.Close()關閉結果集(和釋放底層連接)。但如果程序自行意外地退出了循環,例如中途break & return,結果集就不會被關閉,產生資源泄露。rows.Close方法是冪等的,重複調用不會產生副作用,因此建議使用 defer rows.Close()來關閉結果集。

以上就是在Go中使用資料庫的標準方式。

4.2 單行查詢

如果一個查詢每次最多返回一行,那麼可以用快捷的單行查詢來替代冗長的標準查詢,例如上例可改寫為:

var sum int

err := db.QueryRow("SELECT sum(n) FROM (SELECT generate_series(1,$1) as n) a;", 10).Scan(&sum)if err != nil { fmt.Println(err)}fmt.Println(sum)

不同於Query,如果查詢發生錯誤,錯誤會延遲到調用Scan()時統一返回,減少了一次錯誤處理判斷。同時QueryRow也避免了手動操作結果集的麻煩。

需要注意的是,對於單行查詢,Go將沒有結果的情況視為錯誤。sql包中定義了一個特殊的錯誤常量ErrNoRows,當結果為空時,QueryRow().Scan()會返回它。

4.3 修改數據

什麼時候用Exec,什麼時候用Query,這是一個問題。通常DDL和增刪改使用Exec,返回結果集的查詢使用Query。但這不是絕對的,這完全取決於用戶是否希望想要獲取返回結果。例如在PostgreSQL中:INSERT ... RETURNING *;雖然是一條插入語句,但它也有返回結果集,故應當使用Query而不是Exec。

Query和Exec返回的結果不同,兩者的簽名分別是:

func (s *Stmt) Query(args ...interface{}) (*Rows, error) func (s *Stmt) Exec(args ...interface{}) (Result, error)

Exec不需要返回數據集,返回的結果是Result,Result介面允許獲取執行結果的元數據

type Result interface {

// 用於返回自增ID,並不是所有的關係型資料庫都有這個功能。 LastInsertId() (int64, error) // 返回受影響的行數。 RowsAffected() (int64, error)}

Exec的用法如下所示:

db.Exec(`CREATE TABLE test_users(id INTEGER PRIMARY KEY ,name TEXT);`)

db.Exec(`TRUNCATE test_users;`)stmt, err := db.Prepare(`INSERT INTO test_users(id,name) VALUES ($1,$2) RETURNING id`)

if err != nil {

fmt.Println(err.Error())}res, err := stmt.Exec(1, "Alice")if err != nil { fmt.Println(err)} else { fmt.Println(res.RowsAffected()) fmt.Println(res.LastInsertId())}

相比之下Query則會返回結果集對象*Rows,使用方式見上節。其特例QueryRow使用方式如下:

db.Exec(`CREATE TABLE test_users(id INTEGER PRIMARY KEY ,name TEXT);`)

db.Exec(`TRUNCATE test_users;`)stmt, err := db.Prepare(`INSERT INTO test_users(id,name) VALUES ($1,$2) RETURNING id`)if err != nil { fmt.Println(err.Error())}var returnID interr = stmt.QueryRow(4, "Alice").Scan(&returnID)if err != nil { fmt.Println(err)} else { fmt.Println(returnID)}

同樣的語句使用Exec和Query執行有巨大的差別。如上文所述,Query會返回結果集Rows,而存在未讀取數據的Rows其實會佔用底層連接直到rows.Close()為止。因此,使用Query但不讀取返回結果,會導致底層連接永遠無法釋放。database/sql期望用戶能夠用完就把連接還回來,所以這樣的用法很快就會導致資源耗盡(連接過多)。所以,應該用Exec的語句絕不可用Query來執行。

4.4 準備查詢

在上一節的兩個例子中,沒有直接使用資料庫的Query和Exec方法,而是首先執行了db.Prepare獲取準備好的語句(prepared statement)。準備好的語句Stmt和sql.DB一樣,都可以執行Query、Exec等方法。

準備語句的優勢

在查詢前進行準備是Go語言中的慣用法,多次使用的查詢語句應當進行準備(Prepare)。準備查詢的結果是一個準備好的語句(prepared statement),語句中可以包含執行時所需參數的佔位符(即綁定值)。準備查詢比拼字元串的方式好很多,它可以轉義參數,避免SQL注入。同時,準備查詢對於一些資料庫也省去了解析和生成執行計劃的開銷,有利於性能。

佔位符

PostgreSQL使用$N作為佔位符,N是一個從1開始遞增的整數,代表參數的位置,方便參數的重複使用。MySQL使用?作為佔位符,SQLite兩種佔位符都可以,而Oracle則使用:param1的形式。

MySQL PostgreSQL Oracle

===== ========== ======WHERE col = ? WHERE col = $1 WHERE col = :colVALUES(?, ?, ?) VALUES($1, $2, $3) VALUES(:val1, :val2, :val3)

以PostgreSQL為例,在上面的例子中:"SELECT generate_series(1,$1)" 就用到了$N的佔位符形式,並在後面提供了與佔位符數目匹配的參數個數。

底層內幕

準備語句有著各種優點:安全,高效,方便。但Go中實現它的方式可能和用戶所設想的有輕微不同,尤其是關於和database/sql內部其他對象交互的部分。

在資料庫層面,準備語句Stmt是與單個資料庫連接綁定的。通常的流程是:客戶端向伺服器發送帶有佔位符的查詢語句用於準備,伺服器返回一個語句ID,客戶端在實際執行時,只需要傳輸語句ID和相應的參數即可。因此準備語句無法在連接之間共享,當使用新的資料庫連接時,必須重新準備。

database/sql並沒有直接暴露出資料庫連接。用戶是在DB或Tx上執行Prepare,而不是Conn。因此database/sql提供了一些便利處理,例如自動重試。這些機制隱藏在Driver中實現,而不會暴露在用戶代碼中。其工作原理是:當用戶準備一條語句時,它在連接池中的一個連接上進行準備。Stmt對象會引用它實際使用的連接。當執行Stmt時,它會嘗試會用引用的連接。如果那個連接忙碌或已經被關閉,它會獲取一個新的連接,並在連接上重新準備,然後再執行。

因為當原有連接忙時,Stmt會在其他連接上重新準備。因此當高並發地訪問資料庫時,大量的連接處於忙碌狀態,這會導致Stmt不斷獲取新的連接並執行準備,最終導致資源泄露,甚至超出服務端允許的語句數目上限。所以通常應盡量採用扇入的方式減小資料庫訪問並發數。

查詢的微妙之處

資料庫連接其實是實現了Begin,Close,Prepare方法的介面。

type Conn interface {

Prepare(query string) (Stmt, error) Close() error Begin() (Tx, error)}

所以連接介面上實際並沒有Exec,Query方法,這些方法其實定義在Prepare返回的Stmt上。對於Go而言,這意味著db.Query()實際上執行了三個操作:首先對查詢語句做了準備,然後執行查詢語句,最後關閉準備好的語句。這對資料庫而言,其實是3個來回。設計粗糙的程序與簡陋實現驅動可能會讓應用與資料庫交互的次數增至3倍。好在絕大多數資料庫驅動對於這種情況有優化,如果驅動實現sql.Queryer介面:

type Queryer interface {

Query(query string, args []Value) (Rows, error)}

那麼database/sql就不會再進行Prepare-Execute-Close的查詢模式,而是直接使用驅動實現的Query方法向資料庫發送查詢。對於查詢都是即拼即用,也不擔心安全問題的情況下,直接Query可以有效減少性能開銷。

5. 使用事務

事物是關係型資料庫的核心特性。Go中事務(Tx)是一個持有資料庫連接的對象,它允許用戶在**同一個連接**上執行上面提到的各類操作。

事務基本操作

通過db.Begin()來開啟一個事務,Begin方法會返回一個事務對象Tx。在結果變數Tx上調用Commit()或者Rollback()方法會提交或回滾變更,並關閉事務。在底層,Tx會從連接池中獲得一個連接並在事務過程中保持對它的獨佔。事務對象Tx上的方法與資料庫對象sql.DB的方法一一對應,例如Query,Exec等。事務對象也可以準備(prepare)查詢,由事務創建的準備語句會顯式綁定到創建它的事務。

事務注意事項

使用事務對象時,不應再執行事務相關的SQL語句,例如BEGIN,COMMIT等。這可能產生一些副作用:

  • Tx對象一直保持打開狀態,從而佔用了連接。
  • 資料庫狀態不再與Go中相關變數的狀態保持同步。
  • 事務提前終止會導致一些本應屬於事務內的查詢語句不再屬於事務的一部分,這些被排除的語句有可能會由別的資料庫連接而非原有的事務專屬連接執行。

當處於事務內部時,應當使用Tx對象的方法而非DB的方法,DB對象並不是事務的一部分,直接調用資料庫對象的方法時,所執行的查詢並不屬於事務的一部分,有可能由其他連接執行。

Tx的其他應用場景

如果需要修改連接的狀態,也需要用到Tx對象,即使用戶並不需要事務。例如:

  • 創建僅連接可見的臨時表
  • 設置變數,例如SET @var := somevalue
  • 修改連接選項,例如字符集,超時設置。

在Tx上執行的方法都保證同一個底層連接執行,這使得對連接狀態的修改對後續操作起效。這是Go中實現這種功能的標準方式。

在事務中準備語句

調用Tx.Prepare會創建一個與事務綁定的準備語句。在事務中使用準備語句,有一個特殊問題需要關註:一定要在事務結束前關閉準備語句。

在事務中使用defer stmt.Close()是相當危險的。因為當事務結束後,它會釋放自己持有的資料庫連接,但事務創建的未關閉Stmt仍然保留著對事務連接的引用。在事務結束後執行stmt.Close(),如果原來釋放的連接已經被其他查詢獲取並使用,就會產生競爭,極有可能破壞連接的狀態。

6. 處理空值

可空列(Nullable Column)非常的惱人,容易導致代碼變得醜陋。如果可以,在設計時就應當盡量避免。因為:

  • Go語言的每一個變數都有著默認零值,當數據的零值沒有意義時,可以用零值來表示空值。但很多情況下,數據的零值和空值實際上有著不同的語義。單獨的原子類型無法表示這種情況。
  • 標準庫只提供了有限的四種Nullable type::NullInt64, NullFloat64, NullString, NullBool。並沒有諸如NullUint64,NullYourFavoriteType,用戶需要自己實現。
  • 空值有很多麻煩的地方。例如用戶認為某一列不會出現空值而採用基本類型接收時卻遇到了空值,程序就會崩潰。這種錯誤非常稀少,難以捕捉、偵測、處理,甚至意識到。

空值的解決辦法

使用額外的標記欄位

databasesql提供了四種基本可空數據類型:使用基本類型和一個布爾標記的複合結構體表示可空值。例如:

type NullInt64 struct {

Int64 int64 Valid bool // Valid is true if Int64 is not NULL}

可空類型的使用方法與基本類型一致:

for rows.Next() {

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

使用指針

在Java中通過裝箱(boxing)處理可空類型,即把基本類型包裝成一個類,並通過指針引用。於是,空值語義可以通過指針為空來表示。Go當然也可以採用這種辦法,不過標準庫中並沒有提供這種實現方式。pgx提供了這種形式的可空類型支持。

使用零值表示空值

如果數據本身從語義上就不會出現零值,或者根本不區分零值和空值,那麼最簡便的方法就是使用零值來表示空值。驅動go-pg提供了這種形式的支持。

自定義處理邏輯

任何實現了Scanner介面的類型,都可以作為Scan傳入的地址參數類型。這就允許用戶自己定製複雜的解析邏輯,實現更豐富的類型支持。

type Scanner interface {

// Scan 從資料庫驅動中掃描出一個值,當不能無損地轉換時,應當返回錯誤 // src可能是int64, float64, bool, []byte, string, time.Time,也可能是nil,表示空值。 Scan(src interface{}) error}

在資料庫層面解決

通過對列添加NOT NULL約束,可以確保任何結果都不會為空。或者,通過在SQL中使用COALESCE來為NULL設定默認值。

7. 處理動態列

Scan()函數要求傳遞給它的目標變數的數目,與結果集中的列數正好匹配,否則就會出錯。

但總有一些情況,用戶事先並不知道返回的結果到底有多少列,例如調用一個返回表的存儲過程時。

在這種情況下,使用rows.Columns()來獲取列名列表。在不知道列類型情況下,應當使用sql.RawBytes作為接受變數的類型。獲取結果後自行解析。

cols, err := rows.Columns()

if err != nil { // handle this....}// 目標列是一個動態生成的數組dest := []interface{}{ new(string), new(uint32), new(sql.RawBytes),}// 將數組作為可變參數傳入Scan中。err = rows.Scan(dest...)// ...

8. 連接池

database/sql包里實現了一個通用的連接池,它只提供了非常簡單的介面,除了限制連接數、設置生命周期基本沒有什麼定製選項。但了解它的一些特性也是很有幫助的。

  • 連接池意味著:同一個資料庫上的連續兩條查詢可能會打開兩個連接,在各自的連接上執行。這可能導致一些讓人困惑的錯誤,例如程序員希望鎖表插入時連續執行了兩條命令:LOCK TABLE和INSERT,結果卻會阻塞。因為執行插入時,連接池創建了一個新的連接,而這條連接並沒有持有表鎖。
  • 在需要時,而且連接池中沒有可用的連接時,連接才被創建。
  • 默認情況下連接數量沒有限制,想創建多少就有多少。但伺服器允許的連接數往往是有限的。
  • 用db.SetMaxIdleConns(N)來限制連接池中空閑連接的數量,但是這並不會限制連接池的大小。連接回收(recycle)的很快,通過設置一個較大的N,可以在連接池中保留一些空閑連接,供快速復用(reuse)。但保持連接空閑時間過久可能會引發其他問題,比如超時。設置N=0則可以避免連接空閑太久。
  • 用db.SetMaxOpenConns(N)來限制連接池中**打開**的連接數量。
  • 用db.SetConnMaxLifetime(d time.Duration)來限制連接的生命周期。連接超時後,會在需要時惰性回收復用。

9. 微妙行為

database/sql並不複雜,但某些情況下它的微妙表現仍然會出人意料。

9.1 資源耗盡

不謹慎地使用database/sql會給自己挖許多坑,最常見的問題就是資源枯竭(resource exhaustion):

  • 打開和關閉資料庫(sql.DB)可能會導致資源枯竭;
  • 結果集沒有讀取完畢,或者調用rows.Close()失敗,結果集會一直佔用池裡的連接;
  • 使用Query()執行一些不返回結果集的語句,返回的未讀取結果集會一直佔用池裡的連接;
  • 不了解準備語句(Prepared Statement)的工作原理會產生許多額外的資料庫訪問。

9.2 Uint64

Go底層使用int64來表示整型,使用uint64時應當極其小心。使用超出int64表示範圍的整數作為參數,會產生一個溢出錯誤:

// Error: constant 18446744073709551615 overflows int

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

這種類型的錯誤非常不容易發現,它可能一開始表現的很正常,但是溢出之後問題就來了。

9.3 不合預期的連接狀態

連接的狀態,例如是否處於事務中,所連接的資料庫,設置的變數等,應該通過Go的相關類型來處理,而不是通過SQL語句。用戶不應當對自己的查詢在哪條連接上執行作任何假設,如果需要在同一條連接上執行,需要使用Tx。

舉個例子,通過USE DATABASE改變連接的資料庫對於不少人是習以為常的操作,執行這條語句,隻影響當前連接的狀態,其他連接仍然訪問的是原來的資料庫。如果沒有使用事務Tx,後續的查詢並不能保證仍然由當前的連接執行,所以這些查詢很可能並不像用戶預期的那樣工作。

更糟糕的是,如果用戶改變了連接的狀態,用完之後它成為空連接又回到了連接池,這會污染其他代碼的狀態。尤其是直接在SQL中執行諸如BEGIN或COMMIT這樣的語句。

9.4 驅動的特殊語法

儘管database/sql是一個通用的抽象,但不同的資料庫,不同的驅動仍然會有不同的語法和行為。參數佔位符就是一個例子。

9.5 批量操作

出乎意料的是,標準庫沒有提供對批量操作的支持。即INSERT INTO xxx VALUES (1),(2),...;這種一條語句插入多條數據的形式。目前實現這個功能還需要自己手動拼SQL。

9.6 執行多條語句

database/sql並沒有對在一次查詢中執行多條SQL語句的顯式支持,具體的行為以驅動的實現為準。所以對於

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

這樣的查詢,怎樣執行完全由驅動說了算,用戶並無法確定驅動到底執行了什麼,又返回了什麼。

9.7 事務中的多條語句

因為事務保證在它上面執行的查詢都由同一個連接來執行,因此事務中的語句必需按順序一條一條執行。對於返回結果集的查詢,結果集必須Close()之後才能進行下一次查詢。用戶如果嘗試在前一條語句的結果還沒讀完前就執行新的查詢,連接就會失去同步。這意味著事務中返回結果集的語句都會佔用一次單獨的網路往返。

10. 其他

本文主體基於[Go database/sql tutorial],由我翻譯並進行一些增刪改,修正過時錯誤的內容。轉載保留出處。

閱讀原文請點擊

推薦閱讀:

查看原文 >>
相关文章