Go語言運算元據庫及其常規操作

leepetter 發表於 2021-05-13
Go

Go操作MySQL

安裝: go get -u github.com/go-sql-driver/mysql

GO語言的運算元據庫的驅動原生支援連線池, 並且是併發安全的 標準庫沒有具體的實現 只是列出了一些需要的第三方庫實現的具體內容

//第一次連線MySQL成功
package main

import (
    "database/sql"
    _ "github.com/go-sql-driver/mysql"   // _想當於init()初始化
    "log"
)

func main() {
    // root 使用者名稱 1qa2ws3ed是密碼  後邊的書ip:port  gouse 庫名
    dsn := "root:[email protected](127.0.0.1:3306)/gouse"
    db, err := sql.Open("mysql", dsn)
    if err != nil {
        panic(err)
    }
    // ping是嘗試連線MySQL資料庫

    if err = db.Ping(); err != nil{
        panic(err)
    }
    log.Fatalln("Mysql資料庫連線成功")

}
  • Go呼叫MySQL封裝成函式
package main

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

var db *sql.DB

func InitDB() (err error) {
    dsn := "root:[email protected](127.0.0.1:3306)/gouse"

    db, err = sql.Open("mysql", dsn)
    CheckErr(err)

    err = db.Ping()
    CheckErr(err)
    fmt.Println("資料庫連線成功...")
    // 設定資料庫連線池最大連線數
    db.SetConnMaxLifetime(10)

    //設定最大閒置連線數
    db.SetMaxIdleConns(5)

    return
}

type data struct {
    Username string `json:"username"`
    Password string `json:"password"`
}


func main()  {
    err := InitDB()
    CheckErr(err)

    query, err := db.Query("select username, password from test")
    CheckErr(err)

    for query.Next(){
        line := data{}
        // 查詢資料的時候必須要呼叫scan方法如果 沒有 使用scan  連線通道一直保持連線 無法釋放連線  
        _ = query.Scan(&line.Username, &line.Password)
        fmt.Println(line)
        dataDic := map[string]string{
            "username": line.Username,
            "password": line.Password,
        }
        marshal, _ := json.Marshal(dataDic)
        fmt.Println(string(marshal))
    }


}

func CheckErr(err error) {
    if err != nil {
        fmt.Println(err)
        panic(err)
    }
}
  • GO—MySQL的增刪改查
package main

import (
    "database/sql"
    "encoding/json"
    "fmt"
    "time"

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

var db *sql.DB

// InitDB 資料庫連線初始化
func InitDB() (err error) {
    dsn := "root:[email protected](127.0.0.1:3306)/gouse"

    db, err = sql.Open("mysql", dsn)
    CheckErr(err)

    err = db.Ping()
    CheckErr(err)
    fmt.Println("資料庫連線成功...")
    // 設定資料庫連線池最大連線數
    db.SetConnMaxLifetime(10)

    //設定最大閒置連線數
    db.SetMaxIdleConns(5)

    return

}

type data struct {
    Username string `json:"username"`
    Password string `json:"password"`
}

// SelectQuery 查詢函式
func SelectQuery() {
    sqlStr := "select username, password from test where id > ?"
    query, err := db.Query(sqlStr, 1)
    CheckErr(err)
    defer query.Close()

    fmt.Printf("現在是北京時間 %s , 你今天進步了嗎?\n", time.Now().Format("2006-01-02 15:04:05"))

    for query.Next() {
        line := data{}
        // 查詢資料的時候必須要呼叫scan方法如果 沒有 使用scan  連線通道一直保持連線 無法釋放連線
        _ = query.Scan(&line.Username, &line.Password)
        //fmt.Println(line)
        dataDic := map[string]string{
            "username": line.Username,
            "password": line.Password,
        }
        marshal, _ := json.Marshal(dataDic)
        fmt.Printf("查詢到的資料為 %s\n", string(marshal))
    }
}

// InsertQuery 插入資料
func InsertQuery() {
    // sql 語句
    sqlStr := `insert into test (username,password) values ("kuQi", "123qwe")`
    result, err := db.Exec(sqlStr)
    CheckErr(err)
    id, err := result.LastInsertId()
    CheckErr(err)
    fmt.Printf("插入成功資料的id為 %v", id)
}

// UpdateQuery 更新資料函式
func UpdateQuery(dataField string, user string) {
    sqlStr := `update test set password=? where username=?`
    result, err := db.Exec(sqlStr, dataField, user)
    CheckErr(err)
    rowsAffected, err := result.RowsAffected()
    CheckErr(err)
    fmt.Printf("被更新欄位的id為%d\n", rowsAffected)

}

// DeleteQuery 刪除
func DeleteQuery(id int) {
    sqlStr := `delete from test where id=?`
    result, err := db.Exec(sqlStr, id)
    CheckErr(err)
    rowsAffected, err := result.RowsAffected()
    CheckErr(err)
    if rowsAffected == 0 {
        fmt.Printf("沒有匹配到要刪除的id=%d資料", id)
        return
    }
    fmt.Printf("刪除資料庫的id為%d", id)

}

//CheckErr 異常捕獲函式
func CheckErr(err error) {
    if err != nil {
        fmt.Println(err)
        panic(err)
    }
}

// main 主函式 所有函式的入口
func main() {
    err := InitDB()
    CheckErr(err)

    //InsertQuery()
    UpdateQuery("hahaGolang123", "kuQi")
    SelectQuery()
    DeleteQuery(5)
}
  • MySQL的預處理
什麼是預處理?
普通SQL語句執行過程:
    1.客戶端對SQL語句進行佔位符的替換得到了完整的SQL語句
    2.客戶端傳送完整SQL語句到MySQL服務端
    3.MySQL服務端執行完整的SQL語句並將結果返回終端

預處理的執行過程
    1.先把SQL語句拆分成兩部分,SQL語句部分和引數部分
    2.先把SQL語句部分傳送給MySQL服務端進行SQL預處理
    3.然後引數部分傳送給MySQL服務端,MySQL對SQL語句進行拼接
    4.MySQL服務端執行完整的SQL語句返回結果

為什麼要進行預處理?
  1.為了優化MySQL伺服器重複執行SQL的方法。可以執行伺服器的效能,提前讓伺服器編譯,一次編譯多次執行,節省後續重複編譯的成本
  2.並且避免SQL注入
  • Go實現MySQL預處理
// prepare方法現將SQL傳送到MySQL服務端, 返回一個準備好的狀態用於之後的查詢和命令。返回值可以同時執行多個查詢和命令  ; 命令也就是SQL語句
// PrepareInsert 預處理執行插入語句
func PrepareInsert() {

    defer wg.Done()
    sqlStr := `insert into test (username, password) values (?, ?)`
    // - 預處理 stmt 就是編譯好的sql語句 之後直接傳遞引數即可
    stmt, err := db.Prepare(sqlStr)
    var u1 = uuid.Must(uuid.NewV4())
    CheckErr(err)
    defer stmt.Close()
    i := rand.Int()

    username := fmt.Sprintf("yonghuming%d", i)
    result, err := stmt.Exec(username, u1.String()[:10])
    CheckErr(err)
    rowsAffected, err := result.LastInsertId()
    CheckErr(err)
    fmt.Printf("成功插入id=%d條資料\n", rowsAffected)
}
  • Go語言實現MySQL實現事務操作
// go語言中使用一下三個方法實現MySQL中的事務操作, 開始事務
func (db *DB) Begin()(*Tx, error)

// 提交事務  相當與Python中的conn.commit()
func (tx *Tx) Commit() error   

// 回滾事務
func (tx *Tx) Rollback() error








package main

import (
    "database/sql"
    "fmt"

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

var db *sql.DB

type data struct {
    Username string `json:"username"`
    Password string `json:"password"`
}

// InitDB 資料庫連線初始化
func InitDB() (err error) {
    dsn := "root:[email protected](127.0.0.1:3306)/gouse"

    db, err = sql.Open("mysql", dsn)
    CheckErr(err)

    err = db.Ping()
    CheckErr(err)
    fmt.Println("資料庫連線成功...")
    // 設定資料庫連線池最大連線數
    db.SetMaxOpenConns(100)

    //設定最大閒置連線數
    db.SetMaxIdleConns(5)

    return

}

//CheckErr 異常捕獲函式
func CheckErr(err error) {
    if err != nil {
        fmt.Println(err)
        panic(err)
    }
}

// TranSaCtIon MySQL的事務操作
func TranSaCtIon() {
    // 開啟事務
    tx, err := db.Begin()
    CheckErr(err)

    // 執行多個SQL操作
    sqlStr := `update test set id=id+100000 where password=?`
    result, err := tx.Exec(sqlStr, "07f70f7e-4")
    CheckErr(err)
    id, err := result.LastInsertId()
    if err != nil {
        // 語句回滾
        err := tx.Rollback()
        fmt.Println("事務回滾")
        CheckErr(err)

    }
    fmt.Printf("修改後的id為%d\n", id)

}

func main() {
    err := InitDB()
    CheckErr(err)
    TranSaCtIon()
}

  • sqlx使用

第三方庫sqlx能夠簡化操作,提高開發效率

安裝go get github.com/jmoiron/sqlx

package main

import (
    "fmt"

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

var db *sqlx.DB

// InitDB 資料庫初始化
func InitDB() (err error) {
    dsn := "root:[email protected](127.0.0.1:3306)/gouse"
    db, err = sqlx.Connect("mysql", dsn)
    CheckErr(err)
    db.SetMaxOpenConns(50)
    db.SetMaxIdleConns(10)
    fmt.Println("goUse 資料庫連線成功")
    return
}

//CheckErr 異常捕獲函式
func CheckErr(err error) {
    if err != nil {
        fmt.Println(err)
        panic(err)
    }
}

func main() {
    err := InitDB()
    CheckErr(err)
}


sqlx相較於原生的sql庫好處在於 查詢的時候sql原生的需要next scan 回撥獲取結果

sqlx 查詢只需要定義一個儲存的變數 然後自動就會將查詢的出來的值放入變數中

package main

import (
    "encoding/json"
    "fmt"

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

var db *sqlx.DB

type user struct {
    ID       int    `json:"id"`
    Username string `json:"username"`
    Password string `json:"password"`
}

// InitDB 資料庫初始化
func InitDB() (err error) {
    dsn := "root:[email protected](127.0.0.1:3306)/gouse"
    // Connect 就是連線的同時db.ping()一下
    db, err = sqlx.Connect("mysql", dsn)
    CheckErr(err)
    db.SetMaxOpenConns(50)
    db.SetMaxIdleConns(10)
    fmt.Println("goUse 資料庫連線成功")
    return
}

// SelectDB 查詢單條資料的方法
func SelectDB() {
    sqlStr := `select * from test where id=?`
    var data user
    _ = db.Get(&data, sqlStr, 990)
    //CheckErr(err)
    fmt.Printf("%#v\n", data)
    marshal, err := json.Marshal(data)
    CheckErr(err)
    fmt.Println(string(marshal))
}

// ManySelect 查詢多條資料方法
func ManySelect() {
    sqlStr := `select * from test where id < ?`
    var dataList []user
    err := db.Select(&dataList, sqlStr, 1000)
    CheckErr(err)
    //fmt.Println(dataList)
    marshal, err := json.Marshal(dataList)
    CheckErr(err)
    fmt.Println(string(marshal))
}

//CheckErr 異常捕獲函式
func CheckErr(err error) {
    if err != nil {
        fmt.Println(err)
        panic(err)
    }
}

func main() {
    err := InitDB()
    CheckErr(err)
    SelectDB()
    ManySelect()

}

Go操作Redis

安裝go get -u github.com/go-redis/redis

package main

import (
    "fmt"

    "github.com/go-redis/redis"
)

var redisDB *redis.Client

// InitRedisDB redis資料庫初始化
func InitRedisDB() (err error) {

    redisDB = redis.NewClient(&redis.Options{
        Addr:     "127.0.0.1:6379",
        Password: "",
        DB:       0,
    })
    _, err = redisDB.Ping(redisDB.Context()).Result()
    CheckErr(err)
    fmt.Println("redis 連線成功")
    return
}

//CheckErr 異常捕獲函式
func CheckErr(err error) {
    if err != nil {
        fmt.Println(err)
        panic(err)
    }
}

func main() {
    _ = InitRedisDB()
}
set(key, value):給資料庫中名稱為key的string賦予值value
get(key):返回資料庫中名稱為key的string的value
getset(key, value):給名稱為key的string賦予上一次的value
mget(key1, key2,, key N):返回庫中多個string的value
setnx(key, value):新增string,名稱為key,值為value
setex(key, time, value):向庫中新增string,設定過期時間time
mset(key N, value N):批量設定多個string的值
msetnx(key N, value N):如果所有名稱為key i的string都不存在
incr(key):名稱為key的string1操作
incrby(key, integer):名稱為key的string增加integer
decr(key):名稱為key的string1操作
decrby(key, integer):名稱為key的string減少integer
append(key, value):名稱為key的string的值附加value
substr(key, start, end):返回名稱為key的string的value的子串

NSQ分散式訊息佇列

NSQ是目前比較流行的一個分散式訊息佇列,下面主要是NSQ及GO語言如何操作NSQ

NSQ是GO語言編寫的一個開源的實時分散式記憶體訊息佇列, 其效能十分優異, NSQ的優勢有:

​ 1.NSQ提倡分散式和擴散的拓撲,沒有單點故障,支援容錯和高可用性,並提供可靠的訊息交付保證

​ 2.NSQ支援橫向擴充套件, 沒有任何集中式代理

​ 3.NSQ易於配置和部署,並且內建了管理介面

安裝go get -u github.com/nsqio/go-nsq

Context

在Go HTTP 包的server中,每一個請求都在對應著一個響應,請求處理函式通常會啟動額外的goroutine用來訪問後端的服務,比如資料庫和rpc服務,用來處理一個請求的goroutine通常需要訪問一些與請求特定的資料,比如終端的身份認證資訊、驗證相關的token、請求和截止時間。當一個請求被取消或超時時,所有用來處理該請求的goroutine都應該迅速退出,然後系統才能釋放這些goroutine

如何優雅的結束goroutine釋放資源

// 通道版本
package main

import (
    "fmt"
    "sync"
    "time"
)

var wg sync.WaitGroup

func worker(exitChan <-chan struct{}) {
    defer wg.Done()
Test:
    for {
        fmt.Println("worker")
        time.Sleep(time.Second)
        select {
        case <-exitChan:
            break Test
        default:
        }

    }

}

func main() {
    wg.Add(1)
    c := make(chan struct{})

    go worker(c)
    time.Sleep(10 * time.Second)
    c <- struct{}{}
    close(c)
    wg.Wait()
    fmt.Println("Over")

}
// Context版本
package main

import (
    "context"
    "fmt"
    "sync"
    "time"
)

var wg sync.WaitGroup

func worker(ctx context.Context) {
    defer wg.Done()
Test:
    for {
        fmt.Println("worker")
        time.Sleep(time.Second)
        select {
        case <-ctx.Done():
            break Test
        default:
        }

    }

}

func main() {
    wg.Add(1)
    ctx, cancelFunc := context.WithCancel(context.Background())

    go worker(ctx)
    time.Sleep(10 * time.Second)

    cancelFunc()
    wg.Wait()
    fmt.Println("Over")

}

如果goroutine開啟了新的goroutine,只需要將ctx傳入到新的goroutine中即可

Background() 和 TODO()

go內建兩個函式: Background() 和TUDO(),這兩個函式分別返回了一個實現了context介面的background和todo. 我們程式碼中最開始都是以這兩個內建的上下文物件作為最頂層的partent context,衍生出更多的子上下文物件。

backgroud() 主要用於main函式,初始化以及程式碼測試,作為context這個樹結構的最頂層context,也就是跟context。

todo(),他目前還不知道能幹點啥?

使用context的注意事項

  • 推薦以引數顯示傳遞context
  • 以context作為引數的函式方法,應該把context作為第一個引數
  • 給一個函式傳遞context的時候,不要nil,如果不知道傳遞什麼,就使用context.TODO()
  • context是併發安全的,可以隨意在多個goroutine中傳遞

log標準庫

log包定義了Logger型別, 該型別提供了一些格式化輸出的方法。本包也提供了一個預定義的標準logger,可以通過呼叫函式Print系列,fatal系列和panic系列來使用,比自行建立的logger物件更容易使用。

package main

import "log"

func main() {
    log.Println("這是第一條工作日誌")

    v := "THIS is worker log"
    log.Printf("%#v\n", v)
    // Fatal將會值寫入資訊之後,執行exit(1)
    log.Fatal("之後寫一萬行程式碼 我也不執行了哦")

    // 可以通過log.Panic 引發異常 會將日誌寫入之後引發異常
    log.Panic("測試panic的日誌")

}
  • flag選項(日誌輸出內容設定)
log標準庫提供瞭如下的flag選項,他們是一系列定義好的常量。
const (
    Ldate = 1 << iota
  Ltime
  Lmicroseconds
  Llongfile
  Lshortfile
  LUTC
  LstdFlags = Ldate | Ltime
)



package main
import "log"
func main() {
    // 設定預設附加的內容 
        log.SetFlags(log.Llongfile | log.Ltime)
    // 設定日誌字首
        log.SetPrefix("[go_log] ")
        log.Println("測試日誌")

}
output>>>
[go_log] 19:02:14 /Users/mac/GolandProjects/src/day02/go_log庫/main.go:19: 測試日誌

  • 配置日誌輸出位置

setoutput函式用來設定logger的輸出目的地,預設是標準錯誤輸出

package main

import (
    "log"
    "os"
)

func main() {

    file, err := os.OpenFile("test.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
    if err != nil {
        log.Panic("檔案開啟失敗")
    }
  // 設定了寫入檔案 日誌內容就不會列印到終端了
    log.SetOutput(file)
    log.SetFlags(log.Llongfile | log.Ltime)
    log.SetPrefix("[go_log] ")
    log.Println("測試日誌")

}
我們可以定義一個init初始化函式 將log全部配置好 這樣更加標準化

第三方日誌庫logrus的使用

logrus是GO結構化的logger 與上邊的logger標準庫完全相容

安裝logrusgo get github.com/sirupsen/logrus

package main

import (
    log "github.com/sirupsen/logrus"
)

func main() {
    log.WithFields(log.Fields{
        "animals": "dog",
        "time":    log.FieldKeyTime,
    }).Info("這是啥")

}
  • 日誌級別

Trace、debug、info、warning、error、fatal、panic


    log.Trace("跟蹤?")
    log.Debug("Debug?")
    log.Info("資訊")
    log.Warn("警告?")
    log.Error("Something failed but I'm not quitting.")
    // 記完日誌後會呼叫os.Exit(1) 
    log.Fatal("Bye.")
    // 記完日誌後會呼叫 panic() 
    log.Panic("I'm bailing.")
  • 日誌記錄
package main

import (
    "os"
    "time"

    log "github.com/sirupsen/logrus"
)

func main() {
    file, err := os.OpenFile("logrustest.log", os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0644)
    if err != nil {
        log.Panicln(err)
    }
    log.SetOutput(file)
    for i := 0; i < 100; i++ {
        log.WithFields(log.Fields{
            "animals": "dog",
            "Countey": "China",
            "City":    "BeiJing",
        }).Info("這是啥")
        time.Sleep(time.Second)
    }

    log.Trace("跟蹤?")
    log.Info("資訊")
    log.Warn("警告?")
    // 設定日誌級別, 會記錄info以上級別(warn error fatal panic)
    log.SetLevel(log.InfoLevel)

}

>>>結果
time="2021-02-04T12:00:15+08:00" level=info msg="這是啥" City=BeiJing Countey=China animals=dog
time="2021-02-04T12:00:17+08:00" level=info msg="這是啥" City=BeiJing Countey=China animals=dog
time="2021-02-04T12:00:18+08:00" level=info msg="這是啥" City=BeiJing Countey=China animals=dog
time="2021-02-04T12:00:19+08:00" level=info msg="這是啥" City=BeiJing Countey=China animals=dog

日誌的條目除了使用withfield 和withfields新增的相關日誌,還有一些預設新增的日誌欄位

time 記錄日誌的時間戳 msg 記錄日誌資訊 level記錄日誌級別

  • 日誌格式化

logrus內建一下兩種日誌格式化程式

logrus.TextFormatter logrus.JSONFormatter

log.SetFormatter(&log.JSONFormatter{})
  • 追蹤函式
    log.SetReportCaller(true)
    這樣就會將哪個檔案哪一行 都記錄下來  但是不是特殊需求無需開啟這個 因為會增加效能開銷
本作品採用《CC 協議》,轉載必須註明作者和本文連結