💎一站式轻松地调用各大LLM模型接口,支持GPT4、智谱、星火、月之暗面及文生图 广告
[TOC] ## 参考 示例中的源码: https://github.com/lupguo/go-ddd-sample 但是示例中的应用为单个应用 多应用的源码查考:https://github.com/victorsteven/food-app-server ## 图片智能识别检索应用 架构图 ![](https://img.kancloud.cn/33/49/33493c7f2f97c5637d690319336599e1_1384x1740.png) * 领域层:领域层包含**上传图片、图片短链、图片标签、检索图片**四个实体对象,实体需要通过聚合,提供能力给到应用层使用;同时,需要通过仓储接口抽象好实体持久化存储能力; * 基础层:包含日志功能、Mysql数据库存储功能、Redis缓存等基础层能力; * 接口层:图片Post接收,图片参数识别,调用应用层图片上传应用(采用严格分层,否则接口层可以直接调用领域层); * 应用层:图片上传应用,调用图片领域层;其他类似的保护图片缩放、短链生成、图片检索等应用功能; > 本架构采用从环境变量中读取配置, 使用 `github.com/joho/godotenv` 库 ## 目录划分 ``` . ├── application // [必须]DDD - 应用层 ├── cmd // [必须]参考project-layout,存放CMD │ ├── imgupload // 命令行上传图片 │ └── imgupload_server // 命令行启动Httpd服务 ├── deployments // 参考project-layout,服务部署相关 ├── docs // 参考project-layout,文档相关 ├── domain // [必须]DDD - 领域层 │ ├── entity // - 领域实体 │ ├── repository // - 领域仓储接口 │ ├── service // - 领域服务,多个实体的能力聚合 │ └── valobj // - 领域值对象 ├── infrastructure // [必须]DDD - 基础层 │ └── persistence // - 数据库持久层 ├── interfaces // [必须]DDD - 接口层 │ └── api // - RESTful API接口对外暴露 ├── pkg // [可选]参考project-layout,项目包,还有internel等目录结构,依据服务实际情况考虑 └── tests // [可选]参考project-layout,测试相关 └── mock ``` ## 领域层 - domain ### 领域实体 实体是领域中非常核心的组成,在我们的应用中,直接定义成`entity.UploadImg` <details> <summary>domain/entity/uploadimg.go</summary> ``` package entity import ( "os" "time" ) // UploadImg 上传图片实体 type UploadImg struct { ID uint64 `gorm:"primary_key;auto_increment" json:"id"` Name string `gorm:"size:100;not null;" json:"name"` Path string `gorm:"size:100;" json:"path"` Url string `gorm:"-" json:"url"` Content os.File `gorm:"-" json:"-"` CreatedAt time.Time `gorm:"default:CURRENT_TIMESTAMP" json:"created_at"` UpdatedAt time.Time `gorm:"default:CURRENT_TIMESTAMP" json:"updated_at"` DeletedAt *time.Time `json:"-"` } ``` </details> <br/> ### 领域仓储接口 仓储接口定义了一组方法,用于定义领域实体的与持久化存储相关的操作,实现该接口的持久化存储,都可以操作该领域实体(entity.UploadImg) <details> <summary>domain/repository/uploadimg_repo.go</summary> ``` package repository import "github.com/lupguo/go-ddd-sample/domain/entity" // UploadImgRepo 图片上传相关仓储接口,只要实现了该接口,则可以操作Domain领域实体 type UploadImgRepo interface { Save(*entity.UploadImg) (*entity.UploadImg, error) Get(uint64) (*entity.UploadImg, error) GetAll() ([]entity.UploadImg, error) Delete(uint64) error } ``` </details> <br/> ## 基础层 - infrastructure ### 总仓储结构体 这里定义了总的仓储结构体:`type Repositories struct{}`,其内包含领域层的仓储接口和DB实例,可以方便持久层; 同时通过`gorm.AutoMigrate()`来实现DB的同步 <details> <summary>infrastructure/persistence/db.go</summary> ``` package persistence import ( "github.com/go-sql-driver/mysql" "github.com/jinzhu/gorm" "github.com/lupguo/go-ddd-sample/domain/entity" "github.com/lupguo/go-ddd-sample/domain/repository" "time" ) // Repositories 总仓储机构提,包含多个领域仓储接口,以及一个DB实例 type Repositories struct { UploadImg repository.UploadImgRepo db *gorm.DB } // NewRepositories 初始化所有域的总仓储实例,将实例通过依赖注入方式,将DB实例注入到领域层 func NewRepositories(DbDriver, DbUser, DbPassword, DbPort, DbHost, DbName string) (*Repositories, error) { cfg := &mysql.Config{ User: DbUser, Passwd: DbPassword, Net: "tcp", Addr: DbHost + ":" + DbPort, DBName: DbName, Collation: "utf8mb4_general_ci", Loc: time.FixedZone("Asia/Shanghai", 8*60*60), Timeout: time.Second, ReadTimeout: 30 * time.Second, WriteTimeout: 30 * time.Second, AllowNativePasswords: true, ParseTime: true, } // DBSource := fmt.Sprintf("%s:%s@%s(%s:%s)/%s?charset=utf8mb4&parseTime=True&loc=Local", DbUser, DbPassword, "tcp", DbHost, DbPort, DbName) db, err := gorm.Open(DbDriver, cfg.FormatDSN()) if err != nil { return nil, err } db.LogMode(true) // 初始化总仓储实例 return &Repositories{ UploadImg: NewUploadImgPersis(db), db: db, }, nil } // closes the database connection func (s *Repositories) Close() error { return s.db.Close() } // This migrate all tables func (s *Repositories) AutoMigrate() error { return s.db.AutoMigrate(&entity.UploadImg{}).Error } ``` </details> <br/> ### 上传图片领域仓储接口的实现 persistence.UploadImgPersis结构体实现了领域层的仓储接口,后续只要匹配领域层仓储接口即可以匹配操作领域中的能力 <details> <summary>infrastructure/persistence/uploadimg_persis.go</summary> ``` // persistence 通过依赖注入方式,实现领域对持久化存储的控制反转(IOC) package persistence import ( "errors" "github.com/jinzhu/gorm" "github.com/lupguo/go-ddd-sample/domain/entity" ) // UploadImgPersis 上传图片的持久化结构体 type UploadImgPersis struct { db *gorm.DB } // NewUploadImgPersis 创建上传图片DB存储实例 func NewUploadImgPersis(db *gorm.DB) *UploadImgPersis { return &UploadImgPersis{db} } // Save 保存一张上传图片 func (p *UploadImgPersis) Save(img *entity.UploadImg) (*entity.UploadImg, error) { err := p.db.Create(img).Error if err != nil { return nil, err } return img, nil } // Get 获取一张上传图片 func (p *UploadImgPersis) Get(id uint64) (*entity.UploadImg, error) { var img entity.UploadImg err := p.db.Where("id = ?", id).Take(&img).Error if gorm.IsRecordNotFoundError(err) { return nil, errors.New("upload image not found") } if err != nil { return nil, err } return &img, nil } // GetAll 获取一组上传图片 func (p *UploadImgPersis) GetAll() ([]entity.UploadImg, error) { var imgs []entity.UploadImg err := p.db.Limit(50).Order("created_at desc").Find(&imgs).Error if gorm.IsRecordNotFoundError(err) { return nil, errors.New("upload images not found") } if err != nil { return nil, err } return imgs, nil } // Delete 删除一张图片 func (p *UploadImgPersis) Delete(id uint64) error { var img entity.UploadImg err := p.db.Where("id = ?", id).Delete(&img).Error if err != nil { return err } return nil } ``` </details> <br/> ## 应用层 - application 可能涉及自身或远程领域服务调用 ### 上传图片应用 - 应用层比较薄,主要做业务流程实现,需要对服务进行组合与编排,另外应用层做到承上启下,即对上接口层暴露实例化应用的方法,方便把仓储实现给接管过来,并通过调用具体的仓储实现完成业务 - 上传图片应用,这块统一采用_app结尾,这可可以统一标识应用层文件 - UploadImgApp.db实际是一个仓储层接口在编写应用层时候,看不到任何DB具体实现,同时也看不到任何接口层的入参信息,这就是接口抽象的优势,层之间隔离的比较彻底 - 在应用层还会做一些额外的处理,比如这里的rawUrl()函数组合,非常通用的功能可以考虑放入pkg包内 <details> <summary>application/uploadimg_app.go</summary> ``` package application import ( "github.com/lupguo/go-ddd-sample/domain/entity" "github.com/lupguo/go-ddd-sample/domain/repository" "os" ) type UploadImgAppIer interface { Save(*entity.UploadImg) (*entity.UploadImg, error) Get(uint64) (*entity.UploadImg, error) GetAll() ([]entity.UploadImg, error) Delete(uint64) error } type UploadImgApp struct { db repository.UploadImgRepo } // NewUploadImgApp 初始化上传图片应用 func NewUploadImgApp(db repository.UploadImgRepo) *UploadImgApp { return &UploadImgApp{db: db} } func (app *UploadImgApp) Save(img *entity.UploadImg) (*entity.UploadImg, error) { img, err := app.db.Save(img) if err != nil { return nil, err } img.Url = rawUrl(img.Path) return img, nil } func (app *UploadImgApp) Get(id uint64) (*entity.UploadImg, error) { img, err := app.db.Get(id) if err != nil { return nil, err } img.Url = rawUrl(img.Path) return img, nil } func (app *UploadImgApp) GetAll() ([]entity.UploadImg, error) { imgs, err := app.db.GetAll() if err != nil { return nil, err } for i, img := range imgs { imgs[i].Url = rawUrl(img.Path) } return imgs, nil } func (app *UploadImgApp) Delete(id uint64) error { return app.db.Delete(id) } func rawUrl(path string) string { return os.Getenv("IMAGE_DOMAIN") + os.Getenv("LISTEN_PORT") + path } ``` </details> <br/> ## 接口层 - interfaces 接口层是整体架构的最上层,用于处理信息的输入和输出,这里我们通过_handler来作为统一后缀标识接口层处理文件 <details> <summary>interfaces/api/handler/uploadimg_handler.go</summary> ``` package handler import ( "errors" "fmt" "github.com/labstack/echo" "github.com/lupguo/go-ddd-sample/application" "github.com/lupguo/go-ddd-sample/domain/entity" "io" "io/ioutil" "math/rand" "net/http" "os" "path" "strconv" "time" ) // UploadImgHandle 上传处理 func UploadImgHandle(c echo.Context) error { callback := c.QueryParam("callback") var content struct { Response string `json:"response"` Timestamp time.Time `json:"timestamp"` Random int `json:"random"` } content.Response = "Sent via JSONP" content.Timestamp = time.Now().UTC() content.Random = rand.Intn(1000) return c.JSONP(http.StatusOK, callback, &content) } // UploadImgHandler 图片上传接口层处理 type UploadImgHandler struct { uploadImgApp application.UploadImgAppIer } // NewUploadImgHandler 初始化一个图片上传接口 func NewUploadImgHandler(app application.UploadImgAppIer) *UploadImgHandler { return &UploadImgHandler{uploadImgApp: app} } func (h *UploadImgHandler) Save(c echo.Context) error { forms, err := c.MultipartForm() if err != nil { return err } var imgs []*entity.UploadImg for _, file := range forms.File["upload"] { fo, err := file.Open() if err != nil { continue } // file storage path _, err = os.Stat(os.Getenv("IMAGE_STORAGE")) if err != nil { if os.IsNotExist(err) { if err := os.MkdirAll(os.Getenv("IMAGE_STORAGE"), 0755); err != nil { return err } } else { return err } } // file save ext := path.Ext(file.Filename) tempFile, err := ioutil.TempFile(os.Getenv("IMAGE_STORAGE"), "img_*"+ext) if err != nil { return err } _, err = io.Copy(tempFile, fo) if err != nil { return err } // upload uploadImg := entity.UploadImg{ Name: file.Filename, Path: tempFile.Name(), CreatedAt: time.Time{}, UpdatedAt: time.Time{}, } img, err := h.uploadImgApp.Save(&uploadImg) if err != nil { return err } imgs = append(imgs, img) } return c.JSON(http.StatusOK, imgs) } func (h *UploadImgHandler) Get(c echo.Context) error { strID := c.Param("id") if strID == "" { return errors.New("the input image ID is empty") } id, err := strconv.ParseUint(strID, 10, 0) if err != nil { return err } img, err := h.uploadImgApp.Get(id) if err != nil { return err } return c.JSON(http.StatusOK, img) } func (h *UploadImgHandler) GetAll(c echo.Context) error { imgs, err := h.uploadImgApp.GetAll() if err != nil { return err } return c.JSON(http.StatusOK, imgs) } func (h *UploadImgHandler) Delete(c echo.Context) error { strID := c.Param("id") if strID == "" { return errors.New("the deleted image ID is empty") } id, err := strconv.ParseUint(strID, 10, 0) if err != nil { return err } err = h.uploadImgApp.Delete(id) if err != nil { return err } msg := fmt.Sprintf(`{"msg": "delete Imgage ID:%s success"`, strID) return c.JSON(http.StatusOK, msg) } ``` </details> <br/> ## 服务入口 - main <details> <summary>cmd/imgupload_server/main.go</summary> ``` package main import ( "github.com/joho/godotenv" "github.com/labstack/echo" "github.com/labstack/echo/middleware" "github.com/lupguo/go-ddd-sample/application" "github.com/lupguo/go-ddd-sample/infrastructure/persistence" "github.com/lupguo/go-ddd-sample/interfaces/api/handler" "log" "os" ) func init() { // To load our environmental variables. if err := godotenv.Load(); err != nil { log.Println("no env gotten") } } func main() { // db detail dbDriver := os.Getenv("DB_DRIVER") host := os.Getenv("DB_HOST") password := os.Getenv("DB_PASSWORD") user := os.Getenv("DB_USER") dbname := os.Getenv("DB_NAME") port := os.Getenv("DB_PORT") // 初始化基础层实例 - DB实例 persisDB, err := persistence.NewRepositories(dbDriver, user, password, port, host, dbname) if err != nil { log.Fatal(err) } defer persisDB.Close() // db做Migrate if err := persisDB.AutoMigrate(); err != nil { log.Fatal(err) } // 初始化应用层实例 - 上传图片应用 uploadImgApp := application.NewUploadImgApp(persisDB.UploadImg) // 初始化接口层实例 - HTTP处理 uploadImgHandler := handler.NewUploadImgHandler(uploadImgApp) e := echo.New() e.Use(middleware.Logger()) e.Use(middleware.Recover()) // 静态主页 e.Static("/", "public") // 图片上传 e.POST("/upload", uploadImgHandler.Save) e.GET("/delete/:id", uploadImgHandler.Delete) e.GET("/img/:id", uploadImgHandler.Get) e.GET("/img-list", uploadImgHandler.GetAll) // Start server e.Logger.Fatal(e.Start(os.Getenv("LISTEN_PORT"))) } ``` </details> <br/> ## 总结 1. DDD适合偏复杂业务,DDD不是万能的。简单业务使用DDD会有些杀鸡用牛刀感觉(思考架构三原则:简单、合适、演进),不要拿着DDD这个锤子到处找钉子; 2. DDD分层建议采用严格分层,不跨层调用,而是采用依赖注入方式把相关实例传入下层(例如不要从接口层直接调用存储层方法,因为跨层调用会导致整个调用链变复杂); 3. DDD目录结构命名,这块也是比较关键一点。目前Go是倾向简洁,不希望向Java那么冗余,所以这块命名还可以在DEMO基础上进一步优化; 4. DDD分层会接口一多,代码可读性不好的问题。可以通过好的命名来规避(比如统一后缀、选取合适简短的接口名),同时用依赖倒置思维逐层看接口,以及其依赖; 5. DDD设计步骤,可以按**领域层 -> 基础层 -> 应用层 -> 接口层**,一般是按这个步骤开发; 6. DDD分层后,每层隔离得比较干净,非常适合单元测试和Mock测试(可以参考文末`food-app-server`这个仓库)