Go 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的著名的驅動有:
這裏以pgx
為例,它性能表現不俗,並對PostgreSQL諸多特性與類型有著良好的支持。既可使用Ad-Hoc API,也提供了標準數據庫接口的實現:github.com/jackc/pgx/stdlib
。
import (
"database/sql"
_ "github.com/jackx/pgx/stdlib"
)
使用_
別名來匿名導入驅動,驅動的導出名字不會出現在當前作用域中。導入時,驅動的初始化函數會調用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)
}
整體工作流程如下:
- 使用
db.Query()
來發送查詢到數據庫,獲取結果集Rows
,並檢查錯誤。 - 使用
rows.Next()
作為循環條件,迭代讀取結果集。 - 使用
rows.Scan
從結果集中獲取一行結果。 - 使用
rows.Err()
在退出迭代後檢查錯誤。 - 使用
rows.Close()
關閉結果集,釋放連接。
一些需要詳細說明的地方:
-
db.Query
會返回結果集*Rows
和錯誤。每個驅動返回的錯誤都不一樣,用錯誤字符串來判斷錯誤類型並不是明智的做法,更好的方法是對抽象的錯誤做Type Assertion
,利用驅動提供的更具體的信息來處理錯誤。當然類型斷言也可能產生錯誤,這也是需要處理的。if err.(pgx.PgError).Code == "0A000" { // Do something with that type or error }
rows.Next()
會指明是否還有未讀取的數據記錄,通常用於迭代結果集。迭代中的錯誤會導致rows.Next()
返回false
。-
rows.Scan()
用於在迭代中獲取一行結果。數據庫會使用wire protocal通過TCP/UnixSocket傳輸數據,對Pg而言,每一行實際上對應一條DataRow
消息。Scan
接受變量地址,解析DataRow
消息並填入相應變量中。因為Go語言是強類型的,所以用戶需要創建相應類型的變量並在rows.Scan
中傳入其指針,Scan
函數會根據目標變量的類型執行相應轉換。例如某查詢返回一個單列
string
結果集,用戶可以傳入[]byte
或string
類型變量的地址,Go會將原始二進製數據或其字符串形式填入其中。但如果用戶知道這一列始終存儲著數字字麵值,那麼相比傳入string
地址後手動使用strconv.ParseInt()
解析,更推薦的做法是直接傳入一個整型變量的地址(如上麵所示),Go會替用戶完成解析工作。如果解析出錯,Scan
會返回相應的錯誤。 rows.Err()
用於在退出迭代後檢查錯誤。正常情況下迭代退出是因為內部產生的EOF錯誤,使得下一次rows.Next() == false
,從而終止循環;在迭代結束後要檢查錯誤,以確保迭代是因為數據讀取完畢,而非其他“真正”錯誤而結束的。遍曆結果集的過程實際上是網絡IO的過程,可能出現各種錯誤。健壯的程序應當考慮這些可能,而不能總是假設一切正常。-
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 int
err = 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 = :col
VALUES(?, ?, ?) 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
,用戶需要自己實現。空值有很多麻煩的地方。例如用戶認為某一列不會出現空值而采用基本類型接收時卻遇到了空值,程序就會崩潰。這種錯誤非常稀少,難以捕捉、偵測、處理,甚至意識到。
空值的解決辦法
使用額外的標記字段
database\sql
提供了四種基本可空數據類型:使用基本類型和一個布爾標記的複合結構體表示可空值。例如:
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],由我翻譯並進行一些增刪改,修正過時錯誤的內容。轉載保留出處。
最後更新:2017-08-25 15:03:41