banner
biuaxia

biuaxia

"万物皆有裂痕,那是光进来的地方。"
github
bilibili
tg_channel

【轉載】Go語言操作MySQL

title: 【轉載】Go 語言操作 MySQL
date: 2021-08-11 10:52:59
comment: false
toc: true
category:

  • Golang
    tags:
  • 轉載
  • Go
  • MySQL
  • 操作

本文轉載自:Go 語言操作 MySQL | 李文周的博客


MySQL 是業界常用的關係型資料庫,本文介紹了 Go 語言如何操作 MySQL 資料庫。

Go 操作 MySQL#

連接#

Go 語言中的database/sql包提供了保證 SQL 或類 SQL 資料庫的泛用介面,並不提供具體的資料庫驅動。使用database/sql包時必須注入(至少)一個資料庫驅動。

我們常用的資料庫基本上都有完整的第三方實現。例如:MySQL 驅動

下載依賴#

go get -u github.com/go-sql-driver/mysql

使用 MySQL 驅動#

func Open(driverName, dataSourceName string) (*DB, error)

Open 打開一個 driverName 指定的資料庫,dataSourceName 指定資料源,一般至少包括資料庫檔案名和其它連接必要的信息。

import (
	"database/sql"

	_ "github.com/go-sql-driver/mysql"
)

func main() {
   // DSN:Data Source Name
	dsn := "user:password@tcp(127.0.0.1:3306)/dbname"
	db, err := sql.Open("mysql", dsn)
	if err != nil {
		panic(err)
	}
	defer db.Close()  // 注意這行程式碼要寫在上面err判斷的下面
}

思考題 : 為什麼上面程式碼中的defer db.Close()語句不應該寫在if err != nil的前面呢?

初始化連接#

Open 函數可能只是驗證其參數格式是否正確,實際上並不創建與資料庫的連接。如果要檢查資料源的名稱是否真實有效,應該調用 Ping 方法。

返回的 DB 物件可以安全地被多個 goroutine 並發使用,並且維護其自己的空閒連接池。因此,Open 函數應該僅被調用一次,很少需要關閉這個 DB 物件。

// 定義一個全局物件db
var db *sql.DB

// 定義一個初始化資料庫的函數
func initDB() (err error) {
	// DSN:Data Source Name
	dsn := "user:password@tcp(127.0.0.1:3306)/sql_test?charset=utf8mb4&parseTime=True"
	// 不會校驗帳號密碼是否正確
	// 注意!!!這裡不要使用:=,我們是給全局變數賦值,然後在main函數中使用全局變數db
	db, err = sql.Open("mysql", dsn)
	if err != nil {
		return err
	}
	// 嘗試與資料庫建立連接(校驗dsn是否正確)
	err = db.Ping()
	if err != nil {
		return err
	}
	return nil
}

func main() {
	err := initDB() // 調用初始化資料庫的函數
	if err != nil {
		fmt.Printf("init db failed,err:%v\n", err)
		return
	}
}

其中sql.DB是表示連接的資料庫物件(結構體實例),它保存了連接資料庫相關的所有信息。它內部維護著一個具有零到多個底層連接的連接池,它可以安全地被多個 goroutine 同時使用。

SetMaxOpenConns#

func (db *DB) SetMaxOpenConns(n int)

SetMaxOpenConns設置與資料庫建立連接的最大數目。 如果 n 大於 0 且小於最大閒置連接數,會將最大閒置連接數減小到匹配最大開啟連接數的限制。 如果 n<=0,不會限制最大開啟連接數,默認為 0(無限制)。

SetMaxIdleConns#

func (db *DB) SetMaxIdleConns(n int)

SetMaxIdleConns 設置連接池中的最大閒置連接數。 如果 n 大於最大開啟連接數,則新的最大閒置連接數會減小到匹配最大開啟連接數的限制。 如果 n<=0,不會保留閒置連接。

CRUD#

建庫建表#

我們先在 MySQL 中創建一個名為sql_test的資料庫

CREATE DATABASE sql_test;

進入該資料庫:

use sql_test;

執行以下命令創建一張用於測試的資料表:

CREATE TABLE `user` (
    `id` BIGINT(20) NOT NULL AUTO_INCREMENT,
    `name` VARCHAR(20) DEFAULT '',
    `age` INT(11) DEFAULT '0',
    PRIMARY KEY(`id`)
)ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4;

查詢#

為了方便查詢,我們事先定義好一個結構體來存儲 user 表的資料。

type user struct {
	id   int
	age  int
	name string
}

單行查詢#

單行查詢db.QueryRow()執行一次查詢,並期望返回最多一行結果(即 Row)。QueryRow 總是返回非 nil 的值,直到返回值的 Scan 方法被調用時,才會返回被延遲的錯誤。(如:未找到結果)

func (db *DB) QueryRow(query string, args ...interface{}) *Row

具體示例程式碼:

// 查詢單條資料示例
func queryRowDemo() {
	sqlStr := "select id, name, age from user where id=?"
	var u user
	// 非常重要:確保QueryRow之後調用Scan方法,否則持有的資料庫連接不會被釋放
	err := db.QueryRow(sqlStr, 1).Scan(&u.id, &u.name, &u.age)
	if err != nil {
		fmt.Printf("scan failed, err:%v\n", err)
		return
	}
	fmt.Printf("id:%d name:%s age:%d\n", u.id, u.name, u.age)
}

多行查詢#

多行查詢db.Query()執行一次查詢,返回多行結果(即 Rows),一般用於執行 select 命令。參數 args 表示 query 中的佔位參數。

func (db *DB) Query(query string, args ...interface{}) (*Rows, error)

具體示例程式碼:

// 查詢多條資料示例
func queryMultiRowDemo() {
	sqlStr := "select id, name, age from user where id > ?"
	rows, err := db.Query(sqlStr, 0)
	if err != nil {
		fmt.Printf("query failed, err:%v\n", err)
		return
	}
	// 非常重要:關閉rows釋放持有的資料庫連接
	defer rows.Close()

	// 循環讀取結果集中的資料
	for rows.Next() {
		var u user
		err := rows.Scan(&u.id, &u.name, &u.age)
		if err != nil {
			fmt.Printf("scan failed, err:%v\n", err)
			return
		}
		fmt.Printf("id:%d name:%s age:%d\n", u.id, u.name, u.age)
	}
}

插入資料#

插入、更新和刪除操作都使用Exec方法。

func (db *DB) Exec(query string, args ...interface{}) (Result, error)

Exec 執行一次命令(包括查詢、刪除、更新、插入等),返回的 Result 是對已執行的 SQL 命令的總結。參數 args 表示 query 中的佔位參數。

具體插入資料示例程式碼如下:

// 插入資料
func insertRowDemo() {
	sqlStr := "insert into user(name, age) values (?,?)"
	ret, err := db.Exec(sqlStr, "王五", 38)
	if err != nil {
		fmt.Printf("insert failed, err:%v\n", err)
		return
	}
	theID, err := ret.LastInsertId() // 新插入資料的id
	if err != nil {
		fmt.Printf("get lastinsert ID failed, err:%v\n", err)
		return
	}
	fmt.Printf("insert success, the id is %d.\n", theID)
}

更新資料#

具體更新資料示例程式碼如下:

// 更新資料
func updateRowDemo() {
	sqlStr := "update user set age=? where id = ?"
	ret, err := db.Exec(sqlStr, 39, 3)
	if err != nil {
		fmt.Printf("update failed, err:%v\n", err)
		return
	}
	n, err := ret.RowsAffected() // 操作影響的行數
	if err != nil {
		fmt.Printf("get RowsAffected failed, err:%v\n", err)
		return
	}
	fmt.Printf("update success, affected rows:%d\n", n)
}

刪除資料#

具體刪除資料的示例程式碼如下:

// 刪除資料
func deleteRowDemo() {
	sqlStr := "delete from user where id = ?"
	ret, err := db.Exec(sqlStr, 3)
	if err != nil {
		fmt.Printf("delete failed, err:%v\n", err)
		return
	}
	n, err := ret.RowsAffected() // 操作影響的行數
	if err != nil {
		fmt.Printf("get RowsAffected failed, err:%v\n", err)
		return
	}
	fmt.Printf("delete success, affected rows:%d\n", n)
}

MySQL 預處理#

什麼是預處理?#

普通 SQL 語句執行過程:

  1. 客戶端對 SQL 語句進行佔位符替換得到完整的 SQL 語句。
  2. 客戶端發送完整 SQL 語句到 MySQL 服務端
  3. MySQL 服務端執行完整的 SQL 語句並將結果返回給客戶端。

預處理執行過程:

  1. 把 SQL 語句分成兩部分,命令部分與資料部分。
  2. 先把命令部分發送給 MySQL 服務端,MySQL 服務端進行 SQL 預處理。
  3. 然後把資料部分發送給 MySQL 服務端,MySQL 服務端對 SQL 語句進行佔位符替換。
  4. MySQL 服務端執行完整的 SQL 語句並將結果返回給客戶端。

為什麼要預處理?#

  1. 優化 MySQL 伺服器重複執行 SQL 的方法,可以提升伺服器性能,提前讓伺服器編譯,一次編譯多次執行,節省後續編譯的成本。
  2. 避免 SQL 注入問題。

Go 實現 MySQL 預處理#

database/sql中使用下面的Prepare方法來實現預處理操作。

func (db *DB) Prepare(query string) (*Stmt, error)

Prepare方法會先將 sql 語句發送給 MySQL 服務端,返回一個準備好的狀態用於之後的查詢和命令。返回值可以同時執行多個查詢和命令。

查詢操作的預處理示例程式碼如下:

// 預處理查詢示例
func prepareQueryDemo() {
	sqlStr := "select id, name, age from user where id > ?"
	stmt, err := db.Prepare(sqlStr)
	if err != nil {
		fmt.Printf("prepare failed, err:%v\n", err)
		return
	}
	defer stmt.Close()
	rows, err := stmt.Query(0)
	if err != nil {
		fmt.Printf("query failed, err:%v\n", err)
		return
	}
	defer rows.Close()
	// 循環讀取結果集中的資料
	for rows.Next() {
		var u user
		err := rows.Scan(&u.id, &u.name, &u.age)
		if err != nil {
			fmt.Printf("scan failed, err:%v\n", err)
			return
		}
		fmt.Printf("id:%d name:%s age:%d\n", u.id, u.name, u.age)
	}
}

插入、更新和刪除操作的預處理十分類似,這裡以插入操作的預處理為例:

// 預處理插入示例
func prepareInsertDemo() {
	sqlStr := "insert into user(name, age) values (?,?)"
	stmt, err := db.Prepare(sqlStr)
	if err != nil {
		fmt.Printf("prepare failed, err:%v\n", err)
		return
	}
	defer stmt.Close()
	_, err = stmt.Exec("小王子", 18)
	if err != nil {
		fmt.Printf("insert failed, err:%v\n", err)
		return
	}
	_, err = stmt.Exec("沙河娜扎", 18)
	if err != nil {
		fmt.Printf("insert failed, err:%v\n", err)
		return
	}
	fmt.Println("insert success.")
}

SQL 注入問題#

我們任何時候都不應該自己拼接 SQL 語句!

這裡我們演示一個自行拼接 SQL 語句的示例,編寫一個根據 name 字段查詢 user 表的函數如下:

// sql注入示例
func sqlInjectDemo(name string) {
	sqlStr := fmt.Sprintf("select id, name, age from user where name='%s'", name)
	fmt.Printf("SQL:%s\n", sqlStr)
	var u user
	err := db.QueryRow(sqlStr).Scan(&u.id, &u.name, &u.age)
	if err != nil {
		fmt.Printf("exec failed, err:%v\n", err)
		return
	}
	fmt.Printf("user:%#v\n", u)
}

此時以下輸入字串都可以引發 SQL 注入問題:

sqlInjectDemo("xxx' or 1=1#")
sqlInjectDemo("xxx' union select * from user #")
sqlInjectDemo("xxx' and (select count(*) from user) <10 #")

補充: 不同的資料庫中,SQL 語句使用的佔位符語法不盡相同。

資料庫佔位符語法
MySQL?
PostgreSQL$1, $2
SQLite?$1
Oracle:name

Go 實現 MySQL 事務#

什麼是事務?#

事務:一個最小的不可再分的工作單元;通常一個事務對應一個完整的業務 (例如銀行帳戶轉帳業務,該業務就是一個最小的工作單元),同時這個完整的業務需要執行多次的 DML (insert、update、delete) 語句共同聯合完成。A 轉帳給 B,這裡面就需要執行兩次 update 操作。

在 MySQL 中只有使用了Innodb資料庫引擎的資料庫或表才支持事務。事務處理可以用來維護資料庫的完整性,保證成批的 SQL 語句要麼全部執行,要麼全部不執行。

事務的 ACID#

通常事務必須滿足 4 個條件(ACID):原子性(Atomicity,或稱不可分割性)、一致性(Consistency)、隔離性(Isolation,又稱獨立性)、持久性(Durability)。

條件解釋
原子性一個事務(transaction)中的所有操作,要麼全部完成,要麼全部不完成,不會結束在中間某個環節。事務在執行過程中發生錯誤,會被回滾(Rollback)到事務開始前的狀態,就像這個事務從來沒有執行過一樣。
一致性在事務開始之前和事務結束以後,資料庫的完整性沒有被破壞。這表示寫入的資料必須完全符合所有的預設規則,這包含資料的精確度、串聯性以及後續資料庫可以自發性地完成預定的工作。
隔離性資料庫允許多個並發事務同時對其資料進行讀寫和修改的能力,隔離性可以防止多個事務並發執行時由於交叉執行而導致資料的不一致。事務隔離分為不同級別,包括讀未提交(Read uncommitted)、讀提交(read committed)、可重複讀(repeatable read)和串行化(Serializable)。
持久性事務處理結束後,對資料的修改就是永久的,即便系統故障也不會丟失。

事務相關方法#

Go 語言中使用以下三個方法實現 MySQL 中的事務操作。 開始事務

func (db *DB) Begin() (*Tx, error)

提交事務

func (tx *Tx) Commit() error

回滾事務

func (tx *Tx) Rollback() error

事務示例#

下面的程式碼演示了一個簡單的事務操作,該事物操作能夠確保兩次更新操作要麼同時成功要麼同時失敗,不會存在中間狀態。

// 事務操作示例
func transactionDemo() {
	tx, err := db.Begin() // 開啟事務
	if err != nil {
		if tx != nil {
			tx.Rollback() // 回滾
		}
		fmt.Printf("begin trans failed, err:%v\n", err)
		return
	}
	sqlStr1 := "Update user set age=30 where id=?"
	ret1, err := tx.Exec(sqlStr1, 2)
	if err != nil {
		tx.Rollback() // 回滾
		fmt.Printf("exec sql1 failed, err:%v\n", err)
		return
	}
	affRow1, err := ret1.RowsAffected()
	if err != nil {
		tx.Rollback() // 回滾
		fmt.Printf("exec ret1.RowsAffected() failed, err:%v\n", err)
		return
	}

	sqlStr2 := "Update user set age=40 where id=?"
	ret2, err := tx.Exec(sqlStr2, 3)
	if err != nil {
		tx.Rollback() // 回滾
		fmt.Printf("exec sql2 failed, err:%v\n", err)
		return
	}
	affRow2, err := ret2.RowsAffected()
	if err != nil {
		tx.Rollback() // 回滾
		fmt.Printf("exec ret1.RowsAffected() failed, err:%v\n", err)
		return
	}

	fmt.Println(affRow1, affRow2)
	if affRow1 == 1 && affRow2 == 1 {
		fmt.Println("事務提交啦...")
		tx.Commit() // 提交事務
	} else {
		tx.Rollback()
		fmt.Println("事務回滾啦...")
	}

	fmt.Println("exec trans success!")
}

更強大、更好用的 sqlx 庫

練習題#

  1. 結合net/httpdatabase/sql實現一個使用 MySQL 存儲用戶信息的註冊及登錄的簡易 web 程式。
載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。