[zeromicro/go-zero]api支持引用通过类型(类、结构体)的语法支持设计方案

2024-01-10 355 views
0
背景

在实际业务中,我们经常会对一些通用的结构体(类)放到一个common包中,以减少代码copy带来出错、数据前后不一致等问题。而目前基于api的语法定义是无法使用common结构体(类)的,假设我们有这样两个服务Service A,Service B,A服务引用了Media结构体(类),B服务引用了Video,而Media中又包含Video成员,因此服务A和B有共同的结构体(类)Video,目前的解决方案是在定义每个服务时,对应的api文件都会编写一次各自需要的结构体(类),我们来看一下传统写法

传统写法 A服务中引用media
type (
    Video struct {
        Name string `json:"name"`
        Url string `json:"url"`
        Width int `json:"width"`
        Height int `json:"height"`
        // bytes
        Size int64 `json:"size"`
        // mp4|flv|...
        Ext string `json:"ext"`
    }

    Audio struct {
        Name string `json:"name"`
        Url string `json:"url"`
        // bytes
        Size int64 `json:"size"`
        // mp3|wav|...
        Ext string `json:"ext"`
    }

    Image struct {
        Url string `json:"url"`
        Width int `json:"width"`
        Height int `json:"height"`
        // bytes
        Size int64 `json:"size"`
        // png|jpg|...
        Ext string `json:"ext"`
    }

    Media struct {
        Video Video `json:"video"`
        Audio Audio `json:"audio"`
        Image Image `json:"image"`
    }
)

service A {
    @handler getMedia
    get /a/media/get returns (Media)
}
B服务中引用Video
type (
    Video struct {
        Name string `json:"name"`
        Url string `json:"url"`
        Width int `json:"width"`
        Height int `json:"height"`
        // bytes
        Size int64 `json:"size"`
        // mp4|flv|...
        Ext string `json:"ext"`
    }
)

service B {
    @handler getVideo
    get /b/video/get returns (Video)
}

通过以上示例我们可以知道Video结构体(类)为公用的,也许后面还有其他服务也需要使用Video结构体(类),我们讨论出了一个方案来解决这个问题

方案一:@types

通过@types关键字来声明需要引用的元语言中的结构体(类)

元语言:即生成的目标语言,如api需要生成golang代码,则元语言为golang

@types语法定义
@types{
    $lang(
        $pathOfType
    )
}

$lang: 元语言标识,如golangjava,必须为小写,常见语言在设计完成后会列举出来 $pathOfType: 需要引用的通用结构体(类)的路径,注意:这里的写法和文件路径有所区别,其由$path/$package.$typeName组成,如"github.com/go-zero/common/common.Video",其中github.com/go-zero/common/为结构体(类)所在位置,可以将其理解为元语言中引用Video结构体(类)时的import值,common为元语言中的packageName,Video则为需要引用的通用结构体(类),可以将common.Video理解为元语言中使用Video时的声明类型。

其怎么对应到元语言中的结构体见下文

@types 引用golang中的Video编写示例
@types{
    golang(
        "github.com/go-zero/common/common.Video"
        "github.com/go-zero/common/common.Audio"
    )
    java(
        "com/go-zero/common/common.Video"
        "com/go-zero/common/common.Audio"
    )
}
api对共用结构体(类)翻译处理
  • importPathOfTheTargetLang,structInUse:=filepath.Split($pathOfType)
  • 在翻译后的元语言中import $importPathOfTheTargetLang
  • 获取$structInUse中的结构体$st为Video
  • $st=$structInUse

回到开头提到的服务A和服务B共用Video结构体(类)的问题,我们在api中用@types语言去重新编写一下api,假设我们的golang的module为greet,Video在common包文件夹下,其在golang中的定义如下

vim greet/common/types.go
package common

type (
    Video struct {
        Name   string `json:"name"`
        Url    string `json:"url"`
        Width  int    `json:"width"`
        Height int    `json:"height"`
        // bytes
        Size int64  `json:"size"`
        // mp4|flv|...
        Ext  string `json:"ext"`
    }
)

在元语言golang中使用Video

vim greet/test/test.go
package test

import "greet/common" // greet/common对应$path

func main() {
    var video common.Video // common.Video中common对应$package,Video对应$typeName
}

由以上元语言示例可得知api中引用Video的$pathOfType为greet/common/common.Video

服务A引用Video

@types{
    golang(
        "greet/common/common.Video"
    )
}

type (
    Audio struct {
        Name string `json:"name"`
        Url string `json:"url"`
        // bytes
        Size int64 `json:"size"`
        // mp3|wav|...
        Ext string `json:"ext"`
    }

    Image struct {
        Url string `json:"url"`
        Width int `json:"width"`
        Height int `json:"height"`
        // bytes
        Size int64 `json:"size"`
        // png|jpg|...
        Ext string `json:"ext"`
    }

    Media struct {
        Video common.Video `json:"video"`
        Audio Audio `json:"audio"`
        Image Image `json:"image"`
    }
)

service A {
    @handler getMedia
    get /media/get returns (Media)
}

服务B引用Video

@types{
    golang(
        "greet/common/common.Video"
    )
}

service B {
    @handler getMedia
    get /b/get/video returns (common.Video)
}
服务A/B翻译后的types中Video则会以Video=common.Video形式存在
package types

import "greet/common"

type (
    // 翻译后Video
    Video = common.Video

    Audio struct {
        Name string `json:"name"`
        Url string `json:"url"`
        // bytes
        Size int64 `json:"size"`
        // mp3|wav|...
        Ext string `json:"ext"`
    }

    Image struct {
        Url string `json:"url"`
        Width int `json:"width"`
        Height int `json:"height"`
        // bytes
        Size int64 `json:"size"`
        // png|jpg|...
        Ext string `json:"ext"`
    }

    Media struct {
        Video Video `json:"video"`
        Audio Audio `json:"audio"`
        Image Image `json:"image"`
    }
)

说明:如果以上元语言为java,则greet/common会转换成greet.common

方案二:通过命令行指定import路径

这个方案类似grpc中type结构体的做法,grpc生成举例:

protoc --go_out=plugins=grpc,Mbase/base.proto=foo/bar/base:. foo.proto

其中M{{proto_import}}={{golang_import}}就是指定proto import路径对应目标语言的真实import值

引用grpc方案,api的写法示例:

api文件编写

语法保持不变,沿用api的import语法

定义common.api
$ vim common.api
type Video {
    Name   string `json:"name"`
    Url    string `json:"url"`
    Width  int    `json:"width"`
    Height int    `json:"height"`
        // bytes
    Size int64  `json:"size"`
        // mp4|flv|...
    Ext  string `json:"ext"`
}
import common结构体
vim A.api
import "common/common.api"

type Media{
    // from common.api
    video *Video `json:"video"`
    ....
}

service media-api {
    @handler getMedia
    get /media/get returns (Media)
}
服务结构
greet
├── media
│   └── api
│       ├── etc
│       │   └── media-api.yaml
│       ├── internal
│       │   ├── config
│       │   │   └── config.go
│       │   ├── handler
│       │   │   ├── getmediahandler.go
│       │   │   └── routes.go
│       │   ├── logic
│       │   │   └── getmedialogic.go
│       │   ├── svc
│       │   │   └── servicecontext.go
│       │   └── types
│       │       └── types.go
│       ├── media.api
│       └── media.go
└── common
    ├── common.api
    └── common.go
生成A服务命令
$ cd A/api
$ goctl api go -api A.api -dir . -i common/common.api=common

-i为新增的flag,用于指定api import翻译后目标语言的真实import值

common/common.api=common指定生成目标语言后common中被引用结构体的真实import值,都一个import以英文逗号分割;

以golang举例,其组成形式: {{api_import}}={{golang_import}},因此common/common.api即A.api中定义的import值,common则为生成的types.go中引用Video的真实import值

package types

import "common"

type Media struct{
    // from common.go
    Video *common.Video `json:"video"`
}

回答

0

看到简化开发的新方案很开心😄 还没有完全理解上面的方案,是说以后编写 api 文件,可以引用已有语言的类型定义吗?比如例子里:

@types{
    golang(
        "github.com/go-zero/common/common.Video"
        "github.com/go-zero/common/common.Audio"
    )
    java(
        "com/go-zero/common/common.Video"
        "com/go-zero/common/common.Audio"
    )
}

是不是 github.com/go-zero/common/common.Video 指向的内容是已经定义好的 golang 结构体?

如果这样,是不是把 api 定义文件和 golang 代码文件混在一起了?为什么不能直接在 api 定义文件里直接编写公共/通用结构体定义,然后通过 goctl 生成对应语言的代码呢?

如果理解错了见谅哈 🙏

1

你的前半部分理解是对的,对于你的后面那个问题,【为什么不能直接在 api 定义文件里直接编写公共/通用结构体定义,然后通过 goctl 生成对应语言的代码呢?】 一般来说,公共结构体基本在一个通用文件夹保持不动,而且后续再有新增的结构体则会累计添加即可,无需从api去绕一次,多一次代码编写工作,除此之外,如果真的利用api去编写公共结构体,那么在api中引用时,单纯从api文件来讲可以做到公共结构体存在与否的校验,但是对于生成后的结构体我其实是不知道你最终放到哪里的,会导致某一服务在使用该结构体时增加了找不到结构体的错误,类似proto生成pb一个道理,为了避免这个弊端,因此采用了现有设计方案。

2

一般来说,公共结构体基本在一个通用文件夹保持不动,而且后续再有新增的结构体则会累计添加即可,无需从api去绕一次,多一次代码编写工作

  • 这个应该是实践经验方面的考量,我的经验比较少不好判断。不过从另一个角度看,当新增公共结构体的时候,统一在 api 文件中定义,(反之就会存在 api 文件引用( api 文件)之外的元语言结构体定义),好处是保持结构体定义源头的一致,再就是直接阅读 api 文件就可以了解所以结构体的定义细节(否则就需要去阅读不同元语言结构体定义的文档了)。相应的负担,也就是重新执行一次代码生成。

但是对于生成后的结构体我其实是不知道你最终放到哪里的,会导致某一服务在使用该结构体时增加了找不到结构体的错误

  • 相比手工去维护一个保持不动的通用文件夹,我更乐于把这个工作交给 goctl 去维护,比如现在的 types 文件夹。这样应该不存在找不到结构体定义的问题。

  • 另外,如果按照如下语法在 api 中引用源语言的定义,是不是会导致重复样式代码增多定义文件膨胀?

    // 如果还有 Audio、Image 等就更多了
    @types{
    golang(
        "greet/common/common.Video"
    )
    java(
        "greet/common/common.Video"
    )
    dart(
        "greet/common/common.Video"
    )
    ts(
        "greet/common/common.Video"
    )
    }
1

经过尝试,在不引入新的语法前提下,api 文件按以下方式组织、编写,似乎也可以支持重复引用通过类型(类、结构体)并成功生成正确代码:

1、公共结构体 common.api 文件
type(
    CommonResp struct {
        Code int64 `json:"code"`
        Message string `json:"message"`
    }
)
2、A 服务 user.api 文件
// import "common.api"

type(
    LoginResp struct {
        CommonResp
    }
)
3、B 服务 sys.api 文件
// import "common.api"

type(
    CheckResp struct {
        CommonResp
    }
)
4、入口 admin.api 文件
import "user.api"
import "sys.api"

以上文件遵循现有 api 文件 inport 规则组织,执行:

goctl api go -api admin.api -dir .

生成的代码是正确可用的。存在的问题是:

  1. 不支持嵌套引用,即 user.api 中 // import "common.api" 部分;
  2. 语法检查不支持,在引用 api 文件中,CommonResp 处会提示 can not resolve XXXX
  3. 不支持在引用 api 文件中,CommonRespcmd + click 查看其关联定义;

也许稍微升级插件就可以满足需要了。

2

好处是保持结构体定义源头的一致,再就是直接阅读 api 文件就可以了解所以结构体的定义细节

这个如果在api文件中定义公共结构体,无疑是有这个好处,但是还是会存在那个解决不了的问题就是你指出的第二条

相比手工去维护一个保持不动的通用文件夹,我更乐于把这个工作交给 goctl 去维护,比如现在的 types 文件夹。这样应该不存在找不到结构体定义的问题。

这个明确答案是:goctl也无法实现

另外,如果按照如下语法在 api 中引用源语言的定义,是不是会导致重复样式代码增多定义文件膨胀? 一般只是各个端使用时采取添加,某一端是不会知道其他端(语言)的文件放在哪里或者怎么管理的。

你可以尝试给一下自己的方案,我们一起研究。

7

这个要考虑引入外部工程的公用api文件中的结构体

2

涉及到外部工程我尝试的方法可能就不合适了,不过我也没涉及过😂

4

目前 go-zero 可以支持api引用外部包的结构体了吗?没有看到这方面的示例。

1

现在还不支持 import 结构体吗?