gin框架

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"}}
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>posts/index</title>
</head>
<body>
    {{.title}}
</body>
</html>
{{end}}

users/index.html文件的内容如下:

{{define "users/index.html"}}
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>users/index</title>
</head>
<body>
    {{.title}}
</body>
</html>
{{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", "<a href='https://liwenzhou.com'>李文周的博客</a>")
    })

    router.Run(":8080")
}

index.tmpl中使用定义好的safe模板函数:

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <title>修改模板引擎的标识符</title>
</head>
<body>
<div>{{ . | safe }}</div>
</body>
</html>

静态文件处理

当我们渲染的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.tmplindex.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识别请求数据类型并利用反射机制自动提取请求中QueryStringform表单JSONXML等参数到结构体中。 下面的示例代码演示了.ShouldBind()强大的功能,它能够基于请求自动提取JSONform表单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会按照下面的顺序解析请求中的数据完成绑定:

  1. 如果是 GET 请求,只使用 Form 绑定引擎(query)。
  2. 如果是 POST 请求,首先检查 content-type 是否为 JSONXML,然后再使用 Formform-data)。

文件上传

单个文件上传

文件上传前端页面代码:

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <title>上传文件示例</title>
</head>
<body>
<form action="/upload" method="post" enctype="multipart/form-data">
    <input type="file" name="f1">
    <input type="submit" value="上传">
</form>
</body>
</html>

后端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()默认使用了LoggerRecovery中间件,其中:

  • 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路由

  1. 基本路由
  1. 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")
}

  1. 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){

}
  1. 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")
}

  1. 表单参数
    • 表单传输为post请求,http常见的传输格式4种:
      • application/json
      • application/x-www-form-urlencoded
      • application/xml
      • multipart/form-data
    • 表单参数可以通过PostForm()方法获取,该方法默认解析的是x-www-form-urlencoded或from-data格式的参数
      后台:
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")
}

页面:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>登录</title>
</head>
<body>
    <form action="http://127.0.0.1:8000/form" method="post" enctype="application/x-www-form-urlencoded">
        用户名:<input type="text" name="username">
        <br>
        密&nbsp&nbsp码:<input type="password" name="password">
        兴&nbsp&nbsp趣:
        <input type="checkbox" value="run" name="hobby"> 跑步
        <input type="checkbox" value="game" name="hobby"> 游戏
        <input type="checkbox" value="money" name="hobby"> 金钱
        <br>
        <input type="submit" value="登录">
    </form>
</body>
</html>
  1. 上传单个文件
    • 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")
}

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>登录</title>
</head>
<body>
    <form action="http://127.0.0.1:8000/upload" method="post" enctype="multipart/form-data">
        头像:<input type="file" name="file">
        <br>
        <input type="submit" name="提交">
    </form>
</body>
</html>
  1. 上传多个文件

改:

  <input type="file" name="files"  multiple>
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")
}

  1. routes group
    1. 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))
}
image.png
  1. 路由原理
    1. 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")
}

image.png

表单数据解析和绑定

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>登录</title>
</head>
<body>
    <form action="http://127.0.0.1:8000/loginForm" method="post" enctype="multipart/form-data">
        用户名:<input type="text" name="username">
        <br>
        密&nbsp&nbsp码:<input type="password" name="password">
        <input type="submit" value="登录">
    </form>
</body>
</html>
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")
}

image.png

各种数据格式响应

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":"我的标题"})
    })
<html>
    <hl>
        {{.title}}
    </hl>
</html>

重定向

//重定向
    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)
    })

中间件

image.png
  • 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")
}

image.png
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里传给客户端就可以。
image.png
  • session中间件开发
  • 设计一个通用的session服务,支持内存存储和redis存储。
  • session模块设计
    • 本质上就是一个k-v系统,通过key进行增删改查
    • session可以存储在内存或redis(2个版本)
image.png
  • 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
image.png

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
}

数据库

  • 练习


    image.png
image.png

image.png

image.png

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);

image.png

book_list.html

{{define "book_list2.html"}}
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>书籍列表</title>
</head>
<body>
    <div>
        <a href="/book/new">添加新书</a>
    </div>
    <table border="1">
        <thead>
            <tr>
                <th>ID</th>
                <th>title</th>
                <th>price</th>
                <th>操作</th>
            </tr>
        </thead>
        <tbody>
            {{range .data}}
                <tr>
                    <td>{{.ID}}</td>                {{.ID}}
                    <td>{{.Title}}</td>                {{.ID}}
                    <td>{{.Price}}</td>                {{.ID}}
                    <td><a href="/book/delete?id={{.ID}}}">删除</a></td>
                </tr>
                 {{end}}
        </tbody>
    </table>
</body>
</html>
{{end}}

new_book.html

{{define "new_book.html"}}
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>添加图书信息</title>
</head>
<body>
    <form action="/book/new" method="POST">
        <div>
            <label>书名:
                <input type="text" name="title">
            </label>
        </div>
        <div>
            <label>价格:
                <input type="number" name="price">
            </label>
        </div>
        <div>
            <label>书名:
                <input type="submit" value="点我">
            </label>
        </div>

    </form>
</body>
</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,
    })
}


image.png

image.png

image.png

image.png

image.png

image.png

image.png

image.png

image.png
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,033评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,725评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,473评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,846评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,848评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,691评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,053评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,700评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,856评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,676评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,787评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,430评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,034评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,990评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,218评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,174评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,526评论 2 343