接口定义

go-doudou没有重新造轮子,直接采用Go语言接口类型来做为接口描述语言IDL。用户可以在Go语言接口类型里定义方法,来让go-doudou生成对应的接口代码。

优势

  • 对go-doudou的用户来说,易学易上手
  • 对go-doudou开发者来说,Go语言编译器可以帮我们做语法检查,IDE可以为我们提供语法高亮,省去了开发IDL和IDE插件的工作量

劣势或限制

用接口方法作为接口描述语言存在一些局限性。

  1. 仅支持生成GET, POST, PUT, DELETE接口。默认是POST接口。你可以给方法名加上Get/Post/Put/Delete前缀,指定接口的http请求方法。
  2. 方法签名第一个入参必须是context.Context
  3. 方法签名中的入参和出参仅支持绝大多数常见的Go语言内建类型在新窗口打开,字符串作为键的字典类型,vo包中的自定义结构体类型,以及相对应的切片和指针类型。 当生成代码和OpenAPI 3.0的接口文档的时候,go-doudou只会扫描vo包中的结构体。如果方法签名中出现了在vo包之外定义的结构体类型,go-doudou是不知道它里面有哪些字段的。
  4. 作为特例,你可以用v3.FileModel类型作为入参来上传文件,用*os.File类型作为出参来下载文件。
  5. 不支持别名类型作为结构体的字段。
  6. 不支持函数类型、通道类型和匿名结构体类型作为方法签名的入参和出参。
  7. go-doudou将指针类型的入参视为非必传,非指针类型的入参都视为必传。
  8. 对于OpenAPI 3.0的接口文档生成:
  9. 对于Protobuf,暂时不支持 oneof
  10. 定义stream类型的入参和出参时,参数名称必须以stream作为前缀,例如:stream1stream2streamReqstreamResp等等都可以。

枚举

go-doudou 从v1.0.5起新增对枚举的支持。

定义方法

  1. 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)
}
1
2
3
4
5
6
  1. 定义若干该枚举类型的常量

示例代码

完整的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"`
}
1
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...)

定义规则:

  1. 注解可以写在go语言文档注释中的任意位置,前后都可以有其他文字说明,且无需空格
  2. 注解必须以@符号开头,注解名称中不能有空白字符
  3. 英文括号()里的内容会解析成字符串切片类型的参数,多个参数以英文逗号,分隔,可以不定义任何参数,但是英文括号不能省

示例:@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",
			},
		},
	},
}
1
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",
			},
		},
	},
}
1
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()
1
2

获取到路由名称,调用httpsrv.RouteAnnotationStoreGetParams方法就可以拿到当前路由的注解信息。

以下是示例代码:

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)
			}
		})
	}
}
1
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
}
1
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 v3oneof
  • 定义stream类型的入参和出参时,参数名称必须以stream作为前缀,例如:stream1stream2streamReqstreamResp等等都可以

更多示例

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)
}
1
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