最近尝试用Gin框架写一个小程序后端,自己做了一些学习笔记备忘,也供大家参考。作者水平有限,有任何问题欢迎在文章下面留言交流!
主要参考资料:https://www.qfgolang.com/?special=ginkuangjia&pid=2606
需要go版本在1.6以上
查看自己go版本
$ go version
安装Gin框架
$ go get -u github.com/gin-gonic/gin
国内直接使用这条命令安装大概率会因网络超时安装失败。
解决办法1:更换阿里云的镜像源
// 启用 Go Modules 功能
$ go env -w GO111MODULE=on
// 配置 GOPROXY 环境变量
$ go env -w GOPROXY=https://mirrors.aliyun.com/goproxy/
但是此方法开启了Go Modules,包将会下载到pkg/mod/目录下,且文件名将会带上版本号。但编辑器似乎不一定能够自动识别路径并导入。
解决办法2:终端下载,需要你有代理工具。
还需要注意,mac的终端默认是不走代理的,即使你的v2ray开了全局模式,终端也还是没翻出去。
mac终端的方法:
1、打开.zprofile(在旧版mac系统中则是.bash_profile)
$ vim ~/.zprofile
添加两个函数
function proxy_on{
export http_proxy=http://本机http监听host:本机http监听端口
export https_proxy=http://本机http监听host:本机http监听端口
}
function proxy_off{
unset http_proxy
unset https_proxy
}
本机http监听host和本机http监听端口可以去代理工具查看,这里以V2RayU为例子:打开V2RayU偏好设置,选择Advance就可以看到。
所以在我的电脑上,我添加的两个功能具体写为:
function proxy_on{
export http_proxy=http://127.0.0.1:1087
export https_proxy=http://127.0.0.1:1087
}
function proxy_off{
unset http_proxy
unset https_proxy
}
每次需要终端时,在终端输入以下命令开启代理:
$ proxy_on
使用完后输入以下命令关闭代理:
$ proxy_off
在gin框架中,Engine
被定义成为一个结构体,Engine
代表gin框架的一个结构体定义,其中包含了路由组、中间件、页面渲染接口、框架配置设置等相关内容。
默认的Engine
可以通过**gin.Default
**进行创建,或者使用gin.New()
同样可以创建。两种方式如下所示:
engine1 = gin.Default()
engine2 = gin.New()
gin.Default()
和gin.New()
的区别在于:
gin.Default
也使用gin.New()
创建engine实例,但是会默认使用Logger
和Recovery
中间件。
Logger
是负责进行打印并输出日志的中间件,方便开发者进行程序调试。Recovery
中间件的作用是如果程序执行过程中遇到panic
中断了服务,则Recovery
会恢复程序执行,并返回服务器500内部错误。
通常情况下,我们使用默认的gin.Default创建Engine实例。
engine1.Run([主机地址:端口号])
// 例如:
// 不带参数,默认主机地址为localhost,默认端口为8080
engine1.Run()
// 带参数,主机地址为apphost,端口为8090
engine1.Run("apphost:8090")
在上面我们创建的engine实例中,包含很多方法可以直接处理不同类型的HTTP请求。
http协议中一共定义了八种方法或者称之为类型来表明对请求网络资源(Request-URI)的不同的操作方式,分别是:OPTIONS、HEAD、GET、POST、PUT、DELETE、TRACE、CONNECT。
虽然一共有八种请求操作类型,但是实际开发中常用的就:GET、POST、DELETE等几种。
Context
是gin框架中封装的一个结构体,这是gin框架中最重要,最基础的一个结构体对象。
该结构体可以提供我们操作请求,处理请求,获取数据等相关的操作,通常称之为上下文对象,简单说为我们提供操作环境。
可以通过context.Query
和context.DefaultQuery
获取GET请求携带的参数。
可以通过context.Writer.Write
向请求发起端返回数据。
engine中可以直接进行HTTP请求的处理,在engine中使用Handle
方法进行http请求的处理。Handle
方法包含三个参数,具体如下所示:
func (group *RouterGroup) Handle(httpMethod, relativePath string, handlers ...HandlerFunc) IRoutes
httpMethod
:第一个参数表示要处理的HTTP的请求类型,是GET、POST、DELETE等8种请求类型中的一种。relativePath
:第二个参数表示要解析的接口,由开发者进行定义。handlers
:第三个参数是处理对应的请求的代码的定义。实例:
...
// 通过Handle方法第一个参数指定处理GET类型的请求,解析的接口是/hello
// url:http://localhost:8080/hello?name=syb
// GET请求附带的参数在url中的?后面
engine.Handle("GET", "/hello", func(context *gin.Context) {
// 获取请求接口
fmt.Println(context.FullPath())
// 获取字符串参数
name := context.DefaultQuery("name", "")
fmt.Println(name)
// 返回给前端输出
context.Writer.Write([]byte("Hello ," + name))
})
...
context.FullPath()
:**返回请求接口地址。**上述代码返回的是"/hello"。
context.DefaultQuery(key(string), defaultValue(string))
:**读取GET请求附带的参数。**第一个参数指定要读取的参数的键值key,第二个参数是若读取不到要返回的默认字符串。
context.Writer.Write(([]byte))
:**给前端返回数据。**数据类型是byte字符数组。
...
// 通过Handle方法第一个参数指定处理POST类型的请求,解析的接口是/login
// url:http://localhost:8080/login
engine.Handle("POST", "/login", func(context *gin.Context) {
fmt.Println(context.FullPath())
// 读取用户名
username := context.PostForm("username")
fmt.Println(username)
// 读取用户密码
password := context.PostForm("pwd")
fmt.Println(password)
context.Writer.Write([]byte("User login"))
})
...
context.PostForm(key(string))
:**读取POST请求附带的参数。**参数指定要读取的参数的键值key。
除了Engine中包含的通用的处理方法以外,engine还可以按类型进行直接解析。
Engine
中包含有get
方法、post
方法、delete
方法等与http请求类型对应的方法。
engine中包含GET方法处理HTTP的GET类型的请求。engine的GET方法包含两个参数,编程使用如下所示:
...
engine.GET("/hello", func(context *gin.Context) {
fmt.Println(context.FullPath())
username := context.Query("name")
fmt.Println(username)
context.Writer.Write([]byte("Hello," + username))
})
...
context.Query(key(string))
:**读取GET请求附带的参数。**参数指定要读取的参数的键值key。
...
engine.POST("/login", func(context *gin.Context) {
fmt.Println(context.FullPath())
username, exist := context.GetPostForm("username")
if exist {
fmt.Println(username)
}
password, exists := context.GetPostForm("pwd")
if exists {
fmt.Println(password)
}
context.Writer.Write([]byte("Hello , " + username))
})
...
context.GetPostForm(key(string))
:**读取POST请求附带的参数。**参数指定要读取的参数的键值key。
它与context.PostForm()
的区别在于:它除了返回读取的数据之外,还会返回一个bool值,表示所读取的参数是否存在。
在项目开发中,通常都是遵循RESTful标准进行接口开发。除了GET、POST以外,还会有DELETE等操作。
比如要执行某个删除操作,会发送DELETE类型的请求,同时需要携带一些操作的参数。比如要删除用户,按照RESTful标准会进行如下所示:
...
// 通过DELETE请求删除一个id为24的用户
// url:http://localhost:8080/user/24
// 在服务端gin中,通过路由的:id来定义一个要删除用户的id变量值,同时使用context.Param进行获取
engine.DELETE("/user/:id", DeleteHandle)
func DeleteHandle(context *gin.Context) {
fmt.Println(context.FullPath())
userID := context.Param("id")
fmt.Println(userID)
context.Writer.Write([]byte("Delete user's id : " + userID))
}
...
context.Param(key(string))
:**读取DELETE请求的参数。**参数指定要读取的参数的键值key。
在实际的项目开发中,均是模块化开发。同一模块内的功能接口,往往会有相同的接口前缀。比如如下所示:
在系统中有用户模块,用户有不同注册、登录、用户信息、
注册:http://localhost:9000/user/register
登录:http://localhost:9000/user/login
用户信息:http://localhost:9000/user/info
删除:http://localhost:9000/user/1001
类似这种接口前缀统一,均属于相同模块的功能接口。可以使用路由组进行分类处理。
在web开发中,**“route”(路由)**是指根据url分配到对应的处理程序。
之所以engine中包含通用型的Handle和分类处理的GET、POST等类型的方法,是因为Engine中有RouterGroup
作为匿名字段。
RouteGroup
可以称之为路由组,在gin中定义为结构体:
type RouterGroup struct {
Handlers HandlersChain
basePath string
engine *Engine
root bool
}
RouterGroup
的作用就是为每一个服务请求提供解析功能,并指定每一个请求对应的处理程序。
gin框架中可以使用路由组来实现对路由的分类。
路由组是engine.Group
返回的一个结构体,用于对请求进行分组。
实例:
engine := gin.Default()
routerGroup := engine.Group("/user")
routerGroup.GET("/register", registerHandle)
routerGroup.GET("/login", loginHandle)
routerGroup.GET("/info", infoHandle)
engine.Run(":9000")
func registerHandle(context *gin.Context) {
fullPath := " 用户注册功能 " + context.FullPath()
fmt.Println(fullPath)
context.Writer.WriteString(fullPath)
}
func loginHandle(context *gin.Context) {
fullPath := " 用户登录功能 " + context.FullPath()
fmt.Println(fullPath)
context.Writer.WriteString(fullPath)
}
func infoHandle(context *gin.Context) {
fullPath := " 信息查看功能 " + context.FullPath()
fmt.Println(fullPath)
context.Writer.WriteString(fullPath)
}
但是如果表单数据较多时,使用PostForm和GetPostForm一次获取一个表单数据,开发效率较慢。
Gin框架提供给开发者表单实体绑定的功能,可以将表单数据与结构体绑定。
gin框架提供了数据结构体和表单提交数据绑定的功能,提高表单数据获取的效率。
实例:
以一个用户注册功能来进行讲解表单实体绑定操作。
用户注册需要提交表单数据,假设注册时表单数据包含三项,分别为:username、phone和password。
先创建UserRegister结构体用于接收表单数据,通过tag
标签的方式设置每个字段对应的form
表单中的属性名,通过binding
属性用于设置属性是否是必须。
type UserRegister struct {
Username string `form:"username" binding:"required"`
Phone string `form:"phone" binding:"required"`
Password string `form:"password" binding:"required"`
}
func main() {
engine := gin.Default()
// http://localhost:8080/hello?name=davie&classes=软件工程
engine.GET("/hello", func(context *gin.Context) {
fmt.Println(context.FullPath())
var student Student
err := context.ShouldBindQuery(&student)
if err != nil {
log.Fatal(err.Error())
}
fmt.Println(student.Name)
fmt.Println(student.Classes)
context.Writer.Write([]byte("hello," + student.Name))
})
engine.Run()
}
type Student struct {
Name string `form:"name"`
Classes string `form:"classes"`
}
context.ShouldBindQuery(&结构体)
:**将GET请求附带的参数传入结构体中。**并返回一个error类型数据。
func main(){
engine := gin.Default()
engine.POST("/register", func(context *gin.Context){
fmt.Println(context.FullPath())
var _register Register
err := context.ShouldBind(&_register)
if err != nil{
log.Fatal(err.Error())
return
}
fmt.Println(_register.UserName)
fmt.Println(_register.Phone)
context.Writer.Write([]byte(_register.UserName + " Register "))
})
engine.Run()
}
type Register struct {
UserName string `form:"name"`
Phone string `form:"phone"`
Password string `form:"pwd"`
}
context.ShouldBind(&结构体)
:**将POST请求附带的参数传入结构体中。**并返回一个error类型数据。
当客户端使用Json格式进行数据提交时,可以采用ShouldBindJson对数据进行绑定并自动解析。
实例:
func main() {
engine := gin.Default()
engine.POST("/addstudent", func(context *gin.Context) {
fmt.Println(context.FullPath())
var person Person
err := context.BindJSON(&person)
if err != nil {
log.Fatal(err.Error())
return
}
fmt.Println("姓名:" + person.Name)
fmt.Println("年龄:", person.Age)
context.Writer.Write([]byte(" 添加记录:" + person.Name))
})
engine.Run()
}
type Person struct {
Name string `form:"name"`
Sex string `form:"sex"`
Age int `form:"age"`
}
context.BindJSON(&结构体)
:**将http请求附带的Json格式数据传入结构体中。**并返回一个error类型数据。
在gin框架中,支持返回给前端多种请求数据格式。
context.Writer.Write(([]byte))
:给前端返回[]byte类型数据。
context.Writer.WriteString((string))
:给前端返回string类型数据。
gin框架中的context.JSON
方法可以将map
类型和struct
结构体类型的数据转换成JSON格式的结构化数据,然后返回给客户端。
...
engine := gin.Default()
engine.GET("/hellojson", func(context *gin.Context) {
fullPath := "请求路径:" + context.FullPath()
fmt.Println(fullPath)
// 第一个参数200表示设置请求返回的状态码
context.JSON(200, map[string]interface{}{
"code": 1,
"message": "OK",
"data": fullPath,
})
})
engine.Run(":9000")
...
//通用请求返回结构体定义
type Response struct {
Code int json:"code"
Message string json:"msg"
Data interface{} json:"data"
}
engine.GET("/jsonstruct", func(context *gin.Context) {
fullPath := "请求路径:" + context.FullPath()
fmt.Println(fullPath)
resp := Response{Code: 1, Message: "Ok", Data: fullPath}
context.JSON(200, &resp)
})
除了JSON格式以外,gin框架还支持返回HTML
格式的数据。可以直接渲染HTML页面。
实例:
package main
import (
"fmt"
"github.com/gin-gonic/gin"
"net/http"
)
func main(){
engine := gin.Default()
//设置html目录
engine.LoadHTMLGlob("./html/*")
// 静态资源路径设置
// 第一个参数是图片所在目录名称
// 第二个参数是图片所在目录相对于项目工程文件的路径
engine.Static("/img", "./img")
engine.GET("/hellohtml", func(context *gin.Context) {
fullPath := "请求路径:" + context.FullPath()
fmt.Println(fullPath)
// 第一个参数是表示设置请求返回的状态码
// 第二个参数是html文件名称
// 第三个参数是向html文件传入的变量
context.HTML(http.StatusOK, "index.html", gin.H{
"fullPath":fullPath,
"title":"gin教程",
})
})
engine.Run()
}
index.html源代码:
<html>
<head>
<title>{{.title}}title>
head>
<h1 align="center">syb的Gin学习h1>
{{.fullPath}}
<br />
<div align="center"><img src="../img/jiandang100zhounian.jpeg">div>
html>
运行结果:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mHRaV0GV-1627695899916)(截屏2021-07-23 下午6.23.56.png)]
在web应用服务中,完整的一个业务处理在技术上包含客户端操作、服务器端处理、返回处理结果给客户端三个步骤。
在实际的业务开发和处理中,会有更负责的业务和需求场景。一个完整的系统可能要包含鉴权认证、权限管理、安全检查、日志记录等多维度的系统支持。
鉴权认证、权限管理、安全检查、日志记录等这些保障和支持系统业务属于全系统的业务,和具体的系统业务没有关联,对于系统中的所有业务都适用。
由此,在业务开发过程中,为了更好的梳理系统架构,可以将上述描述所涉及的一些通用业务单独抽离并进行开发,然后以插件化的形式进行对接。这种方式既保证了系统功能的完整,同时又有效的将具体业务和系统功能进行解耦,并且,还可以达到灵活配置的目的。
这种通用业务独立开发并灵活配置使用的组件,一般称之为**“中间件”**,因为其位于服务器和实际业务处理程序之间。其含义就是相当于在请求和具体的业务逻辑处理之间增加某些操作,这种以额外添加的方式不会影响编码效率,也不会侵入到框架中。中间件的位置和角色示意图如下图所示:
engine.Use(<中间件>)
// 如:
engine.Use(Logger(), Recovery())
根据上文的介绍,可以自己定义实现一个特殊需求的中间件,中间件的类型是函数,有两条标准:
func函数
返回值类型为HandlerFunc
比如,我们自定义一个自己的中间件。在前面所学的内容中,我们在处理请求时,为了方便代码调试,通常都将请求的一些信息打印出来。有了中间件以后,为了避免代码多次重复编写,使用统一的中间件来完成。定义一个名为RequestInfos的中间件,在该中间件中打印请求的path和method。
具体代码实现如下所示:
func RequestInfos() gin.HandlerFunc {
return func(context *gin.Context) {
path := context.FullPath()
method := context.Request.Method
fmt.Println("请求Path:", path)
fmt.Println("请求Method:", method)
}
}
func main() {
engine := gin.Default()
engine.Use(RequestInfos())
engine.GET("/query", func(context *gin.Context) {
context.JSON(200, map[string]interface{}{
"code": 1,
"msg": context.FullPath(),
})
})
engine.Run(":9000")
}
运行程序,能够得到正确的返回JSON格式的数据:
{
"code": 1,
"msg": "/query"
}
context.Next
函数可以将中间件代码的执行顺序一分为二。
context.Next
函数调用之前的代码在请求处理之前之前,当程序执行到context.Next
时,会中断向下执行,转而先去执行具体的业务逻辑,执行完业务逻辑处理函数之后,程序会再次回到context.Next
处,继续执行中间件后续的代码。具体用法如下:
func main() {
engine := gin.Default()
engine.Use(RequestInfos())
engine.GET("/query", func(context *gin.Context) {
fmt.Println("使用中间件")
context.JSON(200, map[string]interface{}{
"code": 1,
"msg": context.FullPath(),
})
})
engine.Run(":9000")
}
func RequestInfos() gin.HandlerFunc {
return func(context *gin.Context) {
path := context.FullPath()
method := context.Request.Method
fmt.Println("请求Path:", path)
fmt.Println("请求Method:", method)
// 将中间件代码的执行顺序一分为二
context.Next()
fmt.Println(context.Writer.Status())
}
}
执行程序,输出结果如下:
请求Path: /query
请求Method: GET
使用中间件
200