风离不摆烂学习日志 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 做数据校验的
common.InitData()
初始化 mysql 数据 如果没有表结构 则生成表结构 有则 return 具体逻辑有待研究
多线程执行储存操作日志
// 操作日志中间件处理日志时没有将日志发送到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总结
- 初始化读取配置文件 config.yml
- 初始化日志记录
- 初始化Mysql连接操作
- 初始化权限控制插件
- 初始化字段校验插件
- 如果没有导入数据初始化数据 生成表结构和数据
- 启动3个goroutine 来记录操作日志
- 初始化所有路由和中间件(日志 跨域 JWT…)