Gin + GORM 入門到實戰

程式設計師海軍 發表於 2022-11-24

本文參與了思否技術徵文,歡迎正在閱讀的你也加入。

Hello,大家好,我是海軍,最近一直在學習Go,目前在做專案熟悉Go 階段。 本文來分享一下 Gin + GORM 的一些 開發體驗,有喜歡Go 方向的朋友,歡迎一起交流學習呀!
後續會更新實戰專案,目前在實現一個 技術論壇專案,結尾有效果圖,前端部分完成了,現在在完善後端介面和前端聯調的過程,沒多久就會發布了。 後續,會再寫一篇專案文章。

幹就完了 🤔

導讀目錄

Gin

  • Gin 學習路線
  • Gin 入門安裝
  • Gin 中介軟體使用
  • Gin 獲取請求引數
  • Gin 獲取JSON資料
  • Gin Cookie
  • Gin Session
  • Gin 上傳檔案

GORM

  • 什麼是GORM
  • 如何建Model

Gin

學習路線

圖片

入門安裝

安裝Gin

  1. 下載依賴
$ go get -u github.com/gin-gonic/gin
  1. 將 gin 引入到程式碼中:
import "github.com/gin-gonic/gin"
  1. (可選)如果使用諸如 http.StatusOK 之類的常量,則需要引入 net/http 包:
import "net/http"

demo

package main

import (
    "fmt"
    "github.com/gin-gonic/gin"
)

func main() {
    fmt.Println("Gin 入門學習")

    // 建立一個預設的路由引擎
    r := gin.Default()
    // GET:請求方式;/hello:請求的路徑
    // 當客戶端以GET方法請求/hello路徑時,會執行後面的匿名函式
    r.GET("/hello", func(c *gin.Context) {
        // c.JSON:返回JSON格式的資料
        c.JSON(200, gin.H{
            "message": "Hello world!",
        })
    })
    // 啟動HTTP服務,預設在0.0.0.0:8080啟動服務
    r.Run(":8098")
}

Gin 中介軟體使用

目錄

  • 什麼是 中介軟體?
  • 中介軟體作用域
  • 中介軟體資料共享
  • 中介軟體注意

什麼是中介軟體?

中介軟體是在當客戶端訪問服務端介面之前和之後會做一些事情。
作用: 登陸認證, 許可權校驗 , 記錄日誌, 耗時統計 .....

中介軟體作用域

全域性作用域

main 入口檔案中, 透過 建立的預設路由引擎. use(中介軟體1,中間2,....) 即可註冊使用全域性中介軟體了。

import (
   "Gin/middleware"
   "Gin/router"
   "fmt"
   "github.com/gin-gonic/gin"
   "net/http"
)

func mian() {

// 建立一個預設的路由引擎
var r = gin.Default()  

// 全域性使用 中間價,可以使用一個/多箇中介軟體
r.Use(middleware.GetHttpHost, middleware.GetHttpMethod)
}

建立兩個中介軟體,分別獲取 Host, Method

package middleware

import (
    "fmt"
    "github.com/gin-gonic/gin"
)

func GetHttpHost(r *gin.Context) {
    fmt.Printf("ℹ️ : 請求的地址為------%s ", r.Request.Host)
    r.Set("token", "SDASD12312rtywe")
    fmt.Println()
    r.Next()
}

func GetHttpMethod(r *gin.Context) {
    fmt.Printf("ℹ️ : 請求方法為------%s ", r.Request.Method)
    r.Next()
}

區域性作用域

透過在,路由分組引數路徑後面,新增中介軟體即可,支援新增多箇中介軟體

package router

import "github.com/gin-gonic/gin"
import "Gin/controller/BookStore"
import "Gin/middleware"


func BookRouter(r *gin.Engine) {
    bookRouter := r.Group("/book", middleware.GetLogInfo)  //新增區域性中介軟體
    {
        bookRouter.GET("/", BookStore.BookController{}.Index)

        bookRouter.GET("/:bookName", BookStore.BookController{}.SearchBookName)

        bookRouter.POST("/add", BookStore.BookController{}.Add)

    }
}

中介軟體資料共享

  • c.set(name, value) c*gin.Context
  • c.get(name)
package middleware

import (
    "fmt"
    "github.com/gin-gonic/gin"
)

func GetHttpHost(r *gin.Context) {
    fmt.Printf("ℹ️ : 請求的地址為------%s ", r.Request.Host)
    r.Set("token", "SDASD12312rtywe")
    fmt.Println()
    r.Next()
}
package middleware

import (
    "fmt"
    "github.com/gin-gonic/gin"
    "time"
)

func GetLogInfo(c *gin.Context) {
    receiveToken, _ := c.Get("token")
    fmt.Println("進入中介軟體------")
    fmt.Printf("測試獲取中介軟體中共享的資料---token:  %s", receiveToken)
    fmt.Println()
    c.Next()
}

獲取請求引數

獲取querystring引數

querystring指的是URL中?後面攜帶的引數,例如:/user/search?username=小王子&address=沙河。 獲取請求的querystring引數的方法如下:

func getLoginInfo() {
    r.GET("/login", func(c *gin.Context) {
        username := c.Query("username")
        password := c.Query("password")
        // c.JSON:返回JSON格式的資料
        c.JSON(http.StatusOK, gin.H{
            "info":     "==========",
            "username": username,
            "password": password,
        })
    })
}

⚠️ 注意

  • 可以指定預設的query 值,透過 c.DefaultQuery
username := c.DefaultQuery("username", "admin")
  • 指定接收的 query key ,c.Query("username")
username := c.Query("username")

上面訪問 "http://127.0.0.1:8098/login?username=admin&password=1123123" 即可返回指定的json 值

{
    "info": "==========",
    "password": "1123123",
    "username": "admin"
}

獲取 form 引數

請求的資料透過form表單來提交的,透過 PostForm() 接收

func getFormParams() {
    r.POST("/saveBookInfo", func(c *gin.Context) {
        bookName := c.PostForm("bookName")
        author := c.PostForm("author")
        // c.JSON:返回JSON格式的資料
        c.JSON(http.StatusOK, gin.H{
            "bookName": bookName,
            "author":   author,
        })
        fmt.Println("測試Post")
        fmt.Printf("訪問的介面為--%s,引數為: bookName--- %s, author----%s", "/saveBookInfo", bookName, author)
    })
}

獲取path引數

獲取URl 的 path, 可以透過 c.Param() 接收

func getRouterPathParams() {
    r.GET("/bookStore/:bookName/:author", func(c *gin.Context) {
        bookName := c.Param("bookName")
        author := c.Param("author")
        // c.JSON:返回JSON格式的資料
        c.JSON(http.StatusOK, gin.H{
            "bookName": bookName,
            "author":   author,
        })
        fmt.Println("測試Post")
        fmt.Printf("訪問的介面為--%s,引數為: bookName--- %s, author----%s", "/saveBookInfo", bookName, author)
    })
}

訪問連結🔗 http://127.0.0.1:8098/bookStore/Go/海軍 ,返回

{
    "author": "海軍",
    "bookName": "Go"
}

為了能夠更方便的獲取請求相關引數,提高開發效率,我們可以基於請求的Content-Type識別請求資料型別並利用反射機制自動提取請求中QueryString、form表單、JSON、XML等引數到結構體中。 下面的示例程式碼演示了.ShouldBind()強大的功能,它能夠基於請求自動提取JSON、form表單和QueryString型別的資料,並把值繫結到指定的結構體物件。

Cookie

Cookie 是儲存在客戶端的瀏覽器環境中的, 它可以 在訪問同一個域下的網站 資料共享。 它解決了HTTP 無狀態資料不共享問題。

Cookie 功能

:::info

  • 儲存使用者登陸狀態
  • 儲存使用者瀏覽器記錄
  • 猜你喜歡,智慧推薦等
  • 購物車等
    :::

設定Cookie

SetCookie(name string, value string, maxAge int, path string, domain string, secure bool, httpOnly bool)
namebase
keycookie - key
valuecookie - value
maxAgecookie 過期時間
pathcookie 路徑
domaincookie 路徑的作用域, 本地 localhost ,線上為域名
secure當 secure 為 true 時,cookie 在 HTTP 是無效的, HTTPS 才有效
httpOnly微軟對cookie 的擴充套件,如果設定了 httpOnly, 則無法獲取cookie資訊,防止了XSS 攻擊
c.SetCookie("loginStatus", "登陸成功狀態", 5600, "/", "localhost", false, true)

獲取 cookie

c.Cookie(name)

Session

wallhaven-1keo6g.jpeg

session 基本原理

http協議是無狀態的,就是說你在請求伺服器同時,伺服器不知道哪個是你訪問的,怎麼讓伺服器知道哪個是你訪問的,那麼session 就出來了。
session 和 cookie 不分家,每次說 session 其實就是說 cookie。

服務端 和 客戶端 有狀態通訊原理:
第一次登入時,伺服器給客戶端頒發一個唯一的sessionId, 並透過http的響應頭返回。客戶端(瀏覽器)發現返回的資料中有cookie資料就把這個cookie資料存放到記憶體。下次再傳送http請求時,把記憶體中的cookie資料再塞到http請求頭中,一併發給伺服器,伺服器在解析請求時,發現請求頭中有cookie,就開始識別cookie中的sessionId,拿到sessionId,我們就知道這個請求時由哪個客戶端傳送來的了。

Gin 中使用 session

Gin 本身是沒有提供 session 的,需要使用第三方依賴。

  1. 安裝依賴
go get github.com/gin-contrib/sessions
  1. 匯入依賴 使用
import (
"github.com/gin-contrib/sessions"
"github.com/gin-contrib/sessions/cookie"
"github.com/gin-gonic/gin"
)

func main(){

// 建立一個預設的路由引擎
var r = gin.Default()

// 1. 建立基於cookie的儲存引擎,haijun 引數是用於加密的金鑰,可以隨便填寫
store := cookie.NewStore([]byte("haijun"))

// 2. 設定session中介軟體,引數mysession,指的是session的名字,也是cookie的名字
// store是前面建立的儲存引擎
r.Use(sessions.Sessions("mysession", store))


r.GET("/test", func(c *gin.Context) {
        // 初始化session物件
        session := sessions.Default(c)
                
        // session是鍵值對格式資料,因此需要透過key查詢資料
        session.Set("token", "haijun23123")
        session.Save()

                
        c.JSON(200, gin.H{"token": session.Get("token")})
    })

}


Session 操作

設定 session

session.Set("kay", value)

獲取session

session.Get("key")

刪除session

session.Delete("key")  //刪除單個 session

session.Clear() //刪除全部session

儲存session

ssession.Save()

⚠️注意

  1. session 倉庫其實就是一個 map[interface]interface 物件,所有 session可以儲存任意資料
  2. session 使用的編解碼器是自帶的gob,所以儲存類似: struct、map 這些物件時需要先註冊物件,不然會報錯 gob: type not registered for...
  3. session 儲存引擎支援: cookie、記憶體、mongodb、redis、postgres、memstore、memcached 以及 gorm 支援的各類資料庫(mysql、sqlite)
  4. session 在建立時有一個配置項,可以配置session過期時間、cookie、domain、secure、path等引數
  5. 呼叫 session 方法: Set()、 Delete()、 Clear()、方法後,必須呼叫一次 Save() 方法。否則session資料不會更新

上傳檔案

上傳單檔案

func (bk BookController) UploadOne(c *gin.Context) {
    //獲取檔案
    //c.FormFile("檔案引數名")
    file, err := c.FormFile("file")
    //檔案路徑
    dst := path.Join("./static/images", file.Filename)

    // err == nil 代表上傳成功
    if err == nil {
        c.SaveUploadedFile(file, dst)
        c.JSON(200, gin.H{
            "msg": "上傳檔案成功",
        })

    } else {
        c.JSON(400, gin.H{
            "msg": "上傳檔案失敗----- 🙅",
        })
    }
}

上傳多檔案

// 上傳多個檔案
func (bk BookController) Upload(c *gin.Context) {
    //透過 c.MultipartForm() 獲取多個檔案 
    form, _ := c.MultipartForm()
    filesArr := form.File["file[]"]
    fmt.Println(filesArr)
    //for range 遍歷儲存檔案
    for _, file := range filesArr {
        dst := path.Join("./static/images", file.Filename)
        c.SaveUploadedFile(file, dst)
    }

    c.JSON(200, gin.H{
        "msg":      "上傳檔案成功",
        "fileList": form.File["file[]"],
    })

}

實戰

// 檢驗上傳檔案的格式
func (bk BookController) CheckFileFormat(c *gin.Context) {
    //1.獲取檔案
    file, status := c.FormFile("companyInfo")
    if status == nil {
        //2. 獲取檔案格式
        fileFormat := path.Ext(file.Filename)

        allowFormat := map[string]bool{
            ".jpg": true,
            ".png": true,
        }
        _, ok := allowFormat[fileFormat]
        fmt.Println(fileFormat)
        fmt.Println(ok)
        if ok == true {
            //3。符合接收的格式 建立儲存目錄
            mkDirStatus := os.MkdirAll("./static/fileDir", 0777)

            if mkDirStatus != nil {
                c.String(400, "建立目錄失敗")
                return
            }

            //4。 生成檔名路徑
            newFilePath := path.Join("./static/fileDir", file.Filename)

            //5. 儲存檔案
            c.SaveUploadedFile(file, newFilePath)

            c.String(200, "上傳檔案成功😊")
        } else {
            c.String(400, "上傳檔案失敗")
        }
    } else {
        c.String(400, "上傳檔案失敗")
    }
}

小結

  • 上傳檔案時,型別必須為 multipart/form-data
  • c.FormFile("file") 獲取單個檔案
  • c.MultipartForm() 獲取多個檔案 , 多個檔案的引數名必須是統一的 ,
  • path.Join('路徑',檔名) 拼接檔案路徑
  • c.SaveUploadedFile(file, dst) 儲存檔案,第一個引數為 file , 第二個引數為檔案路徑
  • path.Ext(file.Filename) 獲取到檔案字尾名
  • os.MkdirAll("建立檔案的路徑",許可權code) 建立檔案

GORM

什麼是ORM

什麼是ORM ?O : Object 物件Relationall: 關係Mapping: 對映

 title=

 title=

​相當於定義了一張  資料庫表type Person struct { Id      int Name    string Age     int​}​​結構體例項  相當於 資料行student := Person{1,"小明",28}

ORM 優缺點

優點:

  • 提高開發效率

缺點:

  • 犧牲執行效能
  • 犧牲靈活性
  • 弱化SQL 能力, 手寫SQL 能降低

如何建Model

wallhaven-1k6ljv.jpeg

GoRM 預設模型

GORM內建了一個gorm.Model結構體。gorm.Model是一個包含了ID, CreatedAt, UpdatedAt, DeletedAt四個欄位的Golang結構體。

// gorm.Model 定義
type Model struct {
  ID        uint `gorm:"primary_key"`
  CreatedAt time.Time
  UpdatedAt time.Time
  DeletedAt *time.Time
}

也可以繼承到自己的結構體中

// 將 `ID`, `CreatedAt`, `UpdatedAt`, `DeletedAt`欄位注入到`Student`模型中
type Student struct {
  gorm.Model
  Name string
  Age int
}

Model 定義

type User struct {
  gorm.Model
  Name         string
  Age          sql.NullInt64
  Birthday     *time.Time
  Email        string  `gorm:"type:varchar(100);unique_index"`
  Role         string  `gorm:"size:255"` // 設定欄位大小為255
  MemberNumber *string `gorm:"unique;not null"` // 設定會員號(member number)唯一併且不為空
  Num          int     `gorm:"AUTO_INCREMENT"` // 設定 num 為自增型別
  Address      string  `gorm:"index:addr"` // 給address欄位建立名為addr的索引
  IgnoreMe     int     `gorm:"-"` // 忽略本欄位
}

User 結構體的屬性 Birthday 和 MemberNumber,使用指標,是有什麼含義麼?

預設所有欄位的零值, 比如 0, '', false 或者其它 零值,都不會儲存到資料庫內,使用指標可以避免這種情況。

結構體標記(tags)

結構體標記(tags)

使用結構體宣告模型時,標記(tags)是可選項。
打標記的作用:

是對資料表的欄位做修飾,例如(自增,主鍵,大小,型別,索引........)

結構體標記

結構體標記(Tag)描述
Column指定列名
Type指定列資料型別
Size指定列大小, 預設值255
PRIMARY_KEY將列指定為主鍵
UNIQUE將列指定為唯一
DEFAULT指定列預設值
PRECISION指定列精度
NOT NULL將列指定為非 NULL
AUTO_INCREMENT指定列是否為自增型別
INDEX建立具有或不帶名稱的索引, 如果多個索引同名則建立複合索引
UNIQUE_INDEX和 INDEX 類似,只不過建立的是唯一索引
EMBEDDED將結構設定為嵌入
EMBEDDED_PREFIX設定嵌入結構的字首
-忽略此欄位

關聯相關標記

結構體標記(Tag)描述
MANY2MANY指定連線表
FOREIGNKEY設定外來鍵
ASSOCIATION_FOREIGNKEY設定關聯外來鍵
POLYMORPHIC指定多型型別
POLYMORPHIC_VALUE指定多型值
JOINTABLE_FOREIGNKEY指定連線表的外來鍵
ASSOCIATION_JOINTABLE_FOREIGNKEY指定連線表的關聯外來鍵
SAVE_ASSOCIATIONS是否自動完成 save 的相關操作
ASSOCIATION_AUTOUPDATE是否自動完成 update 的相關操作
ASSOCIATION_AUTOCREATE是否自動完成 create 的相關操作
ASSOCIATION_SAVE_REFERENCE是否自動完成引用的 save 的相關操作
PRELOAD是否自動完成預載入的相關操作

主鍵. 表名. 列名約定

主鍵

GORM 預設會使用名為ID的欄位作為表的主鍵。

type User struct {
  ID   string // 名為`ID`的欄位會預設作為表的主鍵
  Name string
}

// 使用`StudentlID`作為主鍵
type Student struct {
  StudentID int64 `gorm:"primary_key"`
  Name     string
  Age      int64
}

表名

  1. 表名預設是結構體名稱的複數, 也可以取消預設複數表名
type User struct {} // 預設表名是 `users`

// 將 User 的表名設定為 `user`
func (User) TableName() string {
  return "user"
}



// 禁用預設表名的複數形式,如果置為 true,則 `User` 的預設表名是 `user`
db.SingularTable(true)
  1. 也可以透過Table()指定表名:
// 使用User結構體建立名為`student`的表
db.Table("student").CreateTable(&User{})
  1. GORM還支援更改預設表名稱規則:
gorm.DefaultTableNameHandler = func (db *gorm.DB, defaultTableName string) string  {
  return "prefix_" + defaultTableName;
}

"prefix_" 可任意名稱

列名

列名由欄位名稱進行下劃線分割來生成 , 結構體欄位為駝峰命名時,第二個單詞會在前面以下劃線顯示 ,例如:
AddressInfo ---> address_info

type User struct {
  ID        uint      // column name is `id`
  Name      string    // column name is `name`
  Birthday  time.Time // column name is `birthday`
  CreatedAt time.Time // column name is `created_at`
}

可以使用結構體Tag 指定列名

type Animal struct {
  AnimalId    int64     `gorm:"column:beast_id"`         // set column name to `beast_id`
  Birthday    time.Time `gorm:"column:day_of_the_beast"` // set column name to `day_of_the_beast`
  Age         int64     `gorm:"column:age_of_the_beast"` // set column name to `age_of_the_beast`
}

時間戳跟蹤

CreatedAt

如果模型有 CreatedAt欄位,該欄位的值將會是初次建立記錄的時間。

db.Create(&user) // `CreatedAt`將會是當前時間 
// 可以使用`Update`方法來改變`CreateAt`的值 
db.Model(&user).Update("CreatedAt", time.Now()) 

UpdatedAt

如果模型有UpdatedAt欄位,該欄位的值將會是每次更新記錄的時間。

db.Save(&user) // `UpdatedAt`將會是當前時間 
db.Model(&user).Update("name", "jinzhu") 
// `UpdatedAt`將會是當前時間 

DeletedAt

如果模型有DeletedAt欄位,呼叫Delete刪除該記錄時,將會設定DeletedAt欄位為當前時間,而不是直接將記錄從資料庫中刪除。

專案開發中

image.png

image.png

image.png

image.png