- 项目地址:github.com/testerxiaod…
- 参考开源项目:github.com/HammerCloth…
1. 项目开发痛点
在上一篇文章中我引入了gorm/gen工具解决了快速生成数据表daoa方法的痛点,这次来解决一下项目依赖的痛点,还有就是调整项目目录结构,更符合社区的规定。
2. 项目目录设计
github上有一个golang项目的标准结构github.com/golang-stan… 依照社区标准,golang项目采用面向包的设计和架构分层,依照此标准,我对项目结构进行了一定调整。调整之后的目录是:
-
cmd
- app
- app.go // 应用程序启动代码
- generate.go // gen工具代码
- app
-
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
- app
-
scripts // 脚本
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
函数