Golang牽手PostgreSQL"增刪改查+不寫結(jié)構(gòu)快速掃描字段"快速入門
PostgreSQL(也稱postgres)是一款強大的開源對象關(guān)系數(shù)據(jù)庫系統(tǒng)(ORDBMS), 歷經(jīng)30年以上的打磨, 具有高可靠性, 強健壯性, 高性能等優(yōu)點. 詳見官網(wǎng).
本文主要使用的github.com/lib/pq包, 它是一款為Go語言database/sql包(sql包是go定義的一套圍繞SQL或類似SQL數(shù)據(jù)庫的通用接口, 需要結(jié)合具體數(shù)據(jù)庫的驅(qū)動一起使用)定制, 純Go開發(fā)的PostgreSQL驅(qū)動.
什么是不寫結(jié)構(gòu)體?
本文中可以理解為, 查詢數(shù)據(jù)庫配置直接返回鍵值對類型, 或者查詢數(shù)據(jù)返回多行數(shù)據(jù), 不需要針對列值聲明字段, 分別采用Map和Map數(shù)組來接收單行, 和多行數(shù)據(jù). 詳細原理請參考之前的博文:Golang連接MySQL執(zhí)行查詢并解析-告別結(jié)構(gòu)體
接下來, 咱們一起來實現(xiàn)PostgreSQL Golang版本的增刪改查(CRUD)與單行,多行字段快速掃描解析吧!
驅(qū)動安裝
執(zhí)行以下命令安裝postgresql驅(qū)動
- go get github.com/lib/pq
pq包支持的功能
- SSL
- 作為驅(qū)動程序, 與database/sql結(jié)合處理連接相關(guān)操作
- 掃描時間time.Time類型, 如 timestamp[tz], time[tz], date
- 掃描二進制blobs對象(Blob是內(nèi)存中的數(shù)據(jù)緩沖, 用來匹配strign類型的ID和字節(jié)切片), 如: bytea
- PostgreSQL的hstore數(shù)據(jù)類型支持
- 支持COPY FROM
- pq.ParseURL方法用來將urls轉(zhuǎn)化為sql.Open的連接字符串
- 支持許多l(xiāng)ibpq庫兼容的環(huán)境變量
- 支持Unix socket
- 支持通知Notifications, 如:LISTEN/NOTIFY
- 支持pgpass
- GSS (Kerberos) 認證
連接字符串參數(shù)
pq包與libpq類似(libpq是用C編寫的底層接口, 為其他高級語言比如C++,Perl,Python,Tcl和ECPG等提供底層PostgreSQL支持). 建立連接時需要提供連接參數(shù), 一部分支持libpq的參數(shù)也支持pq, 額外的, pq允許在連接字符串中指定運行時參數(shù)(如:search_path或work_mem), libpq則不能在連接字符串中指定運行時參數(shù), 只能在option參數(shù)中指定.
pq包為了兼容libpq包, 下面的連接參數(shù)都支持
- * dbname - 需要連接的數(shù)據(jù)庫名
- * user - 需要使用的用戶名
- * password - 該用戶的密碼
- * host - 需要連接的postgresql主機, unix域名套接字以/開始, 默認是localhost
- * port - postgresql綁定的端口 (默認5432)
- * sslmode - 是否使用SSL (默認是啟用(require), libpq包默認不啟用SSL)
- * fallback_application_name - 失敗時,可以提供一個應(yīng)用程序名來跟蹤.
- * connect_timeout - 連接最大等待秒數(shù), 0或者不指定, 表示不確定時間的等待
- * sslcert - 證書文件位置, 文件中必須包含PEM編碼的數(shù)據(jù)
- * sslkey - 密鑰文件位置, 文件中必須包含PEM編碼的數(shù)據(jù)
- * sslrootcert - 根證書文件位置, 文件中必須包含PEM編碼的數(shù)據(jù)
sslmode 支持一下模式
- * disable - 禁用SSL
- * require - 總是使用SSL(跳過驗證)
- * verify-ca - 總是使用SSL (驗證服務(wù)器提供的證書是由可信的CA簽署的)
- * verify-full - 總是使用SSL(驗證服務(wù)器提供的證書是由受信任的CA簽署的,并驗證服務(wù)器主機名是否與證書中的主機名匹配)
更多連接字符串參數(shù)請參考官方文檔
對包含空格的參數(shù), 需要使用單引號, 如:
- "user=pqgotest password='with spaces'"
使用反斜杠進行轉(zhuǎn)義, 如:
- "user=space\ man password='it\'s valid'"
注意: 如果要設(shè)置client_encoding連接參數(shù)(用于設(shè)置連接的編碼), 必須設(shè)置為"UTF8", 才能與Postgres匹配, 設(shè)置為其他值將會報錯.
除了上面的參數(shù), 在連接字符串中也可以通過后臺設(shè)置運行時參數(shù), 詳細運行時參數(shù), 請參考runtime-config
支持libpq的大部分環(huán)境變量也支持pq包, 詳細環(huán)境變量請參考libpq-envars. 如果沒有設(shè)置環(huán)境變量且連接字符串也沒有提供該參數(shù), 程序會panic崩潰退出, 字符串參數(shù)優(yōu)先級高于環(huán)境變量.
完整"增刪改查"示例代碼
- package main
- import (
- "database/sql"
- "encoding/json"
- "fmt"
- _ "github.com/lib/pq"
- "log"
- )
- const (
- // Initialize connection constants.
- HOST = "172.16.xx.xx"
- PORT = 31976
- DATABASE = "postgres"
- USER = "postgres"
- PASSWORD = "xxx"
- )
- func checkError(err error) {
- if err != nil {
- panic(err)
- }
- }
- type Db struct {
- db *sql.DB
- }
- // 創(chuàng)建表
- func (this *Db) CreateTable() {
- // 以水果庫存清單表inventory為例
- // Drop previous table of same name if one exists. 如果之前存在清單表, 則刪除該表
- _, err := this.db.Exec("DROP TABLE IF EXISTS inventory;")
- checkError(err)
- fmt.Println("Finished dropping table (if existed)")
- // Create table. 創(chuàng)建表, 指定id, name, quantity(數(shù)量)字段, 其中id為主鍵
- _, err = this.db.Exec("CREATE TABLE inventory (id serial PRIMARY KEY, name VARCHAR(50), quantity INTEGER);")
- checkError(err)
- fmt.Println("Finished creating table")
- }
- // 刪除表
- func (this *Db) DropTable() {
- // 以水果庫存清單表inventory為例
- // Drop previous table of same name if one exists. 如果之前存在清單表, 則刪除該表
- _, err := this.db.Exec("DROP TABLE IF EXISTS inventory;")
- checkError(err)
- fmt.Println("Finished dropping table (if existed)")
- }
- // 增加數(shù)據(jù)
- func (this *Db) Insert() {
- // Insert some data into table. 插入3條水果數(shù)據(jù)
- sql_statement := "INSERT INTO inventory (name, quantity) VALUES ($1, $2);"
- _, err := this.db.Exec(sql_statement, "banana", 150)
- checkError(err)
- _, err = this.db.Exec(sql_statement, "orange", 154)
- checkError(err)
- _, err = this.db.Exec(sql_statement, "apple", 100)
- checkError(err)
- fmt.Println("Inserted 3 rows of data")
- }
- // 讀數(shù)據(jù)/查數(shù)據(jù)
- func (this *Db) Read() {
- //讀取數(shù)據(jù)
- // Read rows from table.
- var id int
- var name string
- var quantity int
- sql_statement := "SELECT * from inventory;"
- rows, err := this.db.Query(sql_statement)
- checkError(err)
- defer rows.Close()
- for rows.Next() {
- switch err := rows.Scan(&id, &name, &quantity); err {
- case sql.ErrNoRows:
- fmt.Println("No rows were returned")
- case nil:
- fmt.Printf("Data row = (%d, %s, %d)\n", id, name, quantity)
- default:
- checkError(err)
- }
- }
- }
- // 更新數(shù)據(jù)
- func (this *Db) Update() {
- // Modify some data in table.
- sql_statement := "UPDATE inventory SET quantity = $2 WHERE name = $1;"
- _, err := this.db.Exec(sql_statement, "banana", 200)
- checkError(err)
- fmt.Println("Updated 1 row of data")
- }
- // 刪除數(shù)據(jù)
- func (this *Db) Delete() {
- // Delete some data from table.
- sql_statement := "DELETE FROM inventory WHERE name = $1;"
- _, err := this.db.Exec(sql_statement, "orange")
- checkError(err)
- fmt.Println("Deleted 1 row of data")
- }
- // 數(shù)據(jù)序列化為Json字符串, 便于人工查看
- func Data2Json(anyData interface{}) string {
- JsonByte, err := json.Marshal(anyData)
- if err != nil {
- log.Printf("數(shù)據(jù)序列化為json出錯:\n%s\n", err.Error())
- return ""
- }
- return string(JsonByte)
- }
- //多行數(shù)據(jù)解析
- func QueryAndParseRows(Db *sql.DB, queryStr string) []map[string]string {
- rows, err := Db.Query(queryStr)
- defer rows.Close()
- if err != nil {
- log.Printf("查詢出錯:\nSQL:\n%s, 錯誤詳情\n", queryStr, err.Error())
- return nil
- }
- cols, _ := rows.Columns() //列名
- if len(cols) > 0 {
- var ret []map[string]string //定義返回的映射切片變量ret
- for rows.Next() {
- buff := make([]interface{}, len(cols))
- data := make([][]byte, len(cols)) //數(shù)據(jù)庫中的NULL值可以掃描到字節(jié)中
- for i, _ := range buff {
- buff[i] = &data[i]
- }
- rows.Scan(buff...) //掃描到buff接口中,實際是字符串類型data中
- //將每一行數(shù)據(jù)存放到數(shù)組中
- dataKv := make(map[string]string, len(cols))
- for k, col := range data { //k是index,col是對應(yīng)的值
- //fmt.Printf("%30s:\t%s\n", cols[k], col)
- dataKv[cols[k]] = string(col)
- }
- ret = append(ret, dataKv)
- }
- log.Printf("返回多元素數(shù)組:\n%s", Data2Json(ret))
- return ret
- } else {
- return nil
- }
- }
- //單行數(shù)據(jù)解析 查詢數(shù)據(jù)庫,解析查詢結(jié)果,支持動態(tài)行數(shù)解析
- func QueryAndParse(Db *sql.DB, queryStr string) map[string]string {
- rows, err := Db.Query(queryStr)
- defer rows.Close()
- if err != nil {
- log.Printf("查詢出錯:\nSQL:\n%s, 錯誤詳情\n", queryStr, err.Error())
- return nil
- }
- //rows, _ := Db.Query("SHOW VARIABLES LIKE '%data%'")
- cols, _ := rows.Columns()
- if len(cols) > 0 {
- buff := make([]interface{}, len(cols)) // 臨時slice
- data := make([][]byte, len(cols)) // 存數(shù)據(jù)slice
- dataKv := make(map[string]string, len(cols))
- for i, _ := range buff {
- buff[i] = &data[i]
- }
- for rows.Next() {
- rows.Scan(buff...) // ...是必須的
- }
- for k, col := range data {
- dataKv[cols[k]] = string(col)
- //fmt.Printf("%30s:\t%s\n", cols[k], col)
- }
- log.Printf("返回單行數(shù)據(jù)Map:\n%s", Data2Json(dataKv))
- return dataKv
- } else {
- return nil
- }
- }
- func main() {
- // Initialize connection string. 初始化連接字符串, 參數(shù)包含主機,端口,用戶名,密碼,數(shù)據(jù)庫名,SSL模式(禁用),超時時間
- var connectionString string = fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s sslmode=disable connect_timeout=3", HOST, PORT, USER, PASSWORD, DATABASE)
- // Initialize connection object. 初始化連接對象, 驅(qū)動名為postgres
- db, err := sql.Open("postgres", connectionString)
- defer db.Close()
- checkError(err)
- postgresDb := Db{
- db: db,
- }
- err = postgresDb.db.Ping() //連通性檢查
- checkError(err)
- fmt.Println("Successfully created connection to database")
- postgresDb.CreateTable() //創(chuàng)建表
- postgresDb.Insert() //插入數(shù)據(jù)
- postgresDb.Read() //查詢數(shù)據(jù)
- QueryAndParseRows(postgresDb.db, "SELECT * from inventory;") //直接查詢和解析多行數(shù)據(jù)
- QueryAndParse(postgresDb.db, "SHOW DateStyle;") //直接查詢和解析單行數(shù)據(jù)
- postgresDb.Update() //修改/更新數(shù)據(jù)
- postgresDb.Read()
- postgresDb.Delete() //刪除數(shù)據(jù)
- postgresDb.Read()
- postgresDb.DropTable()
- }
執(zhí)行 go run main.go運行結(jié)果如下:
- Successfully created connection to database
- Finished dropping table (if existed)
- Finished creating table
- Inserted 3 rows of data
- Data row = (1, banana, 150)
- Data row = (2, orange, 154)
- Data row = (3, apple, 100)
- 2020/12/15 22:13:33 返回多元素數(shù)組:
- [{"id":"1","name":"banana","quantity":"150"},{"id":"2","name":"orange","quantity":"154"},{"id":"3","name":"apple","quantity":"100"}]
- 2020/12/15 22:13:33 返回單行數(shù)據(jù)Map:
- {"DateStyle":"ISO, MDY"}
- Updated 1 row of data
- Data row = (2, orange, 154)
- Data row = (3, apple, 100)
- Data row = (1, banana, 200)
- Deleted 1 row of data
- Data row = (3, apple, 100)
- Data row = (1, banana, 200)
- Finished dropping table (if existed)
總結(jié)
本文對pq驅(qū)動包以及連接字符串參數(shù)進行了介紹
示例代碼分別將連接/創(chuàng)建表格/增加行數(shù)據(jù)/更新行數(shù)據(jù)/刪除行數(shù)據(jù)封裝為不同的方法, 便于靈活使用
查詢單行或多行數(shù)據(jù)時, 可以直接使用封裝好的方法, 直接傳入Db指針和查詢語句即可
參考文檔
https://docs.microsoft.com/en-us/azure/postgresql/connect-go
https://pkg.go.dev/github.com/lib/pq
https://www.postgresql.org/
https://www.postgresql.org/docs/current/libpq.html