• Go Web项目学习之项目结构


    风离不摆烂学习日志 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 # 接收前端请求的数据结构
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    项目分析

    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!")
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86

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

    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)
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42

    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{},
    	)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    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)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    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
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56

    main.go总结

    1. 初始化读取配置文件 config.yml
    2. 初始化日志记录
    3. 初始化Mysql连接操作
    4. 初始化权限控制插件
    5. 初始化字段校验插件
    6. 如果没有导入数据初始化数据 生成表结构和数据
    7. 启动3个goroutine 来记录操作日志
    8. 初始化所有路由和中间件(日志 跨域 JWT…)
  • 相关阅读:
    C++11——“=default“和“=delete“函数特性
    图像灰度映射方案对比总结
    【OS】操作系统课程笔记 第四章 中断和处理机调度
    字段映射 mapStruct lombok
    Linux系统奇事【free显示内存95%但是top却看不到谁用】
    第二&三章-项目运行环境&项目经理角色
    MySQL的存储过程
    uniapp开发小程序-pc端小程序下载文件
    Perforce 使用建议
    【数据结构】树状数组C++详解
  • 原文地址:https://blog.csdn.net/qq_49186423/article/details/127987694