这是我参与「第三届青训营 -后端场」笔记创作活动的的第2篇笔记
Gin框架学习
gin的安装
go get -u github.com/gin-gonic/gin
从server.go入手
package main
import (
"os"
"github.com/Moonlight-Zhao/go-project-example/cotroller"
"github.com/Moonlight-Zhao/go-project-example/repository"
"gopkg.in/gin-gonic/gin.v1"
)
// 初始化数据索引、引擎配置;
// 构建路由、启动服务
func main() {
if err := Init("./data/"); err != nil {
os.Exit(-1)
}
// 创建路由
r := gin.Default()
// 绑定路由规则,其中gin.Context封装了request和response
r.GET("/community/page/get/:id", func(c *gin.Context) {
topicId := c.Param("id")
data := cotroller.QueryPageInfo(topicId)
c.JSON(200, data)
})
// 监听端口,默认在8080
// err := r.Run(":6060")
err := r.Run()
if err != nil {
return
}
}
路由路径的设置:
// 设置路由
r := gin.Default()
// 第一个参数是:路径; 第二个参数是:具体操作 func(c *gin.Context)
r.GET("/Get", getting)
r.POST("/Post", posting)
r.PUT("/Put", putting)
r.DELETE("/Delete", deleting)
// 默认启动的是 8080端口
r.Run()
路由分组
// 两个路由组,都可以访问,大括号是为了保证规范
v1 := r.Group("/v1")
{
// 通过 localhost:8080/v1/hello访问,以此类推
v1.GET("/hello", sayHello)
v1.GET("/world", sayWorld)
}
v2 := r.Group("/v2")
{
v2.GET("/hello", sayHello)
v2.GET("/world", sayWorld)
}
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) {...})
}
大量路由实现
- 当路由变多时候,需要建立routers,将不同的模块拆分到不同的go文件。
// routers包下某个router对外开放的方法
func LoadRouter(e *gin.Engine) {
e.Group("v1")
{
v1.GET("/post", postHandler)
v1.GET("/get", getHandler)
}
}
// mian.go
func main() {
r := gin.Default()
// 调用该方法实现注册
routers.LoadRouter(r)
...
r.Run()
}
- 但规模更大时,建立router包,内部划分模块,每个模块有个router.go文件负责该模块的路由注册。
/*
├── routers
│ │
│ ├── say
│ │ ├── sayWorld.go
│ │ └── router.go
│ │
│ ├── hello
│ │ ├── helloWorld.go
│ │ └── router.go
│ │
│ └── setup_router.go
│
└── main.go
*/
// setup_router.go
type Register func(*gin.Engine)
func Init(routers ...Register) *gin.Engine {
// 注册路由
rs := append([]Register{}, routers...)
r := gin.New()
// 遍历调用方法
for _, register := range rs {
register(r)
}
return r
}
// main.go
func main() {
// 设置需要加载的路由配置
r := routers.Init(
say.Routers,
hello.Routers, // 后面还可以有多个
)
r.Run()
}
获取参数
路径参数
// : 只能匹配1个,*可以匹配任意个数
// 此规则能够匹配/user/xxx这种格式,但不能匹配/user/ 或 /user这种格式
router.GET("/user/:name", func(c *gin.Context) {
name := c.Param("name")
c.String(http.StatusOK, "Hello %s", name)
})
// 此规则既能匹配 /user/xxx/ 格式也能匹配 /user/xxx/other1/other2 这种格式,注意只能在最后用
// 注意如果最后没有匹配的(末尾没有/,如果末尾有/则依旧是匹配的),那么会优先使用无该参数的路由,比如上面那个(与代码顺序无关)
router.GET("/user/:name/*action", func(c *gin.Context) {
name := c.Param("name")
action := c.Param("action")
message := name + " is " + action
c.String(http.StatusOK, message)
})
Get方法
r.GET("/user", func(c *gin.Context) {
//指定默认值
name := c.DefaultQuery("name", "normal")
//获取具体值
age := c.Query("age")
c.String(http.StatusOK, fmt.Sprintf("hello %s, your age is %s", name, age))
})
Post方法
r.POST("/form", func(c *gin.Context) {
// 设置默认值
types := c.DefaultPostForm("type", "post")
username := c.PostForm("username")
password := c.PostForm("password")
// 还可以使用Query实现 Get + Post的结合
name := c.Query("name")
c.JSON(200, gin.H{
"username": username,
"password": password,
"types": types,
"name": name,
})
})
文件获取
- 单个文件获取
// 给表单限制上传大小 (默认 32 MiB)
r.MaxMultipartMemory = 8 << 20 // 8 MiB
r.POST("/upload", func(c *gin.Context) {
file, err := c.FormFile("file")
if err != nil {
c.String(500, "上传文件出错")
}
// 上传到指定路径
c.SaveUploadedFile(file, "C:/desktop/"+file.Filename)
c.String(http.StatusOK, "fileName:", file.Filename)
})
- 多个文件获取
// 获取MultipartForm
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 {
// 逐个存
fmt.Println(file.Filename)
}
c.String(200, fmt.Sprintf("upload ok %d files", len(files)))
接受处理
type Login struct {
// binding:"required" 若接收为空值,则报错
User string `form:"username" json:"user" uri:"user" xml:"user" binding:"required"`
Pssword string `form:"password" json:"password" uri:"password" xml:"password" binding:"required"`
}
content-type绑定
r.POST("/loginJSON", func(c *gin.Context) {
// 声明接收的变量
var login Login
// 默认绑定form格式
if err := c.Bind(&login); err != nil {
// 根据请求头中content-type自动推断
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// 输出结果
c.JSON(http.StatusOK, gin.H{
"status": "200",
"user": login.User,
"password": login.Password,
})
})
json绑定
r.POST("/loginJSON", func(c *gin.Context) {
// 声明接收的变量
var json Login
// 将request的body中的数据,按照json格式解析到结构体
if err := c.ShouldBindJSON(&json); err != nil {
// 如果发送的不是json格式,那么输出: "error": "invalid character '-' in numeric literal"
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// 输出结果
c.JSON(http.StatusOK, gin.H{
"status": "200",
"user": json.User,
"password": json.Password,
})
})
响应处理
// 1.JSON
r.GET("/someJSON", func(c *gin.Context) {
c.JSON(200, gin.H{
"message": "Json",
"status": 200,
})
})
// 2.XML
r.GET("/someXML", func(c *gin.Context) {
c.XML(200, gin.H{"message": "abc"})
})
// 3.YAML
r.GET("/someYAML", func(c *gin.Context) {
c.YAML(200, gin.H{"name": "zhangsan"})
})
// 4.protobuf
r.GET("/someProtoBuf", func(c *gin.Context) {
reps := []int64{1, 2}
data := &protoexample.Test{
Reps: reps,
}
c.ProtoBuf(200, data)
})
重定向
// HTTP重定向
r.GET("/index", func(c *gin.Context) {
c.Redirect(http.StatusMovedPermanently, "https://www.baidu.com")
})
// 路由重定向
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"})
})
异步执行
r.GET("/long_async", func(c *gin.Context) {
// 需要搞一个副本
copyContext := c.Copy()
// 异步处理
go func() {
time.Sleep(3 * time.Second)
log.Println("异步执行:" + copyContext.Request.URL.Path)
// 注意不能在这执行重定向的任务,不然panic
}()
})
会话控制
cookie控制
r.GET("/getCookie", func(c *gin.Context) {
// 获取客户端是否携带cookie
cookie, err := c.Cookie("key_cookie")
if err != nil {
cookie = "cookie"
c.SetCookie("key_cookie", "value_cookie", // 参数1、2: key & value
60, // 参数3: 生存时间(秒)
"/", // 参数4: 所在目录
"localhost", // 参数5: 域名
false, // 参数6: 安全相关 - 是否智能通过https访问
true, // 参数7: 安全相关 - 是否允许别人通过js获取自己的cookie
)
}
fmt.Printf("cookie的值是: %s\n", cookie)
})
session控制
package main
import (
"fmt"
"github.com/gin-contrib/sessions"
"github.com/gin-contrib/sessions/cookie"
"github.com/gin-gonic/gin"
)
func main() {
r := gin.Default()
// 注意该密钥不要泄露了
store := cookie.NewStore([]byte("secret"))
//路由上加入session中间件
r.Use(sessions.Sessions("mySession", store))
r.GET("/setSession", func(c *gin.Context) {
// 设置session
session := sessions.Default(c)
session.Set("key", "value")
session.Save()
})
r.GET("/getSession", func(c *gin.Context) {
// 获取session
session := sessions.Default(c)
v := session.Get("key")
fmt.Println(v)
})
r.Run(":8080")
}
cookie和session的区别
Session是服务器用来跟踪用户的一种手段,每个Session都有一个唯一标识:Session ID。当服务器创建了一个Session时,给客户端发送的报文里包含了Set-Cookie字段,其中有一个sid键对值,就是Session ID。客户端收到后将Cookie保存在浏览器中,并且在之后每次发送的报文中都含有Session ID。HTTP就是Session和Cookie两种方式实现用户状态跟踪。其中Session用于服务端,Cookie用于客户端。
- session保存在服务器,客户端不知道其信息;cookie保存在客户端,服务器能够知道其中的信息。
- session中保存的是对象,cookie中保存的是字符串。
- session的运行以来session id,而session id是存在于cookie中。即如果浏览器禁用了cookie,同时session也会失效(可通过其他方式实现)。
- session在用户会话结束后就会关闭,但是cookie保存在客户端,可以长期保存。
- cookie不是很安全,他人可以分析存放在本地的cookie进行cookie欺骗,session相对更加安全。
- 单个cookie保存的数据不能超过4k,很多浏览器限制一个站点最多保存20个cookie。
中间件
func BasicAuth(accounts Accounts) HandlerFunc // 身份认证
func BasicAuthForRealm(accounts Accounts, realm string) HandlerFunc
func Bind(val interface{}) HandlerFunc //拦截请求参数并进行绑定
func ErrorLogger() HandlerFunc //错误日志处理
func ErrorLoggerT(typ ErrorType) HandlerFunc //自定义类型的错误日志处理
func Logger() HandlerFunc //日志记录
func LoggerWithConfig(conf LoggerConfig) HandlerFunc
func LoggerWithFormatter(f LogFormatter) HandlerFunc
func LoggerWithWriter(out io.Writer, notlogged ...string) HandlerFunc
func Recovery() HandlerFunc
func RecoveryWithWriter(out io.Writer) HandlerFunc
func WrapF(f http.HandlerFunc) HandlerFunc //将http.HandlerFunc包装成中间件
func WrapH(h http.Handler) HandlerFunc //将http.Handler包装成中间件
自定义中间件
// HandlerFunc 本质就是一个函数,入参为 *gin.Context
// HandlerFunc defines the handler used by gin middleware as return value.
type HandlerFunc func(*Context)
// 日志类
func MyLogMiddleWare() gin.HandlerFunc {
return func(c *gin.Context) {
fmt.Println("[MyLog] 用户ip:", c.ClientIP())
fmt.Println("[MyLog] 用户request:", c.Request)
}
}
func main() {
r := gin.Default()
// 配置中间件
r.Use(MyLogMiddleWare())
// 注册路由
r.GET("/say", func(c *gin.Context) {
c.String(200, "request: %s", c.Request)
})
r.Run(":8080")
}
局部中间件
// 如果我们自定义的中间件只需要在某个路由上使用,只需要在该路由路径上使用该方法即可.
func (group *RouterGroup) GET(relativePath string, handlers ...HandlerFunc) IRoutes {
return group.handle(http.MethodGet, relativePath, handlers)
}
//局部中间件使用
r.GET("/test", MyLogMiddleWare(), func(c *gin.Context) {
// 页面接收
c.JSON(200, gin.H{"success": "ok"})
})
// 根据分组来添加中间件
v1 := r.Group("v1", MyLogMiddleWare())
// 也可以这样书写
// v1.Use(MyLogMiddleWare())
v1.GET("/c1", func(c *gin.Context) {
// 页面接收
c.JSON(200, gin.H{"request": "ok"})
})
v1.GET("/c2", func(c *gin.Context) {
// 页面接收
c.JSON(200, gin.H{"request": "ok"})
})
处理后续工作
func CalcTimeMiddleWare() gin.HandlerFunc {
return func(c *gin.Context) {
start := time.Now()
c.Next()
// 统计时间
since := time.Since(start)
fmt.Println("程序用时:", since)
}
}
func main() {
r := gin.Default()
// 注册路由
r.GET("/time", CalcTimeMiddleWare(), func(c *gin.Context) {
time.Sleep(2 * time.Second)
c.String(200, "ok")
})
r.Run(":8080")
}
Gin渲染
HTML渲染
// 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")
}
静态文件处理
func main() {
r := gin.Default()
r.Static("/static", "./static")
r.LoadHTMLGlob("templates/**/*")
// ...
r.Run(":8080")
}
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")
}
YAML渲染
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)
})
运行多个服务
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)
}
}