Gin 模板自动生成 DDD 代码结构

该文章的灵感来自陈明勇大佬的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

利用embedtmpl嵌入到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

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

参考资料

相关推荐
why1517 小时前
腾讯(QQ浏览器)后端开发
开发语言·后端·golang
浪裡遊7 小时前
跨域问题(Cross-Origin Problem)
linux·前端·vue.js·后端·https·sprint
声声codeGrandMaster7 小时前
django之优化分页功能(利用参数共存及封装来实现)
数据库·后端·python·django
呼Lu噜8 小时前
WPF-遵循MVVM框架创建图表的显示【保姆级】
前端·后端·wpf
bing_1588 小时前
为什么选择 Spring Boot? 它是如何简化单个微服务的创建、配置和部署的?
spring boot·后端·微服务
学c真好玩8 小时前
Django创建的应用目录详细解释以及如何操作数据库自动创建表
后端·python·django
Asthenia04128 小时前
GenericObjectPool——重用你的对象
后端
Piper蛋窝8 小时前
Go 1.18 相比 Go 1.17 有哪些值得注意的改动?
后端
excel9 小时前
招幕技术人员
前端·javascript·后端
盖世英雄酱581369 小时前
什么是MCP
后端·程序员