本文參與了思否技術徵文,歡迎正在閱讀的你也加入。
Hello,大家好,我是海軍,最近一直在學習Go,目前在做專案熟悉Go 階段。 本文來分享一下 Gin + GORM 的一些 開發體驗,有喜歡Go 方向的朋友,歡迎一起交流學習呀!
後續會更新實戰專案,目前在實現一個 技術論壇專案,結尾有效果圖,前端部分完成了,現在在完善後端介面和前端聯調的過程,沒多久就會發布了。 後續,會再寫一篇專案文章。
幹就完了 ?
導讀目錄
Gin
- Gin 學習路線
- Gin 入門安裝
- Gin 中介軟體使用
- Gin 獲取請求引數
- Gin 獲取JSON資料
- Gin Cookie
- Gin Session
- Gin 上傳檔案
GORM
- 什麼是GORM
- 如何建Model
Gin
學習路線
入門安裝
安裝Gin
- 下載依賴
$ go get -u github.com/gin-gonic/gin
- 將 gin 引入到程式碼中:
import "github.com/gin-gonic/gin"
- (可選)如果使用諸如 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)
name | base | |
---|---|---|
key | cookie - key | |
value | cookie - value | |
maxAge | cookie 過期時間 | |
path | cookie 路徑 | |
domain | cookie 路徑的作用域, 本地 localhost ,線上為域名 | |
secure | 當 secure 為 true 時,cookie 在 HTTP 是無效的, HTTPS 才有效 | |
httpOnly | 微軟對cookie 的擴充套件,如果設定了 httpOnly, 則無法獲取cookie資訊,防止了XSS 攻擊 |
c.SetCookie("loginStatus", "登陸成功狀態", 5600, "/", "localhost", false, true)
獲取 cookie
c.Cookie(name)
Session
session 基本原理
http協議是無狀態的,就是說你在請求伺服器同時,伺服器不知道哪個是你訪問的,怎麼讓伺服器知道哪個是你訪問的,那麼session 就出來了。
session 和 cookie 不分家,每次說 session 其實就是說 cookie。服務端 和 客戶端 有狀態通訊原理:
第一次登入時,伺服器給客戶端頒發一個唯一的sessionId, 並透過http的響應頭返回。客戶端(瀏覽器)發現返回的資料中有cookie資料就把這個cookie資料存放到記憶體。下次再傳送http請求時,把記憶體中的cookie資料再塞到http請求頭中,一併發給伺服器,伺服器在解析請求時,發現請求頭中有cookie,就開始識別cookie中的sessionId,拿到sessionId,我們就知道這個請求時由哪個客戶端傳送來的了。
Gin 中使用 session
Gin 本身是沒有提供 session 的,需要使用第三方依賴。
- 安裝依賴
go get github.com/gin-contrib/sessions
- 匯入依賴 使用
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()
⚠️注意
- session 倉庫其實就是一個 map[interface]interface 物件,所有 session可以儲存任意資料
- session 使用的編解碼器是自帶的gob,所以儲存類似: struct、map 這些物件時需要先註冊物件,不然會報錯 gob: type not registered for...
- session 儲存引擎支援: cookie、記憶體、mongodb、redis、postgres、memstore、memcached 以及 gorm 支援的各類資料庫(mysql、sqlite)
- session 在建立時有一個配置項,可以配置session過期時間、cookie、domain、secure、path等引數
- 呼叫 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: 對映
相當於定義了一張 資料庫表type Person struct { Id int Name string Age int}結構體例項 相當於 資料行student := Person{1,"小明",28}
ORM 優缺點
優點:
- 提高開發效率
缺點:
- 犧牲執行效能
- 犧牲靈活性
- 弱化SQL 能力, 手寫SQL 能降低
如何建Model
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
}
表名
- 表名預設是結構體名稱的複數, 也可以取消預設複數表名
type User struct {} // 預設表名是 `users`
// 將 User 的表名設定為 `user`
func (User) TableName() string {
return "user"
}
// 禁用預設表名的複數形式,如果置為 true,則 `User` 的預設表名是 `user`
db.SingularTable(true)
- 也可以透過Table()指定表名:
// 使用User結構體建立名為`student`的表
db.Table("student").CreateTable(&User{})
- 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欄位為當前時間,而不是直接將記錄從資料庫中刪除。