Go示例
最后更新于:2022-04-02 03:08:02
[TOC]
## 参考
示例中的源码: https://github.com/lupguo/go-ddd-sample
但是示例中的应用为单个应用
多应用的源码查考:https://github.com/victorsteven/food-app-server
## 图片智能识别检索应用
架构图
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/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`
### 领域仓储接口 仓储接口定义了一组方法,用于定义领域实体的与持久化存储相关的操作,实现该接口的持久化存储,都可以操作该领域实体(entity.UploadImg)
## 基础层 - infrastructure ### 总仓储结构体 这里定义了总的仓储结构体:`type Repositories struct{}`,其内包含领域层的仓储接口和DB实例,可以方便持久层; 同时通过`gorm.AutoMigrate()`来实现DB的同步
### 上传图片领域仓储接口的实现 persistence.UploadImgPersis结构体实现了领域层的仓储接口,后续只要匹配领域层仓储接口即可以匹配操作领域中的能力
## 应用层 - application 可能涉及自身或远程领域服务调用 ### 上传图片应用 - 应用层比较薄,主要做业务流程实现,需要对服务进行组合与编排,另外应用层做到承上启下,即对上接口层暴露实例化应用的方法,方便把仓储实现给接管过来,并通过调用具体的仓储实现完成业务 - 上传图片应用,这块统一采用_app结尾,这可可以统一标识应用层文件 - UploadImgApp.db实际是一个仓储层接口在编写应用层时候,看不到任何DB具体实现,同时也看不到任何接口层的入参信息,这就是接口抽象的优势,层之间隔离的比较彻底 - 在应用层还会做一些额外的处理,比如这里的rawUrl()函数组合,非常通用的功能可以考虑放入pkg包内
## 接口层 - interfaces 接口层是整体架构的最上层,用于处理信息的输入和输出,这里我们通过_handler来作为统一后缀标识接口层处理文件
## 服务入口 - main
## 总结 1. DDD适合偏复杂业务,DDD不是万能的。简单业务使用DDD会有些杀鸡用牛刀感觉(思考架构三原则:简单、合适、演进),不要拿着DDD这个锤子到处找钉子; 2. DDD分层建议采用严格分层,不跨层调用,而是采用依赖注入方式把相关实例传入下层(例如不要从接口层直接调用存储层方法,因为跨层调用会导致整个调用链变复杂); 3. DDD目录结构命名,这块也是比较关键一点。目前Go是倾向简洁,不希望向Java那么冗余,所以这块命名还可以在DEMO基础上进一步优化; 4. DDD分层会接口一多,代码可读性不好的问题。可以通过好的命名来规避(比如统一后缀、选取合适简短的接口名),同时用依赖倒置思维逐层看接口,以及其依赖; 5. DDD设计步骤,可以按**领域层 -> 基础层 -> 应用层 -> 接口层**,一般是按这个步骤开发; 6. DDD分层后,每层隔离得比较干净,非常适合单元测试和Mock测试(可以参考文末`food-app-server`这个仓库)
';
domain/entity/uploadimg.go
``` 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:"-"` } ```### 领域仓储接口 仓储接口定义了一组方法,用于定义领域实体的与持久化存储相关的操作,实现该接口的持久化存储,都可以操作该领域实体(entity.UploadImg)
domain/repository/uploadimg_repo.go
``` 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 } ```## 基础层 - infrastructure ### 总仓储结构体 这里定义了总的仓储结构体:`type Repositories struct{}`,其内包含领域层的仓储接口和DB实例,可以方便持久层; 同时通过`gorm.AutoMigrate()`来实现DB的同步
infrastructure/persistence/db.go
``` 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 } ```### 上传图片领域仓储接口的实现 persistence.UploadImgPersis结构体实现了领域层的仓储接口,后续只要匹配领域层仓储接口即可以匹配操作领域中的能力
infrastructure/persistence/uploadimg_persis.go
``` // 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 } ```## 应用层 - application 可能涉及自身或远程领域服务调用 ### 上传图片应用 - 应用层比较薄,主要做业务流程实现,需要对服务进行组合与编排,另外应用层做到承上启下,即对上接口层暴露实例化应用的方法,方便把仓储实现给接管过来,并通过调用具体的仓储实现完成业务 - 上传图片应用,这块统一采用_app结尾,这可可以统一标识应用层文件 - UploadImgApp.db实际是一个仓储层接口在编写应用层时候,看不到任何DB具体实现,同时也看不到任何接口层的入参信息,这就是接口抽象的优势,层之间隔离的比较彻底 - 在应用层还会做一些额外的处理,比如这里的rawUrl()函数组合,非常通用的功能可以考虑放入pkg包内
application/uploadimg_app.go
``` 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 } ```## 接口层 - interfaces 接口层是整体架构的最上层,用于处理信息的输入和输出,这里我们通过_handler来作为统一后缀标识接口层处理文件
interfaces/api/handler/uploadimg_handler.go
``` 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) } ```## 服务入口 - main
cmd/imgupload_server/main.go
``` 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"))) } ```## 总结 1. DDD适合偏复杂业务,DDD不是万能的。简单业务使用DDD会有些杀鸡用牛刀感觉(思考架构三原则:简单、合适、演进),不要拿着DDD这个锤子到处找钉子; 2. DDD分层建议采用严格分层,不跨层调用,而是采用依赖注入方式把相关实例传入下层(例如不要从接口层直接调用存储层方法,因为跨层调用会导致整个调用链变复杂); 3. DDD目录结构命名,这块也是比较关键一点。目前Go是倾向简洁,不希望向Java那么冗余,所以这块命名还可以在DEMO基础上进一步优化; 4. DDD分层会接口一多,代码可读性不好的问题。可以通过好的命名来规避(比如统一后缀、选取合适简短的接口名),同时用依赖倒置思维逐层看接口,以及其依赖; 5. DDD设计步骤,可以按**领域层 -> 基础层 -> 应用层 -> 接口层**,一般是按这个步骤开发; 6. DDD分层后,每层隔离得比较干净,非常适合单元测试和Mock测试(可以参考文末`food-app-server`这个仓库)