Gin
是一个用Go语言编写的web框架。它是一个类似于martini
但拥有更好性能的API框架, 由于使用了httprouter
,速度提高了近40倍。 如果你是性能和高效的追求者, 你会爱上Gin
。
Gin框架介绍
Go世界里最流行的Web框架,Github上有32K+
star。 基于httprouter开发的Web框架。 中文文档齐全,简单易用的轻量级框架。
Gin框架安装与使用
安装
下载并安装Gin
:
go get -u github.com/gin-gonic/gin
第一个Gin示例:
package main
import (
"github.com/gin-gonic/gin"
)
func main() {
// 创建一个默认的路由引擎
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()
}
将上面的代码保存并编译执行,然后使用浏览器打开127.0.0.1:8080/hello
就能看到一串JSON字符串。
RESTful API
REST与技术无关,代表的是一种软件架构风格,REST是Representational State Transfer的简称,中文翻译为“表征状态转移”或“表现层状态转化”。
推荐阅读阮一峰 理解RESTful架构
简单来说,REST的含义就是客户端与Web服务器之间进行交互的时候,使用HTTP协议中的4个请求方法代表不同的动作。
-
GET
用来获取资源 -
POST
用来新建资源 -
PUT
用来更新资源 -
DELETE
用来删除资源。
只要API程序遵循了REST风格,那就可以称其为RESTful API。目前在前后端分离的架构中,前后端基本都是通过RESTful API来进行交互。
例如,我们现在要编写一个管理书籍的系统,我们可以查询对一本书进行查询、创建、更新和删除等操作,我们在编写程序的时候就要设计客户端浏览器与我们Web服务端交互的方式和路径。按照经验我们通常会设计成如下模式:
请求方法 | URL | 含义 |
---|---|---|
GET | /book | 查询书籍信息 |
POST | /create_book | 创建书籍记录 |
POST | /update_book | 更新书籍信息 |
POST | /delete_book | 删除书籍信息 |
同样的需求我们按照RESTful API设计如下:
请求方法 | URL | 含义 |
---|---|---|
GET | /book | 查询书籍信息 |
POST | /book | 创建书籍记录 |
PUT | /book | 更新书籍信息 |
DELETE | /book | 删除书籍信息 |
Gin框架支持开发RESTful API的开发。
func main() {
r := gin.Default()
r.GET("/book", func(c *gin.Context) {
c.JSON(200, gin.H{
"message": "GET",
})
})
r.POST("/book", func(c *gin.Context) {
c.JSON(200, gin.H{
"message": "POST",
})
})
r.PUT("/book", func(c *gin.Context) {
c.JSON(200, gin.H{
"message": "PUT",
})
})
r.DELETE("/book", func(c *gin.Context) {
c.JSON(200, gin.H{
"message": "DELETE",
})
})
}
开发RESTful API的时候我们通常使用Postman来作为客户端的测试工具。
Gin渲染
HTML渲染
我们首先定义一个存放模板文件的templates
文件夹,然后在其内部按照业务分别定义一个posts
文件夹和一个users
文件夹。 posts/index.html
文件的内容如下:
{{define "posts/index.html"}}
posts/index
{{.title}}
{{end}}
users/index.html
文件的内容如下:
{{define "users/index.html"}}
users/index
{{.title}}
{{end}}
Gin框架中使用LoadHTMLGlob()
或者LoadHTMLFiles()
方法进行HTML模板渲染。
func main() {
r := gin.Default()
r.LoadHTMLGlob("templates/**/*")
//r.LoadHTMLFiles("templates/posts/index.html", "templates/users/index.html")
r.GET("/posts/index", func(c *gin.Context) {
c.HTML(http.StatusOK, "posts/index.html", gin.H{
"title": "posts/index",
})
})
r.GET("users/index", func(c *gin.Context) {
c.HTML(http.StatusOK, "users/index.html", gin.H{
"title": "users/index",
})
})
r.Run(":8080")
}
自定义模板函数
定义一个不转义相应内容的safe
模板函数如下:
func main() {
router := gin.Default()
router.SetFuncMap(template.FuncMap{
"safe": func(str string) template.HTML{
return template.HTML(str)
},
})
router.LoadHTMLFiles("./index.tmpl")
router.GET("/index", func(c *gin.Context) {
c.HTML(http.StatusOK, "index.tmpl", "李文周的博客")
})
router.Run(":8080")
}
在index.tmpl
中使用定义好的safe
模板函数:
修改模板引擎的标识符
{{ . | safe }}
静态文件处理
当我们渲染的HTML文件中引用了静态文件时,我们只需要按照以下方式在渲染页面前调用gin.Static
方法即可。
func main() {
r := gin.Default()
r.Static("/static", "./static")
r.LoadHTMLGlob("templates/**/*")
// ...
r.Run(":8080")
}
使用模板继承
Gin框架默认都是使用单模板,如果需要使用block template
功能,可以通过"github.com/gin-contrib/multitemplate"
库实现,具体示例如下:
首先,假设我们项目目录下的templates文件夹下有以下模板文件,其中home.tmpl
和index.tmpl
继承了base.tmpl
:
templates
├── includes
│ ├── home.tmpl
│ └── index.tmpl
├── layouts
│ └── base.tmpl
└── scripts.tmpl
然后我们定义一个loadTemplates
函数如下:
func loadTemplates(templatesDir string) multitemplate.Renderer {
r := multitemplate.NewRenderer()
layouts, err := filepath.Glob(templatesDir + "/layouts/*.tmpl")
if err != nil {
panic(err.Error())
}
includes, err := filepath.Glob(templatesDir + "/includes/*.tmpl")
if err != nil {
panic(err.Error())
}
// 为layouts/和includes/目录生成 templates map
for _, include := range includes {
layoutCopy := make([]string, len(layouts))
copy(layoutCopy, layouts)
files := append(layoutCopy, include)
r.AddFromFiles(filepath.Base(include), files...)
}
return r
}
我们在main
函数中
func indexFunc(c *gin.Context){
c.HTML(http.StatusOK, "index.tmpl", nil)
}
func homeFunc(c *gin.Context){
c.HTML(http.StatusOK, "home.tmpl", nil)
}
func main(){
r := gin.Default()
r.HTMLRender = loadTemplates("./templates")
r.GET("/index", indexFunc)
r.GET("/home", homeFunc)
r.Run()
}
补充文件路径处理
关于模板文件和静态文件的路径,我们需要根据公司/项目的要求进行设置。可以使用下面的函数获取当前执行程序的路径。
func getCurrentPath() string {
if ex, err := os.Executable(); err == nil {
return filepath.Dir(ex)
}
return "./"
}
JSON渲染
func main() {
r := gin.Default()
// gin.H 是map[string]interface{}的缩写
r.GET("/someJSON", func(c *gin.Context) {
// 方式一:自己拼接JSON
c.JSON(http.StatusOK, gin.H{"message": "Hello world!"})
})
r.GET("/moreJSON", func(c *gin.Context) {
// 方法二:使用结构体
var msg struct {
Name string `json:"user"`
Message string
Age int
}
msg.Name = "小王子"
msg.Message = "Hello world!"
msg.Age = 18
c.JSON(http.StatusOK, msg)
})
r.Run(":8080")
}
XML渲染
注意需要使用具名的结构体类型。
func main() {
r := gin.Default()
// gin.H 是map[string]interface{}的缩写
r.GET("/someXML", func(c *gin.Context) {
// 方式一:自己拼接JSON
c.XML(http.StatusOK, gin.H{"message": "Hello world!"})
})
r.GET("/moreXML", func(c *gin.Context) {
// 方法二:使用结构体
type MessageRecord struct {
Name string
Message string
Age int
}
var msg MessageRecord
msg.Name = "小王子"
msg.Message = "Hello world!"
msg.Age = 18
c.XML(http.StatusOK, msg)
})
r.Run(":8080")
}
YMAL渲染
r.GET("/someYAML", func(c *gin.Context) {
c.YAML(http.StatusOK, gin.H{"message": "ok", "status": http.StatusOK})
})
protobuf渲染
r.GET("/someProtoBuf", func(c *gin.Context) {
reps := []int64{int64(1), int64(2)}
label := "test"
// protobuf 的具体定义写在 testdata/protoexample 文件中。
data := &protoexample.Test{
Label: &label,
Reps: reps,
}
// 请注意,数据在响应中变为二进制数据
// 将输出被 protoexample.Test protobuf 序列化了的数据
c.ProtoBuf(http.StatusOK, data)
})
获取参数
获取querystring参数
querystring
指的是URL中?
后面携带的参数,例如:/user/search?username=小王子&address=沙河
。 获取请求的querystring参数的方法如下:
func main() {
//Default返回一个默认的路由引擎
r := gin.Default()
r.GET("/user/search", func(c *gin.Context) {
username := c.DefaultQuery("username", "小王子")
//username := c.Query("username")
address := c.Query("address")
//输出json结果给调用方
c.JSON(http.StatusOK, gin.H{
"message": "ok",
"username": username,
"address": address,
})
})
r.Run()
}
获取form参数
请求的数据通过form表单来提交,例如向/user/search
发送一个POST请求,获取请求数据的方式如下:
func main() {
//Default返回一个默认的路由引擎
r := gin.Default()
r.POST("/user/search", func(c *gin.Context) {
// DefaultPostForm取不到值时会返回指定的默认值
//username := c.DefaultPostForm("username", "小王子")
username := c.PostForm("username")
address := c.PostForm("address")
//输出json结果给调用方
c.JSON(http.StatusOK, gin.H{
"message": "ok",
"username": username,
"address": address,
})
})
r.Run(":8080")
}
获取path参数
请求的参数通过URL路径传递,例如:/user/search/小王子/沙河
。 获取请求URL路径中的参数的方式如下。
func main() {
//Default返回一个默认的路由引擎
r := gin.Default()
r.GET("/user/search/:username/:address", func(c *gin.Context) {
username := c.Param("username")
address := c.Param("address")
//输出json结果给调用方
c.JSON(http.StatusOK, gin.H{
"message": "ok",
"username": username,
"address": address,
})
})
r.Run(":8080")
}
参数绑定
为了能够更方便的获取请求相关参数,提高开发效率,我们可以基于请求的Content-Type
识别请求数据类型并利用反射机制自动提取请求中QueryString
、form表单
、JSON
、XML
等参数到结构体中。 下面的示例代码演示了.ShouldBind()
强大的功能,它能够基于请求自动提取JSON
、form表单
和QueryString
类型的数据,并把值绑定到指定的结构体对象。
// Binding from JSON
type Login struct {
User string `form:"user" json:"user" binding:"required"`
Password string `form:"password" json:"password" binding:"required"`
}
func main() {
router := gin.Default()
// 绑定JSON的示例 ({"user": "q1mi", "password": "123456"})
router.POST("/loginJSON", func(c *gin.Context) {
var login Login
if err := c.ShouldBind(&login); err == nil {
fmt.Printf("login info:%#v\n", login)
c.JSON(http.StatusOK, gin.H{
"user": login.User,
"password": login.Password,
})
} else {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
}
})
// 绑定form表单示例 (user=q1mi&password=123456)
router.POST("/loginForm", func(c *gin.Context) {
var login Login
// ShouldBind()会根据请求的Content-Type自行选择绑定器
if err := c.ShouldBind(&login); err == nil {
c.JSON(http.StatusOK, gin.H{
"user": login.User,
"password": login.Password,
})
} else {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
}
})
// 绑定QueryString示例 (/loginQuery?user=q1mi&password=123456)
router.GET("/loginForm", func(c *gin.Context) {
var login Login
// ShouldBind()会根据请求的Content-Type自行选择绑定器
if err := c.ShouldBind(&login); err == nil {
c.JSON(http.StatusOK, gin.H{
"user": login.User,
"password": login.Password,
})
} else {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
}
})
// Listen and serve on 0.0.0.0:8080
router.Run(":8080")
}
ShouldBind
会按照下面的顺序解析请求中的数据完成绑定:
- 如果是
GET
请求,只使用Form
绑定引擎(query
)。 - 如果是
POST
请求,首先检查content-type
是否为JSON
或XML
,然后再使用Form
(form-data
)。
文件上传
单个文件上传
文件上传前端页面代码:
上传文件示例
后端gin框架部分代码:
func main() {
router := gin.Default()
// 处理multipart forms提交文件时默认的内存限制是32 MiB
// 可以通过下面的方式修改
// router.MaxMultipartMemory = 8 << 20 // 8 MiB
router.POST("/upload", func(c *gin.Context) {
// 单个文件
file, err := c.FormFile("f1")
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"message": err.Error(),
})
return
}
log.Println(file.Filename)
dst := fmt.Sprintf("C:/tmp/%s", file.Filename)
// 上传文件到指定的目录
c.SaveUploadedFile(file, dst)
c.JSON(http.StatusOK, gin.H{
"message": fmt.Sprintf("'%s' uploaded!", file.Filename),
})
})
router.Run()
}
多个文件上传
func main() {
router := gin.Default()
// 处理multipart forms提交文件时默认的内存限制是32 MiB
// 可以通过下面的方式修改
// router.MaxMultipartMemory = 8 << 20 // 8 MiB
router.POST("/upload", func(c *gin.Context) {
// Multipart form
form, _ := c.MultipartForm()
files := form.File["file"]
for index, file := range files {
log.Println(file.Filename)
dst := fmt.Sprintf("C:/tmp/%s_%d", file.Filename, index)
// 上传文件到指定的目录
c.SaveUploadedFile(file, dst)
}
c.JSON(http.StatusOK, gin.H{
"message": fmt.Sprintf("%d files uploaded!", len(files)),
})
})
router.Run()
}
重定向
HTTP重定向
HTTP 重定向很容易。 内部、外部重定向均支持。
r.GET("/test", func(c *gin.Context) {
c.Redirect(http.StatusMovedPermanently, "http://www.sogo.com/")
})
路由重定向
路由重定向,使用HandleContext
:
r.GET("/test", func(c *gin.Context) {
// 指定重定向的URL
c.Request.URL.Path = "/test2"
r.HandleContext(c)
})
r.GET("/test2", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"hello": "world"})
})
Gin路由
普通路由
r.GET("/index", func(c *gin.Context) {...})
r.GET("/login", func(c *gin.Context) {...})
r.POST("/login", func(c *gin.Context) {...})
此外,还有一个可以匹配所有请求方法的Any
方法如下:
r.Any("/test", func(c *gin.Context) {...})
为没有配置处理函数的路由添加处理程序,默认情况下它返回404代码,下面的代码为没有匹配到路由的请求都返回views/404.html
页面。
r.NoRoute(func(c *gin.Context) {
c.HTML(http.StatusNotFound, "views/404.html", nil)
})
路由组
我们可以将拥有共同URL前缀的路由划分为一个路由组。习惯性一对{}
包裹同组的路由,这只是为了看着清晰,你用不用{}
包裹功能上没什么区别。
func main() {
r := gin.Default()
userGroup := r.Group("/user")
{
userGroup.GET("/index", func(c *gin.Context) {...})
userGroup.GET("/login", func(c *gin.Context) {...})
userGroup.POST("/login", func(c *gin.Context) {...})
}
shopGroup := r.Group("/shop")
{
shopGroup.GET("/index", func(c *gin.Context) {...})
shopGroup.GET("/cart", func(c *gin.Context) {...})
shopGroup.POST("/checkout", func(c *gin.Context) {...})
}
r.Run()
}
路由组也是支持嵌套的,例如:
shopGroup := r.Group("/shop")
{
shopGroup.GET("/index", func(c *gin.Context) {...})
shopGroup.GET("/cart", func(c *gin.Context) {...})
shopGroup.POST("/checkout", func(c *gin.Context) {...})
// 嵌套路由组
xx := shopGroup.Group("xx")
xx.GET("/oo", func(c *gin.Context) {...})
}
通常我们将路由分组用在划分业务逻辑或划分API版本时。
路由原理
Gin框架中的路由使用的是httprouter这个库。
其基本原理就是构造一个路由地址的前缀树。
Gin中间件
Gin框架允许开发者在处理请求的过程中,加入用户自己的钩子(Hook)函数。这个钩子函数就叫中间件,中间件适合处理一些公共的业务逻辑,比如登录认证、权限校验、数据分页、记录日志、耗时统计等。
定义中间件
Gin中的中间件必须是一个gin.HandlerFunc
类型。例如我们像下面的代码一样定义一个统计请求耗时的中间件。
// StatCost 是一个统计耗时请求耗时的中间件
func StatCost() gin.HandlerFunc {
return func(c *gin.Context) {
start := time.Now()
c.Set("name", "小王子") // 可以通过c.Set在请求上下文中设置值,后续的处理函数能够取到该值
// 调用该请求的剩余处理程序
c.Next()
// 不调用该请求的剩余处理程序
// c.Abort()
// 计算耗时
cost := time.Since(start)
log.Println(cost)
}
}
注册中间件
在gin框架中,我们可以为每个路由添加任意数量的中间件。
为全局路由注册
func main() {
// 新建一个没有任何默认中间件的路由
r := gin.New()
// 注册一个全局中间件
r.Use(StatCost())
r.GET("/test", func(c *gin.Context) {
name := c.MustGet("name").(string) // 从上下文取值
log.Println(name)
c.JSON(http.StatusOK, gin.H{
"message": "Hello world!",
})
})
r.Run()
}
为某个路由单独注册
// 给/test2路由单独注册中间件(可注册多个)
r.GET("/test2", StatCost(), func(c *gin.Context) {
name := c.MustGet("name").(string) // 从上下文取值
log.Println(name)
c.JSON(http.StatusOK, gin.H{
"message": "Hello world!",
})
})
为路由组注册中间件
为路由组注册中间件有以下两种写法。
写法1:
shopGroup := r.Group("/shop", StatCost())
{
shopGroup.GET("/index", func(c *gin.Context) {...})
...
}
写法2:
shopGroup := r.Group("/shop")
shopGroup.Use(StatCost())
{
shopGroup.GET("/index", func(c *gin.Context) {...})
...
}
中间件注意事项
gin默认中间件
gin.Default()
默认使用了Logger
和Recovery
中间件,其中:
-
Logger
中间件将日志写入gin.DefaultWriter
,即使配置了GIN_MODE=release
。 -
Recovery
中间件会recover任何panic
。如果有panic的话,会写入500响应码。
如果不想使用上面两个默认的中间件,可以使用gin.New()
新建一个没有任何默认中间件的路由。
gin中间件中使用goroutine
当在中间件或handler
中启动新的goroutine
时,不能使用原始的上下文(c *gin.Context),必须使用其只读副本(c.Copy()
)。
运行多个服务
我们可以在多个端口启动服务,例如:
package main
import (
"log"
"net/http"
"time"
"github.com/gin-gonic/gin"
"golang.org/x/sync/errgroup"
)
var (
g errgroup.Group
)
func router01() http.Handler {
e := gin.New()
e.Use(gin.Recovery())
e.GET("/", func(c *gin.Context) {
c.JSON(
http.StatusOK,
gin.H{
"code": http.StatusOK,
"error": "Welcome server 01",
},
)
})
return e
}
func router02() http.Handler {
e := gin.New()
e.Use(gin.Recovery())
e.GET("/", func(c *gin.Context) {
c.JSON(
http.StatusOK,
gin.H{
"code": http.StatusOK,
"error": "Welcome server 02",
},
)
})
return e
}
func main() {
server01 := &http.Server{
Addr: ":8080",
Handler: router01(),
ReadTimeout: 5 * time.Second,
WriteTimeout: 10 * time.Second,
}
server02 := &http.Server{
Addr: ":8081",
Handler: router02(),
ReadTimeout: 5 * time.Second,
WriteTimeout: 10 * time.Second,
}
// 借助errgroup.Group或者自行开启两个goroutine分别启动两个服务
g.Go(func() error {
return server01.ListenAndServe()
})
g.Go(func() error {
return server02.ListenAndServe()
})
if err := g.Wait(); err != nil {
log.Fatal(err)
}
}
gin路由
- 基本路由
gin采用的路由库基于httprouter做的。
地址:https://github.com/julienschmidt/httprouter
-
Restful风格的API
支持Restful风格的API
-
是一种互联网应用程序的API设计理念,URL定位资源,用HTTP描述操作。
获取文章 /blog/getXxx Get blog/Xxx
添加 /blog/addXxx POST /blog/Xxx
修改 /blog/updateXxx PUT /blog/Xxx
添加 /blog/delXxx DELETE /blog/Xxx
package main
import (
"github.com/gin-gonic/gin"
"net/http"
)
//gin的helloworld
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
r.Run(":8000")
}
- API参数
- 可以通过Context的Param方法来获取API参数
package main
import (
"github.com/gin-gonic/gin"
"net/http"
)
//gin的helloworld
func main(){
//1、创建路由
r := gin.Default()
//2、绑定路由规则,执行函数
//gin.Context,封装了request和response
r.GET("/user/:name/*action",func(c *gin.Context){
name := c.Param("name")
action := c.Param("action")
c.String(http.StatusOK,name+" is "+action)
})
r.POST("/xxxPost",getting)
r.PUT("/xxxPut")
//3、监听端口,默认端口8080
r.Run(":8000")
}
func getting(c *gin.Context){
}
- URL参数
- URL 参数可以通过DefaultQuery()或Query()方法获取。
- DefaultQuery()若参数不对,则返回默认值,Query()若不存在,返回空串。
package main
import (
"fmt"
"github.com/gin-gonic/gin"
"net/http"
)
//gin的helloworld
func main(){
//1、创建路由
r := gin.Default()
//2、绑定路由规则,执行函数
//gin.Context,封装了request和response
r.GET("/welcom",func(c *gin.Context){
//第二个参数默认值
name := c.DefaultQuery("name","jack")
c.String(http.StatusOK,fmt.Sprintf("hello %s",name))
})
//3、监听端口,默认端口8080
r.Run(":8000")
}
- 表单参数
- 表单传输为post请求,http常见的传输格式4种:
- application/json
- application/x-www-form-urlencoded
- application/xml
- multipart/form-data
- 表单参数可以通过PostForm()方法获取,该方法默认解析的是x-www-form-urlencoded或from-data格式的参数
后台:
- 表单传输为post请求,http常见的传输格式4种:
package main
import (
"fmt"
"github.com/gin-gonic/gin"
"net/http"
)
//gin的helloworld
func main(){
//1、创建路由
r := gin.Default()
//2、绑定路由规则,执行函数
//gin.Context,封装了request和response
r.POST("/form",func(c *gin.Context){
//表单参数设置默认值
type1 := c.DefaultPostForm("type","alert")
//接收其他的
username := c.PostForm("username")
password := c.PostForm("password")
//多选框
hobbys:= c.PostFormArray("hobby")
c.String(http.StatusOK,fmt.Sprintf("type is %s,username is %s,password is %s,hobby is %v",type1,username,password,hobbys))
//第二个参数默认值
name := c.DefaultQuery("name","jack")
c.String(http.StatusOK,fmt.Sprintf("hello %s",name))
})
//3、监听端口,默认端口8080
r.Run(":8000")
}
页面:
登录
- 上传单个文件
- multipart/form-data格式用于文件上传
- gin文件上传与原生的net/http方法类似,不同在于gin把原生的request封装到c.Request中
package main
import (
"fmt"
"github.com/gin-gonic/gin"
)
//gin的helloworld
func main(){
//1、创建路由
r := gin.Default()
//2、绑定路由规则,执行函数
//gin.Context,封装了request和response
r.POST("/upload",func(c *gin.Context){
//表单取文件
file,_ := c.FormFile("file")
fmt.Println(file.Filename)
//传到项目根目录,名字就用本身的
c.SaveUploadedFile(file,file.Filename)
//打印信息
c.String(200,fmt.Sprintf("'%s' upload",file.Filename))
})
//3、监听端口,默认端口8080
r.Run(":8000")
}
登录
- 上传多个文件
改:
package main
import (
"fmt"
"github.com/gin-gonic/gin"
"net/http"
)
//gin的helloworld
func main(){
//1、创建路由
r := gin.Default()
//2、绑定路由规则,执行函数
//gin.Context,封装了request和response
//限制表单上传文件大小 8M,默认32M
r.MaxMultipartMemory = 8 << 20
r.POST("/upload",func(c *gin.Context){
form,err := c.MultipartForm()
if err != nil{
c.String(http.StatusBadRequest,fmt.Sprintf("get err %s",err.Error()))
}
//获取所有图片
files := form.File["files"]
//遍历所有图片
for _,file := range files{
//逐个存
err := c.SaveUploadedFile(file,file.Filename);
if err != nil {
c.String(http.StatusBadRequest,fmt.Sprintf("get err %s",err.Error()))
return
}
}
c.String(200,fmt.Sprintf("upload ok %d files",len(files)))
})
//3、监听端口,默认端口8080
r.Run(":8000")
}
- routes group
- routes group是为了管理相同的uml
package main
import (
"fmt"
"github.com/gin-gonic/gin"
)
//gin的helloworld
func main(){
//1、创建路由
//默认使用2个中间件Logger(),Recovery()
r := gin.Default()
//路由组1,处理GET请求
v1 := r.Group("/v1")
//{}书写规范
{
v1.GET("/login",login)
v1.GET("/submit",submit)
}
v2 := r.Group("/v2")
{
v2.POST("/login",login)
v2.POST("/submit",submit)
}
//3、监听端口,默认端口8080
r.Run(":8000")
}
func login(c *gin.Context){
name := c.DefaultQuery("name","jack")
c.String(200,fmt.Sprintf("hello %s\n",name))
}
func submit(c *gin.Context){
name := c.DefaultQuery("name","lily")
c.String(200,fmt.Sprintf("hello %s\n",name))
}
- 路由原理
- httproter会将所有路由规则构造一棵前缀树。
package main
import (
"github.com/gin-gonic/gin"
)
//gin的helloworld
func main(){
//1、创建路由
//默认使用2个中间件Logger(),Recovery()
r := gin.Default()
r.POST("/",xxx)
r.POST("search",xxx)
r.POST("support",xxx)
r.POST("/blog/:post",xxx)
r.POST("/contact",xxx)
r.POST("/about",xxx)
//3、监听端口,默认端口8080
r.Run(":8000")
}
gin数据解析和绑定
package main
import (
"github.com/gin-gonic/gin"
"net/http"
)
//定义接收数据的结构体
type Login struct {
//binding:”required"修饰的字段,若接收为空值,则报错,是必须字段
User string `form:"username" json:"user" uri:"user" xml:"user" binding:"required"`
Password string `form:"password" json:"password" uri:"password" xml:"password" binding:"required"`
}
func main(){
//1、创建路由
//默认使用2个中间件Logger(),Recovery()
r := gin.Default()
//r.POST("/",xxx)
//r.POST("search",xxx)
//r.POST("support",xxx)
//r.POST("/blog/:post",xxx)
//r.POST("/contact",xxx)
//r.POST("/about",xxx)
//json绑定
r.POST("loginJSON",func(c *gin.Context){
//声明接收的变量
var json Login
//将request的body中的数据,自动按照JSON格式解析到结构体
if err := c.ShouldBindJSON(&json); err != nil{
//返回错误信息
//gin.H封装了生成JSON数据的工具
c.JSON(http.StatusBadRequest,gin.H{"error":err.Error()})
return
}
//判断用户名密码是否正确
if json.User != "root" || json.Password != "admin"{
c.JSON(http.StatusBadRequest,gin.H{"status":"304"})
return
}
c.JSON(http.StatusOK,gin.H{"status":"200"})
})
//3、监听端口,默认端口8080
r.Run(":8000")
}
表单数据解析和绑定
登录
package main
import (
"github.com/gin-gonic/gin"
"net/http"
)
//定义接收数据的结构体
type Login struct {
//binding:”required"修饰的字段,若接收为空值,则报错,是必须字段
User string `form:"username" json:"user" uri:"user" xml:"user" binding:"required"`
Password string `form:"password" json:"password" uri:"password" xml:"password" binding:"required"`
}
func main(){
//1、创建路由
//默认使用2个中间件Logger(),Recovery()
r := gin.Default()
//json绑定
r.POST("/loginForm",func(c *gin.Context){
//声明接收的变量
var form Login
//bind()默认解析并绑定form格式
//根据请求头中content-type自动推断
if err := c.Bind(&form);err != nil{
c.JSON(http.StatusBadRequest,gin.H{"error":err.Error()})
return
}
//判断用户名密码是否正确
if form.User != "root" || form.Password != "admin"{
c.JSON(http.StatusBadRequest,gin.H{"status":"304"})
return
}
c.JSON(http.StatusOK,gin.H{"status":"200"})
})
//3、监听端口,默认端口8080
r.Run(":8000")
}
URL数据解析和绑定
package main
import (
"github.com/gin-gonic/gin"
"net/http"
)
//定义接收数据的结构体
type Login struct {
//binding:”required"修饰的字段,若接收为空值,则报错,是必须字段
User string `form:"username" json:"user" uri:"user" xml:"user" binding:"required"`
Password string `form:"password" json:"password" uri:"password" xml:"password" binding:"required"`
}
func main(){
//1、创建路由
//默认使用2个中间件Logger(),Recovery()
r := gin.Default()
//json绑定
r.GET("/:user/:password",func(c *gin.Context){
//声明接收的变量
var login Login
//bind()默认解析并绑定form格式
//根据请求头中content-type自动推断
if err := c.ShouldBindUri(&login);err != nil{
c.JSON(http.StatusBadRequest,gin.H{"error":err.Error()})
return
}
//判断用户名密码是否正确
if login.User != "root" || login.Password != "admin"{
c.JSON(http.StatusBadRequest,gin.H{"status":"304"})
return
}
c.JSON(http.StatusOK,gin.H{"status":"200"})
})
//3、监听端口,默认端口8080
r.Run(":8000")
}
各种数据格式响应
package main
import (
"github.com/gin-gonic/gin"
"github.com/gin-gonic/gin/testdata/protoexample"
)
//多种响应方式
func main(){
r := gin.Default()
//1.json
r.GET("/someJSON",func(c *gin.Context){
c.JSON(200,gin.H{"message":"someJSON","status":200})
})
//2.结构体响应
r.GET("/someStruct",func(c *gin.Context){
var msg struct{
Name string
Message string
Number int
}
msg.Name = "root"
msg.Message = "message"
msg.Number = 123
c.JSON(200,msg)
})
//3.XML
r.GET("/someXML",func(c *gin.Context){
c.XML(200,gin.H{"message":"abc"})
})
//4.YAML响应
r.GET("/someYAML",func(c *gin.Context){
c.YAML(200,gin.H{"name":"zhangsan"})
})
//5.protobuf格式,谷歌开发的高效存储读取工具
//如果自己构建一个传输格式,该是什么格式?
r.GET("/someProtoBuf",func(c *gin.Context){
reps := []int64{int64(1),int64(2)}
//定义数据
label := "label"
//传protobuf格式数据
data := &protoexample.Test{
Label: &label,
Reps: reps,
}
c.ProtoBuf(200,data)
})
r.Run(":8000")
}
HTML模板渲染
- gin支持HTML模板,然后根据模板参数进行配置并返回相应的数据,本质上就是字符串替换
- LoadHTMLGlob()方法可以加载模板文件
//html渲染
//加载模板文件
r.LoadHTMLGlob("../template/*")
//r.LoadHTMLFiles("template/index.tmpl")
r.GET("/index",func(c *gin.Context){
//根据文件名渲染
//最终JSON将title替换
c.HTML(200,"index.tmpl",gin.H{"title":"我的标题"})
})
{{.title}}
重定向
//重定向
r.GET("/redirect",func(c *gin.Context){
c.Redirect(http.StatusMovedPermanently,"http://www.baidu.com")
})
- 同步异步
- goroutine机制可以方便的实现异步处理
- 另外,在启动新的goroutine时,不应该使用原始上下文,必须使用它的只读副本。
//异步
r.GET("/long_async",func(c *gin.Context){
//需要搞一个副本
copyContext := c.Copy()
go func(){
time.Sleep(3*time.Second)
log.Println("异步执行"+ copyContext.Request.URL.Path)
}()
})
//同步
r.GET("/long_sync",func(c *gin.Context){
time.Sleep(3*time.Second)
log.Println("同步执行"+ c.Request.URL.Path)
})
中间件
- gin中间件
- gin可以构建中间件,但它只对注册过的路由函数起作用。
- 对于分组路由,嵌套使用中间件,可以限定中间件的作用范围。
- 中间件分为全局中间件、单个路由中间件和群组中间件
- gin中间件必须是一个gin HandlerFunc类型。
全局中间件
- 所有请求都经过此中间件。
//定义中间件
func MiddleWare()gin.HandlerFunc{
return func(c *gin.Context){
t := time.Now()
fmt.Println("中间件开始执行了。")
//设置变量到context到key中,可以通过Get取
c.Set("request","中间件")
//执行中间件
c.Next()
status := c.Writer.Status()
fmt.Println("中间件执行完毕",status)
t2 := time.Since(t)
fmt.Println("time:",t2)
}
}
//注册中间件
r.Use(MiddleWare())
//代码规范
{
r.GET("/middleware",func(c *gin.Context){
//取值
req,_ := c.Get("request")
fmt.Println("request",req)
//页面接收
c.JSON(200,gin.H{"request":req})
})
}
- Next()方法
- 看源码
- 局部中间件
//注册中间件
r.Use(MiddleWare())
//代码规范
{
r.GET("/middleware",func(c *gin.Context){
//取值
req,_ := c.Get("request")
fmt.Println("request",req)
//页面接收
c.JSON(200,gin.H{"request":req})
})
//根路由后面是定义的局部中间件
r.GET("/middleware2",MiddleWare(),func(c *gin.Context){
//取值
req,_ := c.Get("request")
fmt.Println("request2",req)
//页面接收
c.JSON(200,gin.H{"request":req})
})
}
- 中间件练习
- 定义一个程序计时中间件,然后定义2个路由,执行函数后应该打印统计的执行时间
//定义计时中间件
func MiddleTime(c *gin.Context){
start:= time.Now()
//执行中间件
c.Next()
since := time.Since(start)
fmt.Println("程序用时:",since)
}
//另一种形式
r.Use(MiddleTime)
shoppingGroup := r.Group("/shopping")
{
shoppingGroup.GET("/index",shopIndexHandler)
shoppingGroup.GET("/home",shopHomeHandle)
}
func shopHomeHandle(c *gin.Context){
time.Sleep(3*time.Second)
}
func shopIndexHandler(c *gin.Context){
time.Sleep(5*time.Second)
}
- Cookie:
- HTTP是无状态协议,服务器不能记录浏览器的访问状态,也就是说服务器不能区分两次请求是否同一个客户端发出。
- cookie解决http协议无状态的方案之一,
- cookie实际就是服务器保存在浏览器上的一段信息,浏览器有了cookie之后,每次向服务器发送请求时都会同时将信息发送给服务器,服务器收到请求后,就可以根据该信息处理请求。
- cookie由服务器创建,并发送给浏览器,最终由浏览器保存。
cookie的用途:
- 保持用户登录状态
- 京东购物车就是这样用的。
cookie的使用:
- 测试服务端发送cookie给客户端,客户端请求时携带cookie。
package main
import (
"fmt"
"github.com/gin-gonic/gin"
)
func main(){
r := gin.Default()
r.GET("cookie",func(c *gin.Context){
//获取客户端是否携带cookie
cookie,err := c.Cookie("key_cookie")
if err != nil{
cookie = "NotSet"
//设置cookie 时间单位s,path.cookie所在目录,secure是否通过https访问,httpOnly bool是否允许别人获取自己的cookie
c.SetCookie("key_cookie","value_cookie",
60,"/","localhost",false,true)
}
fmt.Printf("cookie的值是:%s\n",cookie)
})
r.Run(":8000")
}
package main
import (
"fmt"
"github.com/gin-gonic/gin"
"net/http"
)
//设置登录权限
func MiddleLogin(c *gin.Context){
//获取客户端cookie并校验
cookie,err := c.Cookie("abc")
if err == nil{
if cookie == "123"{
c.Next()
return
}
}
//返回错误
c.JSON(http.StatusUnauthorized,gin.H{"error":"err"})
c.Abort()//若验证不通过,不再执行之后的信息。
return
}
func main(){
r := gin.Default()
r.GET("/login",func(c *gin.Context){
c.SetCookie("abc","123",60,"/","localhost",false,true)
c.String(200,"login success!")
})
r.GET("/home",MiddleLogin,func(c *gin.Context){
c.JSON(200,gin.H{"data":"home"})
})
r.Run(":8000")
}
- Cookie缺点:
- 不安全
- 增加带宽消耗
- 可以被禁用
- cookie有上限
- session:可以弥补cookie的不足,必须依赖于cookie才能使用,生成一个sessionid放在cookie里传给客户端就可以。
- session中间件开发
- 设计一个通用的session服务,支持内存存储和redis存储。
- session模块设计
- 本质上就是一个k-v系统,通过key进行增删改查
- session可以存储在内存或redis(2个版本)
- session接口设计:
- Set()
- Get()
- Del()
- Save():Session存储,redis延迟加载(用的时候再加载)
- SessionMgr接口设计:
- Init():初始化,加载redis地址
- CreateSession():创建一个新的session
- GetSession(): 通过sessionID获取对应的session对象value
- MemorySession设计:
- 定义MemorySession对象(字段:sessionid,存kv的map,读写锁)
- 构造函数:为了获取对象
- Set()
- Get()
- Del()
- Save()
- MemorySessionMgr设计:
- 定义MemorySessionMgr对象(字段:存放所有session的map,读写锁)
- 构造函数
- Init():初始化,加载redis地址
- CreateSession():创建一个新的session
- GetSession(): 通过sessionID获取对应的session对象value
- RedisSession设计:
- 定义RedisSession对象(字段:sessionid,存kv的map,读写锁,redis连接池,记录内存中map是否被修改的标记)
- 构造函数:为了获取对象
- Set():将session存到内存中的map
- Get():取数据,实现延迟加载
- Del()
- Save():将session存到redis
- RedisSessionMgr设计:
- 定义RedisSessionMgr对象(字段:redis地址,redis密码,连接池,读写锁,大map)
- 构造函数
- Init():初始化,加载redis地址
- CreateSession():创建一个新的session
- GetSession(): 通过sessionID获取对应的session对象value
session.go
package session
type Session interface {
Set(key string,value interface{})error
Get(key string)(interface{},error)
Del(key string)error
Save()error
}
session_mgr.go
package session
//定义管理者,管理所有session
type SessionMgr interface {
//初始化
Init(addr string,options ...string)(err error)
CreateSession(session Session,err error)
Get(sessionId string)(session Session,err error)
}
memory.go
package session
import (
"sync"
"errors"
)
//定义MemorySession对象(字段:sessionid,存kv的map,读写锁)
//- 构造函数:为了获取对象
//- Set()
//- Get()
//- Del()
//- Save()
//对象
type MemorySession struct {
sessionId string
//存kv
data map[string]interface{}
rwlock sync.RWMutex
}
//构造函数
func NewMemorySession(id string) *MemorySession{
s := &MemorySession{
sessionId: id,
data: make(map[string]interface{},16),
}
return s
}
func (m *MemorySession)Set(key string,value interface{})(err error){
//加锁
m.rwlock.Lock()
defer m.rwlock.Unlock()
//设置值
m.data[key] = value
return
}
func (m *MemorySession)Get(key string)(value interface{},err error){
//加锁
m.rwlock.Lock()
defer m.rwlock.Unlock()
value,ok := m.data[key]
if !ok {
err = errors.New("key not exists in session")
return
}
return
}
func (m *MemorySession)Del(key string)(err error){
//加锁
m.rwlock.Lock()
defer m.rwlock.Unlock()
delete(m.data,key)
return
}
func (m *MemorySession)Save()(err error){
return
}
redis_session.go
package session
import (
"encoding/json"
"github.com/gomodule/redigo/redis"
"errors"
"sync"
)
//定义RedisSession对象(字段:sessionid,存kv的map,读写锁)
//- 构造函数:为了获取对象
//- Set()
//- Get()
//- Del()
//- Save()
//对象
type RedisSession struct {
sessionId string
pool *redis.Pool
//设置session,可以先放在内存的map中
//批量的导入redis,提升性能
sessionMap map[string]interface{}
//读写锁
rwlock sync.RWMutex
//记录内存中map是否被操作
flag int
}
//用常量定义状态
const(
//内存数据没变化
SessionFlagNone = iota
//有变化
SessionFlagModify
)
//构造函数
func NewRedisSession(id string,pool *redis.Pool) *RedisSession{
s := &RedisSession{
sessionId: id,
sessionMap: make(map[string]interface{},16),
pool: pool,
flag: SessionFlagNone,
}
return s
}
func (r *RedisSession)Set(key string,value interface{})(err error){
//加锁
r.rwlock.Lock()
defer r.rwlock.Unlock()
//设置值
r.sessionMap[key] = value
//标记
r.flag = SessionFlagModify
return
}
func (r *RedisSession)Get(key string)(value interface{},err error){
//加锁
r.rwlock.Lock()
defer r.rwlock.Unlock()
//先判断内存
value,ok := r.sessionMap[key]
if !ok {
err = errors.New("key not exists in session")
return
}
return
}
//从redis再次加载
func (r *RedisSession)loadFromRedis()(err error){
conn := r.pool.Get()
reply,err := conn.Do("GET",r.sessionId)
if err != nil{
return
}
data,err := redis.String(reply,err)
if err != nil{
return
}
//取到的东西反序列化到内存的map
err = json.Unmarshal([]byte(data),&r.sessionMap)
if err != nil{
return
}
return
}
func (r *RedisSession)Del(key string)(err error){
//加锁
r.rwlock.Lock()
defer r.rwlock.Unlock()
r.flag = SessionFlagModify
delete(r.sessionMap,key)
return
}
//session存到redis
func (r *RedisSession)Save()(err error){
//加锁
r.rwlock.Lock()
defer r.rwlock.Unlock()
//若数据没变,不需要存
if r.flag != SessionFlagModify{
return
}
//内存中的sessionMap进行序列化
data,err := json.Marshal(r.sessionMap)
if err != nil{
return
}
//获取redis连接
conn := r.pool.Get()
//保存kv
_,err = conn.Do("SET",r.sessionId,string(data))
//改状态
r.flag = SessionFlagNone
if err != nil{
return
}
return
}
sessionMgr.go
package session
import (
"errors"
"github.com/gomodule/redigo/redis"
uuid "github.com/satori/go.uuid"
"sync"
"time"
)
//- 定义RedisSessionMgr对象(字段:存放所有session的map,读写锁)
//- 构造函数
//- Init():初始化,加载redis地址
//- CreateSession():创建一个新的session
//- GetSession(): 通过sessionID获取对应的session对象value
type RedisSessionMgr struct {
//redis地址
addr string
//密码
password string
//连接池
pool *redis.Pool
//锁
rwlock sync.RWMutex
//大map
sessionMap map[string]Session
}
//构造
func NewRedisSessionMgr()SessionMgr{
sr := &RedisSessionMgr{
sessionMap: make(map[string]Session,32),
}
return sr
}
func (r *RedisSessionMgr)Init(addr string,options ...string)(err error){
//若有其他参数
if len(options) > 0{
r.password = options[0]
}
//创建连接池
r.pool = myPool(addr,r.password)
r.addr = addr
return
}
func myPool(addr,password string)*redis.Pool{
return &redis.Pool{
MaxIdle: 64,
MaxActive: 1000,
IdleTimeout: 240*time.Second,
Dial: func()(redis.Conn,error){
conn,err := redis.Dial("tcp",addr)
if err != nil{
return nil,err
}
//若有密码,判断
if _,err := conn.Do("AUTH",password);err != nil{
conn.Close()
return nil,err
}
return conn,err
},
//连接测试,开发时写
//上线注释掉
TestOnBorrow:func(conn redis.Conn,t time.Time)error{
_,err := conn.Do("PING")
return err
},
}
}
func (r *RedisSessionMgr)CreateSession()(session Session,err error){
r.rwlock.Lock()
defer r.rwlock.Unlock()
//用uuid作为sessionid
id := uuid.NewV4()
//将uuid转成string
sessionId := id.String()
//创建个session
session = NewRedisSession(sessionId,r.pool)
//加到大map
r.sessionMap[sessionId] = session
return
}
func (r *RedisSessionMgr)Get(sessionId string)(session Session,err error){
r.rwlock.Lock()
defer r.rwlock.Unlock()
session,ok := r.sessionMap[sessionId]
if !ok{
err = errors.New("session not exists")
}
return
}
init.go
package session
import "fmt"
var(
sessionMgr SessionMgr
)
//中间件让用户去选择使用哪个版本
func Init(provider string,addr string,options ...string)(err error){
switch provider {
case "memory":
sessionMgr = NewMemorySessionMgr()
case "redis":
sessionMgr = NewRedisSessionMgr()
default:
fmt.Errorf("不支持")
return
}
err = sessionMgr.Init(addr,options...)
return
}
数据库
-
练习
CREATE TABLE `book`(
`id` INT(50) NOT NULL AUTO_INCREMENT,
`title` VARCHAR(50) DEFAULT NULL,
`price` INT(50) DEFAULT NULL,
PRIMARY KEY (`id`)
)ENGINE=INNODB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8;
INSERT INTO `book`(`id`,`title`,`price`) VALUES(1,'java',50),(2,'go',100),(3,'c',150);
book_list.html
{{define "book_list2.html"}}
书籍列表
ID
title
price
操作
{{range .data}}
{{.ID}} {{.ID}}
{{.Title}} {{.ID}}
{{.Price}} {{.ID}}
删除
{{end}}
{{end}}
new_book.html
{{define "new_book.html"}}
添加图书信息
{{end}}
db.go
package main
import (
"fmt"
_ "github.com/go-sql-driver/mysql"
"github.com/jmoiron/sqlx"
)
var db *sqlx.DB
func initDB()(err error){
addr := "root:root@tcp(127.0.0.1:3306)/go"
db,err = sqlx.Connect("mysql",addr)
if err != nil{
return err
}
//最大连接
db.SetMaxOpenConns(100)
//最大空闲
db.SetMaxIdleConns(16)
return
}
func queryAllBook()(bookList []*Book,err error){
sqlStr := "select id,title,price from book"
err = db.Select(&bookList,sqlStr)
if err != nil{
fmt.Println("查询失败")
return
}
return
}
func insertBook(title string,price int64)(err error){
sqlStr := "insert into book(title,price) values(?,?)"
_,err = db.Exec(sqlStr,title,price)
if err != nil{
fmt.Println("插入失败")
return
}
return
}
func deleteBook(id int64)(err error){
sqlStr := "delete from book where id = ? "
_,err = db.Exec(sqlStr,id)
if err != nil{
fmt.Println("删除失败")
return
}
return
}
model.go
package main
//定义书
type Book struct {
ID int64 `db:"id"`
Title string `db:"title"`
Price int64 `db:"price"`
}
main.go
package main
import (
"github.com/gin-gonic/gin"
"net/http"
)
func main(){
//初始化数据库
err := initDB()
if err != nil{
panic(err)
}
//定义路由
r := gin.Default()
//加载页面
r.LoadHTMLGlob("./templates/*")
//查询图书
r.GET("/book/list",bookListHandler)
r.Run(":8000")
}
func bookListHandler(c *gin.Context) {
booklist,err := queryAllBook()
if err != nil{
c.JSON(http.StatusOK,gin.H{
"code":1,
"msg":err,
})
return
}
//返回数据
c.HTML(http.StatusOK,"book_list2.html",gin.H{
"code":0,
"data":booklist,
})
}