0%

gin实践指南

说明

  • 本次搭建环境为:win10, go 1.20.4

安装

  • 新建并初始化项目
1
2
3
4
cd E:\proj\gowork
mkdir studyGin
cd studyGin
go mod init example.com/myGin
  • 下载并安装 gin
1
E:\proj\gowork\studyGin> go get -u github.com/gin-gonic/gin
  • 在vscode中编写测试代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package main

import (
"net/http"

"github.com/gin-gonic/gin"
)

func main() {
// 1.创建路由
r := gin.Default()
// 2.绑定路由规则,执行的函数
// gin.Context,封装了request和response
r.GET("/", func(c *gin.Context) {
c.String(http.StatusOK, "hello World!")
})
// 3.监听端口,默认在8080
// Run("里面不指定端口号默认为8080")
r.Run(":8000")
}

  • 运行
1
PS E:\proj\gowork\studyGin> go run .\main.go

image-20230621153207713

项目结构

  • 分别新建文件夹,查看到E:\proj\gowork\studyGin>tree /F文件结构如下:
1
2
3
4
5
6
7
8
9
10
11
go.mod
go.sum
│ main.go

├─apis # 存放具体的业务逻辑,比如users.go中放的增删改查的逻辑
├─database # 存放数据库的相关配置
├─middleware # 存放中间件的内容
│ ├─jwt
│ └─middleware
├─models # 存放结构体的文件
└─routers # 存放路由,比如get,post等对应关系

代码结构

  • database/mysql.go数据库连接
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package database

//安装gorm
//go get gorm.io/gorm
// 安装mysql驱动
//go get gorm.io/driver/mysql
import (
"fmt"

"gorm.io/driver/mysql"
"gorm.io/gorm"
)

var Db *gorm.DB

func InitDb() {
dsn := "root:123456@tcp(127.0.0.1:3306)/test?charset=utf8mb4&parseTime=True&loc=Local"
database, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
if err != nil {
fmt.Println("open mysql error:", err)
}
Db = database

}
func Close() {
sqlDB, _ := Db.DB()
sqlDB.Close()
}

  • models\users.go 存放的表结构,实现登录功能(对user实体类的逻辑处理)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
package models

import (
"fmt"

"example.com/myGin/database"
)

type User struct {
Name string `form:"name",json:"name",bingding:"required",gorm:"unique;not null"`
Password string `form:"password",json:"password",bingding:"required"gorm:"NOT NULL"`
Id int `form:"id",gorm:"PRIMARY_KEY"`
}

// 登录
func (u *User) Login() (user1 User, err error) {
obj := database.Db.Where("name=? and password=?", u.Name, u.Password).First(&user1)
if err = obj.Error; err != nil {
fmt.Printf("这是登陆错误 %v 和 %T", err, err)
return
}
fmt.Println(user1)
return

}

// 获取用户列表
func (u *User) UserList() (users []User, err error) {
if err = database.Db.Find(&users).Error; err != nil {
return
}
return

}

注意func (u *User) UserList这里的用法,外面的函数要调用时,必须用User.UserList的方式进行调用

  • api/users.go 主要对models\users.go 代码进行调用,并把json数据返回给前端
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
package apis

import (
"net/http"

"example.com/myGin/models"
"github.com/gin-gonic/gin"
)


func UserLogin(c *gin.Context) {
var user models.User
// c.ShouldBindJSON
if c.Bind(&user) == nil { //把客户端格式传过来的数据绑定到结构体user中去
msg, err := user.Login() // 调用model层的对应方法
if err != nil {
if err.Error() == "record not found" {
c.JSON(http.StatusOK, gin.H{
"msg": "用户不存在",
"user": nil,
})
} else {
c.JSON(http.StatusOK, gin.H{
"msg": "登陆错误",
"user": nil,
})

}

} else {
c.JSON(http.StatusOK, gin.H{
"msg": "登陆成功",
"user": msg,
})
}
} else {
c.JSON(400, gin.H{"JSON=== status": "binding JSON error!"})
}

}

// 获取用户列表
func GetUserList(c *gin.Context) {
var user models.User
users, err := user.UserList()
if err != nil {
fmt.Println("get user list error:", err)
}
c.JSON(http.StatusOK, gin.H{
"msg": users,
})
}



  • routers/router.go 对外的路由处理,比如http,get方法的处理
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package routers

import (
"example.com/myGin/apis"
"github.com/gin-gonic/gin"
)

func InitRouter() *gin.Engine {

router := gin.Default()
router.POST("/login", apis.UserLogin)
router.GET("/GetUserList", apis.GetUserList)
return router
}

  • main.go 代码入口
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package main

import (
"example.com/myGin/database"
"example.com/myGin/routers"
)

func main() {
database.InitDb() // 初始化数据库
defer database.Close()
router := routers.InitRouter() //指定路由
router.Run(":8000") //在8000端口上运行

}


  • 运行main.go,启动服务器
1
2
3
4
5
6
7
PS E:\proj\gowork\studyGin> go run .\main.go

...
GIN-debug] POST /login --> example.com/myGin/apis.UserLogin (3 handlers)
[GIN-debug] [WARNING] You trusted all proxies, this is NOT safe. We recommend you to set a value.
Please check https://pkg.go.dev/github.com/gin-gonic/gin#readme-don-t-trust-all-proxies for details.
[GIN-debug] Listening and serving HTTP on :8000

go编写客户端

  • client\client.go用go编写客户端代码,包名为main,传递参数为json
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
package main

import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"time"
)

// 1、测试post请求的结构体
func TestPostLogin() {
reqMap := map[string]interface{}{
"name": "test1",
"password": "123456",
}

body, err := json.Marshal(reqMap)
if err != nil {
fmt.Println("TestPostReq json.Marshal err:", err)
return
}

url := "http://127.0.0.1:8000/login"

req, err := http.NewRequest(http.MethodPost, url, bytes.NewReader(body))
if err != nil {
fmt.Println("TestPostReq http.NewRequest err:", err)
return
}

req.Header.Set("Content-Type", "application/json")

client := &http.Client{Timeout: 5 * time.Second} // 设置请求超时时长5s
resp, err := client.Do(req)
if err != nil {
fmt.Println("TestPostReq http.DefaultClient.Do() err: ", err)
return
}
defer resp.Body.Close()

respBody, err := ioutil.ReadAll(resp.Body)
if err != nil {
fmt.Println("TestPostReq ioutil.ReadAll() err: ", err)
return
}
fmt.Println("respBody: ", string(respBody))

rsp := make(map[string]interface{})
err = json.Unmarshal(respBody, &rsp)
if err != nil {
fmt.Println("TestPostReq json.Unmarshal() err: ", err)
return
}
fmt.Printf("rsp: %+v", rsp)

// 最后经过字段筛选后,再序列化成json格式即可
// result, err := json.Marshal(rsp)
// if err != nil {
// fmt.Println("TestPostReq json.Marrshal() err2: ", err)
// return
// }
// fmt.Println(string(result))
}

func TestGetUserList() {
resp, err := http.Get("http://127.0.0.1:8000/GetUserList")
if err != nil {
fmt.Println("http get error:", err)
return
}
defer resp.Body.Close()
buf := make([]byte, 1024) // 创建一个切片,用例接受服务器返回的数据
for {
m, err := resp.Body.Read(buf)
if err != nil && err != io.EOF {
fmt.Println("resp.Body.Read error:", err)
return
}
res := string(buf[:m])
fmt.Println("get server content,", res)
break

}
}

func main() {
TestPostLogin()
TestGetUserList()
}

  • 运行client.go
1
2
3
4
5
PS E:\proj\gowork\studyGin> go run .\client\client.go
respBody: {"msg":"登陆成功","user":{"Name":"test1","Password":"123456","Id":3}}
rsp: map[msg:登陆成功 user:map[Id:3 Name:test1 Password:123456]]

get server content, {"msg":[{"Name":"test1","Password":"123456","Id":3},{"Name":"test2","Password":"1234567","Id":4}]}

python 发起请求

1
2
3
4
5
6
7
8
9
import requests

data ={"name": "test1", "password": "123456"}
resp = requests.post("http://127.0.0.1:8000/login", data=data)
print(resp.text)

resp2 = requests.get("http://127.0.0.1:8000/GetUserList")
print(resp2.text)

  • 得到结果如下
1
2
{"msg":"登陆成功","user":{"Name":"test1","Password":"123456","Id":3}}
{"msg":[{"Name":"test1","Password":"123456","Id":3},{"Name":"test2","Password":"1234567","Id":4}]}

HTML 渲染

  • gin 支持使用 LoadHTMLGlob() 或者 LoadHTMLFiles() 对html进行渲染

用户列表

  • 新建一个目录和文件:template\user\list.tmpl代码如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<html>
<head>
<title>用户列表</title>
</head>
<body>
<div>
<p>列表数据:</p>
<!-- 定义一个 结构体切片 -->
{{ $userList := . }}

{{ range $index,$v := $userList.users }}
<p>{{ $index }}--{{ $v.Name }}--{{ $v.Password }}</p>
{{ end }}
</div>
</body>
</html>
  • 初始化路由器代码那里需要修改,新增加入加载 HTML 模板文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// routers/router.go
package routers

import (
"example.com/myGin/apis"
"github.com/gin-gonic/gin"
)

func InitRouter() *gin.Engine {

router := gin.Default()

router.POST("/login", apis.UserLogin)
// 加载 HTML 模板文件
router.LoadHTMLGlob("template/user/*")
router.GET("/GetUserList", apis.GetUserList)
return router
}
  • apis.GetUserList 中,返回的数据修改为采用html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// apis/user.go

// 获取用户列表
func GetUserList(c *gin.Context) {
var user models.User
users, err := user.UserList()
if err != nil {
fmt.Println("get user list error:", err)
}
// c.JSON(http.StatusOK, gin.H{
// "msg": users,
// })
c.HTML(http.StatusOK, "list.tmpl", gin.H{
"users": users,
})
}
  • 运行服务器端
1
PS E:\proj\gowork\studyGin> go run .\main.go
  • 打开浏览器,查看到结果

image-20230626103441913

登录验证

  • 通过上面的实例,缺少一个登录验证的流程,一个典型的验证流程为:登录成功–生成token–获取用户列表(检查token存在,则获取成功,否则获取失败)

  • 中间件中middleware\jwt\jwt.go 对登录后的token进行处理和验证

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
package jwt

import (
"errors"
"fmt"
"log"
"net/http"
"time"

"github.com/golang-jwt/jwt/v5" // go get -u github.com/golang-jwt/jwt/v5

"github.com/gin-gonic/gin"
)

// JWTAuth 中间件,检查token
func JWTAuth() gin.HandlerFunc {
return func(c *gin.Context) {
// //获取到请求头中的token
token := c.Request.Header.Get("token")
if token == "" {
c.JSON(http.StatusOK, gin.H{
"status": -1,
"msg": "请求未携带token无权限访问",
})
c.Abort()
return
}

log.Print("get token: ", token)

j := NewJWT()
// parseToken 解析token包含的信息
claims, err := j.ParseToken(token)
fmt.Println("claims", claims)
if err != nil {
if err == TokenExpired {
c.JSON(http.StatusOK, gin.H{
"status": -1,
"msg": "授权已过期",
})
c.Abort()
return
}
c.JSON(http.StatusOK, gin.H{
"status": -1,
"msg": err.Error(),
})
c.Abort()
return
}
// 继续交由下一个路由处理,并将解析出的信息传递下去
c.Set("claims", claims)
}
}

// JWT 签名结构
type JWT struct {
SigningKey []byte
}

// 一些常量
var (
TokenExpired error = errors.New("Token is expired")
TokenNotValidYet error = errors.New("Token not active yet")
TokenMalformed error = errors.New("That's not even a token")
TokenInvalid error = errors.New("Couldn't handle this token:")
SignKey string = "newtrekWang"
)

// 载荷,可以加一些自己需要的信息
type CustomClaims struct {
ID int `json:"userId"`
Name string `json:"name"`
Password string `json:"password"`
jwt.RegisteredClaims // 设置过期时间
}

// 新建一个jwt实例
func NewJWT() *JWT {
return &JWT{
[]byte(GetSignKey()),
}
}

// 获取signKey
func GetSignKey() string {
return SignKey
}

// 这是SignKey
func SetSignKey(key string) string {
SignKey = key
return SignKey
}

// CreateToken 生成一个token
func (j *JWT) CreateToken(claims CustomClaims) (string, error) {
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString(j.SigningKey)
}

// 解析Tokne
func (j *JWT) ParseToken(tokenString string) (*CustomClaims, error) {
token, err := jwt.ParseWithClaims(tokenString, &CustomClaims{}, func(token *jwt.Token) (interface{}, error) {
return j.SigningKey, nil
})
if err != nil {
return nil, err
}
if claims, ok := token.Claims.(*CustomClaims); ok && token.Valid {
return claims, nil
}
return nil, TokenInvalid
}

// 更新token
func (j *JWT) RefreshToken(tokenString string) (string, error) {

token, err := jwt.ParseWithClaims(tokenString, &CustomClaims{}, func(token *jwt.Token) (interface{}, error) {
return j.SigningKey, nil
})
if err != nil {
return "", err
}
if claims, ok := token.Claims.(*CustomClaims); ok && token.Valid {
claims.RegisteredClaims.ExpiresAt = jwt.NewNumericDate(time.Now().Add(time.Hour * 24))

return j.CreateToken(*claims)
}
return "", TokenInvalid
}

  • apis/user.go 加入生成token代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
import (
...
"example.com/myGin/middleware/jwt" // 引用中间件
"example.com/myGin/models"
"github.com/gin-gonic/gin"
// 引用jwt-v5
jwtgo "github.com/golang-jwt/jwt/v5" // go get -u github.com/golang-jwt/jwt/v5
)

// 生成token
func GengerateToken(c *gin.Context, user models.User) {
// 调用中间件的初始化秘钥
j := &jwt.JWT{
[]byte("newtrekWang"), // 秘钥
}
claims := jwt.CustomClaims{
user.Id,
user.Name,
user.Password,
jwtgo.RegisteredClaims{
ExpiresAt: jwtgo.NewNumericDate(time.Now().Add(time.Hour * 24)), //定义过期时间为24小时
Issuer: "newtrekWang", //签名的发行者
},
}

token, err := j.CreateToken(claims)

if err != nil {
c.JSON(http.StatusOK, gin.H{
"status": -1,
"msg": err.Error(),
})
return
}

log.Println(token)
// 登录成功后的结构体数据
data := LoginResult{
User: user,
Token: token,
}
c.JSON(http.StatusOK, gin.H{
"status": 0,
"msg": "登录成功!",
"data": data,
})
return

}

  • routers\router.go 对请求进行验证
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package routers

import (
"example.com/myGin/apis"
"example.com/myGin/middleware/jwt"
"github.com/gin-gonic/gin"
)

func InitRouter() *gin.Engine {

router := gin.Default()
// 这样使用,并不能进行token验证
// v1 := router.Group("/v1")
// v1.Use(jwt.JWTAuth())
router.POST("/login", apis.UserLogin)
// 加载 HTML 模板文件
router.LoadHTMLGlob("template/user/*")
// 对接口进行token验证
router.GET("/GetUserList", jwt.JWTAuth(), apis.GetUserList)
return router

}
  • 运行服务器端
1
PS E:\proj\gowork\studyGin> go run .\main.go

客户端发起请求

  • 采用python,不带token值,报错
1
2
3
4
5
resp2 = requests.get("http://127.0.0.1:8000/GetUserList")
print(resp2.text)

{"msg":"请求未携带token无权限访问","status":-1}

  • 登录后,读取token,填入token发送请求
1
2
3
4
5
6
7
8
9
10
11
12
13
14
import requests
import json
data ={"name": "test1", "password": "123456"}
resp = requests.post("http://127.0.0.1:8000/login", data=data)
dic = json.loads(resp.text)
token = dic["data"]["Token"]
header = {"token": token}

resp2 = requests.get("http://127.0.0.1:8000/GetUserList", headers=header)
print(resp2.text)

<p>列表数据:</p>
<p>0--test1--123456</p>
<p>1--test11--1234567</p>
  • 直接传送正确的token,获取数据正常
1
2
3
4
5
6
7
8
9
token = "yJhbGciOiJIUzI1Ni......"
header = {"token": token}

resp2 = requests.get("http://127.0.0.1:8000/GetUserList", headers=header)
print(resp2.text)

<p>列表数据:</p>
<p>0--test1--123456</p>
<p>1--test11--1234567</p>

注意

  • 代码来自gin框架实例,其中jwt的验证这里,我用的v5版本,博主用的v4,