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

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

参考资料

相关推荐
[email protected]5 分钟前
ASP.NET Core Web API 参数传递方式
后端·asp.net·.netcore
秋野酱7 分钟前
基于SpringBoot酒店管理系统设计和实现(源码+文档+部署讲解)
java·spring boot·后端
Asthenia041237 分钟前
面试复盘:深入剖析 IOC 容器
后端
ChinaRainbowSea2 小时前
8. RabbitMQ 消息队列 + 结合配合 Spring Boot 框架实现 “发布确认” 的功能
java·spring boot·分布式·后端·rabbitmq·java-rabbitmq
星星电灯猴2 小时前
flutter: 解析 Bloc 实现原理
后端
bcbnb2 小时前
Flutter_bloc框架使用笔记,后续估计都不太会用了(1)
后端
唐静蕴2 小时前
Kotlin语言的安全开发
开发语言·后端·golang
调试人生的显微镜2 小时前
Flutter开发 -- 使用Bloc管理状态
后端
开心猴爷2 小时前
深入解析 Flutter Bloc:从原理到实战
后端
aiopencode3 小时前
Flutter中的BLoC,你所需要知道的一切
后端