接口定义
go-doudou
没有重新造轮子,直接采用Go语言接口类型来做为接口描述语言IDL。用户可以在Go语言接口类型里定义方法,来让go-doudou生成对应的接口代码。
优势
- 对go-doudou的用户来说,易学易上手
- 对go-doudou开发者来说,Go语言编译器可以帮我们做语法检查,IDE可以为我们提供语法高亮,省去了开发IDL和IDE插件的工作量
劣势或限制
用接口方法作为接口描述语言存在一些局限性。
- 仅支持生成
GET
,POST
,PUT
,DELETE
接口。默认是POST
接口。你可以给方法名加上Get
/Post
/Put
/Delete
前缀,指定接口的http请求方法。 - 方法签名第一个入参必须是
context.Context
。 - 方法签名中的入参和出参仅支持绝大多数常见的Go语言内建类型,字符串作为键的字典类型,
vo
包中的自定义结构体类型,以及相对应的切片和指针类型。 当生成代码和OpenAPI 3.0
的接口文档的时候,go-doudou只会扫描vo
包中的结构体。如果方法签名中出现了在vo
包之外定义的结构体类型,go-doudou是不知道它里面有哪些字段的。 - 作为特例,你可以用
v3.FileModel
类型作为入参来上传文件,用*os.File
类型作为出参来下载文件。 - 不支持别名类型作为结构体的字段。
- 不支持函数类型、通道类型和匿名结构体类型作为方法签名的入参和出参。
go-doudou
将指针类型的入参视为非必传,非指针类型的入参都视为必传。- 对于
OpenAPI 3.0
的接口文档生成:- 不支持请求头和响应头,全局参数以及权限校验。你可以把这些内容作为Go语言注释,写在接口声明的上方或者接口方法签名的上方,这些注释会作为
description
的值生成到接口文档里,然后显示在在线接口文档页面的相应位置。 - 不支持Tag Object, Callback Object, Discriminator Object, XML Object, Security Scheme Object, OAuth Flows Object, OAuth Flow Object, Security Requirement Object . 你可能并不会用到这些API,但我需要在这里提一下。
- 不支持请求头和响应头,全局参数以及权限校验。你可以把这些内容作为Go语言注释,写在接口声明的上方或者接口方法签名的上方,这些注释会作为
- 对于Protobuf,暂时不支持
oneof
。 - 定义stream类型的入参和出参时,参数名称必须以
stream
作为前缀,例如:stream1
,stream2
,streamReq
,streamResp
等等都可以。
枚举
go-doudou 从v1.0.5起新增对枚举的支持。
定义方法
- 在
vo
包里定义一个基础类型的别名类型作为枚举类型,并且实现github.com/unionj-cloud/go-doudou/v2/toolkit/openapi/v3
包里的IEnum
接口
type IEnum interface {
StringSetter(value string)
StringGetter() string
UnmarshalJSON(bytes []byte) error
MarshalJSON() ([]byte, error)
}
2
3
4
5
6
- 定义若干该枚举类型的常量
示例代码
完整的demo代码,请移步 go-doudou-tutorials/enumdemo
package vo
import "encoding/json"
//go:generate go-doudou name --file $GOFILE -o
type KeyboardLayout int
const (
UNKNOWN KeyboardLayout = iota
QWERTZ
AZERTY
QWERTY
)
func (k *KeyboardLayout) StringSetter(value string) {
switch value {
case "UNKNOWN":
*k = UNKNOWN
case "QWERTY":
*k = QWERTY
case "QWERTZ":
*k = QWERTZ
case "AZERTY":
*k = AZERTY
default:
*k = UNKNOWN
}
}
func (k *KeyboardLayout) StringGetter() string {
switch *k {
case UNKNOWN:
return "UNKNOWN"
case QWERTY:
return "QWERTY"
case QWERTZ:
return "QWERTZ"
case AZERTY:
return "AZERTY"
default:
return "UNKNOWN"
}
}
func (k *KeyboardLayout) UnmarshalJSON(bytes []byte) error {
var _k string
err := json.Unmarshal(bytes, &_k)
if err != nil {
return err
}
k.StringSetter(_k)
return nil
}
func (k KeyboardLayout) MarshalJSON() ([]byte, error) {
return json.Marshal(k.StringGetter())
}
type Keyboard struct {
Layout KeyboardLayout `json:"layout,omitempty"`
Backlit bool `json:"backlit,omitempty"`
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
注解
go-doudou 从v1.1.7起新增对注解的支持。
开发者可以在接口方法定义上方的go语言文档注释中添加自定义的注解来给接口添加元数据,方便在编写自定义中间件的时候读取这些数据实现统一的业务处理。
定义方法
定义格式:@注解名称(参数1,参数2,参数3...)
。
定义规则:
- 注解可以写在go语言文档注释中的任意位置,前后都可以有其他文字说明,且无需空格
- 注解必须以
@
符号开头,注解名称中不能有空白字符 - 英文括号
()
里的内容会解析成字符串切片类型的参数,多个参数以英文逗号,
分隔,可以不定义任何参数,但是英文括号不能省
示例:@role(admin)
、@permission(create,update,del)
、@inner()
生成代码
go-doudou 通过解析开发者定义的注解,在transport/httpsrv/handler.go
文件中生成github.com/unionj-cloud/go-doudou/v2/framework.AnnotationStore
类型的包级别的实例RouteAnnotationStore
。开发者可以在middleware里通过httpsrv.RouteAnnotationStore
读取注解,实现自定义的业务逻辑。
以下是生成代码的示例代码:
// AnnotationStore类型实际是map[string][]Annotation的别名
// key是路由名称
var RouteAnnotationStore = framework.AnnotationStore{
"GetUser": {
{
Name: "@role",
Params: []string{
"USER",
"ADMIN",
},
},
},
"GetAdmin": {
{
Name: "@role",
Params: []string{
"ADMIN",
},
},
},
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
如果是开发gRPC服务,则会在transport/grpc/annotation.go
文件中生成github.com/unionj-cloud/go-doudou/v2/framework.AnnotationStore
类型的包级别的实例RouteAnnotationStore
。开发者可以在自定义的interceptor里通过grpc.MethodAnnotationStore
读取注解,实现自定义的业务逻辑。
以下是生成的示例代码:
/**
* Generated by go-doudou v2.0.8.
* Don't edit!
*/
package grpc
import (
"github.com/unionj-cloud/go-doudou/v2/framework"
)
var MethodAnnotationStore = framework.AnnotationStore{
"GetUserRpc": {
{
Name: "@role",
Params: []string{
"USER",
"ADMIN",
},
},
},
"GetAdminRpc": {
{
Name: "@role",
Params: []string{
"ADMIN",
},
},
},
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
REST服务中的使用方法
开发者可以在自定义中间件中,通过以下两行代码
paramsFromCtx := httprouter.ParamsFromContext(r.Context())
routeName := paramsFromCtx.MatchedRouteName()
2
获取到路由名称,调用httpsrv.RouteAnnotationStore
的GetParams
方法就可以拿到当前路由的注解信息。
以下是示例代码:
func Auth(client authClient.IAuthClient) func(inner http.Handler) http.Handler {
return func(inner http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
paramsFromCtx := httprouter.ParamsFromContext(r.Context())
routeName := paramsFromCtx.MatchedRouteName()
if !httpsrv.RouteAnnotationStore.HasAnnotation(routeName, "@role") {
inner.ServeHTTP(w, r)
return
}
authHeader := r.Header.Get("Authorization")
baseToken := strings.TrimSpace(strings.TrimPrefix(authHeader, "Bearer "))
if stringutils.IsEmpty(baseToken) {
if err := r.ParseForm(); err != nil {
w.WriteHeader(401)
w.Write([]byte("Unauthorised\n"))
return
}
baseToken = r.FormValue("t")
}
if stringutils.IsEmpty(baseToken) {
w.WriteHeader(401)
w.Write([]byte("Unauthorised\n"))
return
}
if stringutils.IsNotEmpty(baseToken) {
var (
err error
userVo vo.UserVo
)
if _, userVo, err = client.GetUserByToken(r.Context(), nil, baseToken); err != nil {
w.WriteHeader(401)
w.Write([]byte("Unauthorised\n"))
return
}
role := service.USER
if userVo.SuperAdmin {
role = service.SUPER_ADMIN
}
params := httpsrv.RouteAnnotationStore.GetParams(routeName, "@role")
if !sliceutils.StringContains(params, role.StringGetter()) {
w.WriteHeader(403)
w.Write([]byte("Access denied\n"))
return
}
inner.ServeHTTP(w, r.WithContext(service.NewLoginUserContext(r.Context(), userVo)))
} else {
inner.ServeHTTP(w, r)
}
})
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
gRPC服务中的使用方法
先通过 method := fullMethod[strings.LastIndex(fullMethod, "/")+1:]
拿到方法名称,再通过 MethodAnnotationStore.HasAnnotation
判断该方法是否加上了该拦截器关注的注解,如果没有,则放行,有则继续执行后面的业务逻辑。后面可以通过 MethodAnnotationStore.GetParams
拿到注解参数,实现自定义的业务逻辑。
func (interceptor *AuthInterceptor) Authorize(ctx context.Context, fullMethod string) (context.Context, error) {
method := fullMethod[strings.LastIndex(fullMethod, "/")+1:]
if !MethodAnnotationStore.HasAnnotation(method, "@role") {
return ctx, nil
}
token, err := grpc_auth.AuthFromMD(ctx, "Basic")
if err != nil {
return ctx, err
}
user, pass, ok := parseToken(token)
if !ok {
return ctx, status.Error(codes.Unauthenticated, "Provide user name and password")
}
role, exists := interceptor.userStore[vo.Auth{user, pass}]
if !exists {
return ctx, status.Error(codes.Unauthenticated, "Provide user name and password")
}
params := MethodAnnotationStore.GetParams(method, "@role")
if !sliceutils.StringContains(params, role.StringGetter()) {
return ctx, status.Error(codes.PermissionDenied, "Access denied")
}
return ctx, nil
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
gRPC
上文介绍的所有规则都适用于定义gRPC服务。此外还有两点说明:
- 暂不支持
Protobuf v3
的oneof
- 定义stream类型的入参和出参时,参数名称必须以
stream
作为前缀,例如:stream1
,stream2
,streamReq
,streamResp
等等都可以
更多示例
package service
import (
"context"
v3 "github.com/unionj-cloud/go-doudou/v2/toolkit/openapi/v3"
"os"
"usersvc/vo"
)
// Usersvc是用户管理服务
// 你需要设置Authentication请求头,带上bearer token参数来访问被保护的接口,如用户信息查询接口、用户分页查询接口和上传头像接口。
// 你可以在这里加上服务整体的文档说明
type Usersvc interface {
// PageUsers 用户分页查询接口
// 演示如何定义post请求的,application/json类型的接口
// @role(user)
PageUsers(ctx context.Context,
// pagination parameter
query vo.PageQuery) (
// pagination result
data vo.PageRet,
// error
err error)
// GetUser 用户详情接口
// 演示如何定义get请求的,带查询字符串参数的接口
GetUser(ctx context.Context,
// user id
userId int) (
// user detail
data vo.UserVo,
// error
err error)
// PublicSignUp 用户注册接口
// 演示如何定义post请求的,application/x-www-form-urlencoded类型的接口
PublicSignUp(ctx context.Context,
// username
// @validate(gt=0,lte=60)
username string,
// password
// @validate(gt=0,lte=60)
password string,
// image code, optional as it is pointer type
code *string,
) (
// return OK if success
data string, err error)
// PublicLogIn 用户登录接口
// 演示如何定义post请求的,application/x-www-form-urlencoded类型的接口
PublicLogIn(ctx context.Context,
// username
username string,
// password
password string) (
// token
data string, err error)
// UploadAvatar 头像上传接口
// 演示如何定义文件上传接口
// 注意:必须要有至少一个v3.FileModel类型或[]v3.FileModel类型的入参
UploadAvatar(ctx context.Context,
// user avatar
avatar v3.FileModel, id int) (
// return OK if success
data string, err error)
// GetPublicDownloadAvatar 头像下载接口
// 演示如何定义文件下载接口
// 注意:一定要有一个且仅有一个*os.File类型的出参
GetPublicDownloadAvatar(ctx context.Context,
// user id
userId int) (
// avatar file
data *os.File, err error)
// BiStream 演示如何定义双向流RPC
BiStream(ctx context.Context, stream vo.Order) (stream1 vo.Page, err error)
// ClientStream 演示如何定义客户端流RPC
ClientStream(ctx context.Context, stream vo.Order) (data vo.Page, err error)
// ServerStream 演示如何定义服务端流RPC
ServerStream(ctx context.Context, payload vo.Order) (stream vo.Page, err error)
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86