faker-douyin-10. 项目目录整理以及依赖注入

384 阅读4分钟

1. 项目开发痛点

在上一篇文章中我引入了gorm/gen工具解决了快速生成数据表daoa方法的痛点,这次来解决一下项目依赖的痛点,还有就是调整项目目录结构,更符合社区的规定。

2. 项目目录设计

github上有一个golang项目的标准结构github.com/golang-stan… 依照社区标准,golang项目采用面向包的设计和架构分层,依照此标准,我对项目结构进行了一定调整。调整之后的目录是:

  • cmd

    • app
      • app.go // 应用程序启动代码
    • generate.go // gen工具代码
  • config

    • config.yaml
    • config.dev.yaml // 配置文件,区分测试环境和线上环境
  • internal

    • app
      • api // controller层

      • config // 所有配置类以及配置文件解析函数

      • consts // 全局常量

      • dao // Mysql和Redis连接初始化,Mysql表的dal操作

      • log // 日志的初始化:gorm日志以及app日志

      • middleware // 中间件

      • model

        • entity // 实体类
        • dto // UI层输入输出
        • common // ginResponse以及time的解析
      • router // handler函数集合

      • service // service层

    • pkg
  • scripts // 脚本

WechatIMG73.jpeg

api目录暂时没有考虑,因为是一个单体应用,不需要对外暴露proto文件

3. 依赖注入方式选择

没有整理依赖之前的烦恼:

  • 过多的全局变量:*gorm.DB,*global.Config,全局变量在整个项目游离,不安全。
  • 手写了很多的构造函数,还有就是原项目初始化时用了很多初始化动作。改了一个服务类的依赖之后,又需要重新改写初始化函数。

依赖注入方式:

  • 运行时依赖注入:dig、fx
  • 代码生成注入:wire

运行时依赖注入会降低程序性能,而且只有在程序运行期间才能发现错误。代码生成的依赖注入能在编译期间发现问题,我选择google的wire,项目地址:github.com/google/wire

4. 梳理整个项目配置类的依赖

最底层的依赖是整个服务的配置类config.Config

type Config struct {
   Server   Server   `mapstructure:"server"`
   Log      Log      `mapstructure:"log"`
   Mysql    Mysql    `mapstructure:"mysql"`
   Redis    Redis    `mapstructure:"redis"`
   RabbitMq RabbitMq `mapstructure:"rabbitmq"`
   Ssh      Ssh      `mapstructure:"ssh"`
   Ftp      Ftp      `mapstructure:"ftp"`
}

它由NewConfig函数生成,主要就是用viper解析配置文件到Config类,并创建日志目录,为了快速验证我写死了绝对路径,之后再修改。

func NewConfig() *Config {
   var configFile string
   flag.StringVar(&configFile, "config", "", "")
   flag.Parse()
   viper.AutomaticEnv()
   replacer := strings.NewReplacer(".", "_")
   viper.SetEnvKeyReplacer(replacer)
   viper.SetConfigType("yaml")
   if configFile != "" {
      viper.SetConfigFile(configFile)
   } else {
      viper.AddConfigPath("/Users/cengdong/GolandProjects/faker-douyin/config")
      viper.SetConfigName("config")
   }
   conf := &Config{}
   if err := viper.ReadInConfig(); err != nil {
      panic(err)
   }
   if err := viper.Unmarshal(conf); err != nil {
      panic(err)
   }
   if conf.Server.WorkDir == "" {
      pwd, err := os.Getwd()
      if err != nil {
         panic(errors.Wrap(err, "init config: get current dir"))
      }
      conf.Server.WorkDir, _ = filepath.Abs(pwd)
   } else {
      workDir, err := filepath.Abs(conf.Server.WorkDir)
      if err != nil {
         panic(err)
      }
      conf.Server.WorkDir = workDir
   }
   normalizeDir := func(path *string, subDir string) {
      if *path == "" {
         *path = filepath.Join(conf.Server.WorkDir, subDir)
      } else {
         temp, err := filepath.Abs(*path)
         if err != nil {
            panic(err)
         }
         *path = temp
      }
   }
   normalizeDir(&conf.Server.LogDir, "logs")

   initDirectory(conf)
   mode = conf.Server.Mode
   return conf
}

func initDirectory(conf *Config) {
   mkdirFunc := func(dir string, err error) error {
      if err == nil {
         if _, err = os.Stat(dir); os.IsNotExist(err) {
            err = os.MkdirAll(dir, os.ModePerm)
         }
      }
      return err
   }
   err := mkdirFunc(conf.Server.LogDir, nil)
   if err != nil {
      panic(err)
   }
}

var mode string

// IsDev 用于日志器的配置
func IsDev() bool {
   return mode == "debug"
}

依赖Config类生成其他的类

func NewLogger(conf *config.Config) *zap.Logger
func NewGormLogger(conf *config.Config, zapLogger *zap.Logger) *GormLogger
func NewGormMysql(config *config.Config, gormLogger *log.GormLogger) *Query
func NewRedisClient(config *config.Config) *redis.Client
func NewDataRepo(db *Query, rdb *redis.Client) *DataRepo
func NewFtpClient(config *config.Config) *FtpClient
func NewFfmpegClient(config *config.Config) *FfmpegClient
var ProviderSet = wire.NewSet(wire.Struct(new(UserServiceImpl), "*"), wire.Struct(new(CommentServiceImpl), "*"), wire.Struct(new(VideoServiceImpl), "*"),
   wire.Bind(new(UserService), new(*UserServiceImpl)), wire.Bind(new(VideoService), new(*VideoServiceImpl)), wire.Bind(new(CommentService), new(*CommentServiceImpl)))
var ProviderSet = wire.NewSet(wire.Struct(new(CommentController), "*"), wire.Struct(new(UserController), "*"), wire.Struct(new(VideoController), "*"))
var ProviderSet = wire.NewSet(wire.Struct(new(Router), "*"))
func InitGinEngine(router *router.Router, config *config.Config) *gin.Engine
var InjectorSet = wire.NewSet(wire.Struct(new(App), "*"))

这两天为了解决依赖注入的问题参考了不少开源项目以及博客,首先是*gorm.DB,之前是一个全局变量存在,现在定义了一个DataRepo类,可以操作mysql以及redis,并将其作为依赖注入到service层去,之前的ftp连接以及ssh连接也是全局变量,现在注入到具体需要该连接的某一个服务中。

// DataRepo 数据获取源
type DataRepo struct {
   Db  *Query
   Rdb *redis.Client
}
type VideoServiceImpl struct {
   DataRepo       *dao.DataRepo
   FtpClient      *utils.FtpClient
   FfmpegClient   *utils.FfmpegClient
   UserService    UserService
   CommentService CommentService
}

5. 依赖注入代码

internal/app目录下新建app.go文件

//go:build wireinject
// +build wireinject

package app

import (
   v1 "faker-douyin/internal/app/api/v1"
   "faker-douyin/internal/app/config"
   "faker-douyin/internal/app/dao"
   log "faker-douyin/internal/app/log"
   "faker-douyin/internal/app/router"
   "faker-douyin/internal/app/service"
   "faker-douyin/internal/pkg/utils"
   "github.com/google/wire"
)

func CreateApp() (*App, error) {
   wire.Build(
      config.NewConfig,
      log.NewLogger,
      log.NewGormLogger,
      dao.ProviderSet,
      utils.NewFtpClient,
      utils.NewFfmpegClient,
      service.ProviderSet,
      v1.ProviderSet,
      router.ProviderSet,
      InitGinEngine,
      InjectorSet,
   )
   return new(App), nil
}

具体的依赖注入知识,可以参考几篇博客

6. 遇到的问题

项目重构的坑:

  • 对于数据流向的问题没太大争议,但是对于一些日志,中间件等感觉放在哪里都不太合适,目前项目的结构也还存在问题,之后还会微调

依赖注入中间踩了不少坑:

  • 指针类型和具体类型不一样
  • 依赖查找不能递归(应该依赖*config.Config,而不是里面具体的类)
  • 接口类型不能直接参与依赖构建,需要wire.Bind函数

7. 参考博客