整合营销服务商

电脑端+手机端+微信端=数据同步管理

免费咨询热线:

GO 编程:元编程之代码生成

GO 编程:元编程之代码生成

码生成

图灵完备的一个重要特性是计算机程序可以生成另一个程序1,很多人可能认为生成代码在软件中并不常见,但是实际上它在很多场景中都扮演了重要的角色。Go 语言中的测试就使用了代码生成机制,go test 命令会扫描包中的测试用例并生成程序、编译并执行它们,我们在这一节中就会介绍 Go 语言中的代码生成机制。

设计原理

元编程(Metaprogramming)是计算机编程中一个非常重要、也很有趣的概念,维基百科上将元编程描述成一种计算机程序可以将代码看待成数据的能力2。

Metaprogramming is a programming technique in which computer programs have the ability to treat programs as their data.

如果能够将代码看做数据,那么代码就可以像数据一样在运行时被修改、更新和替换;元编程赋予了编程语言更加强大的表达能力,能够让我们将一些计算过程从运行时挪到编译时、通过编译期间的展开生成代码或者允许程序在运行时改变自身的行为。总而言之,元编程其实是一种使用代码生成代码的方式,无论是编译期间生成代码,还是在运行时改变代码的行为都是『生成代码』的一种。

现代的编程语言大都会为我们提供不同的元编程能力,从总体来看,根据『生成代码』的时机不同,我们将元编程能力分为两种类型,其中一种是编译期间的元编程,例如:宏和模板;另一种是运行期间的元编程,也就是运行时,它赋予了编程语言在运行期间修改行为的能力,当然也有一些特性既可以在编译期实现,也可以在运行期间实现。

Go 语言作为编译型的编程语言,它提供了比较有限的运行时元编程能力,例如:反射特性,然而由于性能的问题,反射在很多场景下都不被推荐使用。当然除了反射之外,Go 语言还提供了另一种编译期间的代码生成机制 — go generate,它可以在代码编译之前根据源代码生成代码。

代码生成

Go 语言的代码生成机制会读取包含预编译指令的注释,然后执行注释中的命令读取包中的文件,它们将文件解析成抽象语法树并根据语法树生成新的 Go 语言代码和文件,生成的代码会在项目的编译期间与其他代码一起编译和运行。

//go:generate command argument...

go generate 不会被 go build 等命令自动执行,该命令需要显式的触发,手动执行该命令时会在文件中扫描上述形式的注释并执行后面的执行命令,需要注意的是 go:generate 和前面的 // 之间没有空格,这种不包含空格的注释一般是 Go 语言的编译器指令,而我们在代码中的正常注释都应该保留这个空格4。

代码生成最常见的例子就是官方提供的 stringer5,这个工具可以扫描如下所示的常量定义,然后为当前常量类型 Piller 生成对应的 String() 方法:

// pill.go
package painkiller

//go:generate stringer -type=Pill
type Pill int
const (
    Placebo Pill=iota
    Aspirin
    Ibuprofen
    Paracetamol
    Acetaminophen=Paracetamol
)

当我们在上述文件中加入 //go:generate stringer -type=Pill 注释并调用 go generate 命令时,在同一目录下会出现如下所示的 pill_string.go 文件,该文件中包含两个函数,分别是 _String

// Code generated by "stringer -type=Pill"; DO NOT EDIT.

package painkiller

import "strconv"

func _() {
    // An "invalid array index" compiler error signifies that the constant values have changed.
    // Re-run the stringer command to generate them again.
    var x [1]struct{}
    _=x[Placebo-0]
    _=x[Aspirin-1]
    _=x[Ibuprofen-2]
    _=x[Paracetamol-3]
}

const _Pill_name="PlaceboAspirinIbuprofenParacetamol"

var _Pill_index=[...]uint8{0, 7, 14, 23, 34}

func (i Pill) String() string {
    if i < 0 || i >=Pill(len(_Pill_index)-1) {
        return "Pill(" + strconv.FormatInt(int64(i), 10) + ")"
    }
    return _Pill_name[_Pill_index[i]:_Pill_index[i+1]]
}

这段生成的代码很值得我们学习,它通过编译器的检查提供了非常健壮的 String 方法。我们在这里不展示具体的使用过程,本节将重点分析从执行 go generate 到生成对应 String 方法的整个过程,帮助各位理解代码生成机制的工作原理,代码生成的过程可以分成以下两个部分:

  1. 扫描 Go 语言源文件,查找待执行的 //go:generate 预编译指令;
  2. 执行预编译指令,再次扫描源文件并根据源文件中的代码生成代码;

预编译指令

当我们在命令行中执行 go generate 命令时,它会调用源代码中的 cmd/go/internal/generate.runGenerate 函数扫描包中的预编译指令,该函数会遍历命令行传入包中的全部文件并依次调用 cmd/go/internal/generate.generate

func runGenerate(cmd *base.Command, args []string) {
    ...
    for _, pkg :=range load.Packages(args) {
        ...
        pkgName :=pkg.Name
        for _, file :=range pkg.InternalGoFiles() {
            if !generate(pkgName, file) {
                break
            }
        }
        pkgName +="_test"
        for _, file :=range pkg.InternalXGoFiles() {
            if !generate(pkgName, file) {
                break
            }
        }
    }
}

cmd/go/internal/generate.generate 函数会打开传入的文件并初始化一个用于扫描 cmd/go/internal/generate.Generator 的结构体:

func generate(pkg, absFile string) bool {
    fd, err :=os.Open(absFile)
    if err !=nil {
        log.Fatalf("generate: %s", err)
    }
    defer fd.Close()
    g :=&Generator{
        r:        fd,
        path:     absFile,
        pkg:      pkg,
        commands: make(map[string][]string),
    }
    return g.run()
}

结构体 cmd/go/internal/generate.Generator 的私有方法 cmd/go/internal/generate.Generator.run 会在对应的文件中扫描指令并执行,该方法的实现原理很简单,我们在这里简单展示一下该方法的简化实现:

func (g *Generator) run() (ok bool) {
    input :=bufio.NewReader(g.r)
    for {
        var buf []byte
        buf, err=input.ReadSlice('\n')
        if err !=nil {
            if err==io.EOF && isGoGenerate(buf) {
                err=io.ErrUnexpectedEOF
            }
            break
        }

        if !isGoGenerate(buf) {
            continue
        }

        g.setEnv()
        words :=g.split(string(buf))
        g.exec(words)
    }
    return true
}

上述代码片段会按行读取被扫描的文件并调用 cmd/go/internal/generate.isGoGenerate 判断当前行是否以 //go:generate 注释开头,如果该行确定以 //go:generate 开头,那么就会解析注释中的命令和参数并调用 cmd/go/internal/generate.Generator.exec 运行当前命令。

抽象语法树

stringer 充分利用了 Go 语言标准库对编译器各种能力的支持,其中包括用于解析抽象语法树的 go/ast、用于格式化代码的 go/fmt 等,Go 通过标准库中的这些包对外直接提供了编译器的相关能力,让使用者可以直接在它们上面构建复杂的代码生成机制并实施元编程技术。

作为二进制文件,stringer 命令的入口就是如下所示的 main 函数,在下面的代码中,我们初始化了一个用于解析源文件和生成代码的 Generator,然后开始拼接生成的文件:

func main() {
    types :=strings.Split(*typeNames, ",")
    ...
    g :=Generator{
        trimPrefix:  *trimprefix,
        lineComment: *linecomment,
    }
    ...

    g.Printf("// Code generated by \"stringer %s\"; DO NOT EDIT.\n", strings.Join(os.Args[1:], " "))
    g.Printf("\n")
    g.Printf("package %s", g.pkg.name)
    g.Printf("\n")
    g.Printf("import \"strconv\"\n")

    for _, typeName :=range types {
        g.generate(typeName)
    }

    src :=g.format()

    baseName :=fmt.Sprintf("%s_string.go", types[0])
    outputName=filepath.Join(dir, strings.ToLower(baseName))
    if err :=ioutil.WriteFile(outputName, src, 0644); err !=nil {
        log.Fatalf("writing output: %s", err)
    }
}

从这段代码中我们能看到最终生成文件的轮廓,最上面的调用的几次 Generator.Printf 会在内存中写入文件头的注释、当前包名以及引入的包等,随后会为待处理的类型依次调用 Generator.generate,这里会生成一个签名为 _ 的函数,通过编译器保证枚举类型的值不会改变:

func (g *Generator) generate(typeName string) {
    values :=make([]Value, 0, 100)
    for _, file :=range g.pkg.files {
        file.typeName=typeName
        file.values=nil
        if file.file !=nil {
            ast.Inspect(file.file, file.genDecl)
            values=append(values, file.values...)
        }
    }
    g.Printf("func _() {\n")
    g.Printf("\t// An \"invalid array index\" compiler error signifies that the constant values have changed.\n")
    g.Printf("\t// Re-run the stringer command to generate them again.\n")
    g.Printf("\tvar x [1]struct{}\n")
    for _, v :=range values {
        g.Printf("\t_=x[%s - %s]\n", v.originalName, v.str)
    }
    g.Printf("}\n")
    runs :=splitIntoRuns(values)
    switch {
    case len(runs)==1:
        g.buildOneRun(runs, typeName)
    ...
    }
}

随后调用的 Generator.buildOneRun 会生成两个常量的声明语句并为类型定义 String 方法,其中引用的 stringOneRun 常量是方法的模板,与 Web 服务的前端 HTML 模板比较相似:

func (g *Generator) buildOneRun(runs [][]Value, typeName string) {
    values :=runs[0]
    g.Printf("\n")
    g.declareIndexAndNameVar(values, typeName)
    g.Printf(stringOneRun, typeName, usize(len(values)), "")
}

const stringOneRun=`func (i %[1]s) String() string {
    if %[3]si >=%[1]s(len(_%[1]s_index)-1) {
        return "%[1]s(" + strconv.FormatInt(int64(i), 10) + ")"
    }
    return _%[1]s_name[_%[1]s_index[i]:_%[1]s_index[i+1]]
}

整个生成代码的过程就是使用编译器提供的库解析源文件并按照已有的模板生成新的代码,这与 Web 服务中利用模板生成 HTML 文件没有太多的区别,只是最终生成的文件的用途稍微有一些不同,

小结

Go 语言的标准库中暴露了编译器的很多能力,其中包含词法分析和语法分析,我们可以直接利用这些现成的解析器编译 Go 语言的源文件并获得抽象语法树,有了识别源文件结构的能力,我们就可以根据源文件对应的抽象语法树自由地生成更多的代码,使用元编程技术来减少代码重复、提高工作效率。

下文章来源于GoUpUp ,作者dj

简介

最近在整理我们项目代码的时候,发现有很多活动的代码在结构和提供的功能上都非常相似。为了方便今后的开发,我花了一点时间编写了一个生成代码框架的工具,最大程度地降低重复劳动。代码本身并不复杂,且与项目代码关联性较大,这里就不展开介绍了。在这个过程中,我发现 Go 标准的模板库text/template和html/template使用起来比较束手束脚,很不方便。我从 GitHub 了解到quicktemplate这个第三方模板库,功能强大,语法简单,使用方便。今天我们就来介绍一下quicktemplate。

快速使用

本文代码使用 Go Modules。

先创建代码目录并初始化:

$ mkdir quicktemplate && cd quicktemplate
$ go mod init github.com/darjun/go-daily-lib/quicktemplate

quicktemplate会将我们编写的模板代码转换为 Go 语言代码。因此我们需要安装quicktemplate包和一个名为qtc的编译器:

$ go get -u github.com/valyala/quicktemplate
$ go get -u github.com/valyala/quicktemplate/qtc

首先,我们需要编写quicktemplate格式的模板文件,模板文件默认以.qtpl作为扩展名。下面我编写了一个简单的模板文件greeting.qtpl:

All text outside function is treated as comments.

{% func Greeting(name string, count int) %}
  {% for i :=0; i < count; i++ %}
    Hello, {%s name %}
  {% endfor %}
{% endfunc %}

模板语法非常简单,我们只需要简单了解以下 2 点:

  • 模板以函数为单位,函数可以接受任意类型和数量的参数,这些参数可以在函数中使用。所有函数外的文本都是注释,qtc编译时会忽视注释;
  • 函数内的内容,除了语法结构,其他都会原样输出到渲染后的文本中,包括空格和换行

将greeting.qtpl保存到templates目录,然后执行qtc命令。该命令会生成对应的 Go 文件greeting.qtpl.go,包名为templates。现在,我们就可以使用这个模板了:

package main

import (
  "fmt"

  "github.com/darjun/go-daily-lib/quicktemplate/get-started/templates"
)

func main() {
  fmt.Println(templates.Greeting("dj", 5))
}

调用模板函数,传入参数,返回渲染后的文本:

$ go run .


    Hello, dj

    Hello, dj

    Hello, dj

    Hello, dj

    Hello, dj

{%s name %}执行文本替换,{% for %}循环生成重复文本。输出中出现多个空格和换行,这是因为函数内除了语法结构,其他内容都会原样保留,包括空格和换行

需要注意的是,由于quicktemplate是将模板转换为 Go 代码使用的,所以如果模板有修改,必须先执行qtc命令重新生成 Go 代码,否则修改不生效

语法结构

quicktemplate支持 Go 常见的语法结构,if/for/func/import/return。而且写法与直接写 Go 代码没太大的区别,几乎没有学习成本。只是在模板中使用这些语法时,需要使用{%和%}包裹起来,而且if和for等需要添加endif/endfor明确表示结束。

变量

上面我们已经看到如何渲染传入的参数name,使用{%s name %}。由于name是 string 类型,所以在{%后使用s指定类型。quicktemplate还支持其他类型的值:

  • 整型:{%d int %},{%dl int64 %},{%dul uint64 %};
  • 浮点数:{%f float %}。还可以设置输出的精度,使用{%f.precision float %}。例如{%f.2 1.2345 %}输出1.23;
  • 字节切片([]byte):{%z bytes %};
  • 字符串:{%q str %}或字节切片:{%qz bytes %},引号转义为";
  • 字符串:{%j str %}或字节切片:{%jz bytes %},没有引号;
  • URL 编码:{%u str %},{%uz bytes %};
  • {%v anything %}:输出等同于fmt.Sprintf("%v", anything)。

先编写模板:

{% func Types(a int, b float64, c []byte, d string) %}
  int: {%d a %}, float64: {%f.2 b %}, bytes: {%z c %}, string with quotes: {%q d %}, string without quotes: {%j d %}.
{% endfunc %}

然后使用:

func main() {
  fmt.Println(templates.Types(1, 5.75, []byte{'a', 'b', 'c'}, "hello"))
}

运行:

$ go run .

  int: 1, float64: 5.75, bytes: abc, string with quotes: "hello", string without quotes: hello.

调用函数

quicktemplate支持在模板中调用模板函数、标准库的函数。由于qtc会直接生成 Go 代码,我们甚至还可以在同目录下编写自己的函数给模板调用,模板 A 中也可以调用模板 B 中定义的函数。

我们先在templates目录下编写一个文件rank.go,定义一个Rank函数,传入分数,返回评级:

package templates

func Rank(score int) string {
  if score >= 90 {
    return "A"
  } else if score >= 80 {
    return "B"
  } else if score >= 70 {
    return "C"
  } else if score >= 60 {
    return "D"
  } else {
    return "E"
  }
}

然后我们可以在模板中调用这个函数:

{% import "fmt" %}
{% func ScoreList(name2score map[string]int) %}
  {% for name, score :=range name2score %}
    {%s fmt.Sprintf("%s: score-%d rank-%s", name, score, Rank(score)) %}
  {% endfor %}
{% endfunc %}

编译模板:

$ qtc

编写程序:

func main() {
  name2score := make(map[string]int)
  name2score["dj"] = 85
  name2score["lizi"] = 96
  name2score["hjw"] = 52

  fmt.Println(templates.ScoreList(name2score))
}

运行程序输出:

$ go run .


    dj: score-85 rank-B

    lizi: score-96 rank-A

    hjw: score-52 rank-E


由于我们在模板中用到fmt包,需要先使用{% import %}将该包导入。

在模板中调用另一个模板的函数也是类似的,因为模板最终都会转为 Go 代码。Go 代码中有同样签名的函数。

Web

quicktemplate常用来编写 HTML 页面的模板:

{% func Index(name string) %}
<html>
  <head>
    <title>Awesome Web</title>
  </head>
  <body>
    <h1>Hi, {%s name %}
    <p>Welcome to the awesome web!!!</p>
  </body>
</html>
{% endfunc %}

下面编写一个简单的 Web 服务器:

func index(w http.ResponseWriter, r *http.Request) {
  templates.WriteIndex(w, r.FormValue("name"))
}

func main() {
  mux := http.NewServeMux()
  mux.HandleFunc("/", index)

  server := &http.Server{
    Handler: mux,
    Addr:    ":8080",
  }

  log.Fatal(server.ListenAndServe())
}

qtc会生成一个Write*的方法,它接受一个io.Writer的参数。将模板渲染的结果写入这个io.Writer中,我们可以直接将http.ResponseWriter作为参数传入,非常便捷。

运行:

$ qtc
$ go run .

浏览器输入localhost:8080?name=dj查看结果。

总结

quicktemplate至少有下面 3 个优势:

  • 语法与 Go 语言非常类似,几乎没有学习成本;
  • 会先转换为 Go,渲染速度非常快,比标准库html/template快 20 倍以上;
  • 为了安全考虑,会执行一些编码,避免受到攻击。

从我个人的实际使用情况来看,确实很方便,很实用。感兴趣的还可以去看看qtc生成的 Go 代码。

大家如果发现好玩、好用的 Go 语言库,欢迎到 Go 每日一库 GitHub 上提交 issue

参考

  1. quicktemplate GitHub:https://github.com/valyala/quicktemplate
  2. Go 每日一库 GitHub:https://github.com/darjun/go-daily-lib

文翻译自 https://github.com/evrone/go-clean-template,由于本人翻译水平有限,翻译不当之处烦请指出。希望大家看了这篇文章能有所帮助。感谢捧场。

概括

模板的作用 :

  • 如何组织项目并防止它变成一坨意大利面条式的代码。
  • 在哪里存放业务逻辑,使其保持独立,整洁和可扩展。
  • 如何在微服务扩展时不失控

模版使用了 Robert Martin ( 也叫 Bob 叔叔 ) 的原则[1]

Go-clean-template[2] 此仓库由 Evrone[3] 创建及维护。

目录内容

  • 快速开始
  • 项目结构
  • 依赖注入
  • 整洁架构之道

快速开始

本地开发

# Postgres, RabbitMQ
$ make compose-up
# Run app with migrations
$ make run

集成测试 ( 可以在 CI 中运行 )

# DB, app + migrations, integration tests
$ make compose-up-integration-test

项目结构

├── cmd
│   └── app
│       └── main.go
├── config
│   ├── config.go
│   └── config.yml
├── docs
│   ├── docs.go
│   ├── swagger.json
│   └── swagger.yaml
├── go.mod
├── go.sum
├── integration-test
│   ├── Dockerfile
│   └── integration_test.go
├── internal
│   ├── app
│   │   ├── app.go
│   │   └── migrate.go
│   ├── delivery
│   │   ├── amqp_rpc
│   │   │   ├── router.go
│   │   │   └── translation.go
│   │   └── http
│   │       └── v1
│   │           ├── error.go
│   │           ├── router.go
│   │           └── translation.go
│   ├── domain
│   │   └── translation.go
│   └── service
│       ├── interfaces.go
│       ├── repo
│       │   └── translation_postgres.go
│       ├── translation.go
│       └── webapi
│           └── translation_google.go
├── migrations
│   ├── 20210221023242_migrate_name.down.sql
│   └── 20210221023242_migrate_name.up.sql
└── pkg
    ├── httpserver
    │   ├── options.go
    │   └── server.go
    ├── logger
    │   ├── interface.go
    │   ├── logger.go
    │   └── zap.go
    ├── postgres
    │   ├── options.go
    │   └── postgres.go
    └── rabbitmq
        └── rmq_rpc
            ├── client
            │   ├── client.go
            │   └── options.go
            ├── connection.go
            ├── errors.go
            └── server
                ├── options.go
                └── server.go

cmd/app/main.go

配置和日志实例的初始化main 函数中调用internal/app/app.go 文件中 的 Run 函数,main 函数将会在此 "延续"。

config

配置。首先读取 config.yml,然后用环境变量覆盖相匹配的 yaml 配置。配置的结构体在 config.go 文件中。env-required: true 结构体标签强制您指定一个值 ( 在 yaml 或在环境变量中 )。

对于配置读取,我们选择 cleanenv[4] 库。它在 GitHub 上没有很多 star,但很简单且满足所有的需求。

从 yaml 中读取配置违背了12 要素,但在实践中,它比从环境变量中读取整个配置更方便。假设默认值定义在 yaml 中,敏感的变量定义在环境变量中。

docs

Swagger 文档。可以由 swag[5] 库自动生成。而你不需要自己改正任何事情。

integration-test

集成测试。它们作为单独的容器启动,紧挨着应用程序容器。使用 go-hit[6] 测试 REST API 非常方便。

internal/app

app.go 文件中一般会有一个 Run 函数,它“延续”了main函数。

这是创建所有主要对象的地方。依赖注入通过“ New...”构造函数 ( 参见依赖注入 ) 。这种技术允许我们使用依赖注入原则对应用程序进行分层,使得业务逻辑独立于其他层。

接下来,为了优雅的完成,我们启动服务并在select中等待特定的信号。如果 app.go 代码越来越多,可以将其拆分为多个文件。

对于大量的注入,可以使用 wire[7] 库 ( wire 是一个代码生成工具,它使用依赖注入自动连接组件)。

migrate.go 文件用于数据库自动迁移。如果指定了 migrate 标签的参数,则会包含它。例如 :

$ go run -tags migrate ./cmd/app

internal/delivery

服务的handler层 ( MVC 控制器 )。模板展示了两个服务:

  • RPC ( RabbitMQ 用于传递消息 )
  • REST HTTP ( GIN 框架 )

服务的路由也以同样的风格编写 :

  • Handlers按照应用领域分组 ( 按公共基础 )
  • 对于每个组,都创建自己的路由结构,以及处理接口路径的方法
  • 业务逻辑的结构被注入到路由结构中,由handlers处理调用

internal/delivery/http

简单的 REST 版本控制。对于 v2,我们需要添加具有相同内容的 http/v2 文件夹。在 internal/app 程序文件中添加以下行 :

handler :=gin.New()
v1.NewRouter(handler, translationService)
v2.NewRouter(handler, translationService)

你可以使用任何其他的 HTTP 框架甚至是标准的 net/http 库来代替 Gin。

v1/router.go 和上面的 handler 方法中,有一些注释是用 swag库来生成 swagger 文档的。

internal/domain

业务逻辑的实体 ( 模型 ) 可以在任何层中使用。也可以有方法,例如,用于验证。

internal/service

业务逻辑

  • 方法按应用领域分组 ( 在公共的基础上 )
  • 每个组都有自己的结构
  • 一个文件一个结构

Repositories、 webapi、 rpc 和其他业务逻辑结构被注入到业务逻辑结构中 ( 见依赖注入 )。

internal/service/repo

repository 是业务逻辑使用的抽象存储 ( 数据库 )。

internal/service/webapi

它是业务逻辑使用的抽象 web API。例如,它可能是业务逻辑通过 REST API 访问的另一个微服务。包的名称根据用途而变化。

pkg/rabbitmq

RabbitMQ RPC 模式 :

  • RabbitMQ 内部没有路由
  • 使用Exchange fanout 广播模式,将1 个独立队列绑定到其中,这是最高效的配置。
  • 重新连接断开丢失的连接

依赖注入

为了消除业务逻辑对外部包的依赖,使用了依赖注入。

例如,通过 NewService 构造函数,我们将依赖注入到业务逻辑的结构中。这使得业务逻辑独立 ( 便于移植 )。我们可以重写接口的实现,而不需要对 service 包进行更改。

package service

import (
    // Nothing!
)

type Repository interface {
    Get()
}

type Service struct {
    repo Repository
}

func NewService(r Repository) *Service{
    return &Service{r}
}

func (s *Service) Do()  {
    s.repo.Get()
}

它还允许我们自动生成模拟参数 ( 例如使用 mockery[8] ) 和轻松地编写单元测试。

我们可以不受特定实现的约束,来将一个组件更改为另一个组件。如果新组件实现了该接口,则业务逻辑中不需要进行任何更改。

整洁架构之道

关键点

程序员在编写了大量代码后才意识到应用程序的最佳架构。

一个好的架构允许尽可能推迟决策。

主要原则

Dependency Inversion ( 与 SOLID 相同 ) 是依赖倒置的原则。依赖关系的方向是从外层到内层。由于这个原因,业务逻辑和实体仍然独立于系统的其他部分。

因此,应用程序分为内部和外部两个层次 :

  • 业务逻辑 ( 使用 Go 标准库 )
  • 工具 ( 数据库、其他服务、消息代理、任何其他包和框架 )

Clean Architecture

业务逻辑的内层应该是整洁的,它应该 :

  • 没有从外层导入的包
  • 只使用标准库的功能
  • 通过接口调用外层 !

业务逻辑对 Postgres 或详细的 web API 一无所知。业务逻辑应该具有一个用于处理抽象数据库或抽象 web API 的接口。

外层还有其他限制 :

  • 这一层的所有组成部分都不知道彼此的存在。如何从一个工具调用另一个工具?不是直接,而是只能通过内层的业务逻辑来调用。
  • 对内层的所有调用都是通过接口来完成的
  • 数据以便于业务逻辑的格式传输 ( internal/domain )

例如,你需要从 HTTP ( 控制器 ) 访问数据库。HTTP 和数据库都在外层,这意味着它们对彼此一无所知。它们之间的通信是通过 service ( 业务逻辑 ) 进行的 :

    HTTP > service
           service > repository (Postgres)
           service < repository (Postgres)
    HTTP < service

符号 > 和 < 通过接口显示层与层边界的交集,如图所示 :

Example

或者更复杂的业务逻辑 :

    HTTP > service
           service > repository
           service < repository
           service > webapi
           service < webapi
           service > RPC
           service < RPC
           service > repository
           service < repository
    HTTP < service

层级

Example

整洁架构的术语

  • 实体是业务逻辑操作的结构。它们位于 internal/domain 文件夹中。Domain 暗示我们坚持 DDD ( 领域驱动设计 ) 的原则,这在一定程度上是正确的。在 MVC 术语中,实体就是模型。
  • 用例是位于 internal/service 中的业务逻辑。从整洁架构的角度来看,调用业务逻辑使用 service 一词不是习惯的用法,但是对于一个包名称来说,使用一个单词 ( service ) 比使用两个单词 ( use case ) 更方便。

业务逻辑直接交互的层通常称为基础设施层。它们可以是存储库 internal/service/repo、web API internal/service/webapi、任何pkg,以及其他微服务。在模板中,_ infrastructure 包位于 internal/service 中。

你可以根据需要去选择如何调用入口点。选项如下 :

  • delivery (in our case)
  • controllers
  • transport
  • gateways
  • entrypoints
  • primary
  • input

附加层

经典版本的 整洁架构之道[9] 是为构建大型单体应用程序而设计的,它有4层。

在最初的版本中,外层被分为两个以上的层,两层之间也存在相互依赖关系倒置 ( 定向内部 ),并通过接口进行通信。

在逻辑复杂的情况下,内层也分为两个( 接口分离 )。

复杂的工具可以被划分成更多的附加层,但你应该在确实需要时再添加层。

替代方法

除了整洁架构之道,洋葱架构和六边形架构 ( 端口适配器模式 ) 是类似的。两者都是基于依赖倒置的原则。端口和适配器模式非常接近于整洁架构之道,差异主要在术语上。

写在最后

Freemen App是一款专注于IT程序员求职招聘的一个求职平台,旨在帮助IT技术工作者能更好更快入职及努力协调IT技术者工作和生活的关系,让工作更自由!

程序员专属求职平台

类似的项目

  • https://github.com/bxcodec/go-clean-arch
  • https://github.com/zhashkevych/courses-backend

扩展阅读链接

  • 整洁架构之道[10]
  • 12 要素[11]

参考资料

[1]原则: https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html

[2]Go-clean-template: https://evrone.com/go-clean-template?utm_source=github&utm_campaign=go-clean-template

[3]Evrone: https://evrone.com/?utm_source=github&utm_campaign=go-clean-template

[4]cleanenv: https://github.com/ilyakaznacheev/cleanenv

[5]swag: https://github.com/swaggo/swag

[6]go-hit: https://github.com/Eun/go-hit

[7]wire: https://github.com/google/wire

[8]mockery: https://github.com/vektra/mockery

[9]整洁架构之道: https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html

[10]整洁架构之道: https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html

[11]12 要素: https://12factor.net/ru/

本文转载自Go招聘