该文章的灵感来自陈明勇大佬的fnote项目,该项目内置了自动生成DDD代码结构的脚本,避免构建新模块的时候重复的书写相同的逻辑繁琐的代码,非常实用
该脚本可以通过指定-domain
自动生成类似如下的代码结构
txt
posts/
├── internal/ # 内部实现,限制外部访问
│ ├── domain/ # 领域层
│ │ └── posts.go # 领域模型 (如实体定义)
│ ├── repository/ # 仓储层
│ │ ├── dao/ # 数据访问对象 (DAO)
│ │ │ └── posts.go # PostsDao 实现 (dao.IPostsDao)
│ │ └── repository.go # PostsRepo 实现 (repository.IPostsRepo)
│ ├── service/ # 领域服务层
│ │ └── service.go # PostsService 实现 (service.IPostsService)
│ └── web/ # 接口层
│ ├── handler.go # PostHandler 实现 (Gin 处理器)
│ ├── vo.go # 值对象 (如响应结构体)
│ └── request.go # 请求对象 (如请求参数结构体)
├── module.go # 模块入口,包含 InitPostsModule
└── wire.go # Wire 依赖注入生成文件
前提
将tmpl和tpl与go模板文件关联

构建DDD架构所需结构体
go
// GenDomain 生成DDD结构目录所需参数
type GenDomain struct {
DomainName string
UnderlineName string
TableName string
OutputDir string
}
参数剖析
DomainName指的是实体名
UnderlineName指的是包名(一般是domain的蛇形命名法)
TableName指的是表名
OutputDir指的是生成代码结构的输出目录
定义变量
flag参数
go
domain = flag.String("domain", "", "the name of domain;eg: User")
table = flag.String("table", "", "the name of table;eg: user")
output = flag.String("output", "", "the output directory;eg: internal/user")
underline = new(string)
通过flag
库接受命令行传递的参数,为后续的模板解析传参做铺垫
embed嵌入变量
go
//go:embed templates/domain.tmpl
domainTpl embed.FS
//go:embed templates/dao.tmpl
daoTpl embed.FS
//go:embed templates/repository.tmpl
repositoryTpl embed.FS
//go:embed templates/service.tmpl
serviceTpl embed.FS
//go:embed templates/web.tmpl
HandlerTpl embed.FS
//go:embed templates/request.tmpl
RequestTpl embed.FS
//go:embed templates/vo.tmpl
VOTpl embed.FS
//go:embed templates/module.tmpl
ModuleTpl embed.FS
//go:embed templates/wire.tmpl
WireTpl embed.FS
利用embed
将tmpl
嵌入到go变量当中,供后续解析
校验参数
go
// 校验命令行参数并设置默认值
func validateFlags(domain, table, output, underline *string) error {
if *domain == "" {
return errors.New("domain参数不能为空;eg: -domain=User")
}
if !regexp.MustCompile(`^[a-zA-Z0-9_]+$`).MatchString(*domain) {
return errors.New("domain参数包含非法字符,只能包含字母、数字和下划线")
}
snakeCaseName := stringx.CamelToSnake(*domain)
if *output == "" {
*output = fmt.Sprintf("internal/%s", snakeCaseName)
}
if *table == "" {
*table = snakeCaseName
}
*underline = snakeCaseName
return nil
}
对命令行参数进行校验,不合法则报错,domain参数是必填的,其他变量是选填的,不填会为其提供默认值。
如:
- output为空,默认值为internal/(domain转蛇形命名)
- table为空,默认值为(domain转蛇形命名)
生成模板
go
// 生成模板代码
func genTemplate(fs embed.FS, templatePath string, outputDir string, output string, gen GenDomain) {
tpl, err := template.ParseFS(fs, templatePath)
if err != nil {
panic(fmt.Sprintf("解析模板失败: %s", err.Error()))
}
if err := os.MkdirAll(outputDir, os.ModePerm); err != nil {
panic(fmt.Sprintf("解析目标文件夹失败: %s", err.Error()))
}
outputPath := outputDir + output
dst, err := os.Create(outputPath)
defer dst.Close()
if err != nil {
panic(fmt.Sprintf("创建目标文件失败: %s", err.Error()))
}
if tpl.Execute(dst, gen) != nil {
panic("生成模板文件失败")
}
log.Printf("生成模板文件成功: %s", outputPath)
}
完整代码
本教程基于mongodb数据库,使用的是mongox数据库ORM工具,直接上代码
gen脚本代码结构如下:

在项目根目录的cmd/gen
下编写脚本gen.go
go
package main
import (
"embed"
"flag"
"fmt"
"github.com/chenmingyong0423/gkit/stringx"
"github.com/pkg/errors"
"log"
"os"
"regexp"
"text/template"
)
// GenDomain 生成DDD结构目录所需参数
type GenDomain struct {
DomainName string
UnderlineName string
TableName string
OutputDir string
}
var (
domain = flag.String("domain", "", "the name of domain;eg: User")
table = flag.String("table", "", "the name of table;eg: user")
output = flag.String("output", "", "the output directory;eg: internal/user")
underline = new(string)
//go:embed templates/domain.tmpl
domainTpl embed.FS
//go:embed templates/dao.tmpl
daoTpl embed.FS
//go:embed templates/repository.tmpl
repositoryTpl embed.FS
//go:embed templates/service.tmpl
serviceTpl embed.FS
//go:embed templates/web.tmpl
HandlerTpl embed.FS
//go:embed templates/request.tmpl
RequestTpl embed.FS
//go:embed templates/vo.tmpl
VOTpl embed.FS
//go:embed templates/module.tmpl
ModuleTpl embed.FS
//go:embed templates/wire.tmpl
WireTpl embed.FS
)
func main() {
flag.Parse()
if err := validateFlags(domain, output, table, underline); err != nil {
panic(err.Error())
}
gen := GenDomain{
DomainName: *domain,
OutputDir: *output,
TableName: *table,
UnderlineName: *underline,
}
genTemplate(domainTpl, "templates/domain.tmpl", *output+"/internal/domain", fmt.Sprintf("/%s.go", gen.UnderlineName), gen)
genTemplate(daoTpl, "templates/dao.tmpl", *output+"/internal/repository/dao", fmt.Sprintf("/%s.go", gen.UnderlineName), gen)
genTemplate(repositoryTpl, "templates/repository.tmpl", *output+"/internal/repository", fmt.Sprintf("/%s.go", gen.UnderlineName), gen)
genTemplate(serviceTpl, "templates/service.tmpl", *output+"/internal/service", fmt.Sprintf("/%s.go", gen.UnderlineName), gen)
genTemplate(HandlerTpl, "templates/web.tmpl", *output+"/internal/web", fmt.Sprintf("/%s.go", gen.UnderlineName), gen)
genTemplate(RequestTpl, "templates/request.tmpl", *output+"/internal/web", "/request.go", gen)
genTemplate(VOTpl, "templates/vo.tmpl", *output+"/internal/web", "/vo.go", gen)
genTemplate(ModuleTpl, "templates/module.tmpl", *output, "/module.go", gen)
genTemplate(WireTpl, "templates/wire.tmpl", *output, "/wire.go", gen)
}
// 校验命令行参数并设置默认值
func validateFlags(domain, table, output, underline *string) error {
if *domain == "" {
return errors.New("domain参数不能为空;eg: -domain=User")
}
if !regexp.MustCompile(`^[a-zA-Z0-9_]+$`).MatchString(*domain) {
return errors.New("domain参数包含非法字符,只能包含字母、数字和下划线")
}
snakeCaseName := stringx.CamelToSnake(*domain)
if *output == "" {
*output = fmt.Sprintf("internal/%s", snakeCaseName)
}
if *table == "" {
*table = snakeCaseName
}
*underline = snakeCaseName
return nil
}
// 生成模板代码
func genTemplate(fs embed.FS, templatePath string, outputDir string, output string, gen GenDomain) {
tpl, err := template.ParseFS(fs, templatePath)
if err != nil {
panic(fmt.Sprintf("解析模板失败: %s", err.Error()))
}
if err := os.MkdirAll(outputDir, os.ModePerm); err != nil {
panic(fmt.Sprintf("解析目标文件夹失败: %s", err.Error()))
}
outputPath := outputDir + output
dst, err := os.Create(outputPath)
defer dst.Close()
if err != nil {
panic(fmt.Sprintf("创建目标文件失败: %s", err.Error()))
}
if tpl.Execute(dst, gen) != nil {
panic("生成模板文件失败")
}
log.Printf("生成模板文件成功: %s", outputPath)
}
templates/domain.tmpl
go
package domain
type {{.DomainName}} struct{
}
templates/dao.tmpl
go
package dao
import (
"github.com/chenmingyong0423/go-mongox/v2"
)
type {{.DomainName}} struct {
}
type I{{.DomainName}}Dao interface {
}
var _ I{{.DomainName}}Dao = (*{{.DomainName}}Dao)(nil)
func New{{.DomainName}}Dao(db *mongox.Database) *{{.DomainName}}Dao {
return &{{.DomainName}}Dao{coll: mongox.NewCollection[{{.DomainName}}](db, "{{.TableName}}")}
}
type {{.DomainName}}Dao struct {
coll *mongox.Collection[{{.DomainName}}]
}
templates/repository.tmpl
go
package repository
import (
"github.com/codepzj/Stellux/server/{{.OutputDir}}/internal/repository/dao"
)
type I{{.DomainName}}Repo interface {
}
var _ I{{.DomainName}}Repo = (*{{.DomainName}}Repo)(nil)
func New{{.DomainName}}Repo(dao dao.I{{.DomainName}}Dao) *{{.DomainName}}Repo {
return &{{.DomainName}}Repo{dao: dao}
}
type {{.DomainName}}Repo struct {
dao dao.I{{.DomainName}}Dao
}
templates/service.tmpl
go
package service
import (
"github.com/codepzj/Stellux/server/{{.OutputDir}}/internal/repository"
)
type I{{.DomainName}}Service interface {
}
var _ I{{.DomainName}}Service = (*{{.DomainName}}Service)(nil)
func New{{.DomainName}}Service(repo repository.I{{.DomainName}}Repo) *{{.DomainName}}Service {
return &{{.DomainName}}Service{repo: repo}
}
type {{.DomainName}}Service struct {
repo repository.I{{.DomainName}}Repo
}
templates/web.tmpl
go
package web
import (
"github.com/codepzj/Stellux/server/{{.OutputDir}}/internal/service"
"github.com/gin-gonic/gin"
)
type I{{.DomainName}}Handler interface {
}
var _ I{{.DomainName}}Handler = (*{{.DomainName}}Handler)(nil)
func New{{.DomainName}}Handler(serv service.I{{.DomainName}}Service) *{{.DomainName}}Handler {
return &{{.DomainName}}Handler{serv: serv}
}
type {{.DomainName}}Handler struct {
serv service.I{{.DomainName}}Service
}
func (h *{{.DomainName}}Handler) RegisterGinRoutes(router *gin.Engine) {
}
templates/request.tmpl
go
package web
type {{.DomainName}}Request struct {}
templates/vo.tmpl
go
package web
type {{.DomainName}}VO struct{
}
templates/module.tmpl
go
package {{.UnderlineName}}
import (
"github.com/codepzj/Stellux/server/{{.OutputDir}}/internal/service"
"github.com/codepzj/Stellux/server/{{.OutputDir}}/internal/web"
)
type (
Handler = web.{{.DomainName}}Handler
Service = service.I{{.DomainName}}Service
Module struct {
Hdl *Handler
Svc Service
}
)
templates/wire.tmpl
go
//go:build wireinject
// +build wireinject
package {{.UnderlineName}}
import (
"github.com/chenmingyong0423/go-mongox/v2"
"github.com/codepzj/Stellux/server/{{.OutputDir}}/internal/repository"
"github.com/codepzj/Stellux/server/{{.OutputDir}}/internal/repository/dao"
"github.com/codepzj/Stellux/server/{{.OutputDir}}/internal/service"
"github.com/codepzj/Stellux/server/{{.OutputDir}}/internal/web"
"github.com/google/wire"
)
var Provider = wire.NewSet(
web.New{{.DomainName}}Handler,service.New{{.DomainName}}Service,repository.New{{.DomainName}}Repo,dao.New{{.DomainName}}Dao,
wire.Bind(new(service.I{{.DomainName}}Service), new(*service.{{.DomainName}}Service)),wire.Bind(new(repository.I{{.DomainName}}Repo), new(*repository.{{.DomainName}}Repo)),wire.Bind(new(dao.I{{.DomainName}}Dao), new(*dao.{{.DomainName}}Dao)),
)
func Init{{.DomainName}}Module(db *mongox.Database) *Module {
wire.Build(
Provider,
wire.Struct(new(Module), "Hdl", "Svc"),
)
return nil
}
效果演示
bash
go run cmd/gen/gen.go -domain=Payment -output=internal/payment -table=payment

展示部分生成的代码
repository/dao/payment.go

web/payment.go

module.go

wire.go

成功生成对应的代码结构和目录,只需专注于相应的业务逻辑即可,不需要重新定义和书写重复的构建逻辑,相当于一个脚手架👍👍👍