Go的Web框架

​ 框架一直是敏捷开发中的利器,能让开发者很快的上手并做出应用。

​ Go 是一门正在快速增长的编程语言,专为构建简单、快速且可靠的软件而设计。 golang提供的net/http库已经很好了,对于http的协议的实现非常好,基于此再造框架,也不会是难事,因此生态中出现了很多框架。

1. Beego

Beego是一个RESTful HTTP框架,用于快速开发Go应用程序,包括API,Web应用程序和具有集成的Go特定功能(如接口和结构嵌入)的后端服务。

Beego采用Go原生的http请求,goroutine的并发效率应付高并发。

1.1 Bee工具

安装完Beego后,再get下来bee工具。我们使用Bee来帮助我们生成项目结构,运行项目,实现热部署。使用Bin目录下的bee工具。

例如bee new xxx命令,或者是bee run命令进行新建项目和运行项目。

1.2 项目的组织架构

Bee生成的项目的组织架构,是遵循我们的MVC模式的。

QQ图片20210505220654

img

1.3 beego.Run()的流程

我们直接看到main.go文件里面:

package main

import (
_ "QianfengBeegoDemo1/routers"
"github.com/astaxie/beego"
)

func main() {
//1.引入routers包时,只执行其init方法
beego.Run()
}

然后到我们routers的文件下,执行init方法,会进行http路由的配置,下面是默认的路由配置

package routers

import (
"QianfengBeegoDemo1/controllers"
"github.com/astaxie/beego"
)
func init() {
//执行该方法,传入controller
//现在默认/路由,执行主controller
//热部署,修改后自动的进行更新重新部署项目
beego.Router("/", &controllers.MainController{})
}

init方法分析完毕后,程序会继续往下执行,就到了main函数,在main函数中执行: beego.Run()代码。分析一下Run代码的逻辑,在Run方法内部,主要做了几件事:

  • 解析配置文件,也就是我们的app.conf文件,比如端口,应用名称等信息。
  • 检查是否开启session, 如果开启session, 就会初始化一 个session对象。
  • 是否编译模板,beego框架会在项目启动的时候根据配置把views目录下的所有模板进行预编译,然后存放在map中,这样可以有效的提高模板运行的效率,不需要进行多次编译。
  • 监听服务端口。根据app.conf文件中的端口配置,启动监听。
// beego.Run()默认执行配置文件中的端口,可以自己指定
// beego.Run("localhost")
// beego.Run(":8089")
// beego.Run("127.0.0.1:8089")
func Run(params ...string) {
initBeforeHTTPRun()

if len(params) > 0 && params[0] != "" {
strs := strings.Split(params[0], ":")
if len(strs) > 0 && strs[0] != "" {
BConfig.Listen.HTTPAddr = strs[0]
}
if len(strs) > 1 && strs[1] != "" {
BConfig.Listen.HTTPPort, _ = strconv.Atoi(strs[1])
}

BConfig.Listen.Domains = params
}

BeeApp.Run()
}

进入到initBeforeHTTPRun方法,顾名思义是http初始化之前进行运行的函数。初始化了一些钩子hook,有注册的session,模板,默认错误提示的handler

func initBeforeHTTPRun() {
//init hooks
AddAPPStartHook(
registerMime,
registerDefaultErrorHandler,
registerSession,
registerTemplate,
registerAdmin,
registerGzip,
)

for _, hk := range hooks {
if err := hk(); err != nil {
panic(err)
}
}
}

1.4 路由routers设置

与Javaweb开发一致,需要指定http路由与对于controller的关系

  • 基础路由:直接指派某一url对应的请求方法,并在Get/Post方法中编写逻辑
//1.基础路由
beego.Get("/getUserInfo", func(context *context.Context) {
beego.Info("基础路由get方法 getUserInfo")
context.Output.Body([]byte("获取用户信息"))
})

beego.Post("/inputText", func(context *context.Context) {
beego.Info("基础路由post方法 inputText")
context.Output.Body([]byte("上传信息"))
})

beego.Delete("/deleteInfo", func(c *context.Context) {
beego.Info("基础路由delete方法 deleteInfo")
c.ResponseWriter.Write([]byte("删除用户信息"))
})
  • 固定路由:根据浏览器发出的请求,去执行对于controller对应的Get、Post、Delete请求。例如项目默认的beego.Router("/", &controllers.MainController{})
beego.Router("/", &controllers.MainController{})
//2.固定路由,固定的读取到控制器中的get/post/delete方法
//http://localhost:8080/requset
beego.Router("/request", &controllers.MainController{})

在MainController中:

type MainController struct {
beego.Controller
}

//这个controller中的get请求
func (c *MainController) Get() {
//设置数据字段的内容
c.Data["Website"] = "beego.me"
c.Data["Email"] = "astaxie@gmail.com"
//设置模板文件,view下的模板文件
c.TplName = "index.tpl"
}

//设置固定路由的方法
func (c *MainController) Post () {
c.Ctx.Output.Body([]byte("固定路由的 post类型的请求"))
}

func (c *MainController) Delete () {
c.Ctx.Output.Body([]byte("固定路由的 delete类型的请求"))
}
  • 正则路由:固定路由的基础上,支持匹配一定格式的正则表达式。比如:id;username
//操作不同的数据类型
beego.Router("/getUserInfo/:id:int", &controllers.RegController{})
beego.Router("/getUserInfo/:name:string", &controllers.RegController{})

在controller中:正则路由是指可以在进行,自定义正则,fle的路径和后缀切换以及全匹配等操作。

//这个controller中的get请求
func (c *RegController) Get() {
//拿取userId
id := c.Ctx.Input.Param(":id")
c.Ctx.ResponseWriter.Write([]byte(id))

//拿取name
name := c.Ctx.Input.Param(":name")
c.Ctx.ResponseWriter.Write([]byte(name))
}
  • 自定义路由:这个是在开发的时候大部分都是使用固定匹配想要直接执行对应的逻辑控制方法。

    获取requestbody中的数据,需要在app.conf中开启copyrequestbody = true

//4.自定义路由
beego.Router("/getCustom", &controllers.MainController{}, "GET:Get")
beego.Router("/getUserInfoCustom", &controllers.CustomController{}, "GET:GetUserInfo")
//需要在配置文件中开启copyrequestbody = true
beego.Router("/inputUserInfoCustom", &controllers.CustomController{}, "POST:PostUser")

自定义路由的controller实现

//自定义路由的设置
type CustomController struct {
beego.Controller
}

//这个controller中的get请求
func (c *CustomController) GetUserInfo() {
username := c.GetString("username")
userId := 1
c.Ctx.ResponseWriter.Write([]byte("username is :" + username + " And userId is " + strconv.Itoa(userId)))
}

type User struct {
userId int `json:"userId"`
username string `json:"username"`
}

func (c *CustomController) PostUser() {
user := new(User)
data := c.Ctx.Input.RequestBody

err := json.Unmarshal(data, user)
if err != nil {
beego.Error(err.Error())
}
c.Ctx.WriteString("username is :" + user.username + " And userId is " + strconv.Itoa(user.userId))
}

1.5 model层的数据库操作

  • 配置数据库驱动

与原生的api类型,在run前执行数据库的init方法。操作基本与jdbc类似,注册驱动,创建数据库连接等等。

package models

import (
"github.com/astaxie/beego"
"github.com/astaxie/beego/orm"
_ "github.com/go-sql-driver/mysql"
)

func init() {
//1.注册驱动
orm.RegisterDriver("mysql", orm.DRMySQL)
//2.创建连接
user := beego.AppConfig.String("mysqluser")
pwd := beego.AppConfig.String("mysqlupwd")
host := beego.AppConfig.String("host")
port := beego.AppConfig.String("port")
dbname := beego.AppConfig.String("dbname")

dbConn := user + ":" + pwd + "@tcp(" + host + ":" + port + ")/" + dbname + "?charset=utf8"
err := orm.RegisterDataBase("default", "mysql", dbConn)
if err != nil {
beego.Error(err.Error())
return
}
beego.Info(" connection database success!")
}
  • 对于增删改查操作
//插入
func InsertUser(user User){
neworm := orm.NewOrm()

if _, err := neworm.Insert(&user); err != nil {
beego.Error(err.Error())
}
}
//删除、查询、修改操作,对应如下不再赘述
neworm.Delete()
neworm.Read()
neworm.Update()

1.6 session实现和配置

  • 配置文件
#session
session = true
#存在客户端的cookie全称
sessionname = "QianfengBeegoDemo1"
#session有效期
sessioncmaxlifetime = 1800
  • 代码配置
func main() {
beego.SessionConfig{}.SessionOn = true
beego.Run()
}
  • session功能实现
func (c *CustomController) PostUser() {
user := new(User)
data := c.Ctx.Input.RequestBody

err := json.Unmarshal(data, user)
if err != nil {
beego.Error(err.Error())
}
//存储session
c.SetSession("loginuser", user.username)

//删除session
c.DelSession("loginuser")

c.Ctx.WriteString("username is :" + user.username + " And userId is " + strconv.Itoa(user.userId))
}

2. Iris

Iris:全宇宙最快的 Go 语言 Web 框架。完备 MVC 支持,其它特性如下:

  • 支持定制事件的高可扩展性Websocket API
  • 带有GC, 内存 & redis 提供支持的会话
  • 方便的中间件和插件
  • 完整REST API
  • 能定制HTTP错误
  • 热部署

2.1 处理Http请求和路由组设置(不使用这种)

  • 处理http请求

与beego类似,有基础的路由对应关系,和自定义的 方法类型 + url + 处理逻辑

func main() {
//创建app服务,返回*Application
app := iris.New()

//1.基础路由
app.Get("/get", func(context context.Context) {
path := context.Path()
app.Logger().Info(path)
//获取参数
username := context.URLParam("username")
app.Logger().Info(username)

//返回string类型数据
//context.WriteString("hello world")
//返回json格式的数据
context.JSON(iris.Map{"message": "hello word", "requestCode": 200})
})

app.Post("/postJson", func(context context.Context) {
//1.path
path := context.Path()
app.Logger().Info(path)

//2.Json数据解析
person := new(Person)
if err := context.ReadJSON(person); err != nil {
panic(err.Error())
}

//3.返回json格式数据
context.JSON(iris.Map{"message": "success", "Code": 200, "data": person.Username})
})

//2.自定义路由
app.Handle("GET", "/getHandle", func(context context.Context) {
path := context.Path()
app.Logger().Info("ger request url is " + path)
context.JSON(iris.Map{"message": "hello word", "requestCode": 200})
})

app.Handle("POST", "/postHandle", func(context context.Context) {
path := context.Path()
app.Logger().Info("post request url is " + path)

person := new(Person)
if err := context.ReadJSON(person); err != nil {
panic(err.Error())
}
app.Logger().Infof("request param is %v", *person)
context.JSON(map[string]interface{}{
"message": "success",
"Code": 200,
"data": person.Username,
})
})

//端口监听
app.Run(iris.Addr(":8080"), iris.WithoutServerError(iris.ErrServerClosed))
}
  • 路由组设置

与Java的requestmapping一致,iris也有路由分组,使用app.Party来进行分组派发

app.Party("/users", func(context context.Context) {
//去下一级请求看
context.Next()
})

例如localhost:8080/users/这下面的两个请求

//1.Get请求:localhost:8080/users/register
userParty.Get("/register", func(c context.Context) {
app.Logger().Info("用户注册功能")
c.JSON(map[string]interface{}{"code": 200, "msg": "success"})
})

//2.Post请求:localhost:8080/users/input
userParty.Post("/input", func(c context.Context) {
person := new(Person)
if err := c.ReadJSON(person); err != nil {
panic(err.Error())
}
c.JSON(iris.Map{"message": "success", "Code": 200, "data": person.Username})

})

如果在逻辑中再调用context的next,还会再进入到里面一层,这时就会调用路由的Done方法。

//请求结束调用
usersRouter.Done(func(c context.Context) {
c.Application().Logger().Info("到达了最里面一层" + c.Path())
})

usersRouter.Get("/query", func(c context.Context) {
app.Logger().Info("query")
c.JSON(map[string]interface{}{"code": 200, "msg": "success"})
c.Next()
})

2.2 Iris配置文件

一般的数据库配置、redis缓存配置等等,会通过开始进行配置。

{
"app_name": "CmsProject",
"port": "8080",
"static_path": "/manage/static",
"data_base": {
"drive": "mysql",
"host": "127.0.0.1",
"port": "3306",
"user": "root",
"pwd": "yu271400",
"database": "qfCms"
},
"redis": {
"net_work": "tcp",
"addr": "127.0.0.1",
"port": "6379",
"passwword": "",
"prefix": "qfCms_"
},
"mode": "dev"
}
  • 通过代码进行系统配置
app.Configure(iris.WithConfiguration(iris.Configuration{
DisableInterruptHandler: false,
//找不到路由时,重定向到正确路由
DisablePathCorrection: false,

EnablePathEscape: false,
Charset: "utf-8",
}))
  • 通过配置文件进行配置

app.Configure(iris.WithConfiguration(iris.YAML("src/IrisDemo/3-路由组及Iris配置/configs/iris.yml")))

但我们一般使用json文件(config.json)进行配置,得到一个config对象

//初始化服务器配置
func InitConfig() *AppConfig {
var config *AppConfig
file, err := os.Open("xxx/go/src/irisDemo/14-CmsProject/config.json")
if err != nil {
panic(err.Error())
}
//解码器
decoder := json.NewDecoder(file)
err = decoder.Decode(&config)
if err != nil {
panic(err.Error())
}
//config = &AppConfig{}
return config
}
  • 拿取配置文件中的内容

例如:拿取配置文件中的数据库配置进行拼接。

initConfig := config.InitConfig()
if initConfig == nil {
return nil
}

database := initConfig.DataBase

//"root:yu271400@/qfCms?charset=utf8"
dataSourceName := database.User + ":" + database.Pwd + "@tcp(" + database.Host + ")/" + database.Database + "?charset=utf8"

2.3 MVC包的使用

Iris内置了MVC包,方便我们使用。需要我们注册处理器mvc.New(app).Handle(new(CustomController))

func main() {
app := iris.New()
//注册自定义控制器处理请求
mvc.New(app).Handle(new(CustomController))
app.Run(iris.Addr(":8080"), iris.WithoutServerError(iris.ErrServerClosed))
}

路由对应有三种默认匹配请求、请求类型和请求URL自动匹配处理方法、url对应自定义的controller

//自定义的控制器
type CustomController struct{}

//1.默认匹配请求
func(cc *CustomController) Get() mvc.Result{
return mvc.Response{ ContentType:"text/html",}
}
//2.请求类型和请求URL自动匹配处理方法
func (cc *CustomController) GetInfo() mvc.Result{
return mvc.Response{ ContentType:"text/html",}
}

//3.url对应自定义的controller
func (m *CustomController) BeforeActivation(a mvc.BeforeActivation){
a.Handle("GET","/users/info","QueryInfo")
}
//对应处理请求的方法
func (m *CustomController) QueryInfo() mvc.Result{
return mvc.Response{}
}
//4.配置路由组
//mvc.Configure(app.Party("/user"), func(mvc *mvc.Application) {
// mvc.Handle(new(UserController))
// })
type UserController struct{}

func (m *UserController) BeforeActivation(a mvc.BeforeActivation){
a.Handle("GET","/users/info","QueryInfo")
}
//对应处理请求的方法
func (m *UserController) QueryInfo() mvc.Result{
return mvc.Response{}
}

配置路由组,分组后我们一般使用请求类型和请求URL自动匹配处理方法

func main() {
app := iris.New()
//路由组
admin := mvc.New(app.Party("/admin"))
admin.Handle(new(controller.AdminController))
//端口监听
app.Run(iris.Addr(":8080"), iris.WithoutServerError(iris.ErrServerClosed))
}

/**
* 管理员控制器
*/
type AdminController struct {
//iris框架自动为每个请求都绑定上下文对象
Ctx iris.Context

//service层对象
Service service.AdminService

//session对象
Session *sessions.Session
}

/**
* 处理获取管理员总数的路由请求
* 请求类型:Get
* 请求Url:admin/count
*/
func (ac *AdminController) GetCount() mvc.Result {
count, err := ac.Service.GetAdminCount()
if err != nil {
return mvc.Response{
Object: map[string]interface{}{
"status": utils.RECODE_FAIL,
"message": utils.Recode2Text(utils.RESPMSG_ERRORADMINCOUNT),
"count": 0,
},
}
}
return mvc.Response{
Object: map[string]interface{}{
"status": utils.RECODE_OK,
"count": count,
},
}
}

2.4 session的使用(redis缓存)

与前面大同小异,就不再赘述:一般使用redis缓存来配置session

//启用session
sessManager := sessions.New(sessions.Config{
Cookie: "sessioncookie",
Expires: 24 * time.Hour,
})

//获取redis实例
redis := datasource.NewRedis()
//设置session的同步位置为redis
sessManager.UseDatabase(redis)

//操作举例:
//管理员存在 设置session
ac.Session.Set(ADMIN, admin.AdminId)
//从session中获取信息
userByte := ac.Session.Get(ADMIN)
adminId, err := ac.Session.GetInt64(ADMIN)
//删除session,下次需要从新登录
ac.Session.Delete(ADMIN);

2.5 redis配置和使用

从配置文件中,获取配置生成redis实例。在需要时调用,例如设置session数据库时,调用redis := datasource.NewRedis()

//main中对session的操作
//获取redis实例
redis := datasource.NewRedis()
//设置session的同步位置为redis
sessManager.UseDatabase(redis)

/**
* 返回Redis实例
*/
func NewRedis() *redis.Database {

var database *redis.Database

//项目配置
cmsConfig := config.InitConfig()
if cmsConfig != nil {
iris.New().Logger().Info(" hello ")
rd := cmsConfig.Redis
iris.New().Logger().Info(rd)
database = redis.New(service.Config{
Network: rd.NetWork,
Addr: rd.Addr + ":" + rd.Port,
Password: rd.Password,
Database: "",
MaxIdle: 0,
MaxActive: 10,
IdleTimeout: service.DefaultRedisIdleTimeout,
Prefix: rd.Prefix,
})
} else {
iris.New().Logger().Info(" hello error ")
}
return database
}

2.6 xorm数据库

一般我们进行了数据库与实体对象model的映射关系,类似于springdata jpa

//实例化mysql数据库引擎
engine := datasource.NewMysqlEngine()

/**
* 实例化数据库引擎方法:mysql的数据引擎
*/
func NewMysqlEngine() *xorm.Engine {

initConfig := config.InitConfig()
if initConfig == nil {
return nil
}

database := initConfig.DataBase

//"root:yu271400@/qfCms?charset=utf8"

dataSourceName := database.User + ":" + database.Pwd + "@tcp(" + database.Host + ")/" + database.Database + "?charset=utf8"

//数据库引擎
//engine, err := xorm.NewEngine(database.Drive, "root:yu271400@tcp(127.0.0.1:3306)/qfCms?charset=utf8")
engine, err := xorm.NewEngine(database.Drive, dataSourceName)
//engine, err := xorm.NewEngine(database.Drive, database.User+":"+database.Pwd+"@/"+database.Database+"?charset=utf8")

iris.New().Logger().Info(database)

//根据实体创建表(一般不使用)
//err = engine.CreateTables(new(model.Admin))
//同步数据库结构:主要负责对数据结构实体同步更新到数据库表
err = engine.Sync2(new(
model.Permission),
new(model.City),
new(model.Admin),
new(model.User),
new(model.UserOrder),
new(model.Address),
new(model.Shop),
new(model.OrderStatus),
new(model.FoodCategory))
if err != nil {
panic(err.Error())
}

//设置显示sql语句
engine.ShowSQL(true)
engine.SetMaxOpenConns(10)

return engine
}

不过我们实体需要通过json来指定关系,通过xorm来设置字段属性(主键、数据类型等)

//定义管理员结构体
type Admin struct {
//如果field名称为Id,而且类型为int64,并没有定义tag,则会被xorm视为主键,并且拥有自增属性
AdminId int64 `xorm:"pk autoincr" json:"id"` //主键 自增
AdminName string `xorm:"varchar(32)" json:"admin_name"`
CreateTime time.Time `xorm:"DateTime" json:"create_time"`
Status int64 `xorm:"default 0" json:"status"`
Avatar string `xorm:"varchar(255)" json:"avatar"`
Pwd string `xorm:"varchar(255)" json:"pwd"` //管理员密码
CityName string `xorm:"varchar(12)" json:"city_name"` //管理员所在城市名称
CityId int64 `xorm:"index" json:"city_id"`
City *City `xorm:"- <- ->"` //所对应的城市结构体(基础表结构体)
}
  • 数据库增删改查操作、事务操作
//1.ID查询
var person PersonTable
// select * from person_table where id = 1
engine.Id(1).Get(&person)
//查询多个复合主键的情况
engine.Id(core.PK(1,"davie").Get(&user)

// select * from person_table where person_age = 26 and person_sex = 2
engine.Where(" person_age = ? and person_sex = ?", 26, 2).Get(&person1)
//select * from person_table where person_age = 26 and person_sex = 2
err = engine.Where(" person_age = ? ", 26).And("person_sex = ? ", 2).Find(&persons)
//5、原生SQL语句查询支持 like语法
var personsNative []PersonTable
err = engine.SQL(" select * from person_table where person_name like 't%' ").Find(&personsNative)
//三、增加记录操作
personInsert := PersonTable{
PersonName: "Hello",
PersonAge: 18,
PersonSex: 1,
}
rowNum, err := engine.Insert(&personInsert)
fmt.Println(rowNum) //rowNum 受影响的记录条数
fmt.Println()

//四、删除操作
rowNum, err := engine.Delete(&personInsert)
fmt.Println(rowNum) //rowNum 受影响的记录条数
fmt.Println()

//五、更新操作
rowNum, err = engine.Id(7).Update(&personInsert)
fmt.Println(rowNum) //rowNum 受影响的记录条数
fmt.Println()

//六、统计功能count
count, err := engine.Count(new(PersonTable))
fmt.Println("PersonTable表总记录条数:", count)

//七、事务操作
personsArray := []PersonTable{
PersonTable{
PersonName: "Jack",
PersonAge: 28,
PersonSex: 1,
},
PersonTable{
PersonName: "Mali",
PersonAge: 28,
PersonSex: 1,
},
PersonTable{
PersonName: "Ruby",
PersonAge: 28,
PersonSex: 1,
},
}
//开启事务
session := engine.NewSession()
session.Begin()
for i := 0; i < len(personsArray); i++ {
_, err = session.Insert(personsArray[i])
if err != nil {
//有错误回滚
session.Rollback()
session.Close()
}
}
err = session.Commit()
session.Close()
if err != nil {
panic(err.Error())
}

3. Gin框架

Gin:Go 语言编写的 Web 框架,以更好的性能实现类似 Martini 框架的 API。

Gin是一个golang的微框架,封装比较优雅,API友好,源码注释比较明确,已经发布了1.0版本。具有快速灵活,容错方便等特点。

3.1 请求处理和路由分组

与前两者大同小异,这里不再赘述:

  • 基础请求处理
//通用的handle处理
engine.Handle("GET", "/helloHandle", func(context *gin.Context) {
path := context.FullPath()
fmt.Println(path)
name := context.DefaultQuery("name", "hello")
fmt.Println(name)

context.Writer.Write([]byte("hello, " + name))
})

engine.Handle("POST", "/loginHandle", func(context *gin.Context) {
path := context.FullPath()
fmt.Println(path)

name := context.PostForm("username")
pwd := context.PostForm("password")
fmt.Println(pwd)
context.Writer.Write([]byte("hello, " + name))
})

//分类处理
engine.GET("/hello", func(context *gin.Context) {
fmt.Println("请求路径:", context.FullPath())
username := context.Query("username")
fmt.Println(username)
context.Writer.Write([]byte("hello gin\n"))
})

engine.POST("/login", func(context *gin.Context) {
fmt.Println("请求路径:", context.FullPath())

username := context.PostForm("username")
fmt.Println(username)
//可以判断存在
pwd, exist := context.GetPostForm("password")
if exist {
fmt.Println(pwd)
}
context.Writer.Write([]byte("hello gin\n"))
})
  • 接受请求参数和json数据,返回json数据、路由分组
type User struct {
Username string `form:"username"`
Password string `form:"password"`
}

type Result struct {
Code int
Msg string
}
//抽离出来的handle
func loginHandle(context *gin.Context) {
fmt.Println("请求路径:", context.FullPath())

var user User
err := context.ShouldBindJSON(&user)
if err != nil {
log.Fatal(err.Error())
return
}

//3.返回json数据
context.JSON(200, map[string]interface{} {
"code": 1,
"message": "success",
"data": user,
})
context.JSON(200, &Result{
Code: 200,
Msg: "success",
})
}

//2.表单实体绑定
engine.GET("/helloInfo", func(context *gin.Context) {
fmt.Println("请求路径:", context.FullPath())
var user User
err := context.ShouldBindQuery(&user)
if err != nil {
log.Fatal(err.Error())
return
}
context.Writer.Write([]byte("hello gin\n"))
})
//post请求接受json数据
engine.POST("/loginInfo", loginHandle)

//4.路由组
userGroup := engine.Group("/user")

userGroup.POST("/loginGroup", loginHandle)

3.2 中间件编写使用

​ 在实际的业务开发和处理中,会有更负责的业务和需求场景。一个完整的系统可能要包含鉴权认证、权限管理、安全检查、日志记录等多维度的系统支持。鉴权认证、权限管理、安全检查、日志记录等这些保障和支持系统业务属于全系统的业务,和具体的系统业务没有关联,对于系统中的所有业务都适用,类似于我们spring中的aop

​ 在业务开发过程中,将通用业务单独抽离并进行开发,然后以插件化的形式进行对接。中间件其位于服务器和实际业务处理程序之间。其含义就是相当于在请求和具体的业务逻辑处理之间增加某些操作,这种以额外添加的方式不会影响编码效率,也不会侵入到框架中。中间件的位置和角色示意图如下图所示:

img

  • engine自带的中间件

我们知道我们使用engine := gin.Default()来创建Gin引擎,实际上默认自带两个中间件,打印日志和错误提示。

engine.Use(Logger(), Recovery())

// Use attaches a global middleware to the router. ie. the middleware attached though Use() will be
// included in the handlers chain for every single request. Even 404, 405, static files...
// For example, this is the right place for a logger or error management middleware.
func (engine *Engine) Use(middleware ...HandlerFunc) IRoutes {
engine.RouterGroup.Use(middleware...)
engine.rebuild404Handlers()
engine.rebuild405Handlers()
return engine
}

middleware就是中间件,其类型是HandlerFunc,这个中间件的类型是HandlerFunc其实就是一个函数,绑定了context。

// HandlerFunc defines the handler used by gin middleware as return value.
type HandlerFunc func(*Context)

// Logger instances a Logger middleware that will write the logs to gin.DefaultWriter.
// By default gin.DefaultWriter = os.Stdout.
func Logger() HandlerFunc {
return LoggerWithConfig(LoggerConfig{})
}
  • 自定义中间件

根据上文的介绍,可以自己定义实现一个名为RequestInfos的中间件,在该中间件中打印请求的path和method。

//自定义中间件
//HandlerFunc就是中间件类型
func RequestInfos() gin.HandlerFunc {
return func(context *gin.Context) {
path := context.FullPath()
method := context.Request.Method
fmt.Println("path: " + path + "\nmethod: " + method)
context.Next()
}
}

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

//配置使用中间件打印信息
engine.Use(RequestInfos())

engine.GET("/helloMiddleware", func(context *gin.Context) {
var user User
err := context.ShouldBindQuery(&user)
if err != nil {
log.Fatal(err.Error())
return
}
context.JSON(200, map[string]interface{} {
"code" : 200,
"data" : map[string]interface{} {
"username" : user.Username,
"msg" : "success",
},
})
})

//默认8080
if err := engine.Run(":8080"); err != nil {
log.Fatal(err.Error())
}
}

还可以为某一特定请求配置中间件

//只为这一个请求配置中间件
engine.GET("/queryMiddleware", RequestInfos2(), func(context *gin.Context) {
var user User
err := context.ShouldBindQuery(&user)
if err != nil {
log.Fatal(err.Error())
return
}
context.JSON(200, map[string]interface{} {
"code" : 200,
"data" : map[string]interface{} {
"username" : user.Username,
"msg" : "success",
},
})
})
  • context.Next函数

进入到后面一层,即业务逻辑层进行处理请求,然后还可以等待处理完后,再后续执行context.Next后面的代码。

例如:去执行业务处理逻辑,即后面handle中的处理请求,然后再回中间件中打印状态码。

func RequestInfos2() gin.HandlerFunc {
return func(context *gin.Context) {
path := context.FullPath()
method := context.Request.Method
fmt.Println("path: " + path + "\nmethod: " + method)

//去执行业务处理逻辑,即后面handle中的处理请求
context.Next()

//执行完后再来这里
var user User
if err := context.ShouldBindQuery(&user); err != nil {
log.Fatal(err.Error())
}
fmt.Printf("状态码为:%v\n", context.Writer.Status())
}
}

img

3.3 数据库操作

内部没有集成数据库的框架,所以就使用原生的database/sql下面的数据库操作。

也可以使用我们的xorm来进行数据库操作,操作与Iris上述一致。

3.4 配置文件

配置文件的解析与Iris一致,独立一个Config文件,里面有initConfig方法,使用文件I/O解析路径下的json文件

3.5 跨域访问

类似与SpringBoot中的@CrossOrigin注解,实现跨越功能。例如,在gin接口项目中,前端使用nodejs开发,运行在8080端口,我们访问的应用首页,是: http://localhost:8080。后端服务器的监听端口为8090。一个端口数8080,一个是8090,两者端口不同,因此按照规定,发生了跨域访问。

如上文所述,前端vue开发的功能,使用axios发送POST登录请求。 在请求时发生了跨域访问,为了安全起见,首先发起一个请求测试一下此次访问是否安全,这种测试的请求类型为OPTIONS,又称之为options嗅探, 同时在header中会带上origin,用来判断是否有跨域请求权限。然后服务器相应Access-Control-Allow-Origin的值,该值会与浏览器的origin值进行匹配,如果能够匹配通过,则表示有跨域访问的权限。跨域访问权限检查通过,会正式发送POST请求。

例如:前端端口发送一个8090服务器端的请求,是跨域的,所以先发送一个options请求,看响应结果是否与origin中的目标值一致。

img

  • 中间件来拦截跨域请求

我们采取的方案是在服务端,编写跨域访问的中间件,并设置生效,从而解决跨域访问的问题。跨域访问的中间件编程实现如下所示:

//跨越访问中间件
func Cors() gin.HandlerFunc {
return func(context *gin.Context) {
method := context.Request.Method
origin := context.Request.Header.Get("Origin")

//请求头中添加access-control-allow-origin、access-control-allow-headers这两个key
var headerKeys []string
for key, _ := range context.Request.Header {
headerKeys = append(headerKeys, key)
}
headerStr := strings.Join(headerKeys, ",")
//加上
if headerStr != "" {
fmt.Sprintf("access-control-allow-origin、access-control-allow-headers, %s", headerStr)
} else {
headerStr = "access-control-allow-origin、access-control-allow-headers"
}

//如果有跨域,则设置服务器端访问的返回的响应头里面设置对应的权限
if origin != "" {
//响应头设置,两种函数都是响应头写入,后面是覆盖写入
context.Writer.Header().Set("Access-Control-Allow-Origin", "*")
context.Header("Access-Control-Allow-Origin", "*")
context.Header("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE, UPDATE")
context.Header("Access-Control-Allow-Headers", "Authorization, Content-Length")
context.Header("Access-Control-Expose-Headers", "Content-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers")
context.Header("Access-Control-Max-Age", "172800")
context.Header("Access-Control-Allow-Credentials","false")
context.Set("content-type", "application/json") //设置返回格式为json
}

//捕捉到跨域请求
if method == "OPTIONS" {
context.JSON(http.StatusOK, "Options Request!")
}

//然后会去我们的业务逻辑代码出进行逻辑提交
//例如是去执行我们的Post请求
context.Next()
}
}

下图中我们可以看到,拦截到了option跨域请求,设置了对应的响应头header字段,例如Access-Control-Allow-Origin,Access-Control-Allow-Methods。

1

允许跨域访问,然后去执行我们的原的post请求

img