风离不摆烂学习日志 Day4 — Go Web项目学习之项目结构

创建项目配置代理 下载加速

go 包代理 GOPROXY=https://goproxy.cn,direct

本项目学习自:

[github.com](https://github.com/gnimli/go-web-mini)

项目结构分层

├─common # casbin mysql zap validator 等公共资源
├─config # viper读取配置
├─controller # controller层,响应路由请求的方法
├─dto # 返回给前端的数据结构
├─middleware # 中间件
├─model # 结构体模型
├─repository # 数据库操作
├─response # 常用返回封装,如Success、Fail
├─routes # 所有路由
├─util # 工具方法
└─vo # 接收前端请求的数据结构

项目分析

main.go

package main

import (
   "context"
   "fmt"
   "go-web-mini/common"
   "go-web-mini/config"
   "go-web-mini/middleware"
   "go-web-mini/repository"
   "go-web-mini/routes"
   "net/http"
   "os"
   "os/signal"
   "syscall"
   "time"
)

func main() {

   // 加载配置文件到全局配置结构体
   config.InitConfig()

   // 初始化日志
   common.InitLogger()

   // 初始化数据库(mysql)
   common.InitMysql()

   // 初始化casbin策略管理器
   common.InitCasbinEnforcer()

   // 初始化Validator数据校验
   common.InitValidate()

   // 初始化mysql数据
   common.InitData()

   // 操作日志中间件处理日志时没有将日志发送到rabbitmq或者kafka中, 而是发送到了channel中
   // 这里开启3个goroutine处理channel将日志记录到数据库
   logRepository := repository.NewOperationLogRepository()
   for i := 0; i < 3; i++ {
      go logRepository.SaveOperationLogChannel(middleware.OperationLogChan)
   }

   // 注册所有路由
   r := routes.InitRoutes()

   host := "localhost"
   port := config.Conf.System.Port

   srv := &http.Server{
      Addr:    fmt.Sprintf("%s:%d", host, port),
      Handler: r,
   }

   // Initializing the server in a goroutine so that
   // it won't block the graceful shutdown handling below
   go func() {
      if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
         common.Log.Fatalf("listen: %s\n", err)
      }
   }()

   common.Log.Info(fmt.Sprintf("Server is running at %s:%d/%s", host, port, config.Conf.System.UrlPathPrefix))

   // Wait for interrupt signal to gracefully shutdown the server with
   // a timeout of 5 seconds.
   quit := make(chan os.Signal)
   // kill (no param) default send syscall.SIGTERM
   // kill -2 is syscall.SIGINT
   // kill -9 is syscall.SIGKILL but can't be catch, so don't need add it
   signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
   <-quit
   common.Log.Info("Shutting down server...")

   // The context is used to inform the server it has 5 seconds to finish
   // the request it is currently handling
   ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
   defer cancel()
   if err := srv.Shutdown(ctx); err != nil {
      common.Log.Fatal("Server forced to shutdown:", err)
   }

   common.Log.Info("Server exiting!")

}

main.go

config.InitConfig()

首先我尝试 在config里使用 log包下的日志 打印 报这个错误

package go-web-mini
imports go-web-mini/common
imports go-web-mini/config
imports go-web-mini/common: import cycle not allowed

即不能循环依赖

关于项目中用到的这个包

"github.com/spf13/viper"

viper库
viper 是一个配置解决方案,拥有丰富的特性:

支持 JSON/TOML/YAML/HCL/envfile/Java properties 等多种格式的配置文件;
可以设置监听配置文件的修改,修改时自动加载新的配置;
从环境变量、命令行选项和io.Reader中读取配置;
从远程配置系统中读取和监听修改,如 etcd/Consul;
代码逻辑中显示设置键值。
————————————————
版权声明:本文为CSDN博主「小象裤衩」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/weixin_52000204/article/details/123450735

敲黑板 支持热更新

InitConfig 读取根目录下的配置文件 config.yml

// 设置读取配置信息
func InitConfig() {
	workDir, err := os.Getwd() // os.Getwd(): 为动态路径,你终端cd到哪里,它就取当前的dir(等价于./),用于做小工具

	if err != nil {
		panic(fmt.Errorf("读取应用目录失败:%s \n", err))
	}

	viper.SetConfigName("config")
	viper.SetConfigType("yml")
	viper.AddConfigPath(workDir + "./")

	//common.Log.Info("当前读取配置信息路径为", workDir)  //会有循环依赖错误 日志 依赖这个包
	println("当前读取配置信息路径为", workDir)

	// 读取配置信息
	err = viper.ReadInConfig()

	// 热更新配置
	viper.WatchConfig()
	viper.OnConfigChange(func(e fsnotify.Event) {
		// 将读取的配置信息保存至全局变量Conf
		if err := viper.Unmarshal(Conf); err != nil {
			panic(fmt.Errorf("初始化配置文件失败:%s \n", err))
		}
		// 读取rsa key
		Conf.System.RSAPublicBytes = util.RSAReadKeyFromFile(Conf.System.RSAPublicKey)
		Conf.System.RSAPrivateBytes = util.RSAReadKeyFromFile(Conf.System.RSAPrivateKey)
	})

	if err != nil {
		panic(fmt.Errorf("读取配置文件失败:%s \n", err))
	}
	// 将读取的配置信息保存至全局变量Conf
	if err := viper.Unmarshal(Conf); err != nil {
		panic(fmt.Errorf("初始化配置文件失败:%s \n", err))
	}
	// 读取rsa key
	Conf.System.RSAPublicBytes = util.RSAReadKeyFromFile(Conf.System.RSAPublicKey)
	Conf.System.RSAPrivateBytes = util.RSAReadKeyFromFile(Conf.System.RSAPrivateKey)

}

common.InitLogger()

配置日志输出位置和加载日志插件 Zap

Zap是非常快的、结构化的,分日志级别的Go日志库。

common.InitMysql()

从配置文件中读取 Mysql配置并把表 关联到相应的结构体上

database.go

package common

import (
	"fmt"
	"go-web-mini/config"
	"go-web-mini/model"
	"gorm.io/driver/mysql"
	"gorm.io/gorm"
)

// 全局mysql数据库变量
var DB *gorm.DB

// 初始化mysql数据库
func InitMysql() {
	dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=%s&collation=%s&%s",
		config.Conf.Mysql.Username,
		config.Conf.Mysql.Password,
		config.Conf.Mysql.Host,
		config.Conf.Mysql.Port,
		config.Conf.Mysql.Database,
		config.Conf.Mysql.Charset,
		config.Conf.Mysql.Collation,
		config.Conf.Mysql.Query,
	)
	// 隐藏密码
	showDsn := fmt.Sprintf(
		"%s:******@tcp(%s:%d)/%s?charset=%s&collation=%s&%s",
		config.Conf.Mysql.Username,
		config.Conf.Mysql.Host,
		config.Conf.Mysql.Port,
		config.Conf.Mysql.Database,
		config.Conf.Mysql.Charset,
		config.Conf.Mysql.Collation,
		config.Conf.Mysql.Query,
	)
	//Log.Info("数据库连接DSN: ", showDsn)
	db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
		// 禁用外键(指定外键时不会在mysql创建真实的外键约束)
		DisableForeignKeyConstraintWhenMigrating: true,
		//// 指定表前缀
		//NamingStrategy: schema.NamingStrategy{
		//	TablePrefix: config.Conf.Mysql.TablePrefix + "_",
		//},
	})
	if err != nil {
		Log.Panicf("初始化mysql数据库异常: %v", err)
		panic(fmt.Errorf("初始化mysql数据库异常: %v", err))
	}

	// 开启mysql日志
	if config.Conf.Mysql.LogMode {
		db.Debug()
	}
	// 全局DB赋值
	DB = db
	// 自动迁移表结构
	dbAutoMigrate()
	Log.Infof("初始化mysql数据库完成! dsn: %s", showDsn)
}

// 自动迁移表结构
func dbAutoMigrate() {
	DB.AutoMigrate(
		&model.User{},
		&model.Role{},
		&model.Menu{},
		&model.Api{},
		&model.OperationLog{},
	)
}

common.InitCasbinEnforcer()

1、Casbin 基本介绍

Casbin是一个强大的、高效的开源访问控制框架,网上的说明一大堆,我就不抄了,简单来说,以RABC举例,就是设立控制模型后。在需要判断用户有没有权限能访问的地方,使用Enforce()这个函数就会返回用户能否访问,就这么简单。

2、为什么要使用Casbin

如果没有这个框架,那么你需要一大堆的关联数据库查询才能知道这个用户能否访问,这个在gin的中间件时是不好的方法。所以,我们使用casbin,在前后端分离中,前端每次只要传一个包含用户的JWT,后端就知道当前访问的API是否有权限。另外,Casbin支持多语言,这样在策略不用改变的情况下,别的语言也可以使用。

common.InitValidate()

gin自定义数据校验器:github.com/go-playground/validator/v10 类似于java 的 @Valid 做数据校验的

image-20221122173906131

common.InitData()

初始化 mysql 数据 如果没有表结构 则生成表结构 有则 return 具体逻辑有待研究

image-20221122174533908

多线程执行储存操作日志

// 操作日志中间件处理日志时没有将日志发送到rabbitmq或者kafka中, 而是发送到了channel中
// 这里开启3个goroutine处理channel将日志记录到数据库
logRepository := repository.NewOperationLogRepository()
for i := 0; i < 3; i++ {
   go logRepository.SaveOperationLogChannel(middleware.OperationLogChan)
}

routes.InitRoutes()

初始化所有路由

package routes

import (
	"fmt"
	"github.com/gin-gonic/gin"
	"go-web-mini/common"
	"go-web-mini/config"
	"go-web-mini/middleware"
	"time"
)

// 初始化
func InitRoutes() *gin.Engine {
	//设置模式
	gin.SetMode(config.Conf.System.Mode)

	// 创建带有默认中间件的路由:
	// 日志与恢复中间件
	r := gin.Default()
	// 创建不带中间件的路由:
	// r := gin.New()
	// r.Use(gin.Recovery())

	// 启用限流中间件
	// 默认每50毫秒填充一个令牌,最多填充200个
	fillInterval := time.Duration(config.Conf.RateLimit.FillInterval)
	capacity := config.Conf.RateLimit.Capacity
	r.Use(middleware.RateLimitMiddleware(time.Millisecond*fillInterval, capacity))

	// 启用全局跨域中间件
	r.Use(middleware.CORSMiddleware())

	// 启用操作日志中间件
	r.Use(middleware.OperationLogMiddleware())

	// 初始化JWT认证中间件
	authMiddleware, err := middleware.InitAuth()
	if err != nil {
		common.Log.Panicf("初始化JWT中间件失败:%v", err)
		panic(fmt.Sprintf("初始化JWT中间件失败:%v", err))
	}

	// 路由分组
	apiGroup := r.Group("/" + config.Conf.System.UrlPathPrefix)

	// 注册路由
	InitBaseRoutes(apiGroup, authMiddleware)         // 注册基础路由, 不需要jwt认证中间件,不需要casbin中间件
	InitUserRoutes(apiGroup, authMiddleware)         // 注册用户路由, jwt认证中间件,casbin鉴权中间件
	InitRoleRoutes(apiGroup, authMiddleware)         // 注册角色路由, jwt认证中间件,casbin鉴权中间件
	InitMenuRoutes(apiGroup, authMiddleware)         // 注册菜单路由, jwt认证中间件,casbin鉴权中间件
	InitApiRoutes(apiGroup, authMiddleware)          // 注册接口路由, jwt认证中间件,casbin鉴权中间件
	InitOperationLogRoutes(apiGroup, authMiddleware) // 注册操作日志路由, jwt认证中间件,casbin鉴权中间件

	common.Log.Info("初始化路由完成!")
	return r
}

main.go总结

  1. 初始化读取配置文件 config.yml
  2. 初始化日志记录
  3. 初始化Mysql连接操作
  4. 初始化权限控制插件
  5. 初始化字段校验插件
  6. 如果没有导入数据初始化数据 生成表结构和数据
  7. 启动3个goroutine 来记录操作日志
  8. 初始化所有路由和中间件(日志 跨域 JWT…)