Gin

Gin #

终于来哩!

Gin 是一个 Go (Golang) 编写的轻量级 http web 框架

Gin 的官网:https://gin-gonic.com/zh-cn/

Gin Github 地址:https://github.com/gin-gonic/gin

环境搭建 #

  1. 下载&安装

在项目目录下面执行

go get -u github.com/gin-gonic/gin
  1. 将 gin 引入到代码中
import "github.com/gin-gonic/gin"
  1. 新建Main.go
package main

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

func main() {
	r := gin.Default()
	r.GET("/", func(c *gin.Context) {
		c.String(200, "value %v", "hello gin")
	})
	r.Run()
  r.Run(":9000")//若改变端口
}
  1. Gin,启动!
go run main.go

golang 程序的热加载 #

所谓热加载就是当我们对代码进行修改时,程序能够自动重新加载并执行,这在我们开发中是非常便利的,可以快速进行代码测试,省去了每次手动重新编译

工具 1(推荐):https://github.com/gravityblast/fresh

go get github.com/pilu/fresh
go run github.com/pilu/fresh

工具 2:https://github.com/codegangsta/gin

go get -u github.com/codegangsta/gin

Gin 框架中的路由 #

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

c.String() c.JSON() c.JSONP() c.HTML()

GET POST PUT

例子

package main

import (
	"github.com/gin-gonic/gin"
	"net/http"
)

type Article struct {
	Title string `json:"title"`
}

func main() {
	r := gin.Default()
	r.GET("/", func(c *gin.Context) {
		c.String(http.StatusOK, "value %v", "hello gin!!")
	})
	r.POST("/add", func(c *gin.Context) {
		c.String(200, "post request")
	})
	r.PUT("/edit", func(c *gin.Context) {
		c.String(200, "put 1request")
	})
	r.GET("/json", func(c *gin.Context) {
		c.JSONP(http.StatusOK, gin.H{
			"success": true,
			"msg":     "hello gin",
		})
	})
	r.GET("/jsonp", func(c *gin.Context) {
		a := Article{"jsonp"}
		c.JSONP(http.StatusOK, a)
	})
	err := r.Run("127.0.0.1:8080")
	if err != nil {
		return
	}

}

Gin HTML 模板渲染 #

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

Gin 框架中如果不同目录下面有同名模板的话我们需要给模版起别名

{{ define "admin/index.html" }}
#html内容
{{ end }}

在main.go中这样写

r.LoadHTMLGlob("templates/**/*")

gin 模板基本语法 #

{{.}} 输出数据 #

例子

{{ 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>

<h1>前台模板</h1>

<h3>{{.title}}</h3>

<h4>{{.user.Name}}</h4> <h4>{{.user.Age}}</h4> </body> </html> {{end}}

其中user结构体定义如下

type user struct{
	Age int
	Name string
}

注释 #

可以多行,不能嵌套

{{/* a comment */}}

变量 #

<h4>{{$obj := .title}}</h4>
<h4>{{$obj}}</h4>

移除空格 #

{{- .Name -}}

”-“要紧挨着“{{“和“}}”

比较函数 #

符号含义
eq如果 arg1 == arg2 则返回真
ne如果 arg1 != arg2 则返回真
lt如果 arg1 < arg2 则返回真
le如果 arg1 <= arg2 则返回真
gt如果 arg1 > arg2 则返回真
ge如果 arg1 >= arg2 则返回真

条件判断 #

例子

{{define "default/index.html"}}
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <h2>{{.title}}</h2>
    

    {{if gt .score 90}}
    <p>excellent</p>
    {{else if gt .score 80}}
    <p>great</p>
    {{else if gt .score 60}}
    <p>pass</p>
    {{else}}
    <p>fail</p>
    {{end}}

</body>
</html>
{{end}}

range #

例子

r.GET("/", func(ctx *gin.Context) {
		ctx.HTML(http.StatusOK, "default/index.html", gin.H{
			"title": "首页",
			"score": 88,
			"hobby": []string{"eat", "sleep", "code"},
      "kong": []string{},
		})
	})
<ul>
{{range $key,$value:=.hobby}}
<li>{{$key}}:{{$value}}</li>
{{else}}
<li>空</li>
{{end}}
</ul>
<ul>
{{range $key,$value:=.kong}}
<li>{{$key}}:{{$value}}</li>
{{else}}
<li>空</li>
{{end}}
</ul>

with #

{{with .news}}
<!-- 相当于.news.Title -->
<p>{{.Title}}</p>
<!-- 相当于.news.Content -->
<p>{{.Content}}</p>
{{end}}

预定义函数 #

执行模板时,函数从两个函数字典中查找:首先是模板函数字典,然后是全局函数字典。一 般不在模板内定义函数,而是使用 Funcs 方法添加函数到模板里。

预定义的全局函数如下:

名称功能
and函数返回它的第一个 empty 参数或者最后一个参数; 就是说"and x y"等价于"if x then y else x";所有参数都会执行;
or返回第一个非 empty 参数或者最后一个参数; 亦即"or x y"等价于"if x then x else y";所有参数都会执行;
not返回它的单个参数的布尔值的否定
len返回它的参数的整数类型长度
index执行结果为第一个参数以剩下的参数为索引/键指向的值; 如"index x 1 2 3"返回 x[1][2][3]的值;每个被索引的主体必须是数组、切片或者字典。
print即 fmt.Sprint
printf即 fmt.Sprintf
println即 fmt.Sprintln
html返回与其参数的文本表示形式等效的转义 HTML。 这个函数在 html/template 中不可用。
urlquery以适合嵌入到网址查询中的形式返回其参数的文本表示的转义值。 这个函数在 html/template 中不可用。
js返回与其参数的文本表示形式等效的转义 JavaScript。
call执行结果是调用第一个参数的返回值,该参数必须是函数类型,其余参数作为调用该函 数的参数; 如"call .X.Y 1 2"等价于 go 语言里的 dot.X.Y(1, 2); 其中 Y 是函数类型的字段或者字典的值,或者其他类似情况; call 的第一个参数的执行结果必须是函数类型的值(和预定义函数如 print 明显不同); 该函数类型值必须有 1 到 2 个返回值,如果有 2 个则后一个必须是 error 接口类型; 如果有 2 个返回值的方法返回的 error 非 nil,模板执行会中断并返回给调用模板执行者 该错误;

自定义模板函数 #

main.go中:

LoadHTMLGlob之前SetFuncMap,添加自定义的模版函数

SetFuncMap就是设置了一个Map

r := gin.Default()
r.SetFuncMap(template.FuncMap{
  "UnixToTime": UnixToTime,
})
r.LoadHTMLGlob("templates/*/**")

实现函数

func UnixToTime(timestamp int64) string {
	t := time.Unix(timestamp, 0)
	fmt.Println(t)
	return t.Format("2006-01-02 15:04:05")
}

模版中:

调用函数

函数名 参数1 参数2……

<p>{{UnixToTime .date}}</p>

嵌套 template #

新建public/header.html

{{define "public/header"}}
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>public header</title>
    <style>
        h1 {
            background: #000;
            color: #fff;
            text-align: center;
        }
    </style>
</head>

<body>
    <h1>public header {{.title}}</h1>
</body>

</html>
{{end}}

在其他模版内引入

{{template "public/header" .}}

注意加上最后的“.”

静态文件服务 #

当我们渲染的 HTML 文件中引用了静态文件时,我们需要配置静态 web 服务

r.Static("/static", “./static”) 前面的/static 表示路由 后面的./static 表示路径

若css放在/static/css下,则在html中这样引入

<link rel="stylesheet" href="/static/css/base.css">

路由详解 #

GET POST 以及获取 Get Post 传值 #

Get 请求传值 #

r.GET("/", func(c *gin.Context) {
  username := c.Query("username")
  age := c.Query("age")
  page := c.DefaultQuery("page", "1")

  c.JSON(http.StatusOK, gin.H{
    "username": username,
    "age":      age,
    "page":     page,
  })
})

此时的GET请求应该如下:

GET http://127.0.0.1:8080?username=kamisatoayaka&age=18

Post 请求传值 获取 form 表单数据 #

前台:

<!-- 相当于给模板定义一个名字 define end 成对出现-->
{{ 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>
    <link rel="stylesheet" href="static/css/base.css">
</head>
<body>
    <form action="/doAddUser1" method="post">
        
        用户名:<input type="text" name="username" /> <br><br>
        密码:<input type="password" name="password" /> <br><br>      

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

{{end}}

后台:

//post演示
r.GET("/user", func(c *gin.Context) {
  c.HTML(http.StatusOK, "default/user.html", gin.H{})
})
//获取表单post过来的数据
r.POST("/doAddUser1", func(c *gin.Context) {
  username := c.PostForm("username")
  password := c.PostForm("password")
  age := c.DefaultPostForm("age", "20")

  c.JSON(http.StatusOK, gin.H{
    "username": username,
    "password": password,
    "age":      age,
  })
})

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

定义结构体,添加form标签

json标签的作用是解析的时候把json字段名改成小写

type UserInfo struct {
	Username string `json:"username" form:"username"`
	Password string `json:"password" form:"password"`
}

GET

声明为结构体

传的时候用它的指针!!!

别他妈写出什么user:=&UserInfo{}然后传&user了,你传牛魔呢

或者干脆用user:=new(UserInfo)

直接传user

浪费老子一个早上看你那💩代码

r.GET("/getUser", 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(),
    })
  }
})

POST

r.POST("/doAddUser2", func(c *gin.Context) {
  user := UserInfo{}
  if err := c.ShouldBind(&user); err == nil {
    c.JSON(http.StatusOK, user)
  } else {
    c.JSON(http.StatusBadRequest, gin.H{
      "err": err.Error(),
    })
  }
})

获取 Post Xml 数据 #

r.POST("/xml", func(c *gin.Context) {
  article := Article{}

  xmlSliceData, _ := c.GetRawData() //获取 c.Request.Body 读取请求数据

  fmt.Println(xmlSliceData)

  if err := xml.Unmarshal(xmlSliceData, &article); err == nil {
    c.JSON(http.StatusOK, article)
  } else {
    c.JSON(http.StatusBadRequest, gin.H{
      "err": err.Error(),
    })
  }

})

动态路由 #

r.GET("/list/:cid", func(c *gin.Context) {

  cid := c.Param("cid")
  c.String(200, "%v", cid)

})

路由分组 #

在同文件下分组

func main() { router := gin.Default()

// 简单的路由组: v1 
  v1 := router.Group("/v1") {

  v1.POST("/login", loginEndpoint)

  v1.POST("/submit", submitEndpoint)

  v1.POST("/read", readEndpoint) }

// 简单的路由组: v2 
  v2 := router.Group("/v2") {

  v2.POST("/login", loginEndpoint)

  v2.POST("/submit", submitEndpoint)

  v2.POST("/read", readEndpoint) }

  router.Run(":8080")

}

拆分到不同文件

新建 routes 文件夹,routes 文件下面新建 adminRoutes.go、apiRoutes.go、defaultRoutes.go

在main.go中,导入routers包

routers.AdminRoutersInit(r)
routers.ApiRoutersInit(r)
routers.DefaultRoutersInit(r)

然后在各自的xxx.go下配置路由即可

Gin 中自定义控制器 #

新建controllers文件夹,在其下建立对应的控制器,以admin为例

新建admin文件夹,在其中建立userController.go

package admin

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

type UserController struct{}

func (con UserController) Index(c *gin.Context) {
	c.String(200, "用户列表")
}

func (con UserController) Add(c *gin.Context) {
	c.String(200, "add 用户列表")
}

之所以建立结构体是为了方便继承

然后在对应的router中使用即可

package routers

import (
	"demo4/controllers/admin"

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

func AdminRoutersInit(r *gin.Engine) {
	adminRouters := r.Group("/admin")
	{
		adminRouters.GET("/", func(c *gin.Context) {
			c.String(200, "后台首页")
		})
		adminRouters.GET("/user", admin.UserController{}.Index)
		adminRouters.GET("/user/add", admin.UserController{}.Add)
		adminRouters.GET("/article", func(c *gin.Context) {
			c.String(200, "新闻列表")
		})
	}
}

继承

新建 controller/admin/BaseController.go

package admin import (

  "net/http"

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

type BaseController struct { } 
func (c BaseController) Success(ctx *gin.Context) {
  ctx.String(http.StatusOK, "成功") 
} 
func (c BaseController) Error(ctx *gin.Context) {
  ctx.String(http.StatusOK, "失败") 
}

可以让 UserController继承BaseController

type UserController struct{
	BaseController
}

然后直接调用BaseController的方法

Gin 中间件 #

next #

先跳转到后面的剩余处理程序,再回来继续执行

func initMiddleware(c *gin.Context) {
	start := time.Now().UnixNano()
	fmt.Println("1-我是一个中间件")
	//调用该请求的剩余处理程序
	c.Next()

	fmt.Println("2-我是一个中间件")
	end := time.Now().UnixNano()

	fmt.Println(end - start)
}

Abort #

终止调用请求的剩余处理程序

func initMiddleware(c *gin.Context) {
	start := time.Now().UnixNano()
	fmt.Println("1-我是一个中间件")
	//调用该请求的剩余处理程序
	c.Abort()

	fmt.Println("2-我是一个中间件")
	end := time.Now().UnixNano()

	fmt.Println(end - start)
}

一个路由可以添加多个中间件

r.GET("/login", initMiddleware, initMiddleware2, func(c *gin.Context) {
	c.String(200, "login")
})

全局中间件 #

在main函数中添加:

r.Use(func1,func2,...)

分组路由中间件 #

法1:

adminRouters:=r.Group("/admin",func)

法2:

adminRouters.Use(func)

中间件传值 #

ctx.Set("keyName","value")
key,_=ctx.Get("keyName") //key是空接口类型

Model #

就是把一个常用的功能,放到一个模块里,中间件/控制器/main都可以使用该功能。

例如把时间转换放到一个model里。

文件上传 #

html模版:

<body>
    <h2>演示文件上传</h2>

    <form action="/admin/user/doUpload" method="post" enctype="multipart/form-data">
        
        用户名:<input type="text" name="username" placeholder="用户名" />
        <br>
        <br>
        头 像<input type="file" name="face" />
        <br>    <br>
				头 像2<input type="file" name="face2" />
        <br>    <br>
      	头 像2<input type="file" name="face[]" />
        <br>    <br>
        <input type="submit" value="提交">
    </form>
</body>

Controller:

func (con UserController) DoUpload(c *gin.Context) {
	username := c.PostForm("username")

	file, err := c.FormFile("face")
	file2, err := c.FormFile("face2")
  file,err:= c.FormFile("face[]")
  
  for _,file:=range file{
    dst:=path.Join("./static/upload",file.Filename)
    c.SaveUploadedFile(file,dst)
  }
  
	// file.Filename 获取文件名称  aaa.jpg   ./static/upload/aaa.jpg
	dst := path.Join("./static/upload", file.Filename)
	if err == nil {
		c.SaveUploadedFile(file, dst)
	}
	// c.String(200, "执行上传")
  dst2 := path.Join("./static/upload", file2.Filename)
	if err == nil {
		c.SaveUploadedFile(file2, dst2)
	}
  
	c.JSON(http.StatusOK, gin.H{
		"success":  true,
		"username": username,
		"dst":      dst,
    "dst2": dst2
	})
}

按日期存储文件 #

  1. 获取上传文件
  2. 判断后缀是否合法
  3. 创建保存目录
  4. 生成文件名和保存的目录
  5. 执行上传
func (c UserController) DoAdd(ctx *gin.Context) {
	username := ctx.PostForm("username")
	// 1、获取上传的文件
	file, err1 := ctx.FormFile("face")
	if err1 == nil {
		// 2、获取后缀名 判断类型是否正确 .jpg .png .gif .jpeg
		extName := path.Ext(file.Filename)
		allowExtMap := map[string]bool{".jpg": true, ".png": true, ".gif": true, ".jpeg": true}
		if _, ok := allowExtMap[extName]; !ok {
			ctx.String(200, "文件类型不合法")
			return
		}
		// 3、创建图片保存目录 static/upload/20200623
		day := models.GetDay()
		dir := "./static/upload/" + day
		if err := os.MkdirAll(dir, 0666); err != nil {
			log.Error(err)
		}
		// 4、生成文件名称 144325235235.png
		fileUnixName := strconv.FormatInt(models.GetUnix(), 10)
		// static/upload/20200623/144325235235.png
		saveDir := path.Join(dir, fileUnixName+extName)
		ctx.SaveUploadedFile(file, saveDir)
	}
	ctx.JSON(http.StatusOK, gin.H{"message": "文件上传成功", "username": username})
	// ctx.String(200, username)
} 

SetCookie #

c.SetCookie(name, value string, maxAge int, path, domain string, secure, httpOnly bool)

第一个参数 key

第二个参数 value

第三个参数 过期时间.如果只想设置 Cookie 的保存路径而不想设置存活时间,可以在第三个参数中传递 nil

第四个参数 cookie 的路径

第五个参数 cookie 的路径 Domain 作用域 本地调试配置成 localhost , 正式上线配置成域名

第六个参数是 secure ,当 secure 值为 true 时,cookie 在 HTTP 中是无效,在 HTTPS 中才有效

第七个参数 httpOnly,是微软对 COOKIE 做的扩展。如果在 COOKIE 中设置了“httpOnly”属性,

则通过程序(JS 脚本、applet 等)将无法读取到 COOKIE 信息,防止 XSS 攻击产生

Cookie(获取) #

cookie, err := c.Cookie("name")

Session #

session 是另一种记录客户状态的机制,不同的是 Cookie 保存在客户端浏览器中,而 session保存在服务器上。当客户端浏览器第一次访问服务器并发送请求时,服务器端会创建一个 session 对象,生成一个类似于 key,value 的键值对,然后将 value 保存到服务器 将 key(cookie)返回到浏览器(客户)端。浏览器下次访问时会携带 key(cookie),找到对应的 session(value)。

package main
import ( "github.com/gin-contrib/sessions"
	"github.com/gin-contrib/sessions/cookie"
	"github.com/gin-gonic/gin"
)
func main() {
	r := gin.Default()
	// 创建基于 cookie 的存储引擎,secret11111 参数是用于加密的密钥,可以改成redis等
	store := cookie.NewStore([]byte("secret11111"))
	// 设置 session 中间件,参数 mysession,指的是 session 的名字,也是 cookie 的名字
	// store 是前面创建的存储引擎,我们可以替换成其他存储引擎
	r.Use(sessions.Sessions("mysession", store))
	r.GET("/", func(c *gin.Context) {
		//初始化 session 对象
		session := sessions.Default(c)
		//设置过期时间
		session.Options(sessions.Options{
			MaxAge: 3600 * 6, // 6hrs
		})
		//设置 Session
		session.Set("username", "张三")
		session.Save()
		c.JSON(200, gin.H{"msg": session.Get("username")})
	})
	r.GET("/user", func(c *gin.Context) {
		// 初始化 session 对象
		session := sessions.Default(c)
		// 通过 session.Get 读取 session 值
		username := session.Get("username")
		c.JSON(200, gin.H{"username": username})
	})
	r.Run(":8000")
}

Gorm #

gorm.model #

// gorm.Model 定义
type Model struct {
  ID        uint `gorm:"primary_key"`
  CreatedAt time.Time
  UpdatedAt time.Time
  DeletedAt *time.Time
}

可以用继承加入到自己的结构体中,也可以不加入

默认用ID作为主键

type User struct {
  gorm.Model
  Name         string
  Age          sql.NullInt64
  Birthday     *time.Time
  Email        string  `gorm:"type:varchar(100);unique_index"`
  Role         string  `gorm:"size:255"` // 设置字段大小为255
  MemberNumber *string `gorm:"unique;not null"` // 设置会员号(member number)唯一并且不为空
  Num          int     `gorm:"AUTO_INCREMENT"` // 设置 num 为自增类型
  Address      string  `gorm:"index:addr"` // 给address字段创建名为addr的索引
  IgnoreMe     int     `gorm:"-"` // 忽略本字段
}

tag #

结构体标记(Tag)描述
Column指定列名
Type指定列数据类型
Size指定列大小, 默认值255
PRIMARY_KEY将列指定为主键
UNIQUE将列指定为唯一
DEFAULT指定列默认值
PRECISION指定列精度
NOT NULL将列指定为非 NULL
AUTO_INCREMENT指定列是否为自增类型
INDEX创建具有或不带名称的索引, 如果多个索引同名则创建复合索引
UNIQUE_INDEXINDEX 类似,只不过创建的是唯一索引
EMBEDDED将结构设置为嵌入
EMBEDDED_PREFIX设置嵌入结构的前缀
-忽略此字段
结构体标记(Tag)描述
MANY2MANY指定连接表
FOREIGNKEY设置外键
ASSOCIATION_FOREIGNKEY设置关联外键
POLYMORPHIC指定多态类型
POLYMORPHIC_VALUE指定多态值
JOINTABLE_FOREIGNKEY指定连接表的外键
ASSOCIATION_JOINTABLE_FOREIGNKEY指定连接表的关联外键
SAVE_ASSOCIATIONS是否自动完成 save 的相关操作
ASSOCIATION_AUTOUPDATE是否自动完成 update 的相关操作
ASSOCIATION_AUTOCREATE是否自动完成 create 的相关操作
ASSOCIATION_SAVE_REFERENCE是否自动完成引用的 save 的相关操作
PRELOAD是否自动完成预加载的相关操作

表名 #

type User struct {} // 默认表名是 `users`

// 将 User 的表名设置为 `profiles`
func (User) TableName() string {
  return "profiles"
}

func (u User) TableName() string {
  if u.Role == "admin" {
    return "admin_users"
  } else {
    return "users"
  }
}

// 禁用默认表名的复数形式,如果置为 true,则 `User` 的默认表名是 `user`
db.SingularTable(true)

列名 #

type Animal struct {
  AnimalId    int64     `gorm:"column:beast_id"`         // set column name to `beast_id`
  Birthday    time.Time `gorm:"column:day_of_the_beast"` // set column name to `day_of_the_beast`
  Age         int64     `gorm:"column:age_of_the_beast"` // set column name to `age_of_the_beast`
}