Go语言-Gin框架
Gin是一个用Go编写的web微框架,和php的laravel相比的话,更像是一个库,短小精致,灵活,需要根据使用情况,自己组合装载其他依赖扩展。
一:安装:
我这里用 go mod方式,go mod相当于php的composer,node的npm。
1:首先创建一个项目目录,我这里叫:dao
2:开启对mod支持
set GO111MODULE=on //windows export GO111MODULE=on //linux go env -w GO111MODULE=on //或者直接这样,大于等于1.13不用上面那么麻烦GO111MODULE三个参数:
off:无模块支持,go 会从 GOPATH 和 vendor 文件夹寻找包。
on:模块支持,go 会忽略 GOPATH 和 vendor 文件夹,只根据 go.mod下载依赖。
auto:在 $GOPATH/src外面且根目录有 go.mod文件时,开启模块支持。
3:初始化mod,进入dao目录,执行:
go mod init daoprotobuf //建议这里取项目名称,我这里是dao
可以看到,这里生成go.mod文件,打开看下:
module dao go 1.18先不管,然后我们编辑go.mod文件:
在以上目录中执行命令行:
go mod edit -require github.com/gin-gonic/gin@v1.7.7 #v1.7.7版本 go mod edit -require github.com/gin-gonic/gin@latest # 或者执行最新版本
再打开看一下,里面多了一段require:
module dao go 1.18 require github.com/gin-gonic/gin v1.7.7也可以用编辑器直接编辑go.mod。
4:我们把官方的代码搞进去,这里新建一个main.go文件:
package main import ( "github.com/gin-gonic/gin" "net/http" ) func main() { //1.创建路由 r := gin.Default() //2.绑定路由规则,执行的函数 r.GET("/", func(context *gin.Context) { context.String(http.StatusOK, "Hello World!") }) //3.监听端口,默认8080 r.Run(":8080") }
5:执行一下:
go run main.go这里会自动拉取main.go中的包,我这里报错。
可以看到 我这里访问这个路径不通
go env -w GO111MODULE=on go env -w GOPROXY="https://goproxy.io,direct"然后再执行一下依赖:
go mod tidy #整理包,根据项目中的代码增加缺少的module,删除无用的module
执行成功,这时候可以看看go.mod文件,里面多了一些东西,另外多了一个go.sum文件,细心的人可能会问,这些包下载到啥地方去了?
go env这些包在GOPATH目录下,那可不可以移到项目中呢?很简单,执行一下:
go mod vendor再看一下,多了一个目录,所有的依赖都复制过来了。
执行:go run main.go
二:路由分组
userRouter := r.Group("/v1") //分组 userRouter.GET("/", func(context *gin.Context) { context.String(http.StatusOK, "v1-Hello World!") })
三:拆分单个路由
在实际开发中,如果把路由都写在main中,显得很臃肿,为了目录清晰,我们搞一下分拆。
1:新建一个routers目录,在里面创建一个routers.go文件(文件名可以随便取,为了清晰最好统一),编辑routers.go文件
package routers import ( "net/http" "github.com/gin-gonic/gin" ) //首字母大写表示公有方法 func SetRouter() *gin.Engine { r := gin.Default() r.GET("/", func(context *gin.Context) { context.String(http.StatusOK, "Hello World!") }) r.GET("/user/:name", func(c *gin.Context) { name := c.Param("name") c.String(http.StatusOK, "Hello %s", name) }) return r }其实就是我们把main中写的路由移过来,当然要稍微改一下。
然后回到main.go文件:
package main import ( "dao/routers" // 导入我的路由包,routers 是目录名、也是包名 "fmt" ) func main() { r := routers.SetRouter() if err := r.Run(":8080"); err != nil { fmt.Printf("startup service failed, err:%v\n", err) } }
这样就可以了。
四:路由中间件
全局路由中间件,单个路由中间件,分组中间件,三个路由中间件主要看怎么使用,并不是看定义,在所有路由之前注册就是全局中间件。
package routers import ( "fmt" "net/http" "time" "github.com/gin-gonic/gin" ) /** 定义一个中间件方法,获取程序执行时间,返回类型 gin.HandlerFunc */ func MiddleWare() gin.HandlerFunc { return func(c *gin.Context) { start := time.Now() fmt.Println("中间件开始执行了") // 设置变量到Context的key中, 可以通过Get()取 c.Set("exp", "中间件添加的变量") // 程序继续往下走 c.Next() status := c.Writer.Status() // 返回当前请求的HTTP响应状态代码 fmt.Println("中间件执行完毕", status) diffTime := time.Since(start) // 时间差 fmt.Println("time:", diffTime) } } /** 定义一个局部中间件,name 不是李小龙,那么拦截 */ func MiddleTestName() gin.HandlerFunc { return func(c *gin.Context) { name := c.Param("name") if name != "李小龙" { c.Abort() c.JSON(http.StatusUnauthorized, gin.H{"message": "身份验证失败"}) } else { c.JSON(http.StatusOK, gin.H{"message": "身份验证成功"}) c.Next() } } } //首字母大写表示公有方法 func SetRouter() *gin.Engine { r := gin.Default() r.Use(MiddleWare()) // 注册 全局中间件,因为是在路由前注册,所以可以拦截一下路由,否则也不能全局 r.GET("/", func(context *gin.Context) { context.String(http.StatusOK, "Hello World!") }) // 此路由调用 MiddleTestName() 中间件 r.GET("/user/:name", MiddleTestName(), func(c *gin.Context) { name := c.Param("name") exp, _ := c.Get("exp") // 获取上面中间件添加的Context的key,这里返回两个参数 c.String(http.StatusOK, "Hello %s%s", name, exp) }) userRouter := r.Group("/v1") userRouter.Use(MiddleTestName()) // 群组路由中间件 userRouter.GET("/", func(context *gin.Context) { context.String(http.StatusOK, "v1-Hello World!") }) return r }
五:功能模块之控制器
我这里按照MVC模式干,这里相当于控制器代码,把目录再整理一下
我这里先建立两个控制器文件
注意:同一个目录下的文件,package必须相同。这里有个问题,因为package相同,不同文件的方法也必须不同,不然就冲突报错。可以用struct或者interface解决这个问题。
下面在两个控制定义两个相同的方法:
1:GoodsController.go
package controller import "github.com/gin-gonic/gin" type Goods struct{} //指针写法 func (g *Goods) Index(context *gin.Context) { context.JSON(200, gin.H{"msg": "hello goods"}) } //非指针 func (Goods) List(context *gin.Context) { context.JSON(200, gin.H{"msg": "goods list"}) }
2:HomeController.go
package controller import "github.com/gin-gonic/gin" type Home struct{} //指针写法 func (h *Home) Index(context *gin.Context) { context.JSON(200, gin.H{"msg": "hello home"}) } //非指针 func (Home) List(context *gin.Context) { context.JSON(200, gin.H{"msg": "home list"}) }虽然方法名相同,但struct不一样,所以不会传统,然后路由关联:
首先import导入 "dao/app/controller"
r.GET("/goods", (&controller.Goods{}).Index) r.GET("/home", (&controller.Home{}).Index) r.GET("/list", (controller.Home{}).List)
六:DB
1:我这里用go-gorm,地址:https://gorm.io/,GORM 官方支持的数据库类型有: MySQL, PostgreSQL, SQlite, SQL Server
我们把db连接放在lib\database\Db.go,因为寡人用go mod管理包,直接导入就行
package db import ( _ "gorm.io/driver/mysql" #选择数据库驱动,我这里是mysql _ "gorm.io/gorm" )
或者直接命令下载依赖包
go get -u gorm.io/gorm go get -u gorm.io/driver/sqlite
然后执行:
go mod tidy #拉取包 go mod vendor #同步到目录
继续Db.go数据库连接包:
package db import ( "gorm.io/gorm/schema" "gorm.io/driver/mysql" "gorm.io/gorm" ) // 返回数据库连接句柄 func InitDb() *gorm.DB { db, err := gorm.Open(mysql.New(mysql.Config{ DSN: "root:123456@tcp(127.0.0.1:3306)/blog?charset=utf8&parseTime=True&loc=Local", // DSN data source name DefaultStringSize: 256, // string 类型字段的默认长度 DisableDatetimePrecision: true, // 禁用 datetime 精度,MySQL 5.6 之前的数据库不支持 DontSupportRenameIndex: true, // 重命名索引时采用删除并新建的方式,MySQL 5.7 之前的数据库和 MariaDB 不支持重命名索引 DontSupportRenameColumn: true, // 用 `change` 重命名列,MySQL 8 之前的数据库和 MariaDB 不支持重命名列 SkipInitializeWithVersion: false, // 根据当前 MySQL 版本自动配置 }), &gorm.Config{ // 更多设置 https://gorm.io/docs/gorm_config.html NamingStrategy: schema.NamingStrategy{ // 设置这个,导入包 gorm.io/gorm/schema // TablePrefix:"" //表名前缀 SingularTable: true, //不让表名自动加s //NoLowerCase: true, //关闭驼峰表名转下划线 }, }) if err != nil { panic(err) } return db }
2:使用,我这里在控制器调用db
package controller import ( "dao/app/lib/db" "github.com/gin-gonic/gin" ) type Goods struct{} /** * 结构体名称为:驼峰写法,JsNew,对应数据库表:js_new * 字段,首字母大写 */ type JsNew struct { Id int `gorm:"primaryKey" json:"id"` //主键,json 设置别名,要不然输出为大写开头 Title string `json:"title"` IgnoreMe int `gorm:"-"` // 忽略字段 } //指针写法 func (g *Goods) Index(context *gin.Context) { var news []JsNew db.InitDb().Select("id", "title").First(&news) //结构体映射,获取一条记录,更多用户见官方说明 context.JSON(200, gin.H{"msg": news}) }输出:
// 也可以不用结构体映射。Raw原生写mysql,然后Scan映射map结构 news := make([]map[string]interface{}, 0) db.InitDb().Raw("SELECT id,title FROM js_new LIMIT 1").Scan(&news)
如果使用原生sql,查询用:Raw和Scan,写入用Exec。
七:模板
1:设置,这里是全局设置,设置所有的模板都在app/views/*/下
导入os和path两个包,用于设置绝对路径,go mod运行的路径不是golang路径,用相对路径不好使。
import ( "dao/app/controller" "fmt" "net/http" "os" "path/filepath" "time" "github.com/gin-gonic/gin" )设置模板,我这里设置不同的模板在不同的目录,统一都在wiews下,这样好区分。
r := gin.Default() rootViews, _ := os.Getwd() r.Delims("{{", "}}") //自定义模板分隔符,默认这个 r.LoadHTMLGlob(filepath.Join(rootViews, "app/views/**/*")) //模板路径类似这样:
app/views/goods/index.html
app/views/home/index.html
接着上面的控制器写:
func (g *Goods) Index(context *gin.Context) { context.HTML(http.StatusOK, "goods/index.html", gin.H{ "title": "我是 goods目录下的index.html", }) }
模板:
我的模板文件结构:
这里有个关键的地方,要对应起来,否则找不到模板:
2:layout布局
有些公共的模板,比如头部、尾部等等,我们一般单独整,需要的页面加载进来就行了。
编辑 top.html
然后加入到我们的模板文件中中
{{template "layout/top" .}} 后面这个 点,表示当前页面的变量、top.html中也可以使用。
3:加载静态资源
比如js,css,图片等等,首先在路由中设置:
rootViews, _ := os.Getwd() r.Static("/static", filepath.Join(rootViews, "app/static")) // 加载静态资源路径,css,js r.Static("/temp", filepath.Join(rootViews, "app/temp")) // 加载静态资源路径,图片第一个参数为 api标识,也就是我们在html里面就用这个当成文件路径,第二个标识是我这里的文件绝对路径。
那么模板里面:
static/assets/css/amazeui.min.css">
static/assets/css/app.css">
static/assets/css/common.css">