這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
golang本身沒有提供串連mysql的驅動,但是定義了標準介面供第三方開發驅動。這裡串連mysql可以使用第三方庫,第三方庫推薦使用https://github.com/Go-SQL-Driver/MySQL這個驅動,更新維護都比較好。下面示範下具體的使用,完整程式碼範例可以參考最後。
下載驅動
sudo go get github.com/go-sql-driver/mysql
資料庫連接
db, err := sql.Open("mysql", "user:password@tcp(localhost:3306)/dbname?charset=utf8")
其中串連參數可以有如下幾種形式:通常我們都用第二種。
user@unix(/path/to/socket)/dbname?charset=utf8user:password@tcp(localhost:5555)/dbname?charset=utf8user:password@/dbnameuser:password@tcp([de:ad:be: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個獨立變數userId、userName、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 mainimport ( "database/sql" "fmt" _ "github.com/go-sql-driver/mysql")func main() { insert()}//插入demofunc 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)}//查詢demofunc 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) }}
golang go-sql-drive mysql串連池的實現
golang內部內建了串連池功能,剛開始接觸golang的時候不瞭解這個,還自己搞了一個 sql.Open的對象管理池,真的非常囧啊。
sql.Open函數實際上是返回一個串連池對象,不是單個串連。在open的時候並沒有去串連資料庫,只有在執行query、exce方法的時候才會去實際串連資料庫。在一個應用中同樣的庫串連只需要儲存一個sql.Open之後的db對象就可以了,不需要多次open。
因為普通程式執行完畢之後資源就會被釋放掉,所以這裡嘗試使用web服務進行示範。
開啟web服務
首頁先啟動一個web服務監聽9090連接埠,比較簡單不多做說明。
func startHttpServer() { http.HandleFunc("/pool", pool) err := http.ListenAndServe(":9090", nil) if err != nil { log.Fatal("ListenAndServe: ", err) }}
db對象初始化
聲明一個全域的db對象,並進行初始化。
var db *sql.DBfunc 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出現too many connections的錯誤。設定閑置的串連數則當開啟的一個串連使用完成後可以放在池裡等候下一次使用。
要求方法
上面開啟http請求設定了請求/pool地址的執行方法
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) }}
pool方法就是從user表中查出一條記錄然後存放到map中,最後輸出finish。代碼到這裡就算完了非常簡單,下面來測試一下。首先啟動http服務,然後使用ab進行並發測試訪問:
ab -c 100 -n 1000 'http://localhost:9090/pool'
在資料庫中通過show processlist查看串連進程:
golang資料庫連接池
可以看到有100來個進程。
因為避免了重複建立串連,所以使用串連池可以很明顯的提高效能。有興趣的童靴可以去掉串連池代碼自己測試一下。完整代碼如下:
//資料庫連接池測試package mainimport ( "database/sql" "fmt" _ "github.com/go-sql-driver/mysql" "log" "net/http")var db *sql.DBfunc 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 EOFpackets.go:118: write tcp 192.168.3.90:3306: broken pipe
上面都是錯誤都是go-sql-drive本身的輸出,有的時候還會出現bad connection的錯誤。多請求幾次後串連池會重新開啟新串連這時候就沒有問題了。