Go 數(shù)據(jù)存儲篇(五):建立數(shù)據(jù)庫連接并進行增刪改查操作
前面給大家介紹了 Go 語言中的內(nèi)存存儲和文件存儲,文件存儲的好處是可以持久化數(shù)據(jù),但是并不是 Web 應(yīng)用數(shù)據(jù)存儲的終極方案,因為這樣存儲起來的數(shù)據(jù)檢索和管理起來非常麻煩,為此又誕生了數(shù)據(jù)庫管理系統(tǒng)來處理數(shù)據(jù)的增刪改查。數(shù)據(jù)庫又可以劃分為關(guān)系型數(shù)據(jù)庫(RDBMS)和非關(guān)系型數(shù)據(jù)庫(NoSQL),前者比如 MySQL、Oracle,后者比如 Redis、MongoDB,這里我們以當前最流行的開源關(guān)系型數(shù)據(jù)庫 MySQL 為例進行介紹。
1、初始化數(shù)據(jù)庫
開始之前,我們先要連接到 MySQL 服務(wù)器初始化數(shù)據(jù)庫和數(shù)據(jù)表。
注:如果你還沒有在本地安裝 MySQL 數(shù)據(jù)庫,需要先進行安裝,使用 Docker 啟動或者去 MySQL 官網(wǎng)下載安裝包安裝均可,Mac 系統(tǒng)中還可以使用 Homebrew 進行安裝,然后選擇一個自己喜歡的 GUI 客戶端,學院君本地使用的是 TablePlus。
做好上述準備工作連接到 MySQL 服務(wù)端之后,就可以創(chuàng)建一個名為 test_db 的數(shù)據(jù)庫:

然后在這個數(shù)據(jù)庫中創(chuàng)建一張名為 posts 的測試數(shù)據(jù)表用來存儲文章信息:
CREATE?TABLE?`posts`?(
??`id`?bigint?unsigned?AUTO_INCREMENT,
??`title`?varchar(100)?DEFAULT?NULL,
??`content`?text,
??`author`?varchar(30)?DEFAULT?NULL,
??PRIMARY?KEY?(`id`),
??UNIQUE?KEY?`id`?(`id`)
)?ENGINE=InnoDB?DEFAULT?CHARSET=utf8mb4?COLLATE=utf8mb4_unicode_ci;

2、建立數(shù)據(jù)庫連接
接下來,我們就可以在 Go 程序中編寫代碼建立與數(shù)據(jù)庫的連接,然后對 posts 表進行增刪改查操作了。
Go 語言并沒有提供 MySQL 客戶端擴展包的官方實現(xiàn),只是提供了一個抽象的 database/sql 接口,只要第三方數(shù)據(jù)庫客戶端實現(xiàn)該接口聲明的方法,用戶就可以在不同的第三方數(shù)據(jù)庫客戶端擴展包實現(xiàn)之間進行切換,而不需要調(diào)整任何業(yè)務(wù)代碼。
實現(xiàn) database/sql 接口的 MySQL 第三方擴展包很多,比較流行的有 go-sql-driver/mysql 和 ORM 擴展包 go-gorm/gorm,我們先來看看如何通過 go-sql-driver/mysql 在 Go 程序中與 MySQL 數(shù)據(jù)庫交互。
我們可以在測試代碼 db.go 中編寫一段 init 方法,在每次代碼執(zhí)行 main 入口函數(shù)之前先建立數(shù)據(jù)庫連接:
import?(
????"database/sql"
????_?"github.com/go-sql-driver/mysql"
)
var?Db?*sql.DB
func?init()??{
????var?err?error
????Db,?err?=?sql.Open("mysql",?"root:root@/test_db?charset=utf8mb4&parseTime=true")
????if?err?!=?nil?{
????????panic(err)
????}
}
sql.DB 是一個用于操作數(shù)據(jù)庫的結(jié)構(gòu)體,維護的是一個數(shù)據(jù)庫連接池。數(shù)據(jù)庫連接通過 sql.Open 方法設(shè)置,該方法接收一個數(shù)據(jù)庫驅(qū)動(這里是 mysql)和數(shù)據(jù)源名稱字符串(按照位置填充即可,更多細節(jié)請參考該數(shù)據(jù)庫包的官方文檔):
[username[:password]@][protocol[(address)]]/dbname[?param1=value1&...¶mN=valueN]
注:如果 MySQL 服務(wù)器運行在本地,則
address字段(IP + 端口號)留空。
成功后返回一個 sql.DB 指針,然后你就可以拿著這個指針操作數(shù)據(jù)庫了。
需要注意的是 Open 方法并沒有真正建立連接,也不會對傳入的參數(shù)做任何驗證,它只是負責初始化 sql.DB 結(jié)構(gòu)體字段而已,數(shù)據(jù)庫連接只有在后面真正需要的時候才會建立,是一個懶加載的過程。這樣做的好處是提升應(yīng)用性能,避免不必要的數(shù)據(jù)庫連接開銷。
另外,sql.DB 也不需要關(guān)閉,sql.DB 維護的是一個連接池,在我們的示例代碼中定義了一個全局的 Db 變量來指向它,你還可以在創(chuàng)建 sql.DB 后將其傳遞給要操作數(shù)據(jù)庫的方法。
接下來,我們來看下 Open 方法的參數(shù),第一個參數(shù)是數(shù)據(jù)庫驅(qū)動,要支持這個驅(qū)動需要調(diào)用 sql.Register 方法進行注冊,由于我們使用了 go-sql-driver/mysql 這個第三方包,這一步是在 mysql 包的 init 方法中完成的(driver.go):
func?init()?{
????sql.Register("mysql",?&MySQLDriver{})
}
后面的驅(qū)動結(jié)構(gòu)體需要實現(xiàn) sql 包的 driver.Driver 接口。
當我們通過下面這段代碼引入 mysql 包時:
_?"github.com/go-sql-driver/mysql"
就會調(diào)用 mysql 包的 init 方法。
Go 官方?jīng)]有提供任何數(shù)據(jù)庫驅(qū)動包,只是在 sql.Driver 中聲明了接口,第三方驅(qū)動包只要實現(xiàn)這些接口就好了。另外,我們在導入第三方包的時候,需要在前面加上短劃線 _,這樣做的好處是不會直接使用第三方包中的方法,只能使用官方 database/sql 中提供的方法,當?shù)谌桨壔蛘咝枰{(diào)整數(shù)據(jù)庫驅(qū)動時,不需要修改應(yīng)用中的代碼。
注:如果你對這一塊接口與實現(xiàn)的細節(jié)不清楚,可以回顧 Go 入門教程中的面向?qū)ο缶幊滩糠?/a>。
3、增刪改查示例代碼
數(shù)據(jù)庫初始化完成并設(shè)置好連接配置之后,就可以在 Go 應(yīng)用中與數(shù)據(jù)庫進行交互了。我們將編寫一段對文章表進行增刪改查的示例代碼來演示 Go 語言中的數(shù)據(jù)庫操作。
注:以下所有示例代碼都是在
db.go中編寫。
定義 Post 結(jié)構(gòu)體
首先我們需要定義一個表示文章表數(shù)據(jù)結(jié)構(gòu)的結(jié)構(gòu)體:
type?Post?struct?{
????Id?int
????Title?string
????Content?string
????Author?string
}
創(chuàng)建新文章
然后我們編寫在數(shù)據(jù)庫中創(chuàng)建文章記錄的 Create 方法,其實就是在上述全局 Db 數(shù)據(jù)庫連接上執(zhí)行 SQL 插入語句而已,對應(yīng)的示例代碼如下::
func?(post?*Post)?Create()?(err?error)?{
????sql?:=?"insert?into?posts?(title,?content,?author)?values?(?,??,??)"
????stmt,?err?:=?Db.Prepare(sql)
????if?err?!=?nil?{
????????panic(err)
????}
????defer?stmt.Close()
????res,?err?:=?stmt.Exec(post.Title,?post.Content,?post.Author)
????if?err?!=?nil?{
????????panic(err)
????}
????postId,?_?:=?res.LastInsertId()
????post.Id?=?int(postId)
????return
}
注:這里我們使用了預處理語句,以避免 SQL 注入攻擊,如果你有 PHP 或者其他語言數(shù)據(jù)庫編程基礎(chǔ)的話,應(yīng)該很容易看懂這些代碼。
實際上,我們還可以通過 stmt.QueryRow(post.Title, post.Content, post.Author) 來執(zhí)行插入操作,效果是一樣的,也可以直接通過 Db.Exec 執(zhí)行插入操作:
res,?err?:=?Db.Exec(sql,?post.Title,?post.Content,?post.Author)
獲取單篇文章
創(chuàng)建完文章后,可以通過 Db.QueryRow 執(zhí)行一條 SQL 查詢語句查詢單條記錄并將結(jié)果映射到 Post 結(jié)構(gòu)體中。
func?GetPost(id?int)?(post?Post,?err?error)?{
????post?=?Post{}
????err?=?Db.QueryRow("select?id,?title,?content,?author?from?posts?where?id?=??",?id).
????????Scan(&post.Id,?&post.Title,?&post.Content,?&post.Author)
????return
}
獲取文章列表
我們可以使用 sql.DB 提供的 Query 方法來查詢多條文章記錄:
func?Posts(limit?int)?(posts?[]Post,?err?error)?{
????rows,?err?:=?Db.Query("select?id,?title,?content,?author?from?posts?limit??",?limit)
????if?err?!=?nil?{
????????panic(err)
????}
????defer?rows.Close()
????for?rows.Next()?{
????????post?:=?Post{}
????????err?=?rows.Scan(&post.Id,?&post.Title,?&post.Content,?&post.Author)
????????if?err?!=?nil?{
????????????panic(err)
????????}
????????posts?=?append(posts,?post)
????}
????return
}
該方法返回的是 sql.Rows 接口,它是一個迭代器,你可以通過循環(huán)調(diào)用其 Next 方法返回其中的每個 sql.Row 對象,直到 sql.Rows 中的記錄值為空(此時返回 io.EOF)。
在循環(huán)體中,我們將每個 sql.Row 對象映射到 Post 對象,再將這個 Post 對象添加到 posts 切片中。
其實對于單條記錄,也可以使用類似的方式實現(xiàn),畢竟單條記錄查詢是 SELECT 查詢的特例:
func?GetPost(id?int)?(post?Post,?err?error)?{
????rows,?err?:=?Db.Query("select?id,?title,?content,?author?from?posts?where?id?=???limit?1",?id)
????if?err?!=?nil?{
????????panic(err)
????}
????defer?rows.Close()
????for?rows.Next()?{
????????post?=?Post{}
????????err?=?rows.Scan(&post.Id,?&post.Title,?&post.Content,?&post.Author)
????????if?err?!=?nil?{
????????????panic(err)
????????}
????}
????return
}
當然,前面的 Db.Query 也可以調(diào)整為預處理語句實現(xiàn),只是更繁瑣一些:
stmt,?err?:=?Db.Prepare("select?id,?title,?content,?author?from?posts?limit??")
if?err?!=?nil?{
????panic(err)
}
defer?stmt.Close()
rows,?err?:=?stmt.Query(limit)
if?err?!=?nil?{
????panic(err)
}
...?//?后續(xù)其他操作代碼
更新文章
對于已存在的文章記錄,可以通過執(zhí)行 SQL 更新語句進行修改:
func?(post?*Post)?Update()?(err?error)??{
????stmt,?err?:=?Db.Prepare("update?posts?set?title?=??,?content?=??,?author?=???where?id?=??")
????if?err?!=?nil?{
????????return
????}
????stmt.Exec(post.Title,?post.Content,?post.Author,?post.Id)
????return
}
當然,也可以通過 stmt.QueryRow 以及 Db.Exec 這兩種方式來處理上述操作,使用 Db.Exec 方法更簡潔:
func?(post?*Post)?Update()?(err?error)??{
????_,?err?=?Db.Exec("update?posts?set?title?=??,?content?=??,?author?=???where?id?=??",
????????post.Title,?post.Content,?post.Author,?post.Id)
????return
}
Db.Exec 方法返回的是 sql.Result 接口,該接口支持以下兩個方法:

我們不需要處理這個 Result 對象,所以通過 _ 將其忽略。
刪除文章
刪除操作和更新操作類似,只是將 UPDATE 語句調(diào)整為 DELETE 語句而已:
func?(post?*Post)?Delete()?(err?error)?{
????stmt,?err?:=?Db.Prepare("delete?from?posts?where?id?=??")
????if?err?!=?nil?{
????????return
????}
????stmt.Exec(post.Id)
????return
}
當然上述操作可以通過 stmt.QueryRow 或 Db.Exec 方法來實現(xiàn):
func?(post?*Post)?Delete()?(err?error)?{
????_,?err?=?Db.Exec("delete?from?posts?where?id?=??",?post.Id)
????return
}
4、整體測試
最后,我們在 db.go 中編寫入口函數(shù) main 測試一下上述數(shù)據(jù)庫增刪改查操作是否可以正常運行:
func?main()??{
????post?:=?Post{Title:?"Go?語言數(shù)據(jù)庫操作",?Content:?"基于第三方?go-sql-driver/mysql?包實現(xiàn)?MySQL?數(shù)據(jù)庫增刪改查",?Author:?"學院君"}
????//?創(chuàng)建記錄
????post.Create()
????fmt.Println(post)
????//?獲取單條記錄
????dbPost,?_?:=?GetPost(post.Id)
????fmt.Println(dbPost)
????//?更新記錄
????dbPost.Title?=?"Golang?數(shù)據(jù)庫操作"
????dbPost.Update()
????//?獲取文章列表
????posts,?_?:=?Posts(1)
????fmt.Println(posts)
????//?刪除記錄
????dbPost.Delete()
}
注:運行前,記得通過 Go Module 下載
go-sql-driver/mysql依賴。
在終端運行 db.go,輸出如下,表示這段數(shù)據(jù)庫增刪改查代碼可以正常運行:

好了,關(guān)于數(shù)據(jù)庫增刪改查基本操作就簡單介紹到這里,下篇教程,我們來看看如何在 MySQL 數(shù)據(jù)庫中實現(xiàn)不同表之間的關(guān)聯(lián)查詢和更新。
(全文完)
推薦閱讀

