☰
用 Go database/sql 操作 MySQL:驅動選擇、DSN 格式與增刪改查實戰
2026/10/7 2:06:31 网站建设 项目流程
  • 文档
  • 教程

【免费下载链接】build-web-application-with-golang

A golang ebook intro how to build a web with golang

项目地址:https://gitcode.com/gh_mirrors/bu/build-web-application-with-golang
点击查看免费下载

本節隸屬於《Build Web Application with Golang》第 5 章「訪問資料庫」(章節目錄),聚焦在 Go 中如何透過標準database/sql介面連接 MySQL 資料庫並完成增刪改查(CRUD)。閱讀完本文後,你將掌握主流 MySQL 驅動的取捨依據、DSN(Data Source Name)的四種標準寫法、參數化查詢的實際用法,以及驅動註冊背後的database/sql介面原理,可直接照抄範例在自己專案中落地。

1. 本節在「訪問資料庫」章節中的定位

第 5 章整體路線是:先在 5.1 節「database/sql 介面」 講清楚 Go 官方定義的資料庫驅動標準介面(sql.Register、driver.Driver、driver.Conn、driver.Stmt、driver.Result、driver.Rows等),接著 5.2 至 5.4 節逐一示範目前使用較多的關聯式資料庫驅動(MySQL、SQLite、PostgreSQL)及其用法,5.5 節再基於database/sql標準介面開發一個 ORM 函式庫(beedb)。

本節(5.2)是「標準介面 + 具體資料庫」的第一個實例:由於 Go 官方不提供任何資料庫驅動,只定義介面,因此「選哪個 MySQL 驅動、如何寫 DSN、如何呼叫介面」就成為實戰中第一個要解決的問題。值得一提的是,5.3 節「使用 SQLite 資料庫」 的範例程式與本節幾乎一模一樣,唯一差別只是更換匯入的驅動與sql.Open的 DSN 格式——這正是「所有驅動遵循同一套標準介面」帶來的遷移便利性。

2. 選擇 MySQL 驅動:三種常用驅動與推薦理由

在本書撰寫時期,Go 中支援 MySQL 的驅動主要有以下三種,差別在於是否支援database/sql標準介面,以及是否採用自訂實作介面:

驅動是否支援 database/sql介面類型實作語言
go-sql-driver/mysql支援標準database/sql介面全部使用 Go 撰寫
ziutek/mymysql支援標準database/sql介面,也支援自訂介面全部使用 Go 撰寫
Philio/GoMySQL不支援自訂介面全部使用 Go 撰寫

本節範例以第一個驅動 go-sql-driver/mysql 為例(原作者說明其專案中也是採用它來驅動),並推薦讀者採用,主要理由有三:

  1. 驅動比較新、維護得比較好;
  2. 完全支援database/sql介面,可與第 5.1 節講解的標準介面無縫配合,日後若要遷移資料庫,業務程式碼不需要任何修改;
  3. 支援 keepalive,保持長連線。雖然 mikespook fork 的 mymysql 也支援 keepalive,但不是執行緒安全的;而 go-sql-driver/mysql 從底層就支援 keepalive,安全性更高。

需要說明的是,上述生態反映的是本書撰寫時期的情況,讀者在實際選型時可再確認各驅動當下的維護狀態;但「優先選擇完整支援database/sql標準介面的驅動」這條原則始終適用,因為它能讓資料庫遷移成本降到最低。

3. 建表準備:資料庫 test 與兩張使用者表

接下來的幾個小節(MySQL、SQLite 等)都沿用同一套資料表結構:資料庫test、使用者表userinfo、關聯使用者資訊表userdetail。對應的建表 SQL 如下:

CREATE TABLE `userinfo` ( `uid` INT(10) NOT NULL AUTO_INCREMENT, `username` VARCHAR(64) NULL DEFAULT NULL, `department` VARCHAR(64) NULL DEFAULT NULL, `created` DATE NULL DEFAULT NULL, PRIMARY KEY (`uid`) ); CREATE TABLE `userdetail` ( `uid` INT(10) NOT NULL DEFAULT '0', `intro` TEXT NULL, `profile` TEXT NULL, PRIMARY KEY (`uid`) )

其中userinfo.uid是自增主鍵(AUTO_INCREMENT),userdetail以uid作為關聯鍵(PRIMARY KEY (uid))儲存使用者的擴充資訊(自我介紹與個人檔案)。

倉庫中提供了對應的建表腳本與執行說明,見 en/code/src/apps/ch.5.2/schema.sql 與 en/code/src/apps/ch.5.2/readme.md。需要注意:倉庫內的schema.sql將欄位命名為departname,與本文正文表格中的department略有出入,這是歷史版本的命名差異,實戰時只要讓建表 SQL 與程式中的 SQL 欄位名保持一致即可。

根據 en/code/src/apps/ch.5.2/readme.md,執行本節範例的準備步驟如下:

  1. 安裝並啟動 MySQL(Install and run MySQL);
  2. 依照main.go中的常數建立使用者與資料庫(本例為DB_USER = "user"、DB_PASSWORD = ""、DB_NAME = "test");
  3. 使用 en/code/src/apps/ch.5.2/schema.sql 建立userinfo表;
  4. 執行go get下載並安裝遠端套件(即 go-sql-driver/mysql);
  5. 執行go run main.go執行程式。

4. 完整範例:透過 database/sql 對 MySQL 進行增刪改查

以下是本書原版示範的完整程式碼,展示如何使用database/sql介面對資料庫表執行插入、更新、查詢、刪除四類操作:

package main import ( "database/sql" "fmt" //"time" _ "github.com/go-sql-driver/mysql" ) func main() { db, err := sql.Open("mysql", "astaxie:astaxie@/test?charset=utf8") checkErr(err) //插入資料 stmt, err := db.Prepare("INSERT userinfo SET username=?,department=?,created=?") checkErr(err) res, err := stmt.Exec("astaxie", "研發部門", "2012-12-09") checkErr(err) id, err := res.LastInsertId() checkErr(err) fmt.Println(id) //更新資料 stmt, err = db.Prepare("update userinfo set username=? where uid=?") checkErr(err) res, err = stmt.Exec("astaxieupdate", id) checkErr(err) affect, err := res.RowsAffected() checkErr(err) fmt.Println(affect) //查詢資料 rows, err := db.Query("SELECT * FROM userinfo") checkErr(err) for rows.Next() { var uid int var username string var department string var created string err = rows.Scan(&uid, &username, &department, &created) checkErr(err) fmt.Println(uid) fmt.Println(username) fmt.Println(department) fmt.Println(created) } //刪除資料 stmt, err = db.Prepare("delete from userinfo where uid=?") checkErr(err) res, err = stmt.Exec(id) checkErr(err) affect, err = res.RowsAffected() checkErr(err) fmt.Println(affect) db.Close() } func checkErr(err error) { if err != nil { panic(err) } }

倉庫中還提供了一份更貼近工程實戰的版本(將連線資訊抽成常數、使用fmt.Sprintf動態組裝 DSN、defer db.Close()關閉連線、輸出帶標籤的提示訊息),完整程式見 en/code/src/apps/ch.5.2/main.go:

// Example code for Chapter 5.2 from "Build Web Application with Golang" // Purpose: Use SQL driver to perform simple CRUD operations. package main import ( "database/sql" "fmt" _ "github.com/go-sql-driver/mysql" ) const ( DB_USER = "user" DB_PASSWORD = "" DB_NAME = "test" ) func main() { dbSouce := fmt.Sprintf("%v:%v@/%v?charset=utf8", DB_USER, DB_PASSWORD, DB_NAME) db, err := sql.Open("mysql", dbSouce) checkErr(err) defer db.Close() fmt.Println("Inserting") stmt, err := db.Prepare("INSERT userinfo SET username=?,departname=?,created=?") checkErr(err) res, err := stmt.Exec("astaxie", "software developement", "2012-12-09") checkErr(err) id, err := res.LastInsertId() checkErr(err) fmt.Println("id of last inserted row =", id) fmt.Println("Updating") stmt, err = db.Prepare("update userinfo set username=? where uid=?") checkErr(err) res, err = stmt.Exec("astaxieupdate", id) checkErr(err) affect, err := res.RowsAffected() checkErr(err) fmt.Println(affect, "row(s) changed") fmt.Println("Querying") rows, err := db.Query("SELECT * FROM userinfo") checkErr(err) for rows.Next() { var uid int var username, department, created string err = rows.Scan(&uid, &username, &department, &created) checkErr(err) fmt.Println("uid | username | department | created") fmt.Printf("%3v | %6v | %6v | %6v\n", uid, username, department, created) } fmt.Println("Deleting") stmt, err = db.Prepare("delete from userinfo where uid=?") checkErr(err) res, err = stmt.Exec(id) checkErr(err) affect, err = res.RowsAffected() checkErr(err) fmt.Println(affect, "row(s) changed") } func checkErr(err error) { if err != nil { panic(err) } }

從上述程式碼可以看出,Go 操作 MySQL 資料庫是非常方便的:整個流程就是「開啟連線 → Prepare 預備語句 → Exec 執行(或 Query 查詢)→ 處理結果」,不需要手動管理連線池與驅動細節。

5. 關鍵函式與 DSN 格式詳解

5.1 sql.Open():開啟一個已註冊的資料庫驅動

sql.Open()函式用來開啟一個註冊過的資料庫驅動。go-sql-driver 在內部透過init()函式註冊了mysql這個驅動名稱,因此第一個參數傳入"mysql";第二個參數是DSN(Data Source Name),它是 go-sql-driver 定義的資料庫連線與配置資訊。DSN 支援如下四種格式:

user@unix(/path/to/socket)/dbname?charset=utf8 user:password@tcp(localhost:5555)/dbname?charset=utf8 user:password@/dbname user:password@tcp([de:ad:be:ef::ca:fe]:80)/dbname

四種格式的語意分別是:

  • user@unix(/path/to/socket)/dbname?charset=utf8:透過 Unix socket 連線(本機連線常用,不需密碼);
  • user:password@tcp(localhost:5555)/dbname?charset=utf8:透過 TCP 連線,可指定主機與埠號;
  • user:password@/dbname:最簡形式,省略連線協定與主機,採用驅動預設值(TCP 連線至本機 3306);
  • user:password@tcp([de:ad:be:ef::ca:fe]:80)/dbname:透過 TCP 連線 IPv6 位址的寫法,位址需以方括號包住。

四種格式都支援在/dbname之後以?附加查詢參數(例如?charset=utf8指定連線字元集為 UTF-8,避免中文亂碼)。注意sql.Open()只負責建立 DB 物件(驗證驅動名稱與格式),實際的網路連線通常在使用時(如第一次 Query/Exec)才建立。

5.2 db.Prepare():預備 SQL 語句

db.Prepare()函式用來回傳準備要執行的 SQL 操作,執行後回傳準備完畢的執行狀態(*sql.Stmt)。預備語句可以重複執行,且允許用?佔位符在Exec/Query時動態代入參數,範例中的插入、更新、刪除都先經過Prepare。

5.3 db.Query():直接執行 SQL 並回傳結果集

db.Query()函式用來直接執行 SQL 並回傳Rows結果。範例中查詢全部userinfo資料後,以for rows.Next()迴圈迭代每一筆資料,再透過rows.Scan(&uid, &username, &department, &created)將每列資料依序掃描到對應的變數位址中(Scan 的參數數量與順序必須與查詢結果的欄位一一對應)。

5.4 stmt.Exec():執行預備好的 SQL 語句

stmt.Exec()函式用來執行stmt準備好的 SQL 語句,傳入的參數會依序取代 SQL 中的?佔位符。插入操作執行後透過res.LastInsertId()取得自增主鍵uid(供後續更新與刪除使用);更新與刪除操作執行後透過res.RowsAffected()取得受影響的資料筆數。這兩個方法正好對應 5.1 節 中介紹的driver.Result介面(LastInsertId() (int64, error)與RowsAffected() (int64, error))。

6. 參數化查詢與 SQL 注入防護

仔細觀察範例可以發現,所有傳入的參數都是=?對應的資料,例如:

stmt, err := db.Prepare("INSERT userinfo SET username=?,department=?,created=?") res, err := stmt.Exec("astaxie", "研發部門", "2012-12-09")

Exec的第二個及之後的參數只作為資料值被綁定,不會被拼進 SQL 語法結構,因此這種參數化查詢(prepared statement)的方式可以在一定程度上防止 SQL 注入——即使使用者輸入含有惡意 SQL 片段,也只會被當成字串字面值處理。這是 Web 開發中操作資料庫必須養成的習慣;關於 SQL 注入的成因與更完整的防護手段,可參考本書 9.4 節「避免 SQL 注入」。

7. 深入原理:為什麼匯入驅動要寫_?背後是 database/sql 介面

7.1import _ "github.com/go-sql-driver/mysql"的意義

範例中匯入驅動時使用了_前綴,新手常對這個寫法感到困惑。這是 Go 設計的巧妙之處:變數賦值時_用來忽略不需要的返回值,套件匯入時_則表示「引入後面的套件而不直接使用其中定義的函式、變數等資源」。Go 在套件被匯入時會自動呼叫該套件的init()函式完成初始化,因此匯入驅動套件後,驅動會自動在init()中呼叫sql.Register("mysql", ...)完成驅動註冊,接下來的程式碼就可以直接透過sql.Open("mysql", ...)使用它。詳見 5.1 節「database/sql 介面」。

7.2 驅動註冊與 DB 內部的連線池

sql.Register是database/sql提供的註冊函式,第三方驅動都在init()中呼叫它,database/sql內部以一個 map 儲存使用者定義的驅動(var drivers = make(map[string]driver.Driver)),因此可以同時註冊多個不重名的驅動(例如同時匯入 MySQL 與 SQLite 驅動)。

sql.Open回傳的DB物件內部還維護著一個簡易連線池:DB結構中的freeConn []driver.Conn即空閒連線列表。每次db.conn會先判斷freeConn的長度是否大於 0,大於 0 表示有可複用的連線,直接取出使用;否則建立新連線再回傳。執行完操作後(例如db.prepare流程中的defer dc.releaseConn)會呼叫db.putConn把連線放回池中。這意味著使用標準介面時,連線的建立與複用都由database/sql層統一管理,開發者無需自行實作連線池,也正因如此,5.1 節 特別提醒不要自行快取driver.Conn後在多個 goroutine 之間共享——Conn只能應用於單一 goroutine。

8. 總結與延伸閱讀

本節示範了完整的「Go + MySQL」最小實戰閉環:選定驅動(go-sql-driver/mysql)→ 準備資料表 → 用database/sql標準介面完成插入、更新、查詢、刪除 → 透過參數化查詢防禦 SQL 注入。全流程的執行環境與跑通步驟,還可參考倉庫中的 en/code/src/apps/ch.5.2/readme.md、en/code/src/apps/ch.5.2/schema.sql 與 en/code/src/apps/ch.5.2/main.go。

由於所有操作都建立在標準database/sql介面之上,下一節 5.3「使用 SQLite 資料庫」 的程式幾乎一字不改即可切換資料庫——只換匯入的驅動與sql.Open的 DSN。繼續閱讀可依次參考:

  • 章節目錄
  • 上一節:database/sql 介面
  • 下一節:使用 SQLite 資料庫
  • 文档
  • 教程

【免费下载链接】build-web-application-with-golang

A golang ebook intro how to build a web with golang

项目地址:https://gitcode.com/gh_mirrors/bu/build-web-application-with-golang
点击查看免费下载

相关推荐

上一篇:kube-bench与Kubernetes API交互:client-go在检测过程中的应用
下一篇:Mockoon常见问题解答:新手入门必备知识

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询