手把手带你从0到1封装Gin框架:02 框架规划与路由重构

879 阅读3分钟

项目源码

Github

前言

我们在上篇对项目做了初始化,并启动了一个简单的HTTP服务,在文末也提到了实际中的项目更为复杂,也更加庞大,需要有一个好的规划去实现,这样才能提高扩展性和可维护性

所以我们在这里先规划一下大概的一个框架:

➜  eve_api git:(main) ✗ tree -d
.
├── app
│   ├── api ---- 接口
│   │   ├── admin
│   │   └── app
│   ├── event
│   │   ├── event ---- 事件实体
│   │   └── listener ---- 事件监听器
│   ├── middleware ---- 中间件
│   ├── model ---- 数据库模型
│   ├── request ---- 请求实体
│   ├── response ---- 返回实体
│   ├── route ---- 路由
│   ├── service ---- service
│   └── task ---- 定时任务
├── cmd ---- 命令
├── internal ---- 外部库封装
│   ├── bootstrap ---- 启动初始化
│   ├── config ---- 配置文件
│   ├── event ---- 时间模块
│   ├── global ---- 全局变量
│   ├── logger ---- 日志
│   ├── mysql ---- 数据库
│   ├── redis ---- redis
│   ├── server ---- http服务
│   │   ├── middleware
│   │   └── route
│   ├── tool ---- 工具
│   └── validator ---- 验证器
├── test ---- 单元测试
└── tmp ---- 临时文件目录
    └── log ---- 日志文件

如上所示,我们先对项目做了结构上的规划,然后再陆续填充内容

本篇先对上篇实现的简单HTTP服务进行重构

HTTP服务重构

上篇中我们实现的HTTP服务如下:

r := gin.New()

r.GET("/", func(c *gin.Context) {
	c.JSON(200, gin.H{"Message": "Hello World"})
})

err := r.Run(":8082")
if err != nil {
	return
}

可以看到是分三步

  • 实例化一个对象
  • 注册路由
  • 启动

那我们也可以封装一个http包,包中包含http对象以及实现http对象的两个方法:GenRouterRun

新建internal/server/http.go文件:

package server

import (
	"context"
	"eve/internal/server/route"
	"fmt"
	"github.com/gin-gonic/gin"
	"log"
	"net/http"
	"os"
	"os/signal"
	"syscall"
	"time"
)

type Http struct {
	engine *gin.Engine // gin框架的核心组件
	port   string      // http服务端口号
}

// New 实例化http对象
func New() *Http {
	entity := &Http{
		engine: gin.New(),
		port:   ":8082",
	}

	return entity
}

// GenRouter 注册路由
// 这里需要调用外部的route库,所以先定义一个Interface,并在外部实现Interface
func (h *Http) GenRouter(r route.RouterGeneratorInterface) {
	r.AddRoute(h.engine)
}

// Run 启动服务
func (h *Http) Run() {
	srv := &http.Server{
		Addr:    h.port,
		Handler: h.engine,
	}

	go func() {
		err := srv.ListenAndServe()
		if err != nil {
			fmt.Println(err)
		}
	}()

	h.ListenSignal(srv)
}

func (h *Http) ListenSignal(srv *http.Server) {
	quit := make(chan os.Signal, 1)

	signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)

	<-quit
	log.Println("shutdown server")

	ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
	defer cancel()
	if err := srv.Shutdown(ctx); err != nil {
		log.Fatal("server shutdown ")
	}
}

新增internal/server/route/route.go文件:

package route

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

type RouterGeneratorInterface interface {
	AddRoute(server *gin.Engine)
}

新增app/route/route.go文件:

package route

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

type AppRouter struct{}

func (*AppRouter) AddRoute(e *gin.Engine) {
	e.GET("/ping", func(c *gin.Context) {
		c.JSON(200, gin.H{"message": "ping"})
	})

	e.GET("/pong", func(c *gin.Context) {
		c.JSON(200, gin.H{"message": "pong"})
	})
}

func New() *AppRouter {
	return &AppRouter{}
}

然后再修改我们的启动文件cmd/start.go

// Package cmd /*
package cmd

import (
	"eve/app/route"
	"eve/internal/server"
	"github.com/spf13/cobra"
)

// startCmd represents the start command
var startCmd = &cobra.Command{
	Use:   "start",
	Short: "start serve",
	Long:  `start serve`,
	Run: func(cmd *cobra.Command, args []string) {
		run()
	},
}

func init() {
	rootCmd.AddCommand(startCmd)
}

func run() {
	http := server.New()
	http.GenRouter(route.New())
	http.Run()
}

启动服务:

➜  eve_api git:(main) ✗ go run main.go start
[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
 - using env:   export GIN_MODE=release
 - using code:  gin.SetMode(gin.ReleaseMode)

[GIN-debug] GET    /ping                     --> eve/app/route.(*AppRouter).AddRoute.func1 (1 handlers)
[GIN-debug] GET    /pong                     --> eve/app/route.(*AppRouter).AddRoute.func2 (1 handlers)

可以看到重写的http服务已经生效

commit-hash: e8400fc

路由分组

再看app/route/route.go文件AddRoute方法:

func (*AppRouter) AddRoute(e *gin.Engine) {
	e.GET("/ping", func(c *gin.Context) {
		c.JSON(200, gin.H{"message": "ping"})
	})

	e.GET("/pong", func(c *gin.Context) {
		c.JSON(200, gin.H{"message": "pong"})
	})
}

它是在AddRoute方法中直接注册路由的,单模块的话是没有问题的,但是正常情况下我们可能需要对客户端、管理后台等提供接口,多模块的情况下这个写法就显得杂乱,这就需要用到Gin框架的路由分组功能:

srv := gin.New()
appGroup := srv.Group("/app")
{
	appGroup.GET("/a", func(c *gin.Context) {
		c.JSON(200, gin.H{"message": "a"})
	})
	
	appGroup.GET("/b", func(c *gin.Context) {
		c.JSON(200, gin.H{"message": "b"})
	})
}

adminGroup := srv.Group("/admin")
{
	adminGroup.POST("/login", func(c *gin.Context) {
		c.JSON(200, gin.H{"message": "login"})
	})
}

可以看到我们创建了两个分组,分别对应/app/admin,然后在各个分组内再注册各自的接口,看起来就更清晰,也方便后期添加不同的中间件

那么我们重新修改app/route/route.go文件:

package route

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

type AppRouter struct{}

func (*AppRouter) AddRoute(e *gin.Engine) {
	genAdminRouter(e.Group("/admin"))
	genAppRouter(e.Group("/app"))
}

func New() *AppRouter {
	return &AppRouter{}
}

新增app/route/admin.go文件:

package route

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

func genAdminRouter(rg *gin.RouterGroup) {
	rg.GET("/a", func(c *gin.Context) {
		c.JSON(200, gin.H{"message": "a"})
	})
}

新增app/route/app.go文件:

package route

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

func genAppRouter(rg *gin.RouterGroup) {

	rg.GET("/b", func(c *gin.Context) {
		c.JSON(200, gin.H{"message": "b"})
	})
}

可以看到我们把路由注册放到了appadmin两个文件中,这样看起来就更清晰,后边维护起来也更加方便

总结

  • 对项目整体结构做了规划
  • 重构了http服务
  • 实现了路由分组

commit-hash: 2b17fb7