1. 程式人生 > >Go操作mysql實現增刪改查及連線池

Go操作mysql實現增刪改查及連線池

golang本身沒有提供連線的MySQL的驅動,但是定義了標準介面供第三方開發驅動。這裡連線的MySQL可以使用第三方庫,第三方庫推薦使用https://github.com/Go-SQL-Driver/MySQL這個驅動,更新維護都比較好。下面演示下具體的使用,完整程式碼示例可以參考最後。

下載驅動

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

如果提示這樣的失敗資訊:無法下載,$ GOPATH未設定。有關詳細資訊,請參閱:go help gopath,可以使用如下命令解決

sudo env GOPATH=/Users/chenjiebin/golang go get github.com/go-sql-driver/mysql

GOPATH的值根據自行環境進行替換。

建立測試表

在mysql test庫中建立測試表

CREATE TABLE IF NOT EXISTS `test`.`user` (
 `user_id` INT(11) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '使用者編號',
 `user_name` VARCHAR(45) NOT NULL COMMENT '使用者名稱稱',
 `user_age` TINYINT(3) UNSIGNED NOT NULL DEFAULT 0 COMMENT '使用者年齡',
 `user_sex` TINYINT(3) UNSIGNED NOT NULL DEFAULT 0 COMMENT '使用者性別',
 PRIMARY KEY (`user_id`))
 ENGINE = InnoDB
 AUTO_INCREMENT = 1
 DEFAULT CHARACTER SET = utf8
 COLLATE = utf8_general_ci
 COMMENT = '使用者表'

資料庫連線

資料庫連線使用datebase / sql開啟函式進行連線

db, err := sql.Open("mysql", "user:[email protected](localhost:5555)/dbname?charset=utf8")

其中連線引數可以有如下幾種形式:

user @ unix(/ path / to / socket)/ dbname?charset = utf8
user:password @ tcp(localhost:5555)/ dbname?charset = utf8
user:password @ / dbname
user:password @ tcp([de:ad:是:EF :: CA:FE]:80)/ DBNAME

通常我們都用第二種。

插入操作

stmt, err := db.Prepare(`INSERT user (user_name,user_age,user_sex) values (?,?,?)`)
checkErr(err)
res, err := stmt.Exec("tony", 20, 1)
checkErr(err)
id, err := res.LastInsertId()
checkErr(err)
fmt.Println(id)

這裡使用結構化操作,不推薦使用直接拼接SQL語句的方法。

查詢操作

rows, err := db.Query("SELECT * FROM user")
checkErr(err)

for rows.Next() {
	var userId int
	var userName string
	var userAge int
	var userSex int
	rows.Columns()
	err = rows.Scan(&userId, &userName, &userAge, &userSex)
	checkErr(err)
	fmt.Println(userId)
	fmt.Println(userName)
	fmt.Println(userAge)
	fmt.Println(userSex)
}

這裡查詢的方式使用宣告4個獨立變數使用者id,使用者名稱,userAge,userSex來儲存查詢出來的每一行的值。在實際開發中通常會封裝資料庫的操作,對這樣的查詢通常會考慮返回字典型別。

//構造scanArgs、values兩個陣列,scanArgs的每個值指向values相應值的地址
columns, _ := rows.Columns()
scanArgs := make([]interface{}, len(columns))
values := make([]interface{}, len(columns))
for i := range values {
	scanArgs[i] = &values[i]
}

for rows.Next() {
	//將行資料儲存到record字典
	err = rows.Scan(scanArgs...)
	record := make(map[string]string)
	for i, col := range values {
		if col != nil {
			record[columns[i]] = string(col.([]byte))
		}
	}
	fmt.Println(record)
}

修改操作

stmt, err := db.Prepare(`UPDATE user SET user_age=?,user_sex=? WHERE user_id=?`)
checkErr(err)
res, err := stmt.Exec(21, 2, 1)
checkErr(err)
num, err := res.RowsAffected()
checkErr(err)
fmt.Println(num)

刪除操作

stmt, err := db.Prepare(`DELETE FROM user WHERE user_id=?`)
checkErr(err)
res, err := stmt.Exec(1)
checkErr(err)
num, err := res.RowsAffected()
checkErr(err)
fmt.Println(num)

修改和刪除操作都比較簡單,同插入資料類似,只是使用的RowsAffected來獲取影響的資料行數。

完整程式碼

package main

import (
	"database/sql"
	"fmt"
	_ "github.com/go-sql-driver/mysql"
)

func main() {
	insert()
}

//插入demo
func insert() {
	db, err := sql.Open("mysql", "root:@/test?charset=utf8")
	checkErr(err)

	stmt, err := db.Prepare(`INSERT user (user_name,user_age,user_sex) values (?,?,?)`)
	checkErr(err)
	res, err := stmt.Exec("tony", 20, 1)
	checkErr(err)
	id, err := res.LastInsertId()
	checkErr(err)
	fmt.Println(id)
}

//查詢demo
func query() {
	db, err := sql.Open("mysql", "root:@/test?charset=utf8")
	checkErr(err)

	rows, err := db.Query("SELECT * FROM user")
	checkErr(err)

	//普通demo
	//for rows.Next() {
	//	var userId int
	//	var userName string
	//	var userAge int
	//	var userSex int

	//	rows.Columns()
	//	err = rows.Scan(&userId, &userName, &userAge, &userSex)
	//	checkErr(err)

	//	fmt.Println(userId)
	//	fmt.Println(userName)
	//	fmt.Println(userAge)
	//	fmt.Println(userSex)
	//}

	//字典型別
	//構造scanArgs、values兩個陣列,scanArgs的每個值指向values相應值的地址
	columns, _ := rows.Columns()
	scanArgs := make([]interface{}, len(columns))
	values := make([]interface{}, len(columns))
	for i := range values {
		scanArgs[i] = &values[i]
	}

	for rows.Next() {
		//將行資料儲存到record字典
		err = rows.Scan(scanArgs...)
		record := make(map[string]string)
		for i, col := range values {
			if col != nil {
				record[columns[i]] = string(col.([]byte))
			}
		}
		fmt.Println(record)
	}
}

//更新資料
func update() {
	db, err := sql.Open("mysql", "root:@/test?charset=utf8")
	checkErr(err)

	stmt, err := db.Prepare(`UPDATE user SET user_age=?,user_sex=? WHERE user_id=?`)
	checkErr(err)
	res, err := stmt.Exec(21, 2, 1)
	checkErr(err)
	num, err := res.RowsAffected()
	checkErr(err)
	fmt.Println(num)
}

//刪除資料
func remove() {
	db, err := sql.Open("mysql", "root:@/test?charset=utf8")
	checkErr(err)

	stmt, err := db.Prepare(`DELETE FROM user WHERE user_id=?`)
	checkErr(err)
	res, err := stmt.Exec(1)
	checkErr(err)
	num, err := res.RowsAffected()
	checkErr(err)
	fmt.Println(num)
}

func checkErr(err error) {
	if err != nil {
		panic(err)
	}
}

小結

整體上來說都比較簡單,就是查詢那邊使用字典來儲存返回資料比較複雜一些。既然說到資料庫連線,通常應用中都會使用連線池來減少連線開銷,關於連線池下次整理一下再放上來。

參考資料:“go web程式設計”,go web程式設計中對資料庫連線做了比較詳細的解說,值得一看。

 

golang內部自帶了連線池功能,剛開始接觸golang的時候不瞭解這個,還自己搞了一個sql.Open的物件管理池,真的非常囧啊。

sql.Open函式實際上是返回一個連線池物件,不是單個連線。在開放的時候並沒有去連線資料庫,只有在執行查詢,EXCE方法的時候才會去實際連線資料庫。在一個應用中同樣的庫連線只需要儲存一個sql.Open之後的DB物件就可以了,不需要多次開啟。

golang中關於mysql的增加改查我在前面的一篇文章中有說明了,不瞭解的小夥們可以先去了解一下:golang連線mysql操作示例增留改查

因為普通程式執行完畢之後資源就會被釋放掉,所以這裡嘗試使用網路服務進行演示。

開啟網路服務

首頁先啟動一個網路服務監聽9090埠,比較簡單不多做說明。

func startHttpServer() {
	http.HandleFunc("/pool", pool)
	err := http.ListenAndServe(":9090", nil)
	if err != nil {
		log.Fatal("ListenAndServe: ", err)
	}
}

DB物件初始化

宣告一個全域性的資料庫物件,並進行初始化。

var db *sql.DB

func init() {
	db, _ = sql.Open("mysql", "root:@tcp(127.0.0.1:3306)/test?charset=utf8")
	db.SetMaxOpenConns(2000)
	db.SetMaxIdleConns(1000)
	db.Ping()
}

連線池的實現關鍵在於SetMaxOpenConns和SetMaxIdleConns,其中:

SetMaxOpenConns用於設定最大開啟的連線數,預設值為0表示不限制
.SetMaxIdleConns用於設定閒置的連線數。

設定最大的連線數,可以避免併發太高導致連線mysql出現太多連線的錯誤。設定閒置的連線數則當開啟的一個連線使用完成後可以放在池裡等候下一次使用。

請求方法

上面開啟HTTP請求設定了請求/池地址的執行方法

func pool(w http.ResponseWriter, r *http.Request) {
	rows, err := db.Query("SELECT * FROM user limit 1")
	defer rows.Close()
	checkErr(err)

	columns, _ := rows.Columns()
	scanArgs := make([]interface{}, len(columns))
	values := make([]interface{}, len(columns))
	for j := range values {
		scanArgs[j] = &values[j]
	}

	record := make(map[string]string)
	for rows.Next() {
		//將行資料儲存到record字典
		err = rows.Scan(scanArgs...)
		for i, col := range values {
			if col != nil {
				record[columns[i]] = string(col.([]byte))
			}
		}
	}

	fmt.Println(record)
	fmt.Fprintln(w, "finish")
}

func checkErr(err error) {
	if err != nil {
		fmt.Println(err)
		panic(err)
	}
}

池方法就是從使用者表中查出一條記錄然後存放到地圖中,最後輸出完成程式碼到這裡就算完了非常簡單,下面來測試一下首先啟動HTTP服務,然後使用AB進行併發測試訪問。:

$ ab -c 100 -n 1000 'http://localhost:9090/pool'

在資料庫中通過show processlist檢視連線程序:

 

可以看到有100來個程序。

。因為避免了重複建立連線,所以使用連線池可以很明顯的提高效能有興趣的童靴可以去掉連線池程式碼自己測試一下完整程式碼如下:

//資料庫連線池測試
package main

import (
	"database/sql"
	"fmt"
	_ "github.com/go-sql-driver/mysql"
	"log"
	"net/http"
)

var db *sql.DB

func init() {
	db, _ = sql.Open("mysql", "root:@tcp(127.0.0.1:3306)/test?charset=utf8")
	db.SetMaxOpenConns(2000)
	db.SetMaxIdleConns(1000)
	db.Ping()
}

func main() {
	startHttpServer()
}

func startHttpServer() {
	http.HandleFunc("/pool", pool)
	err := http.ListenAndServe(":9090", nil)
	if err != nil {
		log.Fatal("ListenAndServe: ", err)
	}
}

func pool(w http.ResponseWriter, r *http.Request) {
	rows, err := db.Query("SELECT * FROM user limit 1")
	defer rows.Close()
	checkErr(err)

	columns, _ := rows.Columns()
	scanArgs := make([]interface{}, len(columns))
	values := make([]interface{}, len(columns))
	for j := range values {
		scanArgs[j] = &values[j]
	}

	record := make(map[string]string)
	for rows.Next() {
		//將行資料儲存到record字典
		err = rows.Scan(scanArgs...)
		for i, col := range values {
			if col != nil {
				record[columns[i]] = string(col.([]byte))
			}
		}
	}

	fmt.Println(record)
	fmt.Fprintln(w, "finish")
}

func checkErr(err error) {
	if err != nil {
		fmt.Println(err)
		panic(err)
	}
}

小結

golang這邊實現的連線池只提供了SetMaxOpenConns和SetMaxIdleConns方法進行連線池方面的配置。在使用的過程中有一個問題就是資料庫本身對連線有一個超時時間的設定,如果超時時間到了資料庫會單方面斷掉連線,此時再用連線池內的連線進行訪問就會出錯。

packets.go:32: unexpected EOF
packets.go:118: write tcp 192.168.3.90:3306: broken pipe

上面都是錯誤都是go-sql-drive本身的輸出,有的時候還會出現壞連線的錯誤。多請求幾次後連線池會重新開啟新連線這時候就沒有問題了。關於這個問題自己有初步的解決方法,但是感覺不太完美,下次再放上來。