欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

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

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

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

概念

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

它的大致原理如下:

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

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

特點

database/sql 具有如下特點:

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

準(zhǔn)備

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

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ù)庫

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

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()
}

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

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

這里驅(qū)動名稱為 mysqldatabase/sql 之所以能夠識別這個驅(qū)動名稱,是因為在匿名導(dǎo)入 github.com/go-sql-driver/mysql 時,這個庫內(nèi)部調(diào)用了 sql.Register 將其注冊給了 database/sql。

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

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

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

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

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

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

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

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

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

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

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

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

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

我們可以通過如下方法,控制連接池的一些參數(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)驗來修改,以上參數(shù)能夠滿足一些中小項目的使用,如果是大型項目,則可以適當(dāng)調(diào)高參數(shù)。

聲明模型

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

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ù)庫表中的字段一一對應(yīng)。

其中 Salary 類型定義如下:

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

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

創(chuàng)建

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

這里使用 Exec 方法來實現(xiàn)創(chuàng)建一個用戶:

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()
}

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

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

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

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

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

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

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

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

接口具體實現(xiàn)有數(shù)據(jù)庫驅(qū)動程序來完成。

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

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

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

比如我們需要創(chuàng)建兩個用戶,則可以先使用 db.Prepare 創(chuàng)建一個 *sql.Stmt 對象,然后多次調(diào)用 *sql.Stmt.Exec 方法來插入數(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ù)處理對象是需要關(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ù)先將一個數(shù)據(jù)庫連接和一個條 SQL 語句綁定并返回 *sql.Stmt 結(jié)構(gòu)體,它代表了這個綁定后的連接對象,是并發(fā)安全的。

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

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

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

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

查詢

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

因為 Exec 方法只會執(zhí)行 SQL,不會返回結(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)
	}
	// 處理錯誤
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return users, nil
}

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

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

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

rows.Scan() 方法可以將結(jié)果掃描到傳遞進(jìn)來的指針對象。因為我們使用了 SELECT * 來查詢,所以會返回全部字段的數(shù)據(jù),按順序?qū)?user 對象相應(yīng)的字段指針傳遞進(jìn)來即可。

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

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

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

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

以上,我們查詢了多條用戶,*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
	}
	// 處理錯誤
	if err := row.Err(); err != nil {
		return user, err
	}
	return user, nil
}

查詢單條記錄會返回 *sql.Row 結(jié)構(gòu)體,它實際上是對 *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()*sql.Row 會自動幫我們處理好,返回查詢結(jié)果集中的第一條數(shù)據(jù)。

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

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

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

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

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

以上代碼可以改為:

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

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

更新

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

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

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,也會執(zhí)行到這里
		return fmt.Errorf("no user with id %d", id)
	}
	return nil
}

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

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

刪除

使用 *sql.DB.ExecContext 方法實現(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ù)基本是開發(fā) Web 項目時比不可少的數(shù)據(jù)庫功能,database/sql 提供了對事務(wù)的支持。

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

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 用于開啟一個事務(wù),第一個參數(shù)為 context.Context,第二個參數(shù)為 *sql.TxOptions 對象,用來配置事務(wù)選項,Isolation 字段用來設(shè)置數(shù)據(jù)庫隔離級別。

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

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

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

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

處理 NULL

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

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

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

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

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

這個時候,我們有兩種方法解決此問題:

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

因為指針類型可能為 nil,所以可以使用 nil 來對應(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 用來記錄值,Valid 用來標(biāo)記是否為 NULL。

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

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

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

// 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
}

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

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

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

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

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

自定義字段類型

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

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

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

如果只是將其映射為 Go 中的 string,則操作時要格外小心,如果忘記寫一個 ", 等,程序?qū)⒖赡軋箦e。

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

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

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

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

// 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
}

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

未知列

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

此時,我們可以使用 *sql.Rows.Columns 方法獲取所有列名,這將返回一個切片,這個切片長度,即為字段個數(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,它實際上是 []byte 的別名
		new(sql.RawBytes), // updated_at
	}
	for rows.Next() {
		if err := rows.Scan(res...); err != nil {
			return res, err
		}
	}
	return res, rows.Err()
}

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

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

總結(jié)

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

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

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

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

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

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

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

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

相關(guān)文章

  • GO中Json解析的幾種方式

    GO中Json解析的幾種方式

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

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

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

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

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

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

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

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

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

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

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

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

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

    go語言?http模型reactor示例詳解

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

    使用go gin來操作cookie的講解

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

    基于Golang編寫一個聊天工具

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

最新評論