GoWeb之Gin项目脚手架搭建

一、Gin框架简单使用

文档地址:​​https://www.kancloud.cn/shuangdeyu/gin_book/949411​​​(看云手册)和​​https://gin-gonic.com/zh-cn/docs/​​(官方文档)

1.1. Gin项目简介

Gin 是一个 go 写的 web 框架,具有高性能的优点。Go世界里面最流行的Web框架,基于​​httprouter​​​开发的Web框架。 ​​中文文档​​齐全,简单易用的轻量级框架。

1.2. 简单示例

package main

import "github.com/gin-gonic/gin"

func main() {
r := gin.Default()
r.GET("/ping", func(c *gin.Context) {
c.JSON(200, gin.H{
"message": "pong",
})
})
r.Run() // 监听并在 0.0.0.0:8080 上启动服务
}

是不是很简单,但是如何我们写一个项目的API接口服务,不可能将所有的接口和配置信息中间件都写到一个文件里面,这时候我们就需要考虑下项目结构。好的项目结构可以方便我们扩展和使用。

二、项目目录规范

.
├── api 存放接口的目录
│ └── home.go
├── cache 缓冲相关的目录
│ ├── cache.go
│ └── redis_cache.go
├── config 项目配置地址
│ ├── config.go
│ └── config.toml
├── global 全局变量redis、Mongodb连接
│ └── global.go
├── go.mod
├── go.sum
├── Dockerfile 镜像构建文件
├── logs 存放日志目录部署需要配置为其他目录
│ ├── system.log -> system.log.20210606.log
│ ├── system.log.20210605.log
│ └── system.log.20210606.log
├── middleware 中间件目录
│ ├── core_middle.go
│ └── log_middle.go
├── models 实体映射
│ └── home.go
├── repository 实体针对数据操作
│ └── home_repository.go
├── router.go 路由配置
├── server.go 启动配置
├── server_other.go 非win系统启动配置
├── server_win.go win系统启动配置
├── service 业务操作
│ └── home_serivce.go
├── utils 工具目录,消息,工具方法
│ ├── message
│ └── tools
│ └── type_utils.go
└── view 返回或者接受的实体
└── home_view.go

项目的包结构整理不好,很容易出现循环引用的问题,对于golang的新手很不友好啊!看了很多开源项目目录结构很多是把所有的go文件都放到根目录中,看的脑壳疼。

三、Gin启动和路由

3.1. 服务启动

这里我们考虑golang项目开发的时候很多是在win开发,但是在linux系统部署,考虑以后需要平滑重启(平滑重启就是升级server不停止业务),我们先引入一个组件:endless。

go get -u github.com/fvbock/endless

win服务启动配置:

// +build windows  

package main

import (
"github.com/gin-gonic/gin"
"net/http"
"time"
)

func InitServer(address string, router *gin.Engine) Server {
gin.SetMode(gin.ReleaseMode)
return &http.Server{
Addr: address,
Handler: router,
ReadTimeout: 10 * time.Second,
WriteTimeout: 10 * time.Second,
MaxHeaderBytes: 1 << 20,
}
}

非win服务启动:

// +build !windows

package main

import (
"github.com/fvbock/endless"
"github.com/gin-gonic/gin"
"time"
)

func InitServer(address string, router *gin.Engine) Server {
gin.SetMode(gin.ReleaseMode)
s := endless.NewServer(address, router)
s.ReadHeaderTimeout = 10 * time.Millisecond
s.WriteTimeout = 10 * time.Second
s.MaxHeaderBytes = 1 << 20
return s
}
​// +build !windows​

最后我们将这个启动的调用方法main方法中:

package main

import (
"context"
"github.com/gin-gonic/gin"
"go.mongodb.org/mongo-driver/mongo"
"go_init/global"
)

type Server interface {
ListenAndServe() error
}

func RunServer() {
// 关闭连接
defer func(Mongo *mongo.Client, ctx context.Context) {
err := Mongo.Disconnect(ctx)
if err != nil {
panic("关闭连接失败")
}
}(global.Mongo, context.TODO())
// 初始化路由
router := InitRouter(gin.Default())
InitServer(global.Config.Server.Part, router).ListenAndServe().Error()
}

func main() {
RunServer()
}

3.2. 路由配置

​InitRouter(gin.Default())​​handler​
import (
"github.com/gin-gonic/gin"
"go_init/api"
"go_init/middleware"
)

func InitRouter(engine *gin.Engine) *gin.Engine {
engine.Use(middleware.CorsMiddle())
engine.Use(middleware.LogMiddle())
group := engine.Group("api")
{
HomeRouter(group)
}
return engine
}



func HomeRouter(g *gin.RouterGroup) {
homeController := api.NewHomeController()
home := g.Group("home")
{
home.POST("add", homeController.HomeAdd)
home.GET("list", homeController.HomeIndex)
}
}

3.3. 跨域中间件

现在项目基本上都是前后端分离的项目,跨域是必不可少的。

import (
"github.com/gin-gonic/gin"
"net/http"
)

func CorsMiddle() gin.HandlerFunc {
return func(c *gin.Context) {
method := c.Request.Method
origin := c.Request.Header.Get("Origin")
c.Header("Access-Control-Allow-Origin", origin)
c.Header("Access-Control-Allow-Headers", "Content-Type,AccessToken,X-CSRF-Token, Authorization, Token,X-Token,X-User-Id")
c.Header("Access-Control-Allow-Methods", "POST, GET, OPTIONS,DELETE,PUT")
c.Header("Access-Control-Expose-Headers", "Content-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers, Content-Type")
c.Header("Access-Control-Allow-Credentials", "true")

// 放行所有OPTIONS方法
if method == "OPTIONS" {
c.AbortWithStatus(http.StatusNoContent)
}
// 处理请求
c.Next()
}
}

关于日志的中间件,我们后面在介绍

四、整合数据库之MongoDB

这里数据我选择了MongoDB,也可以选择MySQL,PG等等。

go get -u go.mongodb.org/mongo-driver
​go mod tidy​

这里我先添加全局的global文件,存放我们服务运行的时候需要一直保持的连接配置等信息

var (
Config config.Config // 配置类
Mongo *mongo.Client // mongodb的连接
DB *mongo.Database // mongodb指定库的连接
Redis *cache.RedisPool // redis的连接池
)

看一下Mongodb的初始化方法:

func InitMongoDB() (*mongo.Client, *mongo.Database) {
m := Config.Mongodb
clientOptions := options.Client().ApplyURI(m.Link())
client, err := mongo.Connect(context.TODO(), clientOptions)
if err != nil {
panic("连接MongoDB失败")
}
err = client.Ping(context.TODO(), nil)
if err != nil {
panic("Ping => 测试连接失败")
}
return client, client.Database(m.Database)
}

最后在global文件中增加一个init方法。用于调用初始化Mongodb的方法。

func init() {
Config = config.InitConfig()
Mongo, DB = InitMongoDB()
Redis = InitRedisPool()
}

五、整合日志之Logrus

golang的日志框架很多,选一个用的顺手的就好,就选Logrus就好,其他的整合大同小异。

先添加依赖包:

go get -u github.com/sirupsen/logrus
go get -u github.com/lestrrat-go/file-rotatelogs
go get -u github.com/rifflock/lfshook

这里涉及日志的格式格式输出、日志写入文件和日志分割等。

import (
"fmt"
"github.com/gin-gonic/gin"
rotatelogs "github.com/lestrrat-go/file-rotatelogs"
"github.com/rifflock/lfshook"
"github.com/sirupsen/logrus"
"go_init/global"
"os"
"path"
"time"
)

func LogMiddle() gin.HandlerFunc {
logCfg := global.Config.Logger
// 日志文件
fileName := path.Join(logCfg.FilePath, logCfg.FileName)
// 写入文件
src, err := os.OpenFile(fileName, os.O_APPEND|os.O_WRONLY, os.ModeAppend)
if err != nil {
fmt.Println("Error", err)
}
// 实例化
logger := logrus.New()
//设置日志级别
logger.SetLevel(logrus.DebugLevel)
//设置输出
logger.Out = src
// 设置 rotatelogs
logWriter, err := rotatelogs.New(
// 分割后的文件名称
fileName+".%Y%m%d.log",
// 生成软链,指向最新日志文件
rotatelogs.WithLinkName(fileName),
// 设置最大保存时间(7天)
rotatelogs.WithMaxAge(7*24*time.Hour),
// 设置日志切割时间间隔(1天)
rotatelogs.WithRotationTime(24*time.Hour),
)

writeMap := lfshook.WriterMap{
logrus.InfoLevel: logWriter,
logrus.FatalLevel: logWriter,
logrus.DebugLevel: logWriter,
logrus.WarnLevel: logWriter,
logrus.ErrorLevel: logWriter,
logrus.PanicLevel: logWriter,
}

logger.AddHook(lfshook.NewHook(writeMap, &logrus.JSONFormatter{
TimestampFormat: "2006-01-02 15:04:05",
}))


return func(c *gin.Context) {
//开始时间
startTime := time.Now()
//处理请求
c.Next()
//结束时间
endTime := time.Now()
// 执行时间
latencyTime := endTime.Sub(startTime)
//请求方式
reqMethod := c.Request.Method
//请求路由
reqUrl := c.Request.RequestURI
//状态码
statusCode := c.Writer.Status()
//请求ip
clientIP := c.ClientIP()

// 日志格式
logger.WithFields(logrus.Fields{
"status_code": statusCode,
"latency_time": latencyTime,
"client_ip": clientIP,
"req_method": reqMethod,
"req_uri": reqUrl,
}).Info()
}
}

六、整合缓冲之Redis

我们这里整合redis需要对redis的一些方法做一下简单封装

go get -u github.com/gomodule/redigo

初始化的redis连接池也放在global文件中:

func InitRedisPool() *cache.RedisPool {
r := Config.Redis
return &cache.RedisPool{
Pool: redis.Pool{
Dial: func() (redis.Conn, error) {
return redis.Dial(r.Dial,
fmt.Sprintf("%s:%s", r.Ip, r.Part),
redis.DialPassword(r.Password),
redis.DialDatabase(r.Database))
},
MaxIdle: r.MaxIdle,
MaxActive: r.MaxActive,
},
}
}

下面是对于redis简单的封装

import (
"encoding/json"
"fmt"
"github.com/gomodule/redigo/redis"
)

var (
SET = "SET"
EXISTS = "EXISTS"
GET = "GET"
EXPIRE = "EXPIRE"
DEL = "DEL"
KEYS = "KEYS"
)

type RedisPool struct {
Pool redis.Pool
}

// Conn 获取数据连接
func (p *RedisPool) conn() redis.Conn {
return p.Pool.Get()
}

// Set 设置数据
func (p *RedisPool) Set(key string, value interface{}, time int) (bool, error) {
conn := p.conn()
data, err := json.Marshal(value)
if err != nil {
return false, err
}
if _, err = conn.Do(SET, key, data); err != nil {
return false, err
}
_, _ = conn.Do(EXPIRE, key, time)
return true, nil
}

// Exists 是否存在
func (p *RedisPool) Exists(key string) bool {
conn := p.conn()
flag, err := redis.Bool(conn.Do(EXISTS, key))
if err != nil {
return false
}
return flag
}

// Get 获取数据
func (p *RedisPool) Get(key string) ([]byte, error) {
conn := p.conn()
data, err := redis.Bytes(conn.Do(GET, key))
if err != nil {
return nil, err
}
return data, err
}

// Delete 删除
func (p *RedisPool) Delete(key string) (bool, error) {
conn := p.conn()
return redis.Bool(conn.Do(DEL, key))
}

// BlurryDel 模糊删除
func (p *RedisPool) BlurryDel(key string) error {
conn := p.conn()
keys, err := redis.Strings(conn.Do(KEYS, fmt.Sprintf("*%s*", key)))
if err != nil {
return err
}
for _, key := range keys {
p.Delete(key)
}
return nil
}

简单使用:选判断缓冲是否存在,存在就从缓冲中拿到数据后返回,不存在就去查询,查询之后在放入缓冲中。

// Select 获取所有数据
func (h *HomeService) Select() ([]models.Home, error) {
data, err := global.Redis.Get(CacheStudent)
if err != nil {
all, err := h.HomeRepository.FindAll()
if err != nil {
return nil, err
}
set, err := global.Redis.Set(CacheStudent, all, 1000000)
if err != nil {
logrus.Infof("发生错误:%s, %v", err.Error(), set)
}
return all, nil
}
var result []models.Home
err = json.Unmarshal(data, &result)
return result, err
}

七、镜像构建

golang项目构建docker镜像也很好用,下面我贴一下构建文件。可以参考一下。

FROM golang:1.16.3 as builder

# 设置容器环境变量
ENV GOPROXY=https://goproxy.cn
ENV GOOS=linux
ENV GOARCH=amd64
ENV CGO_ENABLED=0

COPY . /app

WORKDIR /app

RUN go get -u github.com/fvbock/endless

RUN go build -ldflags="-s -w" -installsuffix cgo -o go_init

FROM alpine as prod

# 开放端口
EXPOSE 9091

# 创建一个目录
RUN mkdir -p /app/logs

RUN chmod 666 /app/logs

RUN ls /app

COPY --from=builder /app/go_init /app

COPY --from=builder /app/config.toml /app

# 启动
CMD ["/app/go_init", "--config=/app/config.toml"]

七、项目地址

对你有帮助,记得点个star,感谢!