道者编程

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",
	})
}

模板:

{{ define "goods/index.html" }}
<html>
    <h1>
        {{.title}}
    h1>

html>
{{ end }}

我的模板文件结构:


这里有个关键的地方,要对应起来,否则找不到模板:


 

2:layout布局

有些公共的模板,比如头部、尾部等等,我们一般单独整,需要的页面加载进来就行了。


编辑 top.html

{{define "layout/top"}}
头部
{{ end }}

然后加入到我们的模板文件中中

{{ define "goods/index.html" }}

{{template "layout/top" .}}
<html>
    <h1>
        {{.title}}
    h1>

html>
{{ end }}

{{template "layout/top" .}} 后面这个 点,表示当前页面的变量、top.html中也可以使用。


<li {{if eq .meta.categoryId "-1"}} class="active" {{end}}><a href="/" >首页a>li>
{{range $index,$value := .comuln}}
<li {{if eq $.meta.categoryId $value }} class="active" {{end}} ><a href="/c/{{$value}}">{{$index}}a>li>
{{end}}
这个例子有if和遍历,range遍历的时候要注意,range中如果直接 .使用控制器传递过来的变量,这里会报错,因为rang是一个闭包,如果要使用,前面加 $,比如这里的红色部分

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

 



最新评论:
我要评论:

看不清楚