亚洲乱码中文字幕综合,中国熟女仑乱hd,亚洲精品乱拍国产一区二区三区,一本大道卡一卡二卡三乱码全集资源,又粗又黄又硬又爽的免费视频

Go使用database/sql操作數(shù)據(jù)庫(kù)的教程指南

 更新時(shí)間:2023年06月12日 09:18:56   作者:江湖十年  
Go?語(yǔ)言中,有一個(gè)名為database/sql的標(biāo)準(zhǔn)庫(kù),提供了統(tǒng)一的編程接口,使開(kāi)發(fā)人員能夠以一種通用的方式與各種關(guān)系型數(shù)據(jù)庫(kù)進(jìn)行交互,本文就來(lái)和大家講講它的具體操作吧

在現(xiàn)代軟件開(kāi)發(fā)中,數(shù)據(jù)庫(kù)扮演著至關(guān)重要的角色,用于存儲(chǔ)和管理應(yīng)用程序的數(shù)據(jù)。針對(duì)不同的數(shù)據(jù)庫(kù)系統(tǒng),開(kāi)發(fā)人員通常需要使用特定的數(shù)據(jù)庫(kù)驅(qū)動(dòng)來(lái)操作數(shù)據(jù)庫(kù),這往往需要開(kāi)發(fā)人員掌握不同的驅(qū)動(dòng)編程接口。在 Go 語(yǔ)言中,好在有一個(gè)名為 database/sql 的標(biāo)準(zhǔn)庫(kù),提供了統(tǒng)一的編程接口,使開(kāi)發(fā)人員能夠以一種通用的方式與各種關(guān)系型數(shù)據(jù)庫(kù)進(jìn)行交互。

概念

database/sql 包通過(guò)提供統(tǒng)一的編程接口,實(shí)現(xiàn)了對(duì)不同數(shù)據(jù)庫(kù)驅(qū)動(dòng)的抽象。

它的大致原理如下:

  • Driver 接口定義:database/sql/driver 包中定義了一個(gè) Driver 接口,該接口用于表示一個(gè)數(shù)據(jù)庫(kù)驅(qū)動(dòng)。驅(qū)動(dòng)開(kāi)發(fā)者需要實(shí)現(xiàn)該接口來(lái)提供與特定數(shù)據(jù)庫(kù)的交互能力。
  • Driver 注冊(cè):驅(qū)動(dòng)開(kāi)發(fā)者需要在程序初始化階段,通過(guò)調(diào)用 database/sql 包提供的 sql.Register() 方法將自己的驅(qū)動(dòng)注冊(cè)到 database/sql 中。這樣,database/sql 就能夠識(shí)別和使用該驅(qū)動(dòng)。
  • 數(shù)據(jù)庫(kù)連接池管理:database/sql 維護(hù)了一個(gè)數(shù)據(jù)庫(kù)連接池,用于管理數(shù)據(jù)庫(kù)連接。當(dāng)通過(guò) sql.Open() 打開(kāi)一個(gè)數(shù)據(jù)庫(kù)連接時(shí),database/sql 會(huì)在合適的時(shí)機(jī)調(diào)用注冊(cè)的驅(qū)動(dòng)來(lái)創(chuàng)建一個(gè)具體的連接,并將其添加到連接池中。連接池會(huì)負(fù)責(zé)連接的復(fù)用、管理和維護(hù)工作,并且這是并發(fā)安全的。
  • 統(tǒng)一的編程接口:database/sql 定義了一組統(tǒng)一的編程接口供用戶使用,如 Prepare()、Exec()Query() 等方法,用于準(zhǔn)備 SQL 語(yǔ)句、執(zhí)行 SQL 語(yǔ)句和執(zhí)行查詢等操作。這些方法會(huì)接收參數(shù)并調(diào)用底層驅(qū)動(dòng)的相應(yīng)方法來(lái)執(zhí)行實(shí)際的數(shù)據(jù)庫(kù)操作。
  • 接口方法的實(shí)現(xiàn):驅(qū)動(dòng)開(kāi)發(fā)者需要實(shí)現(xiàn) database/sql/driver 中定義的一些接口方法,以此來(lái)支持上層 database/sql 包提供的 Prepare()、Exec()Query() 等方法,以提供底層數(shù)據(jù)庫(kù)的具體實(shí)現(xiàn)。當(dāng) database/sql 調(diào)用這些方法時(shí),實(shí)際上會(huì)調(diào)用注冊(cè)的驅(qū)動(dòng)的相應(yīng)方法來(lái)執(zhí)行具體的數(shù)據(jù)庫(kù)操作。

通過(guò)以上的機(jī)制,database/sql 包能夠?qū)崿F(xiàn)對(duì)不同數(shù)據(jù)庫(kù)驅(qū)動(dòng)的統(tǒng)一封裝和調(diào)用。用戶可以使用相同的編程接口來(lái)進(jìn)行數(shù)據(jù)庫(kù)操作,無(wú)需關(guān)心底層驅(qū)動(dòng)的具體細(xì)節(jié)。這種設(shè)計(jì)使得代碼更具可移植性和靈活性,方便切換和適配不同的數(shù)據(jù)庫(kù)。

特點(diǎn)

database/sql 具有如下特點(diǎn):

  • 統(tǒng)一的編程接口:database/sql 庫(kù)提供了一組統(tǒng)一的接口,使得開(kāi)發(fā)人員可以使用相同的方式操作不同的數(shù)據(jù)庫(kù),而不需要學(xué)習(xí)特定數(shù)據(jù)庫(kù)的 API。
  • 驅(qū)動(dòng)支持:通過(guò)導(dǎo)入第三方數(shù)據(jù)庫(kù)驅(qū)動(dòng)程序,database/sql 可以與多種常見(jiàn)的關(guān)系型數(shù)據(jù)庫(kù)系統(tǒng)進(jìn)行交互,如 MySQL、PostgreSQL、SQLite 等。
  • 預(yù)防 SQL 注入:database/sql 庫(kù)通過(guò)使用預(yù)編譯語(yǔ)句和參數(shù)化查詢等技術(shù),有效預(yù)防了 SQL 注入攻擊。
  • 支持事務(wù):事務(wù)是一個(gè)優(yōu)秀的 SQL 包必備功能。

準(zhǔn)備

為了演示 database/sql 用法,我準(zhǔn)備了如下 MySQL 數(shù)據(jù)庫(kù)表:

DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(50) DEFAULT NULL COMMENT '用戶名',
  `email` varchar(255) NOT NULL DEFAULT '' COMMENT '郵箱',
  `age` tinyint(4) NOT NULL DEFAULT '0' COMMENT '年齡',
  `birthday` datetime DEFAULT NULL COMMENT '生日',
  `salary` varchar(128) DEFAULT NULL COMMENT '薪水',
  `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
  `updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`),
  UNIQUE KEY `u_email` (`email`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='用戶表';

你可以使用 MySQL 命令行或圖形化工具創(chuàng)建這張表。

連接數(shù)據(jù)庫(kù)

要使用 database/sql 操作數(shù)據(jù)庫(kù),首先要建立與數(shù)據(jù)庫(kù)的連接:

package main
import (
	"database/sql"
	"log"
	_ "github.com/go-sql-driver/mysql"
)
func main() {
	db, err := sql.Open("mysql",
		"user:password@tcp(127.0.0.1:3306)/demo?charset=utf8mb4&parseTime=true&loc=Local")
	if err != nil {
		log.Fatal(err)
	}
	defer db.Close()
}

因?yàn)槲覀円B接 MySQL 數(shù)據(jù)庫(kù),所以需要導(dǎo)入 MySQL 數(shù)據(jù)庫(kù)驅(qū)動(dòng) github.com/go-sql-driver/mysql。database/sql 由 Go 語(yǔ)言官方團(tuán)隊(duì)設(shè)計(jì),而數(shù)據(jù)庫(kù)驅(qū)動(dòng)程序則由社區(qū)維護(hù),其他關(guān)系型數(shù)據(jù)庫(kù)驅(qū)動(dòng)列表可在這里查看。

與數(shù)據(jù)庫(kù)建立連接的代碼非常簡(jiǎn)單,只需調(diào)用 sql.Open() 函數(shù)即可。它接收兩個(gè)參數(shù),驅(qū)動(dòng)名稱和 DSN。

這里驅(qū)動(dòng)名稱為 mysql,database/sql 之所以能夠識(shí)別這個(gè)驅(qū)動(dòng)名稱,是因?yàn)樵谀涿麑?dǎo)入 github.com/go-sql-driver/mysql 時(shí),這個(gè)庫(kù)內(nèi)部調(diào)用了 sql.Register 將其注冊(cè)給了 database/sql。

func init() {
	sql.Register("mysql", &MySQLDriver{})
}

在 Go 語(yǔ)言中,一個(gè)包的 init 方法會(huì)在導(dǎo)入時(shí)會(huì)被自動(dòng)調(diào)用,這里完成了驅(qū)動(dòng)程序的注冊(cè)。這樣在調(diào)用 sql.Open() 時(shí)才能找到 mysql 驅(qū)動(dòng)。

第二個(gè)參數(shù) DSN 全稱 Data Source Name,數(shù)據(jù)庫(kù)的源名稱,其格式如下:

username:password@protocol(address)/dbname?param=value

下面是我們提供的 DSN 各部分解釋:

  • user:password:數(shù)據(jù)庫(kù)的用戶名和密碼。根據(jù)實(shí)際情況,你需要使用你自己的用戶名和密碼。
  • tcp(127.0.0.1:3306):連接數(shù)據(jù)庫(kù)服務(wù)器的協(xié)議、數(shù)據(jù)庫(kù)服務(wù)器的地址和端口號(hào)。在這個(gè)例子中,使用的是本地主機(jī) 127.0.0.1 和 MySQL 默認(rèn)端口號(hào) 3306。你可以根據(jù)實(shí)際情況修改為你自己的數(shù)據(jù)庫(kù)服務(wù)器地址和端口號(hào)。
  • /demo:數(shù)據(jù)庫(kù)的名稱。在這個(gè)例子中,數(shù)據(jù)庫(kù)名稱是 demo。你可以根據(jù)實(shí)際情況修改為你自己的數(shù)據(jù)庫(kù)名稱。
  • charset=utf8mb4:指定數(shù)據(jù)庫(kù)的字符集為 UTF-8。這里使用的是 UTF-8 的變體 UTF-8mb4,支持更廣泛的字符范圍。
  • parseTime=true:?jiǎn)⒂脮r(shí)間解析。這個(gè)參數(shù)使得數(shù)據(jù)庫(kù)驅(qū)動(dòng)程序能夠?qū)?shù)據(jù)庫(kù)中的時(shí)間類型字段(datetime)解析為 Go 語(yǔ)言的 time.Time 類型。
  • loc=Local:設(shè)置時(shí)區(qū)為本地時(shí)區(qū)。這個(gè)參數(shù)指定數(shù)據(jù)庫(kù)驅(qū)動(dòng)程序使用本地的時(shí)區(qū)。

sql.Open() 調(diào)用后將返回一個(gè) *sql.DB 類型,可以用來(lái)操作數(shù)據(jù)庫(kù)。

另外,我們調(diào)用 defer db.Close() 來(lái)釋放數(shù)據(jù)庫(kù)連接。其實(shí)這一步操作也可以不做,database/sql 底層連接池會(huì)幫我們處理。一旦關(guān)閉了連接,就不可以再繼續(xù)使用這個(gè) db 對(duì)象了。

*sql.DB 的設(shè)計(jì)是用來(lái)作為長(zhǎng)連接使用的,所以不需要頻繁的進(jìn)行 OpenClose 操作。如果我們需要連接多個(gè)數(shù)據(jù)庫(kù),則可以為每個(gè)不同的數(shù)據(jù)庫(kù)創(chuàng)建一個(gè) *sql.DB 對(duì)象,保持這些對(duì)象為 Open 狀態(tài),不必頻繁使用 Close 來(lái)切換連接。

值得注意的是,其實(shí) sql.Open() 并沒(méi)有真正建立數(shù)據(jù)庫(kù)連接,它只是準(zhǔn)備好了一切,以備后續(xù)使用,連接將在第一次被使用時(shí)延遲建立。

這樣的設(shè)計(jì)雖然合理,可也有些違反直覺(jué),sql.Open() 甚至不會(huì)校驗(yàn) DSN 參數(shù)的合法性。不過(guò)我們可以使用 db.Ping() 方法來(lái)主動(dòng)檢查連接是否能被正確建立。

if err := db.Ping(); err != nil {
    log.Fatal(err)
}

使用 sql.Open() 并不會(huì)建立一個(gè)唯一的數(shù)據(jù)庫(kù)連接,事實(shí)上,database/sql 會(huì)維護(hù)一個(gè)連接池。

我們可以通過(guò)如下方法,控制連接池的一些參數(shù):

db.SetMaxOpenConns(25)                 // 設(shè)置最大的并發(fā)連接數(shù)(in-use + idle)
db.SetMaxIdleConns(25)                 // 設(shè)置最大的空閑連接數(shù)(idle)
db.SetConnMaxLifetime(5 * time.Minute) // 設(shè)置連接的最大生命周期

這些參數(shù)設(shè)置可以根據(jù)經(jīng)驗(yàn)來(lái)修改,以上參數(shù)能夠滿足一些中小項(xiàng)目的使用,如果是大型項(xiàng)目,則可以適當(dāng)調(diào)高參數(shù)。

聲明模型

連接建立好后,理論上我們就可以操作數(shù)據(jù)庫(kù)進(jìn)行 CRUD 了。不過(guò)為了寫(xiě)出的代碼更具可維護(hù)性,我們往往需要定義模型來(lái)映射數(shù)據(jù)庫(kù)表。

user 表映射后的模型如下:

type User struct {
	ID        int
	Name      sql.NullString
	Email     string
	Age       int
	Birthday  *time.Time
	Salary    Salary
	CreatedAt time.Time
	UpdatedAt string
}

模型在 Go 中使用 struct 表示,結(jié)構(gòu)體字段同數(shù)據(jù)庫(kù)表中的字段一一對(duì)應(yīng)。

其中 Salary 類型定義如下:

type Salary struct {
    Month int `json:"month"`
    Year  int `json:"year"`
}

關(guān)于 Name、Salary 兩個(gè)字段的特殊性,我將分別在 處理 NULL 和 自定義字段類型 部分講解。

創(chuàng)建

*sql.DB 提供了 Exec 方法來(lái)執(zhí)行一條 SQL 命令,可以用來(lái)創(chuàng)建、更新、刪除表數(shù)據(jù)等。

這里使用 Exec 方法來(lái)實(shí)現(xiàn)創(chuàng)建一個(gè)用戶:

func CreateUser(db *sql.DB) (int64, error) {
    birthday := time.Date(2000, 1, 1, 0, 0, 0, 0, time.Local)
    user := User{
        Name:     sql.NullString{String: "jianghushinian007", Valid: true},
        Email:    "jianghushinian007@outlook.com",
        Age:      10,
        Birthday: &birthday,
        Salary: Salary{
            Month: 100000,
            Year:  10000000,
        },
    }
    res, err := db.Exec(`INSERT INTO user(name, email, age, birthday, salary) VALUES(?, ?, ?, ?, ?)`,
        user.Name, user.Email, user.Age, user.Birthday, user.Salary)
    if err != nil {
        return 0, err
    }
    return res.LastInsertId()
}

首先我們實(shí)例化了一個(gè) User 對(duì)象 user,并對(duì)相應(yīng)字段進(jìn)行賦值。

接著使用 db.Exec 方法來(lái)執(zhí)行 SQL 語(yǔ)句:

INSERT INTO user(name, email, age, birthday, salary) VALUES(?, ?, ?, ?, ?)

其中 ? 作為參數(shù)占位符,不同數(shù)據(jù)庫(kù)驅(qū)動(dòng)程序的占位符可能不同,可以參考數(shù)據(jù)庫(kù)驅(qū)動(dòng)的文檔。

我們將這 5 個(gè)參數(shù)順序傳遞給 db.Exec 方法,即可完成用戶的創(chuàng)建。

db.Exec 方法調(diào)用后將返回 sql.Result 保存結(jié)果以及一個(gè) error 來(lái)標(biāo)記錯(cuò)誤。

sql.Result 是一個(gè)接口,它包含兩個(gè)方法:

  • LastInsertId() (int64, error):返回新插入的用戶 ID。
  • RowsAffected() (int64, error):返回當(dāng)前操作受影響的行數(shù)。

接口具體實(shí)現(xiàn)有數(shù)據(jù)庫(kù)驅(qū)動(dòng)程序來(lái)完成。

調(diào)用 CreateUser 函數(shù)即可創(chuàng)建一個(gè)新的用戶:

if id, err := CreateUser(db); err != nil {
    log.Fatal(err)
} else {
    log.Println("id:", id)
}

此外,database/sql 還提供了預(yù)處理方法 *sql.DB.Prepare 創(chuàng)建一個(gè)準(zhǔn)備好的 SQL 語(yǔ)句,在循環(huán)中使用預(yù)處理,則可以減少與數(shù)據(jù)庫(kù)的交互次數(shù)。

比如我們需要?jiǎng)?chuàng)建兩個(gè)用戶,則可以先使用 db.Prepare 創(chuàng)建一個(gè) *sql.Stmt 對(duì)象,然后多次調(diào)用 *sql.Stmt.Exec 方法來(lái)插入數(shù)據(jù):

func CreateUsers(db *sql.DB) ([]int64, error) {
	stmt, err := db.Prepare("INSERT INTO user(name, email, age, birthday, salary) VALUES(?, ?, ?, ?, ?)")
	if err != nil {
		panic(err)
	}
	// 注意:預(yù)處理對(duì)象是需要關(guān)閉的
	defer stmt.Close()
	birthday := time.Date(2000, 2, 2, 0, 0, 0, 0, time.Local)
	users := []User{
		{
			Name:     sql.NullString{String: "", Valid: true},
			Email:    "jianghushinian007@gmail.com",
			Age:      20,
			Birthday: &birthday,
			Salary: Salary{
				Month: 200000,
				Year:  20000000,
			},
		},
		{
			Name:  sql.NullString{String: "", Valid: false},
			Email: "jianghushinian007@163.com",
			Age:   30,
		},
	}
	var ids []int64
	for _, user := range users {
		res, err := stmt.Exec(user.Name, user.Email, user.Age, user.Birthday, user.Salary)
		if err != nil {
			return nil, err
		}
		id, err := res.LastInsertId()
		if err != nil {
			return nil, err
		}
		ids = append(ids, id)
	}
	return ids, nil
}

db.Prepare 是預(yù)先將一個(gè)數(shù)據(jù)庫(kù)連接和一個(gè)條 SQL 語(yǔ)句綁定并返回 *sql.Stmt 結(jié)構(gòu)體,它代表了這個(gè)綁定后的連接對(duì)象,是并發(fā)安全的。

通過(guò)使用預(yù)處理,可以避免在循環(huán)中執(zhí)行多次完整的 SQL 語(yǔ)句,從而顯著減少了數(shù)據(jù)庫(kù)交互次數(shù),這可以提高應(yīng)用程序的性能和效率。

使用預(yù)處理,會(huì)在 db.Prepare 時(shí)從連接池獲取一個(gè)連接,之后循環(huán)執(zhí)行 stmt.Exec,最終釋放連接。

如果使用 db.Exec,則每次循環(huán)時(shí)都需要:獲取連接-執(zhí)行 SQL-釋放連接,這幾個(gè)步驟,大大增加了與數(shù)據(jù)庫(kù)的交互次數(shù)。

不要忘記調(diào)用 stmt.Close() 關(guān)閉連接,這個(gè)方法是密等的,可以多次調(diào)用。

查詢

現(xiàn)在數(shù)據(jù)庫(kù)里已經(jīng)有了數(shù)據(jù),我們就可以查詢數(shù)據(jù)了。

因?yàn)?Exec 方法只會(huì)執(zhí)行 SQL,不會(huì)返回結(jié)果,所以不適用于查詢數(shù)據(jù)。

*sql.DB 提供了 Query 方法執(zhí)行查詢操作:

func GetUsers(db *sql.DB) ([]User, error) {
	rows, err := db.Query("SELECT * FROM user;")
	if err != nil {
		return nil, err
	}
	defer func() { _ = rows.Close() }()
	var users []User
	for rows.Next() {
		var user User
		if err := rows.Scan(&user.ID, &user.Name, &user.Email, &user.Age,
			&user.Birthday, &user.Salary, &user.CreatedAt, &user.UpdatedAt); err != nil {
			log.Println(err.Error())
			continue
		}
		users = append(users, user)
	}
	// 處理錯(cuò)誤
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return users, nil
}

db.Query 返回查詢結(jié)果集 *sql.Rows,這是一個(gè)結(jié)構(gòu)體。

rows.Next() 方法用來(lái)判斷是否還有下一條結(jié)果,可以用于 for 循環(huán)(題外話:這有點(diǎn)像 Python 的迭代器,只不過(guò)下一個(gè)值不是直接返回,而是通過(guò) Scan 方法獲?。?。

如果存在下一條結(jié)果,rows.Next() 將返回 true。

rows.Scan() 方法可以將結(jié)果掃描到傳遞進(jìn)來(lái)的指針對(duì)象。因?yàn)槲覀兪褂昧?SELECT * 來(lái)查詢,所以會(huì)返回全部字段的數(shù)據(jù),按順序?qū)?user 對(duì)象相應(yīng)的字段指針傳遞進(jìn)來(lái)即可。

rows.Scan() 會(huì)將一行記錄分別填入指定的變量中,并且會(huì)自動(dòng)根據(jù)目標(biāo)變量的類型處理類型轉(zhuǎn)換的問(wèn)題,比如數(shù)據(jù)庫(kù)中是 varchar 類型,會(huì)映射成 Go 中的 string,但如果與之對(duì)應(yīng)的目標(biāo)變量是 int,那么轉(zhuǎn)換失敗就會(huì)返回 error。

CreatedAt 字段是 time.Time 類型,之所以能夠被正確處理,是因?yàn)樵谡{(diào)用 sql.Open() 時(shí)傳遞的 DSN 包含 parseTime=true 參數(shù)。

當(dāng) rows.Next() 返回為 false 時(shí),即不再有下一條記錄。我們也就將全部查詢出來(lái)的用戶都存儲(chǔ)到 users 切片中了。

循環(huán)結(jié)束后,切記一定要調(diào)用 rows.Err() 來(lái)處理錯(cuò)誤。

以上,我們查詢了多條用戶,*sql.DB 還提供了 QueryRow 方法可以查詢單條記錄:

func GetUser(db *sql.DB, id int64) (User, error) {
	var user User
	row := db.QueryRow("SELECT * FROM user WHERE id = ?", id)
	err := row.Scan(&user.ID, &user.Name, &user.Email, &user.Age,
		&user.Birthday, &user.Salary, &user.CreatedAt, &user.UpdatedAt)
	switch {
	case err == sql.ErrNoRows:
		return user, fmt.Errorf("no user with id %d", id)
	case err != nil:
		return user, err
	}
	// 處理錯(cuò)誤
	if err := row.Err(); err != nil {
		return user, err
	}
	return user, nil
}

查詢單條記錄會(huì)返回 *sql.Row 結(jié)構(gòu)體,它實(shí)際上是對(duì) *sql.Rows 的一層包裝:

type Row struct {
	// One of these two will be non-nil:
	err  error // deferred error for easy chaining
	rows *Rows
}

我們不再需要調(diào)用 rows.Next() 判斷是否有下一條結(jié)果,調(diào)用 row.Sca() 時(shí) *sql.Row 會(huì)自動(dòng)幫我們處理好,返回查詢結(jié)果集中的第一條數(shù)據(jù)。

如果 row.Sca() 返回的錯(cuò)誤類型為 sql.ErrNoRows 說(shuō)明沒(méi)有查詢到符合條件的數(shù)據(jù),這對(duì)于判斷錯(cuò)誤類型特別有用。

database/sql 顯然不能枚舉出所有數(shù)據(jù)庫(kù)的錯(cuò)誤類型,有些針對(duì)不同數(shù)據(jù)庫(kù)的指定錯(cuò)誤類型,通常由數(shù)據(jù)庫(kù)驅(qū)動(dòng)程序來(lái)定義。

可以按照如下方式判斷特定的 MySQL 錯(cuò)誤類型:

if driverErr, ok := err.(*mysql.MySQLError); ok {
    if driverErr.Number == 1045 {
        // 處理被拒絕的錯(cuò)誤
    }
}

不過(guò)像 1045 這種魔法數(shù)字最好不要出現(xiàn)在代碼中,mysqlerr 包提供了 MySQL 錯(cuò)誤類型的枚舉。

以上代碼可以改為:

if driverErr, ok := err.(*mysql.MySQLError); ok  {
    if driverErr.Number == mysqlerr.ER_ACCESS_DENIED_ERROR {
        // 處理被拒絕的錯(cuò)誤
    }
}

最后,同樣不要忘記調(diào)用 row.Err() 處理錯(cuò)誤。

更新

更新操作同創(chuàng)建一樣可以使用 *sql.DB.Exec 方法來(lái)實(shí)現(xiàn),不過(guò)這里我們將使用 *sql.DB.ExecContext 方法來(lái)實(shí)現(xiàn)。

ExecContext 方法與 Exec 方法在使用上沒(méi)什么兩樣,只不過(guò)第一個(gè)參數(shù)需要接收一個(gè) context.Context,它允許你控制和取消執(zhí)行 SQL 語(yǔ)句的操作。使用上下文可以在需要的情況下設(shè)置超時(shí)時(shí)間、處理請(qǐng)求取消等操作。

func UpdateUserName(db *sql.DB, id int64, name string) error {
	ctx := context.Background()
	res, err := db.ExecContext(ctx, "UPDATE user SET name = ? WHERE id = ?", name, id)
	if err != nil {
		return err
	}
	affected, err := res.RowsAffected()
	if err != nil {
		return err
	}
	if affected == 0 {
		// 如果新的 name 等于原 name,也會(huì)執(zhí)行到這里
		return fmt.Errorf("no user with id %d", id)
	}
	return nil
}

這里使用 res.RowsAffected() 獲取了當(dāng)前操作影響的行數(shù)。

注意,如果更新后的字段結(jié)果沒(méi)有變化,res.RowsAffected() 返回 0。

刪除

使用 *sql.DB.ExecContext 方法實(shí)現(xiàn)刪除用戶:

func DeleteUser(db *sql.DB, id int64) error {
	ctx := context.Background()
	res, err := db.ExecContext(ctx, "DELETE FROM user WHERE id = ?", id)
	if err != nil {
		return err
	}
	affected, err := res.RowsAffected()
	if err != nil {
		return err
	}
	if affected == 0 {
		return fmt.Errorf("no user with id %d", id)
	}
	return nil
}

事務(wù)

事務(wù)基本是開(kāi)發(fā) Web 項(xiàng)目時(shí)比不可少的數(shù)據(jù)庫(kù)功能,database/sql 提供了對(duì)事務(wù)的支持。

如下示例使用事務(wù)來(lái)更新用戶:

func Transaction(db *sql.DB, id int64, name string) error {
	ctx := context.Background()
	tx, err := db.BeginTx(ctx, &sql.TxOptions{Isolation: sql.LevelSerializable})
	if err != nil {
		return err
	}
	_, execErr := tx.ExecContext(ctx, "UPDATE user SET name = ? WHERE id = ?", name, id)
	if execErr != nil {
		if rollbackErr := tx.Rollback(); rollbackErr != nil {
			log.Fatalf("update failed: %v, unable to rollback: %v\n", execErr, rollbackErr)
		}
		log.Fatalf("update failed: %v", execErr)
	}
	return tx.Commit()
}

*sql.DB.BeginTx 用于開(kāi)啟一個(gè)事務(wù),第一個(gè)參數(shù)為 context.Context,第二個(gè)參數(shù)為 *sql.TxOptions 對(duì)象,用來(lái)配置事務(wù)選項(xiàng),Isolation 字段用來(lái)設(shè)置數(shù)據(jù)庫(kù)隔離級(jí)別。

事務(wù)中執(zhí)行的 SQL 語(yǔ)句需要放在 tx 對(duì)象的 ExecContext 方法中執(zhí)行,而不是 db.ExecContext。

如果執(zhí)行 SQL 過(guò)程中出現(xiàn)錯(cuò)誤,可以使用 tx.Rollback() 進(jìn)行事務(wù)回滾。

如果沒(méi)有錯(cuò)誤,則可以使用 tx.Commit() 提交事務(wù)。

tx 同樣支持 Prepare 方法,可以點(diǎn)擊這里查看使用示例。

處理 NULL

在創(chuàng)建 User 模型時(shí),我們定義 Name 字段類型為 sql.NullString,而非普通的 string 類型,這是為了支持?jǐn)?shù)據(jù)庫(kù)中的 NULL 類型。

數(shù)據(jù)庫(kù)中 name 字段定義如下:

`name` varchar(50) DEFAULT NULL COMMENT '用戶名'

那么 name 在數(shù)據(jù)庫(kù)中可能的值將有三種情況:NULL、空字符串 '' 以及有值的字符串 'n1'。

我們知道,Go 語(yǔ)言中 string 類型的默認(rèn)值即為空字符串 '',但是 string 無(wú)法表示 NULL 值。

這個(gè)時(shí)候,我們有兩種方法解決此問(wèn)題:

  • 使用指針類型。
  • 使用 sql.NullString 類型。

因?yàn)橹羔橆愋涂赡転?nil,所以可以使用 nil 來(lái)對(duì)應(yīng) NULL 值。這就是 User 模型中 Birthday 字段類型定義為 *time.Time 的緣故。

sql.NullString 類型定義如下:

type NullString struct {
	String string
	Valid  bool // Valid is true if String is not NULL
}

String 用來(lái)記錄值,Valid 用來(lái)標(biāo)記是否為 NULL。

NullString 結(jié)構(gòu)體的值和數(shù)據(jù)庫(kù)中實(shí)際存儲(chǔ)的值,有如下映射關(guān)系:

valuevalue for MySQL
{String:n1 Valid:true}'n1'
{String: Valid:true}''
{String: Valid:false}NULL

此外,sql.NullString 類型還實(shí)現(xiàn)了 sql.Scanner 和 driver.Valuer 兩個(gè)接口:

// Scan implements the Scanner interface.
func (ns *NullString) Scan(value any) error {
	if value == nil {
		ns.String, ns.Valid = "", false
		return nil
	}
	ns.Valid = true
	return convertAssign(&ns.String, value)
}
// Value implements the driver Valuer interface.
func (ns NullString) Value() (driver.Value, error) {
	if !ns.Valid {
		return nil, nil
	}
	return ns.String, nil
}

這兩個(gè)接口分別用在 *sql.Row.Scan 方法和 *sql.DB.Exec 方法。

即在使用 *sql.DB.Exec 方法執(zhí)行 SQL 時(shí),我們可能需要將 Name 字段的值存入 MySQL,此時(shí) database/sql 會(huì)調(diào)用 sql.NullString 類型的 Value() 方法,獲取其將要存儲(chǔ)于數(shù)據(jù)庫(kù)中的值。

在使用 *sql.Row.Scan 方法時(shí),我們可能需要將從數(shù)據(jù)庫(kù)獲取到的 name 字段值映射到 User 結(jié)構(gòu)體字段 Name 上,此時(shí) database/sql 會(huì)調(diào)用 sql.NullString 類型的 Scan() 方法,把從數(shù)據(jù)庫(kù)中查詢的值賦值給 Name 字段。

如果你使用的字段不是 string 類型,database/sql 還提供了 sql.NullBool、sql.NullFloat64 等類型供用戶使用。

但是,這并不能枚舉出所有 MySQL 數(shù)據(jù)庫(kù)支持的字段類型,所以如果能夠盡量避免,還是不建議數(shù)據(jù)庫(kù)字段允許 NULL 值。

自定義字段類型

有些時(shí)候,我們保存在數(shù)據(jù)庫(kù)中的數(shù)據(jù)有著特定的格式,比如 salary 字段在數(shù)據(jù)庫(kù)中存儲(chǔ)的值為 {"month":100000,"year":10000000}。

數(shù)據(jù)庫(kù)中 salary 字段定義如下:

`salary` varchar(128) DEFAULT NULL COMMENT '薪水'

如果只是將其映射為 Go 中的 string,則操作時(shí)要格外小心,如果忘記寫(xiě)一個(gè) ", 等,程序?qū)⒖赡軋?bào)錯(cuò)。

因?yàn)?salary 值明顯是一個(gè) JSON 格式,我們可以定義一個(gè) struct 來(lái)映射其內(nèi)容:

type Salary struct {
	Month int `json:"month"`
	Year  int `json:"year"`
}

這還不夠,自定義類型無(wú)法支持 *sql.Row.Scan 方法和 *sql.DB.Exec 方法。

不過(guò),我想你已經(jīng)猜到了,我們可以參考 sql.NullString 類型讓 Salary 同樣實(shí)現(xiàn) sql.Scannerdriver.Valuer 兩個(gè)接口:

// Scan implements sql.Scanner
func (s *Salary) Scan(src any) error {
	if src == nil {
		return nil
	}
	var buf []byte
	switch v := src.(type) {
	case []byte:
		buf = v
	case string:
		buf = []byte(v)
	default:
		return fmt.Errorf("invalid type: %T", src)
	}
	err := json.Unmarshal(buf, s)
	return err
}
// Value implements driver.Valuer
func (s Salary) Value() (driver.Value, error) {
	v, err := json.Marshal(s)
	return string(v), err
}

這樣,存儲(chǔ)和查詢數(shù)據(jù)的操作,Salary 類型都能夠支持。

未知列

極端情況下,我們可能不知道使用 *sql.DB.Query 方法查詢的結(jié)果集中數(shù)據(jù)的列名以及字段個(gè)數(shù)。

此時(shí),我們可以使用 *sql.Rows.Columns 方法獲取所有列名,這將返回一個(gè)切片,這個(gè)切片長(zhǎng)度,即為字段個(gè)數(shù)。

示例代碼如下:

func HandleUnknownColumns(db *sql.DB, id int64) ([]interface{}, error) {
	var res []interface{}
	rows, err := db.Query("SELECT * FROM user WHERE id = ?", id)
	if err != nil {
		return res, err
	}
	defer func() { _ = rows.Close() }()
	// 如果不知道列名稱,可以使用 rows.Columns() 查找列名稱列表
	cols, err := rows.Columns()
	if err != nil {
		return res, err
	}
	fmt.Printf("columns: %v\n", cols) // [id name email age birthday salary created_at updated_at]
	fmt.Printf("columns length: %d\n", len(cols))
	// 獲取列類型信息
	types, err := rows.ColumnTypes()
	if err != nil {
		return nil, err
	}
	for _, typ := range types {
		// id: &{name:id hasNullable:true hasLength:false hasPrecisionScale:false nullable:false length:0 databaseType:INT precision:0 scale:0 scanType:0x1045d68a0}
		fmt.Printf("%s: %+v\n", typ.Name(), typ)
	}
	res = []interface{}{
		new(int),            // id
		new(sql.NullString), // name
		new(string),         // email
		new(int),            // age
		new(time.Time),      // birthday
		new(Salary),         // salary
		new(time.Time),      // created_at
		// 如果不知道列類型,可以使用 sql.RawBytes,它實(shí)際上是 []byte 的別名
		new(sql.RawBytes), // updated_at
	}
	for rows.Next() {
		if err := rows.Scan(res...); err != nil {
			return res, err
		}
	}
	return res, rows.Err()
}

除了獲取列名和字段個(gè)數(shù),我們還可以使用 *sql.Rows.ColumnTypes 方法獲取每個(gè) column 的詳細(xì)信息。

如果我們不知道某個(gè)字段在數(shù)據(jù)庫(kù)中的類型,則可以將其映射為 sql.RawBytes 類型,它實(shí)際上是 []byte 的別名。

總結(jié)

database/sql 包統(tǒng)一了 Go 語(yǔ)言操作數(shù)據(jù)庫(kù)的編程接口,避免了操作不同數(shù)據(jù)庫(kù)需要學(xué)習(xí)多套 API 的窘境。

使用 sql.Open() 建立數(shù)據(jù)庫(kù)連接并不會(huì)立刻生效,連接會(huì)在合適的時(shí)候延遲建立。

我們可以使用 *sql.DB.Exec / *sql.DB.ExecContext 來(lái)執(zhí)行 SQL 命令。其實(shí)除了 Exec 有方法對(duì)應(yīng)的 ExecContext 版本,文中提到的 *sql.DB.Ping*sql.DB.Query、*sql.DB.QueryRow、*sql.DB.Prepare 方法也都有對(duì)應(yīng)的 XxxContext 版本,你可以自行測(cè)試。

如果被執(zhí)行的 SQL 語(yǔ)句中包含 MySQL 關(guān)鍵字,則需要使用反引號(hào)(`)將關(guān)鍵字進(jìn)行包裹,否則你將得到 Error 1064 (42000): You have an error in your SQL syntax; 錯(cuò)誤。

*sql.DB.BeginTx 可以開(kāi)啟一個(gè)事務(wù),事務(wù)需要顯式的 CommitRollback,MySQL 驅(qū)動(dòng)還支持使用 *sql.TxOptions 設(shè)置事務(wù)隔離級(jí)別。

對(duì)于 NULL 類型,database/sql 提供了 sql.NullString 等類型的支持。我們也可以為自定義類型實(shí)現(xiàn) sql.Scannerdriver.Valuer 兩個(gè)接口,來(lái)實(shí)現(xiàn)特定邏輯。

對(duì)于未知列和字段類型,我們可以使用 *sql.Rows.Columnssql.RawBytes 等來(lái)解決,雖然這極大的增加了靈活性,不過(guò)不到萬(wàn)不得已不建議使用,使用更加明確的代碼可以減少 BUG 的數(shù)量和提高可維護(hù)性。

以上就是Go使用database/sql操作數(shù)據(jù)庫(kù)的教程指南的詳細(xì)內(nèi)容,更多關(guān)于Go database/sql的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!

相關(guān)文章

  • GO中Json解析的幾種方式

    GO中Json解析的幾種方式

    本文主要介紹了GO中Json解析的幾種方式,詳細(xì)的介紹了幾種方法,?文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧
    2024-01-01
  • Go json omitempty如何實(shí)現(xiàn)可選屬性

    Go json omitempty如何實(shí)現(xiàn)可選屬性

    在Go語(yǔ)言中,使用`omitempty`可以幫助我們?cè)谶M(jìn)行JSON序列化和反序列化時(shí),忽略結(jié)構(gòu)體中的零值或空值,本文介紹了如何通過(guò)將字段類型改為指針類型,并在結(jié)構(gòu)體的JSON標(biāo)簽中添加`omitempty`來(lái)實(shí)現(xiàn)這一功能,例如,將float32修改為*float32
    2024-09-09
  • 詳解Go中指針的原理與引用

    詳解Go中指針的原理與引用

    在?Go?中,指針是強(qiáng)大而重要的功能,它允許開(kāi)發(fā)人員直接處理內(nèi)存地址并實(shí)現(xiàn)高效的數(shù)據(jù)操作,本文主要帶大家了解下指針在?Go?中的工作原理以及對(duì)于編寫(xiě)高效、高性能代碼的重要性,希望對(duì)大家有所幫助
    2023-09-09
  • Golang并發(fā)編程中Context包的使用與并發(fā)控制

    Golang并發(fā)編程中Context包的使用與并發(fā)控制

    Golang的context包提供了在并發(fā)編程中傳遞取消信號(hào)、超時(shí)控制和元數(shù)據(jù)的功能,本文就來(lái)介紹一下Golang并發(fā)編程中Context包的使用與并發(fā)控制,感興趣的可以了解一下
    2024-11-11
  • Golang根據(jù)job數(shù)量動(dòng)態(tài)控制每秒?yún)f(xié)程的最大創(chuàng)建數(shù)量方法詳解

    Golang根據(jù)job數(shù)量動(dòng)態(tài)控制每秒?yún)f(xié)程的最大創(chuàng)建數(shù)量方法詳解

    這篇文章主要介紹了Golang根據(jù)job數(shù)量動(dòng)態(tài)控制每秒?yún)f(xié)程的最大創(chuàng)建數(shù)量方法
    2024-01-01
  • 從生成CRD到編寫(xiě)自定義控制器教程示例

    從生成CRD到編寫(xiě)自定義控制器教程示例

    這篇文章主要為大家介紹了從生成CRD到編寫(xiě)自定義控制器的教程示例,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2022-05-05
  • Golang中HTTP路由設(shè)計(jì)的使用與實(shí)現(xiàn)

    Golang中HTTP路由設(shè)計(jì)的使用與實(shí)現(xiàn)

    這篇文章主要介紹了Golang中HTTP路由設(shè)計(jì)的使用與實(shí)現(xiàn),為什么要設(shè)計(jì)路由規(guī)則,因?yàn)槁酚梢?guī)則是HTTP的請(qǐng)求按照一定的規(guī)則 ,匹配查找到對(duì)應(yīng)的控制器并傳遞執(zhí)行的邏輯,需要的朋友可以參考下
    2023-05-05
  • go語(yǔ)言?http模型reactor示例詳解

    go語(yǔ)言?http模型reactor示例詳解

    這篇文章主要介紹了go語(yǔ)言?http模型reactor,接下來(lái)看一段基于reactor的示例,這里運(yùn)行通過(guò)?go?run?main.go,本文結(jié)合示例代碼給大家介紹的非常詳細(xì),需要的朋友可以參考下
    2023-01-01
  • 使用go gin來(lái)操作cookie的講解

    使用go gin來(lái)操作cookie的講解

    今天小編就為大家分享一篇關(guān)于使用go gin來(lái)操作cookie的講解,小編覺(jué)得內(nèi)容挺不錯(cuò)的,現(xiàn)在分享給大家,具有很好的參考價(jià)值,需要的朋友一起跟隨小編來(lái)看看吧
    2019-04-04
  • 基于Golang編寫(xiě)一個(gè)聊天工具

    基于Golang編寫(xiě)一個(gè)聊天工具

    這篇文章主要為大家詳細(xì)介紹了如何使用?Golang?構(gòu)建一個(gè)簡(jiǎn)單但功能完善的聊天工具,利用?WebSocket?技術(shù)實(shí)現(xiàn)即時(shí)通訊的功能,需要的小伙伴可以參考下
    2023-11-11

最新評(píng)論