Gin 入门

Uncategorized
22k words

:关于Gin部分的内容为笔者年初所写

年初开始踉踉跄跄地学Golang的后端框架Gin了,如果是学过Java后端的人来说应该学Go和Gin很快,但是怎么说呢,笔者的Java和当时学的后端学的依托答辩,所以学GolangGin的时候相当于也在捡起以前的一些知识,加上笔者自认为不是天赋型选手,感觉自己学习的进度也比较慢,争取在后期更加注重自己的学习效率问题

构建路由

RESTful API是目前比较成熟的一套互联网应用程序的API设计理论,设计我们的路由时建议参考RESTful API指南

在RESTful架构中,每个网站代表一种资源,不同的请求方式表示执行不同的操作

方法 作用
GET 从服务器取出资源(一项或多项)
POST 在服务器新建一个资源
PUT 在服务器更新资源(客户端提供改变后完整的资源)
DELETE 从服务器删除资源
package main

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

func main() {
	//创建一个默认的路由引擎
	r := gin.Default()
	// 配置路由
	r.GET("/", func(c *gin.Context) {
		c.String(200, "根目录")
	})
	r.GET("/news", func(c *gin.Context) {
		c.String(200, "新闻页面")
	})
	r.POST("/posturl", func(c *gin.Context) {
		c.String(200, "这是一个POST返回数据")
	})

	// 启动HTTP服务,默认在8080端口
	r.Run()

	// 如果需要热加载的话需要安装fresh
} 

响应JSON请求

c.JSON()

r.GET("/json1", func(c *gin.Context) {
		c.JSON(http.StatusOK, gin.H{
			"success": true,
			"msg":     "This is json1",
		})
	})

	r.GET("/json2", func(c *gin.Context) {
		c.JSON(200, map[string]interface{}{ //此处需要两个参数,一个状态码,一个map类型的空接口,也可以用gin.H,H是map[string]interface{} 的快捷方式
			"success": true,
			"msg":     "This is json2",
		})
	})

gin.H{}

在上述响应JSON请求中我们使用gin.H{}这个类型构造了一个键值对象,gin.H{}是一个map[string]interface{},该类型帮助开发者很方便的构建出一个map对象,不只用于c.JSON方法,也可以用于其他场景

响应JOSNP请求

r.GET("/jsonp", func(c *gin.Context) {
	c.JSONP(200, map[string]interface{}{
		"success": true,
		"msg":     "This is jsonp",
	})
}) 

什么是JSONP

JSONP是一种跨域数据访问技术,它允许在一个域名下使用脚本来获取来自另一个域名下的JSON数据,通常用于第三放服务获取数据

  • 只支持GET

JSONP的基本利用原理是利用script标签的跨域特性,通过动态创建script标签,以URL的形式加载远程的JSON数据,通过执行回调函数的方式将获取到的JSON数据传到客户端

总的来说,JSON是一种数据格式,JSONP是一种数据传输协议。JSONP是一种为解决跨域访问数据而生的一种技术方案

模板 template

模板放在不同目录里面的配置方法

Gin框架如果不同目录下有同名的模板的话我们需要用define协助定义

{{define "admin/index.html"}}

xxx

{{end}}

相当于给模板定义一个名字,define end成对出现

Gin模板基本语法

输出数据

{{.}}

比较函数

比较函数 作用
eq 如果arg1 == arg2,返回true
ne 如果arg1 != arg2,返回true
lt 如果arg1 < arg2,返回true
le 如果arg1 <= arg2,返回true
gt 如果arg1 > arg2,返回true
ge 如果arg1 >= arg2,返回true

条件判断

{{if pipeline}} T1 {{end}}
{{if pipeline}} T1 {{else}} T0 {{end}}
{{if pipeline}} T1 {{else if pipeline}} T0 {{else}} T2 {{end}}

循环

{{range $key,$value := .obj}}
	{{$value}}
{{else}}
	{{pipeline的值其长度为0}}
{{end}}

实例

r.GET("/admin", func(c *gin.Context) {
	c.HTML(http.StatusOK, "admin/login.html", gin.H{
		"title": "这是admin首页",
		"score": 80,
		"hobby": []string{"吃饭", "睡觉", "写代码"}, //创建数组
	})
})
<ul>
    {{range $key,$value := .hobby}}
    <li>{{$key}} : {{$value}}</li>
    {{end}}
</ul>

结构体循环打印

r.GET("/dnews", func(c *gin.Context) {
	news := &Article{
		Title:   "观众看到的新闻标题",
		Content: "观众看到的新闻内容",
	}
	c.HTML(http.StatusOK, "default/news.html", gin.H{
		"title": "这是default新闻页面",
		"news":  news,
		"newslist": []interface{}{ //定义了一个空接口,用于存结构体
			&Article{
				Title:   "观众看到的新闻标题-1",
				Content: "观众看到的新闻内容-1",
			},
			&Article{
				Title:   "观众看到的新闻标题-2",
				Content: "观众看到的新闻内容-2",
			},
		},
	})
})
<ul>
    {{range $key, $value := .newslist}}
    <li>{{$key}} {{$value.Title}} : {{$value.Content}}</li>
    {{end}}
</ul>

with

解构结构体

预定义函数

执行模板时,函数从两个函数字典中查找:

  • 模板函数字典

  • 全局函数字典

一般不在模板内定义函数,而是使用Funcs方法添加函数到模板里

add

函数返回它的第一个空参数或者最后一个参数

and x y <=> if x then y else x

or

返回第一个非空参数或者最后一个参数

or x y <=> if x then x else y

not

返回它单个参数的布尔否定值

len

返回它的参数的类型长度

index

执行结果为 第一个参数以剩下的参数为索引/键 指向的值

自定义模板函数

//提前写好自定义函数
func UnixToTime(timestamp int) string {
	t := time.Unix(int64(timestamp), 0)
	return t.Format("2006-01-02 15:04:05") //注意这是固定的格式
}

func main() {
    r := gin.Default()
	//自定义函数 :放在创建默认路由引擎后,加载模板前
	r.SetFuncMap(template.FuncMap{
		"UnixToTime": UnixToTime,
	})
	//加载模板
	r.LoadHTMLGlob("templates/*/*")
	
	r.GET("/admin", func(c *gin.Context) {
		c.HTML(http.StatusOK, "admin/login.html", gin.H{
			"title":    "这是admin首页",
			"score":    80,
			"hobby":    []string{"吃饭", "睡觉", "写代码"}, //创建数组
			"unixtime": 1676777941, //传入unix时间戳数据
		})
	})

需要注意的点是自定义函数声明的位置 : 放在创建默认路由引擎后,加载模板前

公共模板(嵌套template)

{{define "public/PageHeader.html"}}
<style>
    h1{
        background-color: #000;
        color: #fff;
        text-align: center;
    }
</style>
<h1> Welcome to my world</h1>
{{end}}
{{define "default/index.html"}}
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    {{template "public/PageHeader.html"}} 
    <--!-->此处嵌套了template "public/PageHeader.html"</--!-->
    <h1>This is Default index.html</h1>
    <h2>{{.title}}</h2>
    {{template "public/PageFooter.html"}}
</body>
</html>

{{end}}

index.html

{{define "admin/login.html"}}
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    {{template "public/PageHeader.html"}}
    <h1>This is Admin login.html</h1>
    <h2>{{.title}}</h2>
    <!-- 条件判断 -->
    {{if ge .score 60}}
    及格了捏
    {{else}}
    不及格哦
    {{end}}
    <!-- 循环 -->
    
    <ul>
        {{range $key,$value := .hobby}}
        <li>{{$key}} : {{$value}}</li>
        {{end}}
    </ul>

    <br>
    {{.unixtime}}
    <br>
    {{UnixToTime .unixtime}}
    {{template "public/PageFooter.html"}}
</body>
</html>
{{end}}

login.html

静态web服务

加载文件

配置web静态目录,第一个参数表示路由,第二个参数表示映射的目录

//配置静态路由
r.Static("/xxxx", "./static")
// 实例
r.Static("/static", "./static") //第二个参数表示映射的目录

index.html,我们在index.html中引入这个css

{{define "default/index.html"}}
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <link rel="stylesheet" href="/static/css/base.css">
    <!-- 引入"static/css/base.css" -->
</head>
<body>
    {{template "public/PageHeader.html"}}
    <h1>This is Default index.html</h1>
    <h2>{{.title}}</h2>
    {{template "public/PageFooter.html"}}
</body>
</html>

{{end}}

通过路径/static/css/base.css访问该CSS文件

http://127.0.0.1:8080/static/css/base.css

由于index.html引入了该css文件,我们可以看到index.html上的h1和h2的样式发生了改变

加载图片

将图片放置在目标路径下之后,在html代码中引入图片

{{define "default/index.html"}}
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <link rel="stylesheet" href="/static/css/base.css">
    <!-- 引入"static/css/base.css",注意最好在最前面加上'/',这样是突出目标文件和根目录的关系,以免出错 -->
</head>
<body>
    {{template "public/PageHeader.html"}}
    <h1>This is Default index.html</h1>
    <h2>{{.title}}</h2>
    <!-- 在此处放置图片 -->
    <img src="/static/img/test.png" alt="">
    {{template "public/PageFooter.html"}}
</body>
</html>

{{end}}

Gin获取查询参数

Query

Querystring parameters,查询参数

使用方法

r.GET("/", func(c *gin.Context) {
	username := c.Query("username")
	age := c.Query("age")
	page := c.DefaultQuery("page", "1") //DefaultQuery(key string, defaultValue string) string
	c.JSON(http.StatusOK, gin.H{
		"username": username,
		"age":      age,
		"page":     page,
	})
})

配置了三个接收参数,uesenmae,age,page,请求URL

http://127.0.0.1:8080/?username=AnatomyX&age=21&page=17

返回

{"age":"21","page":"17","username":"AnatomyX"}

获取POST参数

我们先创建一个获取POST数据的路由(们)

  • r.GET(“/user”, func(…)) 负责展示访问/user后的页面

  • r.POST(‘/AddUser’, func(xxx)) 负责在响应了/AddUser事件后,获取POST数据

//POST提交数据,访问/user,填写表单
	r.GET("/user", func(c *gin.Context) {
		//设置访问/user时显示的页面
		c.HTML(200, "default/user.html", gin.H{})
		//func (*gin.Context).HTML(code int, name string, obj any)
		//每次写这种方法的时候就是要注意具体的参数
	})

	//当'/user'页面点击了'提交'后,获取POST的数据
	r.POST("/AddUser", func(c *gin.Context) { //注意是r.POST
		//通过PosTForM获取表单数据
		username := c.PostForm("username") //获取username的数据,赋值给username
		password := c.PostForm("password") //获取password的数据,赋值给password
		//以JSON的格式输出
		c.JSON(http.StatusOK, gin.H{
			"username": username,
			"password": password,
		})
	})

并以JSON的方式输出

{{define "default/user.html"}}
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <!-- 定义一个form表单 -->
    <form action="AddUser" method="post">
        <!-- 绑定username -->
        Username: <input type="text" name="username"> 
        <!-- 绑定password -->
        Password: <input type="text" name="password">

        <input type="submit" value="Submit">
    </form>
</body>
</html>

{{end}}

这样当我们在/user页面填完表单后,会跳转到/AddUser这个页面以JSON的格式输出我们刚刚POST提交的数据

获取GET POST传递的数据绑定到结构体

c.ShouldBind()

c.ShouldBind()可以更好的控制绑定

支持绑定urlencoded formmultipart form

  • 如果是GET请求,只使用Form绑定引擎(query)

  • 如果是POST请求,首先检查content-type是否为JSON或者XML,然后再使用Form绑定到结构体

首先定义一个结构体,并打上标签

//将GET POST传递的数据绑定到结构体 - 创建结构体
type UserInfo struct {
	Username string `json:"username" form:"username"` //切记不要冒号":"后 空一格,这是有格式规定的
	Password string `json:"password" form:"password"`
}

这里一定要注意标签的格式,不要擅自在:后加一个空格(space)

将GET传递的值绑定到结构体

main.go

//获取GET传递的值绑定到结构体
r.GET("/getUser", func(c *gin.Context) {
	//实例化结构体
	user := &UserInfo{}
	if err := c.ShouldBind(&user); err == nil {
		fmt.Printf("%#v", user) //%#v 先输出结构体名字,再输出结构体(字段名字+字段值)
		c.JSON(http.StatusOK, user)
	} else {
		c.JSON(http.StatusOK, gin.H{
			"err": err.Error(),
		})
	}
})

user.html

{{define "default/user.html"}}
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <!-- 定义一个form表单 -->
    <form action="/getUser" method="post">
        <!-- 绑定username -->
        Username: <input type="text" name="username"> 
        <!-- 绑定password -->
        Password: <input type="text" name="password">

        <input type="submit" value="Submit">
    </form>
</body>
</html>

{{end}}

将POST传递的值绑定到结构体

main.go

//获取POST传递的值绑定到结构体
r.POST("/postUser", func(c *gin.Context) {
	//实例化结构体
	user := &UserInfo{}
	if err := c.ShouldBind(&user); err == nil {
		fmt.Printf("%#v", user)
		c.JSON(http.StatusOK, user)
	} else {
		c.JSON(http.StatusOK, gin.H{
			"err": err.Error(),
		})
	}
})
{{define "default/user.html"}}
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <!-- 定义一个form表单 -->
    <form action="/postUser" method="post">
        <!-- 绑定username -->
        Username: <input type="text" name="username"> 
        <!-- 绑定password -->
        Password: <input type="text" name="password">

        <input type="submit" value="Submit">
    </form>
</body>
</html>

{{end}}

获取POST XML的数据

//获取 POST XML数据
	r.POST("/xml", func(c *gin.Context) {
		article := &Article{}
		xmlSliceData, _ := c.GetRawData() //从c.Request.Body读取请求数据,返回[]byte切片
		fmt.Println("xmlSliceData:", xmlSliceData)
		if err := xml.Unmarshal(xmlSliceData, &article); err == nil {
			// xml.Unmarshal接收到的数据xmlSliceData解构,按照article的参数读取,并存值到&article
			c.JSON(http.StatusOK, article)
		} else {
			c.JSON(http.StatusBadRequest, gin.H{
				"err": err.Error(),
			})
		}

	})

当我们向127.0.0.1/xml通过POST的方式传递xml的数据包

配置动态路由

// 动态路由
r.GET("/dynamicRouting/:cid", func(c *gin.Context) {
	cid := c.Param("cid")
	c.String(http.StatusOK, "%v", cid)
})

这里通过/:cid即设置了动态路由,我们通过以下类型访问

127.0.0.1/dynamicRouting/123
127.0.0.1/dynamicRouting/124
127.0.0.1/dynamicRouting/125

会得到类似以下回显

动态路由的适用情景

以下回答来自ChatGPT


动态路由在以下情境中非常有用:

  1. 多租户应用程序:在多租户应用程序中,不同的租户可能需要不同的路由规则,例如使用不同的域名或子域名来访问他们的应用程序。在这种情况下,动态路由可以允许您根据租户身份动态更改路由规则,从而使请求得到正确的处理。

  2. A/B测试:在A/B测试期间,您可能需要将一小部分的流量引导到不同的代码路径上,以测试新功能或改进。动态路由允许您根据用户ID、IP地址或其他标识符将流量路由到不同的代码路径上,以进行测试或实验。

  3. 负载均衡:动态路由还可以用于负载均衡。您可以根据服务器的可用性或负载情况来动态更改路由规则,从而将流量均衡分配到不同的服务器上。

  4. 动态API网关:在API网关中,动态路由可用于根据不同的URL路径、HTTP方法或其他标识符将请求路由到不同的后端服务上。这种动态路由的灵活性使得API网关可以适应快速变化的需求和不断扩展的后端服务。

总之,动态路由可以使您根据需要动态地更改路由规则,从而使您的应用程序更加灵活和可扩展。


路由分组 | 路由文件抽离

main.go

package main

import (
	"RoutingGroupandRouting/routers"

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

func main() {
	//创建一个默认的路由引擎
	r := gin.Default()
	routers.DefaultRoutersInit(r)
	routers.AdminRoutersInit(r)
	routers.ApiRoutersInit(r)
	r.Run()
}

/routers/defaultRouters.go

package routers

import (
	"net/http"

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

func DefaultRoutersInit(r *gin.Engine) {
	defaultRouters := r.Group("/")
	{
		defaultRouters.GET("/", func(c *gin.Context) {
			c.String(http.StatusOK, "this is defaultRouters's home page")
		})
		defaultRouters.GET("/news", func(c *gin.Context) {
			c.String(http.StatusOK, "this is defaultRouters's news page")
		})
		defaultRouters.GET("/article", func(c *gin.Context) {
			c.String(http.StatusOK, "this is defaultRouters's article page")
		})
	}
}

/routers/adminRouters.go

package routers

import (
	"net/http"

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

func AdminRoutersInit(r *gin.Engine) {
	adminRouters := r.Group("/admin")
	{
		adminRouters.GET("/", func(c *gin.Context) {
			c.String(http.StatusOK, "this is admin's home page")
		})
		adminRouters.GET("/users", func(c *gin.Context) {
			c.String(http.StatusOK, "this is admin's users page")
		})
		adminRouters.GET("/article", func(c *gin.Context) {
			c.String(http.StatusOK, "this is admin's article page")
		})
	}
}

/router/apiRouters

package routers

import (
	"net/http"

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

func ApiRoutersInit(r *gin.Engine) {
	apiRouter := r.Group("/api")
	{
		apiRouter.GET("/", func(c *gin.Context) {
			c.String(http.StatusOK, "this is the api's home page")
		})
		apiRouter.GET("/users", func(c *gin.Context) {
			c.String(http.StatusOK, "this is users' api page")
		})
		apiRouter.GET("/admin", func(c *gin.Context) {
			c.String(http.StatusOK, "this is admin's api page")
		})
	}
}

控制器抽离

这里只简单创建了一个控制器抽离的情况

├─controllers
│  ├─admin
│  │      userController.go
│  │
│  ├─api
│  └─frontEnd

我们创建放置控制器的文件夹controllers,下方依次根据控制器的从属关系和作用创建文件夹和控制器go文件,例如这里只抽离了admin中user相关的控制器

RoutingGroupandRouting/controllers/admin/userController.go

package admin

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

func UserShow(c *gin.Context) {
	c.String(200, "this is show user function")
}

func UserAdd(c *gin.Context) {
	c.String(200, "this is add user function")
}

func UserArticle(c *gin.Context) {
	c.String(200, "this is user's article function")
}

adminRouters.go

package routers

import (
	"RoutingGroupandRouting/controllers/admin"
	"net/http"

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

func AdminRoutersInit(r *gin.Engine) {
	adminRouters := r.Group("/admin")
	{
		adminRouters.GET("/", func(c *gin.Context) {
			c.String(http.StatusOK, "this is admin's home page")
		})
		adminRouters.GET("/users", admin.UserShow)
		adminRouters.GET("/article", admin.UserArticle)
		adminRouters.GET("/useradd", admin.UserAdd)
	}
}

路由分组、路由文件抽离、控制器抽离,这些都是为了方便小组开发,毕竟如果项目很大是不可能一个人开发完的。如果全部写在一个文件里面,多人开发起来就是个问题,所以才会有路由文件抽离控制器抽离等,让外部文件里面的方法来处理对应的业务逻辑

控制器继承

userController.go

// 控制器继承
type UserController struct {
}

func (con UserController) Show(c *gin.Context) {
	c.String(200, "this is show user function")
}

adminRouters.go

adminRouters.GET("/users", admin.UserController{}.Show)

用结构体来实现控制器的继承这种方法实则是模拟大型项目的目录规范化

Gin中间件

Gin框架允许开发者在处理请求的过程中,加入自己的hook函数,这个hook函数就叫中间件,中间件适合处理一些公共的业务逻辑,比如登录认证,权限校验,数据分页,记录日志,耗时统计等等

通俗的讲:中间件就是匹配路由前和匹配路由完成后执行的一系列操作

adminRouters.go

package routers

import (
	"RoutingGroupandRouting/controllers/admin"
	"fmt"
	"net/http"

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

func initMiddleware(c *gin.Context) {
	fmt.Println("this is initMiddleware")
} //我们在此处定义了一个中间件函数,放在下文路由请求的前面,这样在请求路由之前就会执行中间件函数

func AdminRoutersInit(r *gin.Engine) {
	adminRouters := r.Group("/admin")
	{
		adminRouters.GET("/", initMiddleware, func(c *gin.Context) {
			c.String(http.StatusOK, "this is admin's home page")
		})
		adminRouters.GET("/users", initMiddleware, admin.UserController{}.Show)
		adminRouters.GET("/article", initMiddleware, admin.UserArticle)
		adminRouters.GET("/useradd", initMiddleware, admin.UserAdd)
		adminRouters.GET("/error", initMiddleware, admin.UserController{}.Error)
	}
}

文件上传

main.go

// 本代码展示了Gin中的文件上传
package main

import (
	"log"
	"net/http"
	"path"

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

func main() {
	r := gin.Default()
	// 为multipart forms设置较低的内存限制(默认32MiB)
	r.MaxMultipartMemory = 8 << 20

	//切记要加载模板
	r.LoadHTMLGlob("template/**/*")

	r.GET("/", func(c *gin.Context) {
		c.HTML(http.StatusOK, "web/index.html", gin.H{})
	})

	r.GET("/upload", func(c *gin.Context) {
		c.HTML(http.StatusOK, "web/upload.html", gin.H{})
	})

	r.POST("/uploadFile", func(c *gin.Context) {
		//通过PostForm获取传过来的username
		username := c.PostForm("username")
		//通过FormFile获取传过来的file文件
		file, _ := c.FormFile("avatar") //返回两个参数,*multipart.FileHeader, error
		log.Println(file.Filename)

		dst := path.Join("./static/uploadfile", file.Filename) //设置保存路径
		c.SaveUploadedFile(file, dst)
		// c.String(http.StatusOK, fmt.Sprintf("'%s' uploaded!", file.Filename))
		c.JSON(http.StatusOK, gin.H{
			"success":  true,
			"username": username,
			"dst":      dst,
		})

		//可以在写完整的业务代码之前看看是否页面会跳转到此处,经过检验是没有问题的
		// c.String(200, "Upload successfully")
	})

	r.Run()

}

upload.html

{{define "web/upload.html"}}

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <h1>this is an upload page !</h1>
    <!-- action: 提交后执行的函数 -->
    <form action="/uploadFile" method="post" enctype="multipart/form-data">
     <!--注意这里的enctype参数-->
        <br>
        用户名:<input type="text" name="username" placeholder="用户名">
        <!-- placeholder: 可描述输入字段预期值的简短的提示信息,该提示在用户输入值之前显示在输入字段中 -->
        <br>
        头像:<input type="file" name="avatar">
        <br>
        <input type="submit" value="Submit">
    </form>
</body>
</html>

{{end}}

踩坑:一定要注意在敲前端文件(html)的时候给一些数据命名成了什么,比如上文upload.html这个文件中,笔者给头像文件命名为了avatar,这样在传递数据的时候,头像这个文件其实应该用avatar来接收,但是笔者一直用的file,导致一直报错,可能是笔者前端学的实在是依托答辩的原因

html中的enctype

为什么说在文件上传中enctype这个参数很重要,因为html表单如何打包数据文件是取决于enctype

  • application/x-www-form-urlencoded :在发送前编码所有字符(默认)(空格被编码为‘+’,特殊字符被编码为ASCII十六进制字符)

  • multipart/form-data:不对字符编码,在使用包含文件上传控件的表单时必须使用

  • text/plain:空格替换为‘+’号,但不对特殊字符编码

所以如果涉及到上传文件的话,需要在定义表单时加个参数enctype=multipart/form-data

根据时间存储文件

只需要在上述的main.go代码中的存储文件名部分加上计算当前时间的步骤即可

main.go - /uploadFile POST

r.POST("/uploadFile", func(c *gin.Context) {
		//通过PostForm获取传过来的username
		username := c.PostForm("username")
		//通过FormFile获取传过来的file文件
		file, _ := c.FormFile("avatar") //返回两个参数,*multipart.FileHeader, error
		log.Println(file.Filename)
		folderName := fmt.Sprintf("%d-%d-%d", time.Now().Year(), time.Now().Month(), time.Now().Day())
		dst := path.Join("./static/uploadfile", folderName, file.Filename) //设置保存路径
		c.SaveUploadedFile(file, dst)
		// c.String(http.StatusOK, fmt.Sprintf("'%s' uploaded!", file.Filename))
		c.JSON(http.StatusOK, gin.H{
			"success":  true,
			"username": username,
			"dst":      dst,
		})

		//可以在写完整的业务代码之前看看是否页面会跳转到此处,经过检验是没有问题的
		// c.String(200, "Upload successfully")
	})

这样就可以看到接收到了上传的文件,并且存储在以当天日期命名的文件夹中

Cookie

Http是无状态协议,即假如多次访问同一个网站,每一次之间的访问都是没有关系的。

如果我们要实现多个页面之间共享数据的话我们可以使用Cookie或者Session实现

Cookie存储于访问者计算机的浏览器中,可以让我们用同一个浏览器访问同一个域名的时候共享数据

Cookie实现的功能

  • 保持用户登录登录状态

  • 保持用户浏览历史记录

  • 猜你喜欢,智能推荐

  • 电商网站加入购物车

设置Cookie

c.SetCookie("username", "张三", 3600, "/", "localhost", false, true)

获取Cookie

c.Cookie("username")
//如
username := c.Cookie("username")
c.String(200, "cookie="+username)

多个二级域名共享Cookie

c.SetCookie("username", "张三", 3600, "/", ".xxxxx.com", false, true)

通过设置作用域为.xxxxx这样即可在a.xxxxxb.xxxxx共享Cookie

Session

由于http协议无状态,当多个人在请求服务器的时候,服务器并不能分辨谁是谁(除非携带了一些识别字段,不过这属于业务层内容,不在讨论范围,这里单说http协议)

为了让服务器知道请求是谁发出的,所以有了session

另外sessioncookie是不分家的,我们每次说到session,其实默认就是要使用cookie

第一次登陆,服务器给客户端发送一个唯一的sessionid,并通过http的响应头返回。

客户端(浏览器)发现返回的数据中有cookie数据就把这个cookie数据存放到内存。下次再发送http请求,就把内存中的cookie数据塞回http请求头中,一并发给服务器。

服务器在解析请求时,发现请求头中有cookie,就开始识别cookie中的sessionid,拿到sessionid,我们就知道这个请求是由哪个客户端发送过来的了

Gin中需要使用Session中间件来实现Session的功能

需要引入github.com/gin-contrib/sessions这个包

main.go

import (
    "github.com/gin-contrib/sessions"
	"github.com/gin-contrib/sessions/cookie"
)
	
	//创建基于Cookie的存储引擎,"secret"参数是用于加密的密钥
	store := cookie.NewStore([]byte("secret"))
	//配置Session中间件
	r.Use(sessions.Sessions("mySession", store))

...



	r.GET("/sessionSet", func(c *gin.Context) {
		session := sessions.Default(c)
		session.Set("username", "张三")
		session.Save() //一定要保存
	})

	r.GET("/sessionShow", func(c *gin.Context) {
		session := sessions.Default(c)
		username := session.Get("username")
		c.String(http.StatusOK, "username = %v", username)
	})

session的默认过期时间是30天,可以通过session.Options(sessions.Options{ MaxAge: 3600 * 6, //6hrs })设置Session过期时间,MaxAge单位是s

session会保存在服务器

但是仅仅这样,当我们在使用分布式(负载均衡)的时候,其他的服务器是无法创建和获取Session的,所以我们需要将Session保存在redis或者mongodb或者mysql中

关于为什么调用session.Delete()或者session.Clear()后session仍在

这是因为调用了Session的方法如Set,Delete,Clear后,必须调用一次Save方法,否则Session数据不会更新,笔者又躺在这个坑里面好久

保存到redis数据库实现分布式Session

将上文中创建基于Cookie的存储引擎的代码修改为如下即可

//创建基于Redis的存储引擎
store, _ := redis.NewStore(10, "tcp", "localhost:6379", "", []byte("secret")) //

10表示最大连接数,tcp表示连接方法,localhost:6379表示服务端口,""表示密码,此处密码为空,[]byte(“secret”)表示用于加密的密钥

可以看到之前redis里面为空,我们访问了127.0.0.1/setSession后,redis里存入了一条数据,即为session

使用sessions中间件注意要点

  • session仓库其实就是一个map[interface]interface对象,所有session可以存储任意数据

  • session使用的编码解码器是自带的gob,所以存储类似:struct,map这些对象时需要先注册对象,否则会报错gob"type not registered for...

    • session存储引擎支持cookie,内存,mongodb,redis,postgres,memsotre,memcached以及gorm支持的各类数据库mysql,sqlite
    • session在创建时有一个配置项,可以配置session过期时间,cookie,domain,secure,path等参数