Compare commits

..

49 Commits

Author SHA1 Message Date
kevin
6c4a4be5d2 update shorturl doc 2020-08-29 20:27:52 +08:00
kevin
6e3d99e869 reorg imports 2020-08-29 14:31:51 +08:00
Keson
0f97b2019a rpc generation support windows (#28)
* add execute files

* add protoc-osx

* add rpc generation

* add rpc generation

* add: rpc template generation

* update usage

* fixed env prepare for project in go path

* optimize gomod cache

* add README.md

* format error

* reactor templatex.go

* remove waste code

* update project.go & README.md

* update project.go & README.md

* rpc generation supports windows
2020-08-29 14:30:17 +08:00
kevin
0cf4ed46a1 update shorturl doc 2020-08-29 00:36:36 +08:00
kevin
3affe62ae4 update shorturl doc 2020-08-29 00:28:57 +08:00
Keson
0734bbcab3 update handler generation (#27)
* add execute files

* add protoc-osx

* add rpc generation

* add rpc generation

* add: rpc template generation

* update usage

* fixed env prepare for project in go path

* optimize gomod cache

* add README.md

* format error

* reactor templatex.go

* remove waste code

* update project.go & README.md

* update project.go & README.md
2020-08-29 00:15:15 +08:00
kevin
f411178a4f refine rpc generator 2020-08-28 22:44:41 +08:00
kevin
72132ce399 refine goctl rpc generator 2020-08-28 21:22:35 +08:00
Keson
db16115037 rpc service generation (#26)
* add execute files

* add protoc-osx

* add rpc generation

* add rpc generation

* add: rpc template generation

* update usage

* fixed env prepare for project in go path

* optimize gomod cache

* add README.md

* format error

* reactor templatex.go

* remove waste code
2020-08-28 19:24:58 +08:00
kevin
71bbf91a63 update shorturl doc 2020-08-27 23:29:56 +08:00
kevin
69ccc61cfe update shorturl doc 2020-08-27 23:16:07 +08:00
kevin
a94cf653f0 better image rendering 2020-08-27 23:00:40 +08:00
kevin
77e23ad65d add quick example 2020-08-27 22:54:18 +08:00
kingxt
38806e7237 fix config yaml gen (#25)
* optimized

* format

Co-authored-by: kingxt <dream4kingxt@163.com>
2020-08-27 15:23:19 +08:00
kevin
a987d12237 sort imports on api generation 2020-08-27 14:40:05 +08:00
kevin
33208e6ef6 return zero value instead of nil on generated logic 2020-08-27 13:49:31 +08:00
kevin
5d8a3c07cd disable cpu stat in wsl linux 2020-08-27 13:22:44 +08:00
kevin
1c24e71568 use yaml, and detect go.mod in current dir 2020-08-27 11:44:35 +08:00
kevin
229544f3ca move test code into internal package 2020-08-26 15:18:45 +08:00
kevin
c575fa7f95 fix ci script 2020-08-26 14:59:04 +08:00
kevin
fe2252184a update ci configuration 2020-08-26 14:53:12 +08:00
kevin
1a8014c704 add more tests 2020-08-26 14:32:35 +08:00
kevin
30e52707ae add more tests 2020-08-26 14:19:16 +08:00
kingxt
73b61e09ed fix format (#23)
* fir format

* fix bug

Co-authored-by: kingxt <dream4kingxt@163.com>
2020-08-26 11:32:55 +08:00
kevin
9b8595a85e add more tests 2020-08-25 22:42:42 +08:00
kevin
015e284515 add more tests 2020-08-25 20:21:59 +08:00
kevin
456b395860 use predefined endpoint separator 2020-08-25 18:36:30 +08:00
kevin
f3c367a323 add fatal to stderr 2020-08-25 16:59:14 +08:00
kevin
a32028c4fb add etcd deploy yaml 2020-08-25 16:32:01 +08:00
kevin
b4572fa064 add more tests 2020-08-24 23:09:46 +08:00
kevin
ccbabf6f58 add more tests 2020-08-24 18:18:58 +08:00
kevin
5989444227 add more tests 2020-08-23 22:33:20 +08:00
kevin
dc286a03f5 add more tests 2020-08-23 15:53:10 +08:00
kevin
b82c02ed16 add more tests 2020-08-22 23:08:33 +08:00
kevin
59ba4ecc5b accelerate tests 2020-08-21 23:24:07 +08:00
kevin
5e7b514ae2 make tests parallel 2020-08-21 23:15:45 +08:00
kevin
2b1466e41e add more tests 2020-08-21 23:09:35 +08:00
kevin
9c9f80518f update readme 2020-08-21 22:51:04 +08:00
kevin
25973d6b59 update doc, add architecture picture 2020-08-21 20:09:53 +08:00
kevin
6237d01948 make test stable 2020-08-21 16:57:17 +08:00
kevin
49316b113e update readme 2020-08-21 16:52:17 +08:00
kevin
6a673e8cb0 add more tests 2020-08-21 16:42:08 +08:00
kingxt
0efa28ddbd fix generate api demo (#19)
Co-authored-by: kingxt <dream4kingxt@163.com>
2020-08-21 13:47:35 +08:00
kevin
0b6a13fe84 add more tests 2020-08-20 22:53:18 +08:00
kevin
11aa6668e8 add more tests 2020-08-20 15:35:13 +08:00
kevin
267a283328 reorg imports 2020-08-20 10:46:39 +08:00
kevin
2d8366b30e update keywords.md 2020-08-20 10:44:14 +08:00
Keson
db83843558 gocctl model v20200819 (#18)
* rename snake、came method

* new: generate model from data source

* add change log md

* update model doc

* update  doc

* beauty code
2020-08-20 10:29:18 +08:00
kevin
50565c9765 update doc 2020-08-19 22:34:54 +08:00
116 changed files with 5052 additions and 430 deletions

View File

@@ -1,3 +1,4 @@
ignore:
- "example/*"
- "tools/*"
- "doc"
- "example"
- "tools"

View File

@@ -1,36 +0,0 @@
run:
# concurrency: 6
timeout: 5m
skip-dirs:
- core
- doc
- example
- rest
- rpcx
- tools
linters:
disable-all: true
enable:
- bodyclose
- deadcode
- errcheck
- gosimple
- govet
- ineffassign
- staticcheck
- structcheck
- typecheck
- unused
- varcheck
# - dupl
linters-settings:
issues:
exclude-rules:
- linters:
- staticcheck
text: 'SA1019: (baseresponse.BoolResponse|oldresponse.FormatBadRequestResponse|oldresponse.FormatResponse)|SA5008: unknown JSON option ("optional"|"default=|"range=|"options=)'

View File

@@ -213,7 +213,10 @@ func TestTimingWheel_SetTimer(t *testing.T) {
}
for _, test := range tests {
test := test
t.Run(stringx.RandId(), func(t *testing.T) {
t.Parallel()
var count int32
ticker := timex.NewFakeTicker()
tick := func() {
@@ -291,7 +294,10 @@ func TestTimingWheel_SetAndMoveThenStart(t *testing.T) {
}
for _, test := range tests {
test := test
t.Run(stringx.RandId(), func(t *testing.T) {
t.Parallel()
var count int32
ticker := timex.NewFakeTicker()
tick := func() {
@@ -376,7 +382,10 @@ func TestTimingWheel_SetAndMoveTwice(t *testing.T) {
}
for _, test := range tests {
test := test
t.Run(stringx.RandId(), func(t *testing.T) {
t.Parallel()
var count int32
ticker := timex.NewFakeTicker()
tick := func() {
@@ -454,7 +463,10 @@ func TestTimingWheel_ElapsedAndSet(t *testing.T) {
}
for _, test := range tests {
test := test
t.Run(stringx.RandId(), func(t *testing.T) {
t.Parallel()
var count int32
ticker := timex.NewFakeTicker()
tick := func() {
@@ -542,7 +554,10 @@ func TestTimingWheel_ElapsedAndSetThenMove(t *testing.T) {
}
for _, test := range tests {
test := test
t.Run(stringx.RandId(), func(t *testing.T) {
t.Parallel()
var count int32
ticker := timex.NewFakeTicker()
tick := func() {

View File

@@ -0,0 +1,4 @@
apiVersion: v1
kind: Namespace
metadata:
name: discov

View File

@@ -0,0 +1,368 @@
apiVersion: v1
kind: Service
metadata:
name: etcd
namespace: discov
spec:
ports:
- name: etcd-port
port: 2379
protocol: TCP
targetPort: 2379
selector:
app: etcd
---
apiVersion: v1
kind: Pod
metadata:
labels:
app: etcd
etcd_node: etcd0
name: etcd0
namespace: discov
spec:
containers:
- command:
- /usr/local/bin/etcd
- --name
- etcd0
- --initial-advertise-peer-urls
- http://etcd0:2380
- --listen-peer-urls
- http://0.0.0.0:2380
- --listen-client-urls
- http://0.0.0.0:2379
- --advertise-client-urls
- http://etcd0:2379
- --initial-cluster
- etcd0=http://etcd0:2380,etcd1=http://etcd1:2380,etcd2=http://etcd2:2380,etcd3=http://etcd3:2380,etcd4=http://etcd4:2380
- --initial-cluster-state
- new
image: quay.io/coreos/etcd:latest
name: etcd0
ports:
- containerPort: 2379
name: client
protocol: TCP
- containerPort: 2380
name: server
protocol: TCP
affinity:
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchExpressions:
- key: app
operator: In
values:
- etcd
topologyKey: "kubernetes.io/hostname"
restartPolicy: Always
---
apiVersion: v1
kind: Service
metadata:
labels:
etcd_node: etcd0
name: etcd0
namespace: discov
spec:
ports:
- name: client
port: 2379
protocol: TCP
targetPort: 2379
- name: server
port: 2380
protocol: TCP
targetPort: 2380
selector:
etcd_node: etcd0
---
apiVersion: v1
kind: Pod
metadata:
labels:
app: etcd
etcd_node: etcd1
name: etcd1
namespace: discov
spec:
containers:
- command:
- /usr/local/bin/etcd
- --name
- etcd1
- --initial-advertise-peer-urls
- http://etcd1:2380
- --listen-peer-urls
- http://0.0.0.0:2380
- --listen-client-urls
- http://0.0.0.0:2379
- --advertise-client-urls
- http://etcd1:2379
- --initial-cluster
- etcd0=http://etcd0:2380,etcd1=http://etcd1:2380,etcd2=http://etcd2:2380,etcd3=http://etcd3:2380,etcd4=http://etcd4:2380
- --initial-cluster-state
- new
image: quay.io/coreos/etcd:latest
name: etcd1
ports:
- containerPort: 2379
name: client
protocol: TCP
- containerPort: 2380
name: server
protocol: TCP
affinity:
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchExpressions:
- key: app
operator: In
values:
- etcd
topologyKey: "kubernetes.io/hostname"
restartPolicy: Always
---
apiVersion: v1
kind: Service
metadata:
labels:
etcd_node: etcd1
name: etcd1
namespace: discov
spec:
ports:
- name: client
port: 2379
protocol: TCP
targetPort: 2379
- name: server
port: 2380
protocol: TCP
targetPort: 2380
selector:
etcd_node: etcd1
---
apiVersion: v1
kind: Pod
metadata:
labels:
app: etcd
etcd_node: etcd2
name: etcd2
namespace: discov
spec:
containers:
- command:
- /usr/local/bin/etcd
- --name
- etcd2
- --initial-advertise-peer-urls
- http://etcd2:2380
- --listen-peer-urls
- http://0.0.0.0:2380
- --listen-client-urls
- http://0.0.0.0:2379
- --advertise-client-urls
- http://etcd2:2379
- --initial-cluster
- etcd0=http://etcd0:2380,etcd1=http://etcd1:2380,etcd2=http://etcd2:2380,etcd3=http://etcd3:2380,etcd4=http://etcd4:2380
- --initial-cluster-state
- new
image: quay.io/coreos/etcd:latest
name: etcd2
ports:
- containerPort: 2379
name: client
protocol: TCP
- containerPort: 2380
name: server
protocol: TCP
affinity:
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchExpressions:
- key: app
operator: In
values:
- etcd
topologyKey: "kubernetes.io/hostname"
restartPolicy: Always
---
apiVersion: v1
kind: Service
metadata:
labels:
etcd_node: etcd2
name: etcd2
namespace: discov
spec:
ports:
- name: client
port: 2379
protocol: TCP
targetPort: 2379
- name: server
port: 2380
protocol: TCP
targetPort: 2380
selector:
etcd_node: etcd2
---
apiVersion: v1
kind: Pod
metadata:
labels:
app: etcd
etcd_node: etcd3
name: etcd3
namespace: discov
spec:
containers:
- command:
- /usr/local/bin/etcd
- --name
- etcd3
- --initial-advertise-peer-urls
- http://etcd3:2380
- --listen-peer-urls
- http://0.0.0.0:2380
- --listen-client-urls
- http://0.0.0.0:2379
- --advertise-client-urls
- http://etcd3:2379
- --initial-cluster
- etcd0=http://etcd0:2380,etcd1=http://etcd1:2380,etcd2=http://etcd2:2380,etcd3=http://etcd3:2380,etcd4=http://etcd4:2380
- --initial-cluster-state
- new
image: quay.io/coreos/etcd:latest
name: etcd3
ports:
- containerPort: 2379
name: client
protocol: TCP
- containerPort: 2380
name: server
protocol: TCP
affinity:
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchExpressions:
- key: app
operator: In
values:
- etcd
topologyKey: "kubernetes.io/hostname"
restartPolicy: Always
---
apiVersion: v1
kind: Service
metadata:
labels:
etcd_node: etcd3
name: etcd3
namespace: discov
spec:
ports:
- name: client
port: 2379
protocol: TCP
targetPort: 2379
- name: server
port: 2380
protocol: TCP
targetPort: 2380
selector:
etcd_node: etcd3
---
apiVersion: v1
kind: Pod
metadata:
labels:
app: etcd
etcd_node: etcd4
name: etcd4
namespace: discov
spec:
containers:
- command:
- /usr/local/bin/etcd
- --name
- etcd4
- --initial-advertise-peer-urls
- http://etcd4:2380
- --listen-peer-urls
- http://0.0.0.0:2380
- --listen-client-urls
- http://0.0.0.0:2379
- --advertise-client-urls
- http://etcd4:2379
- --initial-cluster
- etcd0=http://etcd0:2380,etcd1=http://etcd1:2380,etcd2=http://etcd2:2380,etcd3=http://etcd3:2380,etcd4=http://etcd4:2380
- --initial-cluster-state
- new
image: quay.io/coreos/etcd:latest
name: etcd4
ports:
- containerPort: 2379
name: client
protocol: TCP
- containerPort: 2380
name: server
protocol: TCP
affinity:
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchExpressions:
- key: app
operator: In
values:
- etcd
topologyKey: "kubernetes.io/hostname"
restartPolicy: Always
---
apiVersion: v1
kind: Service
metadata:
labels:
etcd_node: etcd4
name: etcd4
namespace: discov
spec:
ports:
- name: client
port: 2379
protocol: TCP
targetPort: 2379
- name: server
port: 2380
protocol: TCP
targetPort: 2380
selector:
etcd_node: etcd4

View File

@@ -4,9 +4,8 @@ import (
"os"
"testing"
"github.com/tal-tech/go-zero/core/fs"
"github.com/stretchr/testify/assert"
"github.com/tal-tech/go-zero/core/fs"
)
const (

View File

@@ -213,6 +213,7 @@ func Infof(format string, v ...interface{}) {
func Must(err error) {
if err != nil {
msg := formatWithCaller(err.Error(), 3)
log.Print(msg)
output(severeLog, levelFatal, msg)
os.Exit(1)
}

View File

@@ -22,19 +22,30 @@ var (
cores uint64
)
// if /proc not present, ignore the cpu calcuation, like wsl linux
func init() {
cpus, err := perCpuUsage()
logx.Must(err)
cores = uint64(len(cpus))
if err != nil {
logx.Error(err)
return
}
cores = uint64(len(cpus))
sets, err := cpuSets()
logx.Must(err)
if err != nil {
logx.Error(err)
return
}
quota = float64(len(sets))
cq, err := cpuQuota()
if err == nil {
if cq != -1 {
period, err := cpuPeriod()
logx.Must(err)
if err != nil {
logx.Error(err)
return
}
limit := float64(cq) / float64(period)
if limit < quota {
@@ -44,10 +55,16 @@ func init() {
}
preSystem, err = systemCpuUsage()
logx.Must(err)
if err != nil {
logx.Error(err)
return
}
preTotal, err = totalCpuUsage()
logx.Must(err)
if err != nil {
logx.Error(err)
return
}
}
func RefreshCpu() uint64 {

View File

@@ -195,6 +195,13 @@ ts需要指定webapi所在目录
goctl api dart -api user/user.api -dir ./src
```
## 根据mysql ddl或者datasource生成model文件
```shell script
$ goctl model mysql -src={filename} -dir={dir} -cache={true|false}
```
详情参考[model文档](https://github.com/tal-tech/go-zero/blob/master/tools/goctl/model/sql/README.MD)
## 根据定义好的简单go文件生成mongo代码文件(仅限golang使用)
```shell
goctl model mongo -src {{yourDir}}/xiao/service/xhb/user/model/usermodel.go -cache yes
@@ -218,7 +225,7 @@ type User struct {
o是改字段需要生产的操作函数 可以取得get,find,set 分别表示生成返回单个对象的查询方法,返回多个对象的查询方法,设置该字段方法
生成的目标文件会覆盖该简单go文件
## goctl rpc生成
## goctl rpc生成(业务剥离中,暂未开放)
命令 `goctl rpc proto -proto ${proto} -service ${serviceName} -project ${projectName} -dir ${directory} -shared ${shared}`
如: `goctl rpc proto -proto test.proto -service test -project xjy -dir .`

BIN
doc/images/architecture.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 333 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 140 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 111 KiB

View File

@@ -10,23 +10,28 @@
## 2. 关键词替换
支持关键词重叠,自动选用最长的关键词,代码示例如下:
```go
replacer := stringx.NewReplacer(map[string]string{
"PHP": "PPT",
"世界上": "吹牛",
"日本": "法国",
"日本的首都": "东京",
"东京": "日本的首都",
})
fmt.Println(replacer.Replace("PHP是世界上最好的语言"))
fmt.Println(replacer.Replace("日本的首都是东京"))
```
可以得到:
```
PPT是吹牛最好的语言
东京是日本的首都
```
示例代码见`example/stringx/replace/replace.go`
## 3. 查找敏感词
代码示例如下:
```go
filter := stringx.NewTrie([]string{
"AV演员",
@@ -47,6 +52,8 @@ fmt.Println(keywords)
## 4. 敏感词过滤
代码示例如下:
```go
filter := stringx.NewTrie([]string{
"AV演员",

647
doc/shorturl.md Normal file
View File

@@ -0,0 +1,647 @@
# 使用go-zero从0到1快速构建高并发的短链服务
## 0. 什么是短链服务?
短链服务就是将长的URL网址通过程序计算等方式转换为简短的网址字符串。
写此短链服务是为了从整体上演示go-zero构建完整微服务的过程算法和实现细节尽可能简化了所以这不是一个高阶的短链服务。
## 1. 短链微服务架构图
<img src="images/shorturl-arch.png" alt="架构图" width="800" />
## 2. 准备工作
* 安装etcd, mysql, redis
* 准备goctl工具
* 直接从`https://github.com/tal-tech/go-zero/releases`下载最新版,后续会加上自动更新
* 也可以从源码编译在任意目录下进行目的是为了编译goctl工具
1. `git clone https://github.com/tal-tech/go-zero`
2.`tools/goctl`目录下编译goctl工具`go build goctl.go`
3. 将生成的goctl放到`$PATH`确保goctl命令可运行
* 创建工作目录`shorturl`
*`shorturl`目录下执行`go mod init shorturl`初始化`go.mod`
## 3. 编写API Gateway代码
* 通过goctl生成`shorturl.api`并编辑,为了简洁,去除了文件开头的`info`,代码如下:
```go
type (
shortenReq struct {
url string `form:"url"`
}
shortenResp struct {
shortUrl string `json:"shortUrl"`
}
)
type (
expandReq struct {
key string `form:"key"`
}
expandResp struct {
url string `json:"url"`
}
)
service shorturl-api {
@server(
handler: ShortenHandler
)
get /shorten(shortenReq) returns(shortenResp)
@server(
handler: ExpandHandler
)
get /expand(expandReq) returns(expandResp)
}
```
type用法和go一致service用来定义get/post/head/delete等api请求解释如下
* `service shorturl-api {`这一行定义了service名字
* `@server`部分用来定义server端用到的属性
* `handler`定义了服务端handler名字
* `get /shorten(shortenReq) returns(shortenResp)`定义了get方法的路由、请求参数、返回参数等
* 使用goctl生成API Gateway代码
```shell
goctl api go -api shorturl.api -dir api
```
生成的文件结构如下:
```
.
├── api
│   ├── etc
│   │   └── shorturl-api.yaml // 配置文件
│   ├── internal
│   │   ├── config
│   │   │   └── config.go // 定义配置
│   │   ├── handler
│   │   │   ├── expandhandler.go // 实现expandHandler
│   │   │   ├── routes.go // 定义路由处理
│   │   │   └── shortenhandler.go // 实现shortenHandler
│   │   ├── logic
│   │   │   ├── expandlogic.go // 实现ExpandLogic
│   │   │   └── shortenlogic.go // 实现ShortenLogic
│   │   ├── svc
│   │   │   └── servicecontext.go // 定义ServiceContext
│   │   └── types
│   │   └── types.go // 定义请求、返回结构体
│   └── shorturl.go // main入口定义
├── go.mod
├── go.sum
└── shorturl.api
```
* 启动API Gateway服务默认侦听在8888端口
```shell
go run api/shorturl.go -f api/etc/shorturl-api.yaml
```
* 测试API Gateway服务
```shell
curl -i "http://localhost:8888/shorten?url=http://www.xiaoheiban.cn"
```
返回如下:
```http
HTTP/1.1 200 OK
Content-Type: application/json
Date: Thu, 27 Aug 2020 14:31:39 GMT
Content-Length: 15
{"shortUrl":""}
```
可以看到我们API Gateway其实啥也没干就返回了个空值接下来我们会在rpc服务里实现业务逻辑
* 可以修改`internal/svc/servicecontext.go`来传递服务依赖(如果需要)
* 实现逻辑可以修改`internal/logic`下的对应文件
* 可以通过`goctl`生成各种客户端语言的api调用代码
* 到这里你已经可以通过goctl生成客户端代码给客户端同学并行开发了支持多种语言详见文档
## 4. 编写shorten rpc服务
* 在`rpc/shorten`目录下编写`shorten.proto`文件
可以通过命令生成proto文件模板
```shell
goctl rpc template -o shorten.proto
```
修改后文件内容如下:
```protobuf
syntax = "proto3";
package shorten;
message shortenReq {
string url = 1;
}
message shortenResp {
string key = 1;
}
service shortener {
rpc shorten(shortenReq) returns(shortenResp);
}
```
* 用`goctl`生成rpc代码在`rpc/shorten`目录下执行命令
```shell
goctl rpc proto -src shorten.proto
```
文件结构如下:
```
rpc/shorten
├── etc
│   └── shorten.yaml // 配置文件
├── internal
│   ├── config
│   │   └── config.go // 配置定义
│   ├── logic
│   │   └── shortenlogic.go // rpc业务逻辑在这里实现
│   ├── server
│   │   └── shortenerserver.go // 调用入口, 不需要修改
│   └── svc
│   └── servicecontext.go // 定义ServiceContext传递依赖
├── pb
│   └── shorten.pb.go
├── shorten.go // rpc服务main函数
├── shorten.proto
└── shortener
├── shortener.go // 提供了外部调用方法,无需修改
├── shortener_mock.go // mock方法测试用
└── types.go // request/response结构体定义
```
直接可以运行,如下:
```shell
$ go run shorten.go -f etc/shorten.yaml
Starting rpc server at 127.0.0.1:8080...
```
`etc/shorten.yaml`文件里可以修改侦听端口等配置
## 5. 编写expand rpc服务
* 在`rpc/expand`目录下编写`expand.proto`文件
可以通过命令生成proto文件模板
```shell
goctl rpc template -o expand.proto
```
修改后文件内容如下:
```protobuf
syntax = "proto3";
package expand;
message expandReq {
string key = 1;
}
message expandResp {
string url = 1;
}
service expander {
rpc expand(expandReq) returns(expandResp);
}
```
* 用`goctl`生成rpc代码在`rpc/expand`目录下执行命令
```shell
goctl rpc proto -src expand.proto
```
文件结构如下:
```
rpc/expand
├── etc
│   └── expand.yaml // 配置文件
├── expand.go // rpc服务main函数
├── expand.proto
├── expander
│   ├── expander.go // 提供了外部调用方法,无需修改
│   ├── expander_mock.go // mock方法测试用
│   └── types.go // request/response结构体定义
├── internal
│   ├── config
│   │   └── config.go // 配置定义
│   ├── logic
│   │   └── expandlogic.go // rpc业务逻辑在这里实现
│   ├── server
│   │   └── expanderserver.go // 调用入口, 不需要修改
│   └── svc
│   └── servicecontext.go // 定义ServiceContext传递依赖
└── pb
└── expand.pb.go
```
修改`etc/expand.yaml`里面的`ListenOn`的端口为`8081`,因为`8080`已经被`shorten`服务占用了
修改后运行,如下:
```shell
$ go run expand.go -f etc/expand.yaml
Starting rpc server at 127.0.0.1:8081...
```
`etc/expand.yaml`文件里可以修改侦听端口等配置
## 6. 修改API Gateway代码调用shorten/expand rpc服务
* 修改配置文件`shorter-api.yaml`,增加如下内容
```yaml
Shortener:
Etcd:
Hosts:
- localhost:2379
Key: shorten.rpc
Expander:
Etcd:
Hosts:
- localhost:2379
Key: expand.rpc
```
通过etcd自动去发现可用的shorten/expand服务
* 修改`internal/config/config.go`如下增加shorten/expand服务依赖
```go
type Config struct {
rest.RestConf
Shortener rpcx.RpcClientConf // 手动代码
Expander rpcx.RpcClientConf // 手动代码
}
```
* 修改`internal/logic/expandlogic.go`,如下:
```go
type ExpandLogic struct {
ctx context.Context
logx.Logger
expander rpcx.Client // 手动代码
}
func NewExpandLogic(ctx context.Context, svcCtx *svc.ServiceContext) ExpandLogic {
return ExpandLogic{
ctx: ctx,
Logger: logx.WithContext(ctx),
expander: svcCtx.Expander, // 手动代码
}
}
func (l *ExpandLogic) Expand(req types.ExpandReq) (*types.ExpandResp, error) {
// 手动代码开始
resp, err := expander.NewExpander(l.expander).Expand(l.ctx, &expander.ExpandReq{
Key: req.Key,
})
if err != nil {
return nil, err
}
return &types.ExpandResp{
Url: resp.Url,
}, nil
// 手动代码结束
}
```
增加了对`expander`服务的依赖,并通过调用`expander`的`Expand`方法实现短链恢复到url
* 修改`internal/logic/shortenlogic.go`,如下:
```go
type ShortenLogic struct {
ctx context.Context
logx.Logger
shortener rpcx.Client // 手动代码
}
func NewShortenLogic(ctx context.Context, svcCtx *svc.ServiceContext) ShortenLogic {
return ShortenLogic{
ctx: ctx,
Logger: logx.WithContext(ctx),
shortener: svcCtx.Shortener, // 手动代码
}
}
func (l *ShortenLogic) Shorten(req types.ShortenReq) (*types.ShortenResp, error) {
// 手动代码开始
resp, err := shortener.NewShortener(l.shortener).Shorten(l.ctx, &shortener.ShortenReq{
Url: req.Url,
})
if err != nil {
return nil, err
}
return &types.ShortenResp{
ShortUrl: resp.Key,
}, nil
// 手动代码结束
}
```
增加了对`shortener`服务的依赖,并通过调用`shortener`的`Shorten`方法实现url到短链的变换
* 修改`internal/svc/servicecontext.go`,如下:
```go
type ServiceContext struct {
Config config.Config
Shortener rpcx.Client // 手动代码
Expander rpcx.Client // 手动代码
}
func NewServiceContext(config config.Config) *ServiceContext {
return &ServiceContext{
Config: config,
Shortener: rpcx.MustNewClient(config.Shortener), // 手动代码
Expander: rpcx.MustNewClient(config.Expander), // 手动代码
}
}
```
通过ServiceContext在不同业务逻辑之间传递依赖
至此API Gateway修改完成虽然贴的代码多但是期中修改的是很少的一部分为了方便理解上下文我贴了完整代码接下来处理CRUD+cache
## 7. 定义数据库表结构并生成CRUD+cache代码
* shorturl下创建rpc/model目录`mkdir -p rpc/model`
* 在roc/model目录下编写创建shorturl表的sql文件`shorturl.sql`,如下:
```sql
CREATE TABLE `shorturl`
(
`id` bigint(10) NOT NULL AUTO_INCREMENT,
`key` varchar(255) NOT NULL DEFAULT '' COMMENT 'shorten key',
`url` varchar(255) DEFAULT '' COMMENT 'original url',
PRIMARY KEY(`id`),
UNIQUE KEY `key_index`(`key`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
```
* 创建DB和table
```sql
create database gozero;
```
```sql
source shorturl.sql;
```
* 在`rpc/model`目录下执行如下命令生成CRUD+cache代码`-c`表示使用`redis cache`
```shell
goctl model mysql ddl -c -src shorturl.sql -dir .
```
也可以用`datasource`命令代替`ddl`来指定数据库链接直接从schema生成
生成后的文件结构如下:
```
rpc/model
├── shorturl.sql
├── shorturlmodel.go // CRUD+cache代码
└── vars.go // 定义常量和变量
```
## 8. 修改shorten/expand rpc代码调用crud+cache代码
* 修改`rpc/expand/etc/expand.yaml`,增加如下内容:
```yaml
DataSource: root:@tcp(localhost:3306)/gozero
Table: shorturl
Cache:
- Host: localhost:6379
```
可以使用多个redis作为cache支持redis单点或者redis集群
* 修改`rpc/expand/internal/config.go`,如下:
```go
type Config struct {
rpcx.RpcServerConf
DataSource string // 手动代码
Table string // 手动代码
Cache cache.CacheConf // 手动代码
}
```
增加了mysql和redis cache配置
* 修改`rpc/expand/internal/svc/servicecontext.go`,如下:
```go
type ServiceContext struct {
c config.Config
Model *model.ShorturlModel // 手动代码
}
func NewServiceContext(c config.Config) *ServiceContext {
return &ServiceContext{
c: c,
Model: model.NewShorturlModel(sqlx.NewMysql(c.DataSource), c.Cache, c.Table), // 手动代码
}
}
```
* 修改`rpc/expand/internal/logic/expandlogic.go`,如下:
```go
type ExpandLogic struct {
ctx context.Context
logx.Logger
model *model.ShorturlModel // 手动代码
}
func NewExpandLogic(ctx context.Context, svcCtx *svc.ServiceContext) *ExpandLogic {
return &ExpandLogic{
ctx: ctx,
Logger: logx.WithContext(ctx),
model: svcCtx.Model, // 手动代码
}
}
func (l *ExpandLogic) Expand(in *expand.ExpandReq) (*expand.ExpandResp, error) {
// 手动代码开始
res, err := l.model.FindOne(in.Key)
if err != nil {
return nil, err
}
return &expand.ExpandResp{
Url: res.Url,
}, nil
// 手动代码结束
}
```
* 修改`rpc/shorten/etc/shorten.yaml`,增加如下内容:
```yaml
DataSource: root:@tcp(localhost:3306)/gozero
Table: shorturl
Cache:
- Host: localhost:6379
```
可以使用多个redis作为cache支持redis单点或者redis集群
* 修改`rpc/shorten/internal/config.go`,如下:
```go
type Config struct {
rpcx.RpcServerConf
DataSource string // 手动代码
Table string // 手动代码
Cache cache.CacheConf // 手动代码
}
```
增加了mysql和redis cache配置
* 修改`rpc/shorten/internal/svc/servicecontext.go`,如下:
```go
type ServiceContext struct {
c config.Config
Model *model.ShorturlModel // 手动代码
}
func NewServiceContext(c config.Config) *ServiceContext {
return &ServiceContext{
c: c,
Model: model.NewShorturlModel(sqlx.NewMysql(c.DataSource), c.Cache, c.Table), // 手动代码
}
}
```
* 修改`rpc/shorten/internal/logic/shortenlogic.go`,如下:
```go
const keyLen = 6
type ShortenLogic struct {
ctx context.Context
logx.Logger
model *model.ShorturlModel // 手动代码
}
func NewShortenLogic(ctx context.Context, svcCtx *svc.ServiceContext) *ShortenLogic {
return &ShortenLogic{
ctx: ctx,
Logger: logx.WithContext(ctx),
model: svcCtx.Model, // 手动代码
}
}
func (l *ShortenLogic) Shorten(in *shorten.ShortenReq) (*shorten.ShortenResp, error) {
// 手动代码开始,生成短链接
key := hash.Md5Hex([]byte(in.Url))[:keyLen]
_, err := l.model.Insert(model.Shorturl{
Shorten: key,
Url: in.Url,
})
if err != nil {
return nil, err
}
return &shorten.ShortenResp{
Key: key,
}, nil
// 手动代码结束
}
```
至此代码修改完成,凡事手动修改的代码我加了标注
## 9. 完整调用演示
* shorten api调用
```shell
~ curl -i "http://localhost:8888/shorten?url=http://www.xiaoheiban.cn"
```
返回如下:
```http
HTTP/1.1 200 OK
Content-Type: application/json
Date: Sat, 29 Aug 2020 10:49:49 GMT
Content-Length: 21
{"shortUrl":"f35b2a"}
```
* expand api调用
```shell
curl -i "http://localhost:8888/expand?key=f35b2a"
```
返回如下:
```http
HTTP/1.1 200 OK
Content-Type: application/json
Date: Sat, 29 Aug 2020 10:51:53 GMT
Content-Length: 34
{"url":"http://www.xiaoheiban.cn"}
```
## 10. Benchmark
因为写入依赖于mysql的写入速度就相当于压mysql了所以压测只测试了expand接口相当于从mysql里读取并利用缓存shorten.lua里随机从db里获取了100个热key来生成压测请求
![Benchmark](images/shorturl-benchmark.png)
可以看出在我的MacBook Pro上能达到3万+的qps。
## 11. 总结
我们一直强调**工具大于约定和文档**。
go-zero不只是一个框架更是一个建立在框架+工具基础上的,简化和规范了整个微服务构建的技术体系。
我们在保持简单的同时也尽可能把微服务治理的复杂度封装到了框架内部,极大的降低了开发人员的心智负担,使得业务开发得以快速推进。
通过go-zero+goctl生成的代码包含了微服务治理的各种组件包括并发控制、自适应熔断、自适应降载、自动缓存控制等可以轻松部署以承载巨大访问量。

View File

@@ -42,7 +42,7 @@ func main() {
ListenOn: *listen,
}, func(grpcServer *grpc.Server) {
unary.RegisterGreeterServer(grpcServer, &GreetServer{
RpcProxy: rpcx.NewRpcProxy(*server),
RpcProxy: rpcx.NewProxy(*server),
})
})
proxy.Start()

View File

@@ -8,8 +8,9 @@ import (
func main() {
replacer := stringx.NewReplacer(map[string]string{
"PHP": "PPT",
"世界上": "吹牛",
"日本": "法国",
"日本的首都": "东京",
"东京": "日本的首都",
})
fmt.Println(replacer.Replace("PHP是世界上最好的语言"))
fmt.Println(replacer.Replace("日本的首都是东京"))
}

3
go.mod
View File

@@ -8,6 +8,7 @@ require (
github.com/alicebob/miniredis v2.5.0+incompatible
github.com/dchest/siphash v1.2.1
github.com/dgrijalva/jwt-go v3.2.0+incompatible
github.com/dsymonds/gotoc v0.0.0-20160928043926-5aebcfc91819
github.com/fatih/color v1.9.0 // indirect
github.com/frankban/quicktest v1.7.2 // indirect
github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8
@@ -55,7 +56,7 @@ require (
golang.org/x/tools v0.0.0-20200410132612-ae9902aceb98 // indirect
google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f // indirect
google.golang.org/grpc v1.29.1
google.golang.org/protobuf v1.25.0 // indirect
google.golang.org/protobuf v1.25.0
gopkg.in/cheggaaa/pb.v1 v1.0.28
gopkg.in/yaml.v2 v2.2.8
honnef.co/go/tools v0.0.1-2020.1.4 // indirect

2
go.sum
View File

@@ -48,6 +48,8 @@ github.com/dchest/siphash v1.2.1 h1:4cLinnzVJDKxTCl9B01807Yiy+W7ZzVHj/KIroQRvT4=
github.com/dchest/siphash v1.2.1/go.mod h1:q+IRvb2gOSrUnYoPqHiyHXS0FOBBOdl6tONBlVnOnt4=
github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/dsymonds/gotoc v0.0.0-20160928043926-5aebcfc91819 h1:9778zj477h/VauD8kHbOtbytW2KGQefJ/wUGE5w+mzw=
github.com/dsymonds/gotoc v0.0.0-20160928043926-5aebcfc91819/go.mod h1:MvzMVHq8BH2Ji/o8TGDocVA70byvLrAgFTxkEnmjO4Y=
github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4 h1:qk/FSDDxo05wdJH28W+p5yivv7LuLYLRXPPD8KQCtZs=
github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=

View File

@@ -6,6 +6,23 @@
[![Release](https://img.shields.io/github/v/release/tal-tech/go-zero.svg?style=flat-square)](https://github.com/tal-tech/go-zero)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
## 0. go-zero介绍
go-zero是一个集成了各种工程实践的web和rpc框架。通过弹性设计保障了大并发服务端的稳定性经受了充分的实战检验。
go-zero 包含极简的 API 定义和生成工具 goctl可以根据定义的 api 文件一键生成 Go, iOS, Android, Kotlin, Dart, TypeScript, JavaScript 代码,并可直接运行。
使用go-zero的好处
* 轻松获得支撑千万日活服务的稳定性
* 内建级联超时控制、限流、自适应熔断、自适应降载等微服务治理能力,无需配置和额外代码
* 微服务治理中间件可无缝集成到其它现有框架使用
* 极简的API描述一键生成各端代码
* 自动校验客户端请求参数合法性
* 大量微服务治理和并发工具包
<img src="doc/images/architecture.png" alt="架构图" width="1500" />
## 1. go-zero框架背景
18年初晓黑板后端在经过频繁的宕机后决定从`Java+MongoDB`的单体架构迁移到微服务架构,经过仔细思考和对比,我们决定:
@@ -57,33 +74,22 @@ go-zero是一个集成了各种工程实践的包含web和rpc框架有如下
![弹性设计](doc/images/resilience.jpg)
## 4. go-zero框架收益
* 保障大并发服务端的稳定性,经受了充分的实战检验
* 极简的API定义
* 一键生成Go, iOS, Android, Dart, TypeScript, JavaScript代码并可直接运行
* 服务端自动校验参数合法性
## 5. go-zero近期开发计划
## 4. go-zero近期开发计划
* 自动生成API mock server便于客户端开发
* 自动生成服务端功能测试
## 6. Installation
## 5. Installation
1. 在项目目录下通过如下命令安装:
在项目目录下通过如下命令安装:
```shell
go get -u github.com/tal-tech/go-zero
```
```shell
go get -u github.com/tal-tech/go-zero
```
2. 代码里导入go-zero
## 6. Quick Start
```go
import "github.com/tal-tech/go-zero"
```
## 7. Quick Start
0. 完整示例请查看[从0到1快速构建一个高并发的微服务系统](doc/shorturl.md)
1. 编译goctl工具
@@ -97,7 +103,7 @@ go-zero是一个集成了各种工程实践的包含web和rpc框架有如下
```go
type Request struct {
Name string `path:"name"`
Name string `path:"name,options=you|me"` // 框架自动验证请求参数是否合法
}
type Response struct {
@@ -173,17 +179,18 @@ go-zero是一个集成了各种工程实践的包含web和rpc框架有如下
...
```
## 8. Benchmark
## 7. Benchmark
![benchmark](doc/images/benchmark.png)
[测试代码见这里](https://github.com/smallnest/go-web-framework-benchmark)
## 9. 文档
## 8. 文档 (逐步完善中)
* [从0到1快速构建一个高并发的微服务系统](doc/shorturl.md)
* [goctl使用帮助](doc/goctl.md)
* [关键字替换和敏感词过滤工具](doc/keywords.md)
## 10. 微信交流群
## 9. 微信交流群
添加我的微信kevwan请注明go-zero我拉进go-zero社区群🤝

View File

@@ -209,6 +209,6 @@ func (s *engine) use(middleware Middleware) {
func convertMiddleware(ware Middleware) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(ware(next.ServeHTTP))
return ware(next.ServeHTTP)
}
}

View File

@@ -1,6 +1,7 @@
package httpx
import (
"errors"
"net/http"
"strings"
"testing"
@@ -17,6 +18,24 @@ func init() {
logx.Disable()
}
func TestError(t *testing.T) {
const body = "foo"
w := tracedResponseWriter{
headers: make(map[string][]string),
}
Error(&w, errors.New(body))
assert.Equal(t, http.StatusBadRequest, w.code)
assert.Equal(t, body, strings.TrimSpace(w.builder.String()))
}
func TestOk(t *testing.T) {
w := tracedResponseWriter{
headers: make(map[string][]string),
}
Ok(&w)
assert.Equal(t, http.StatusOK, w.code)
}
func TestOkJson(t *testing.T) {
w := tracedResponseWriter{
headers: make(map[string][]string),

101
rpcx/client_test.go Normal file
View File

@@ -0,0 +1,101 @@
package rpcx
import (
"context"
"fmt"
"log"
"net"
"testing"
"github.com/stretchr/testify/assert"
"github.com/tal-tech/go-zero/core/logx"
"github.com/tal-tech/go-zero/rpcx/internal/mock"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/grpc/test/bufconn"
)
func init() {
logx.Disable()
}
func dialer() func(context.Context, string) (net.Conn, error) {
listener := bufconn.Listen(1024 * 1024)
server := grpc.NewServer()
mock.RegisterDepositServiceServer(server, &mock.DepositServer{})
go func() {
if err := server.Serve(listener); err != nil {
log.Fatal(err)
}
}()
return func(context.Context, string) (net.Conn, error) {
return listener.Dial()
}
}
func TestDepositServer_Deposit(t *testing.T) {
tests := []struct {
name string
amount float32
res *mock.DepositResponse
errCode codes.Code
errMsg string
}{
{
"invalid request with negative amount",
-1.11,
nil,
codes.InvalidArgument,
fmt.Sprintf("cannot deposit %v", -1.11),
},
{
"valid request with non negative amount",
0.00,
&mock.DepositResponse{Ok: true},
codes.OK,
"",
},
}
directClient := MustNewClient(RpcClientConf{
Endpoints: []string{"foo"},
App: "foo",
Token: "bar",
Timeout: 1000,
}, WithDialOption(grpc.WithInsecure()), WithDialOption(grpc.WithContextDialer(dialer())))
targetClient, err := NewClientWithTarget("foo", WithDialOption(grpc.WithInsecure()),
WithDialOption(grpc.WithContextDialer(dialer())))
assert.Nil(t, err)
clients := []Client{
directClient,
targetClient,
}
for _, tt := range tests {
for _, client := range clients {
t.Run(tt.name, func(t *testing.T) {
cli := mock.NewDepositServiceClient(client.Conn())
request := &mock.DepositRequest{Amount: tt.amount}
response, err := cli.Deposit(context.Background(), request)
if response != nil {
assert.True(t, len(response.String()) > 0)
if response.GetOk() != tt.res.GetOk() {
t.Error("response: expected", tt.res.GetOk(), "received", response.GetOk())
}
}
if err != nil {
if e, ok := status.FromError(err); ok {
if e.Code() != tt.errCode {
t.Error("error code: expected", codes.InvalidArgument, "received", e.Code())
}
if e.Message() != tt.errMsg {
t.Error("error message: expected", tt.errMsg, "received", e.Message())
}
}
}
})
}
}
}

42
rpcx/config_test.go Normal file
View File

@@ -0,0 +1,42 @@
package rpcx
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/tal-tech/go-zero/core/discov"
"github.com/tal-tech/go-zero/core/service"
"github.com/tal-tech/go-zero/core/stores/redis"
)
func TestRpcClientConf(t *testing.T) {
conf := NewDirectClientConf([]string{"localhost:1234"}, "foo", "bar")
assert.True(t, conf.HasCredential())
conf = NewEtcdClientConf([]string{"localhost:1234", "localhost:5678"}, "key", "foo", "bar")
assert.True(t, conf.HasCredential())
}
func TestRpcServerConf(t *testing.T) {
conf := RpcServerConf{
ServiceConf: service.ServiceConf{},
ListenOn: "",
Etcd: discov.EtcdConf{
Hosts: []string{"localhost:1234"},
Key: "key",
},
Auth: true,
Redis: redis.RedisKeyConf{
RedisConf: redis.RedisConf{
Type: redis.NodeType,
},
Key: "foo",
},
StrictControl: false,
Timeout: 0,
CpuThreshold: 0,
}
assert.True(t, conf.HasEtcd())
assert.NotNil(t, conf.Validate())
conf.Redis.Host = "localhost:5678"
assert.Nil(t, conf.Validate())
}

View File

@@ -0,0 +1,62 @@
package auth
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
"google.golang.org/grpc/metadata"
)
func TestParseCredential(t *testing.T) {
tests := []struct {
name string
withNil bool
withEmptyMd bool
app string
token string
}{
{
name: "nil",
withNil: true,
},
{
name: "empty md",
withEmptyMd: true,
},
{
name: "empty",
},
{
name: "valid",
app: "foo",
token: "bar",
},
}
for _, test := range tests {
test := test
t.Run(test.name, func(t *testing.T) {
t.Parallel()
var ctx context.Context
if test.withNil {
ctx = context.Background()
} else if test.withEmptyMd {
ctx = metadata.NewIncomingContext(context.Background(), metadata.MD{})
} else {
md := metadata.New(map[string]string{
"app": test.app,
"token": test.token,
})
ctx = metadata.NewIncomingContext(context.Background(), md)
}
cred := ParseCredential(ctx)
assert.False(t, cred.RequireTransportSecurity())
m, err := cred.GetRequestMetadata(context.Background())
assert.Nil(t, err)
assert.Equal(t, test.app, m[appKey])
assert.Equal(t, test.token, m[tokenKey])
})
}
}

View File

@@ -3,7 +3,9 @@ package p2c
import (
"context"
"fmt"
"runtime"
"strconv"
"sync"
"testing"
"github.com/stretchr/testify/assert"
@@ -33,19 +35,31 @@ func TestP2cPicker_Pick(t *testing.T) {
tests := []struct {
name string
candidates int
threshold float64
}{
{
name: "single",
candidates: 1,
threshold: 0.9,
},
{
name: "two",
candidates: 2,
threshold: 0.5,
},
{
name: "multiple",
candidates: 100,
threshold: 0.95,
},
}
for _, test := range tests {
test := test
t.Run(test.name, func(t *testing.T) {
t.Parallel()
const total = 10000
builder := new(p2cPickerBuilder)
ready := make(map[resolver.Address]balancer.SubConn)
for i := 0; i < test.candidates; i++ {
@@ -55,7 +69,9 @@ func TestP2cPicker_Pick(t *testing.T) {
}
picker := builder.Build(ready)
for i := 0; i < 10000; i++ {
var wg sync.WaitGroup
wg.Add(total)
for i := 0; i < total; i++ {
_, done, err := picker.Pick(context.Background(), balancer.PickInfo{
FullMethodName: "/",
Ctx: context.Background(),
@@ -64,11 +80,16 @@ func TestP2cPicker_Pick(t *testing.T) {
if i%100 == 0 {
err = status.Error(codes.DeadlineExceeded, "deadline")
}
done(balancer.DoneInfo{
Err: err,
})
go func() {
runtime.Gosched()
done(balancer.DoneInfo{
Err: err,
})
wg.Done()
}()
}
wg.Wait()
dist := make(map[interface{}]int)
conns := picker.(*p2cPicker).conns
for _, conn := range conns {
@@ -76,7 +97,8 @@ func TestP2cPicker_Pick(t *testing.T) {
}
entropy := mathx.CalcEntropy(dist)
assert.True(t, entropy > .95, fmt.Sprintf("entropy is %f, less than .95", entropy))
assert.True(t, entropy > test.threshold, fmt.Sprintf("entropy is %f, less than %f",
entropy, test.threshold))
})
}
}

View File

@@ -0,0 +1,123 @@
package internal
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
"google.golang.org/grpc"
)
func TestWithStreamClientInterceptors(t *testing.T) {
opts := WithStreamClientInterceptors()
assert.NotNil(t, opts)
}
func TestWithUnaryClientInterceptors(t *testing.T) {
opts := WithUnaryClientInterceptors()
assert.NotNil(t, opts)
}
func TestChainStreamClientInterceptors_zero(t *testing.T) {
var vals []int
interceptors := chainStreamClientInterceptors()
_, err := interceptors(context.Background(), nil, new(grpc.ClientConn), "/foo",
func(ctx context.Context, desc *grpc.StreamDesc, cc *grpc.ClientConn, method string,
opts ...grpc.CallOption) (grpc.ClientStream, error) {
vals = append(vals, 1)
return nil, nil
})
assert.Nil(t, err)
assert.ElementsMatch(t, []int{1}, vals)
}
func TestChainStreamClientInterceptors_one(t *testing.T) {
var vals []int
interceptors := chainStreamClientInterceptors(func(ctx context.Context, desc *grpc.StreamDesc,
cc *grpc.ClientConn, method string, streamer grpc.Streamer, opts ...grpc.CallOption) (
grpc.ClientStream, error) {
vals = append(vals, 1)
return streamer(ctx, desc, cc, method, opts...)
})
_, err := interceptors(context.Background(), nil, new(grpc.ClientConn), "/foo",
func(ctx context.Context, desc *grpc.StreamDesc, cc *grpc.ClientConn, method string,
opts ...grpc.CallOption) (grpc.ClientStream, error) {
vals = append(vals, 2)
return nil, nil
})
assert.Nil(t, err)
assert.ElementsMatch(t, []int{1, 2}, vals)
}
func TestChainStreamClientInterceptors_more(t *testing.T) {
var vals []int
interceptors := chainStreamClientInterceptors(func(ctx context.Context, desc *grpc.StreamDesc,
cc *grpc.ClientConn, method string, streamer grpc.Streamer, opts ...grpc.CallOption) (
grpc.ClientStream, error) {
vals = append(vals, 1)
return streamer(ctx, desc, cc, method, opts...)
}, func(ctx context.Context, desc *grpc.StreamDesc, cc *grpc.ClientConn, method string,
streamer grpc.Streamer, opts ...grpc.CallOption) (grpc.ClientStream, error) {
vals = append(vals, 2)
return streamer(ctx, desc, cc, method, opts...)
})
_, err := interceptors(context.Background(), nil, new(grpc.ClientConn), "/foo",
func(ctx context.Context, desc *grpc.StreamDesc, cc *grpc.ClientConn, method string,
opts ...grpc.CallOption) (grpc.ClientStream, error) {
vals = append(vals, 3)
return nil, nil
})
assert.Nil(t, err)
assert.ElementsMatch(t, []int{1, 2, 3}, vals)
}
func TestWithUnaryClientInterceptors_zero(t *testing.T) {
var vals []int
interceptors := chainUnaryClientInterceptors()
err := interceptors(context.Background(), "/foo", nil, nil, new(grpc.ClientConn),
func(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn,
opts ...grpc.CallOption) error {
vals = append(vals, 1)
return nil
})
assert.Nil(t, err)
assert.ElementsMatch(t, []int{1}, vals)
}
func TestWithUnaryClientInterceptors_one(t *testing.T) {
var vals []int
interceptors := chainUnaryClientInterceptors(func(ctx context.Context, method string, req,
reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
vals = append(vals, 1)
return invoker(ctx, method, req, reply, cc, opts...)
})
err := interceptors(context.Background(), "/foo", nil, nil, new(grpc.ClientConn),
func(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn,
opts ...grpc.CallOption) error {
vals = append(vals, 2)
return nil
})
assert.Nil(t, err)
assert.ElementsMatch(t, []int{1, 2}, vals)
}
func TestWithUnaryClientInterceptors_more(t *testing.T) {
var vals []int
interceptors := chainUnaryClientInterceptors(func(ctx context.Context, method string, req,
reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
vals = append(vals, 1)
return invoker(ctx, method, req, reply, cc, opts...)
}, func(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn,
invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
vals = append(vals, 2)
return invoker(ctx, method, req, reply, cc, opts...)
})
err := interceptors(context.Background(), "/foo", nil, nil, new(grpc.ClientConn),
func(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn,
opts ...grpc.CallOption) error {
vals = append(vals, 3)
return nil
})
assert.Nil(t, err)
assert.ElementsMatch(t, []int{1, 2, 3}, vals)
}

View File

@@ -0,0 +1,111 @@
package internal
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
"google.golang.org/grpc"
)
func TestWithStreamServerInterceptors(t *testing.T) {
opts := WithStreamServerInterceptors()
assert.NotNil(t, opts)
}
func TestWithUnaryServerInterceptors(t *testing.T) {
opts := WithUnaryServerInterceptors()
assert.NotNil(t, opts)
}
func TestChainStreamServerInterceptors_zero(t *testing.T) {
var vals []int
interceptors := chainStreamServerInterceptors()
err := interceptors(nil, nil, nil, func(srv interface{}, stream grpc.ServerStream) error {
vals = append(vals, 1)
return nil
})
assert.Nil(t, err)
assert.ElementsMatch(t, []int{1}, vals)
}
func TestChainStreamServerInterceptors_one(t *testing.T) {
var vals []int
interceptors := chainStreamServerInterceptors(func(srv interface{}, ss grpc.ServerStream,
info *grpc.StreamServerInfo, handler grpc.StreamHandler) error {
vals = append(vals, 1)
return handler(srv, ss)
})
err := interceptors(nil, nil, nil, func(srv interface{}, stream grpc.ServerStream) error {
vals = append(vals, 2)
return nil
})
assert.Nil(t, err)
assert.ElementsMatch(t, []int{1, 2}, vals)
}
func TestChainStreamServerInterceptors_more(t *testing.T) {
var vals []int
interceptors := chainStreamServerInterceptors(func(srv interface{}, ss grpc.ServerStream,
info *grpc.StreamServerInfo, handler grpc.StreamHandler) error {
vals = append(vals, 1)
return handler(srv, ss)
}, func(srv interface{}, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error {
vals = append(vals, 2)
return handler(srv, ss)
})
err := interceptors(nil, nil, nil, func(srv interface{}, stream grpc.ServerStream) error {
vals = append(vals, 3)
return nil
})
assert.Nil(t, err)
assert.ElementsMatch(t, []int{1, 2, 3}, vals)
}
func TestChainUnaryServerInterceptors_zero(t *testing.T) {
var vals []int
interceptors := chainUnaryServerInterceptors()
_, err := interceptors(context.Background(), nil, nil,
func(ctx context.Context, req interface{}) (interface{}, error) {
vals = append(vals, 1)
return nil, nil
})
assert.Nil(t, err)
assert.ElementsMatch(t, []int{1}, vals)
}
func TestChainUnaryServerInterceptors_one(t *testing.T) {
var vals []int
interceptors := chainUnaryServerInterceptors(func(ctx context.Context, req interface{},
info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) {
vals = append(vals, 1)
return handler(ctx, req)
})
_, err := interceptors(context.Background(), nil, nil,
func(ctx context.Context, req interface{}) (interface{}, error) {
vals = append(vals, 2)
return nil, nil
})
assert.Nil(t, err)
assert.ElementsMatch(t, []int{1, 2}, vals)
}
func TestChainUnaryServerInterceptors_more(t *testing.T) {
var vals []int
interceptors := chainUnaryServerInterceptors(func(ctx context.Context, req interface{},
info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) {
vals = append(vals, 1)
return handler(ctx, req)
}, func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo,
handler grpc.UnaryHandler) (resp interface{}, err error) {
vals = append(vals, 2)
return handler(ctx, req)
})
_, err := interceptors(context.Background(), nil, nil,
func(ctx context.Context, req interface{}) (interface{}, error) {
vals = append(vals, 3)
return nil, nil
})
assert.Nil(t, err)
assert.ElementsMatch(t, []int{1, 2, 3}, vals)
}

View File

@@ -0,0 +1,30 @@
package internal
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
"google.golang.org/grpc"
)
func TestWithDialOption(t *testing.T) {
var options ClientOptions
agent := grpc.WithUserAgent("chrome")
opt := WithDialOption(agent)
opt(&options)
assert.Contains(t, options.DialOptions, agent)
}
func TestWithTimeout(t *testing.T) {
var options ClientOptions
opt := WithTimeout(time.Second)
opt(&options)
assert.Equal(t, time.Second, options.Timeout)
}
func TestBuildDialOptions(t *testing.T) {
agent := grpc.WithUserAgent("chrome")
opts := buildDialOptions(WithDialOption(agent))
assert.Contains(t, opts, agent)
}

View File

@@ -1,12 +1,15 @@
package clientinterceptors
import (
"context"
"errors"
"testing"
"github.com/stretchr/testify/assert"
"github.com/tal-tech/go-zero/core/breaker"
"github.com/tal-tech/go-zero/core/stat"
rcodes "github.com/tal-tech/go-zero/rpcx/internal/codes"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
@@ -49,3 +52,30 @@ func TestBreakerInterceptorDeadlineExceeded(t *testing.T) {
assert.True(t, errs[err] > 0)
assert.True(t, errs[breaker.ErrServiceUnavailable] > 0)
}
func TestBreakerInterceptor(t *testing.T) {
tests := []struct {
name string
err error
}{
{
name: "nil",
err: nil,
},
{
name: "with error",
err: errors.New("mock"),
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
cc := new(grpc.ClientConn)
err := BreakerInterceptor(context.Background(), "/foo", nil, nil, cc,
func(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn,
opts ...grpc.CallOption) error {
return test.err
})
assert.Equal(t, test.err, err)
})
}
}

View File

@@ -0,0 +1,37 @@
package clientinterceptors
import (
"context"
"errors"
"testing"
"github.com/stretchr/testify/assert"
"google.golang.org/grpc"
)
func TestDurationInterceptor(t *testing.T) {
tests := []struct {
name string
err error
}{
{
name: "nil",
err: nil,
},
{
name: "with error",
err: errors.New("mock"),
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
cc := new(grpc.ClientConn)
err := DurationInterceptor(context.Background(), "/foo", nil, nil, cc,
func(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn,
opts ...grpc.CallOption) error {
return test.err
})
assert.Equal(t, test.err, err)
})
}
}

View File

@@ -0,0 +1,37 @@
package clientinterceptors
import (
"context"
"errors"
"testing"
"github.com/stretchr/testify/assert"
"google.golang.org/grpc"
)
func TestPromMetricInterceptor(t *testing.T) {
tests := []struct {
name string
err error
}{
{
name: "nil",
err: nil,
},
{
name: "with error",
err: errors.New("mock"),
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
cc := new(grpc.ClientConn)
err := PromMetricInterceptor(context.Background(), "/foo", nil, nil, cc,
func(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn,
opts ...grpc.CallOption) error {
return test.err
})
assert.Equal(t, test.err, err)
})
}
}

View File

@@ -0,0 +1,50 @@
package clientinterceptors
import (
"context"
"strconv"
"sync"
"testing"
"time"
"github.com/stretchr/testify/assert"
"google.golang.org/grpc"
)
func TestTimeoutInterceptor(t *testing.T) {
timeouts := []time.Duration{0, time.Millisecond * 10}
for _, timeout := range timeouts {
t.Run(strconv.FormatInt(int64(timeout), 10), func(t *testing.T) {
interceptor := TimeoutInterceptor(timeout)
cc := new(grpc.ClientConn)
err := interceptor(context.Background(), "/foo", nil, nil, cc,
func(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn,
opts ...grpc.CallOption) error {
return nil
},
)
assert.Nil(t, err)
})
}
}
func TestTimeoutInterceptor_timeout(t *testing.T) {
const timeout = time.Millisecond * 10
interceptor := TimeoutInterceptor(timeout)
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
defer cancel()
var wg sync.WaitGroup
wg.Add(1)
cc := new(grpc.ClientConn)
err := interceptor(ctx, "/foo", nil, nil, cc,
func(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn,
opts ...grpc.CallOption) error {
defer wg.Done()
tm, ok := ctx.Deadline()
assert.True(t, ok)
assert.True(t, tm.Before(time.Now().Add(timeout+time.Millisecond)))
return nil
})
wg.Wait()
assert.Nil(t, err)
}

View File

@@ -0,0 +1,53 @@
package clientinterceptors
import (
"context"
"sync"
"sync/atomic"
"testing"
"github.com/stretchr/testify/assert"
"github.com/tal-tech/go-zero/core/trace"
"google.golang.org/grpc"
"google.golang.org/grpc/metadata"
)
func TestTracingInterceptor(t *testing.T) {
var run int32
var wg sync.WaitGroup
wg.Add(1)
cc := new(grpc.ClientConn)
err := TracingInterceptor(context.Background(), "/foo", nil, nil, cc,
func(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn,
opts ...grpc.CallOption) error {
defer wg.Done()
atomic.AddInt32(&run, 1)
return nil
})
wg.Wait()
assert.Nil(t, err)
assert.Equal(t, int32(1), atomic.LoadInt32(&run))
}
func TestTracingInterceptor_GrpcFormat(t *testing.T) {
var run int32
var wg sync.WaitGroup
wg.Add(1)
md := metadata.New(map[string]string{
"foo": "bar",
})
carrier, err := trace.Inject(trace.GrpcFormat, md)
assert.Nil(t, err)
ctx, _ := trace.StartServerSpan(context.Background(), carrier, "user", "/foo")
cc := new(grpc.ClientConn)
err = TracingInterceptor(ctx, "/foo", nil, nil, cc,
func(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn,
opts ...grpc.CallOption) error {
defer wg.Done()
atomic.AddInt32(&run, 1)
return nil
})
wg.Wait()
assert.Nil(t, err)
assert.Equal(t, int32(1), atomic.LoadInt32(&run))
}

View File

@@ -0,0 +1,159 @@
// Code generated by protoc-gen-go.
// source: deposit.proto
// DO NOT EDIT!
/*
Package mock is a generated protocol buffer package.
It is generated from these files:
deposit.proto
It has these top-level messages:
DepositRequest
DepositResponse
*/
package mock
import proto "github.com/golang/protobuf/proto"
import fmt "fmt"
import math "math"
import (
context "golang.org/x/net/context"
grpc "google.golang.org/grpc"
)
// Reference imports to suppress errors if they are not otherwise used.
var _ = proto.Marshal
var _ = fmt.Errorf
var _ = math.Inf
// This is a compile-time assertion to ensure that this generated file
// is compatible with the proto package it is being compiled against.
// A compilation error at this line likely means your copy of the
// proto package needs to be updated.
const _ = proto.ProtoPackageIsVersion2 // please upgrade the proto package
type DepositRequest struct {
Amount float32 `protobuf:"fixed32,1,opt,name=amount" json:"amount,omitempty"`
}
func (m *DepositRequest) Reset() { *m = DepositRequest{} }
func (m *DepositRequest) String() string { return proto.CompactTextString(m) }
func (*DepositRequest) ProtoMessage() {}
func (*DepositRequest) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{0} }
func (m *DepositRequest) GetAmount() float32 {
if m != nil {
return m.Amount
}
return 0
}
type DepositResponse struct {
Ok bool `protobuf:"varint,1,opt,name=ok" json:"ok,omitempty"`
}
func (m *DepositResponse) Reset() { *m = DepositResponse{} }
func (m *DepositResponse) String() string { return proto.CompactTextString(m) }
func (*DepositResponse) ProtoMessage() {}
func (*DepositResponse) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{1} }
func (m *DepositResponse) GetOk() bool {
if m != nil {
return m.Ok
}
return false
}
func init() {
proto.RegisterType((*DepositRequest)(nil), "mock.DepositRequest")
proto.RegisterType((*DepositResponse)(nil), "mock.DepositResponse")
}
// Reference imports to suppress errors if they are not otherwise used.
var _ context.Context
var _ grpc.ClientConn
// This is a compile-time assertion to ensure that this generated file
// is compatible with the grpc package it is being compiled against.
const _ = grpc.SupportPackageIsVersion4
// Client API for DepositService service
type DepositServiceClient interface {
Deposit(ctx context.Context, in *DepositRequest, opts ...grpc.CallOption) (*DepositResponse, error)
}
type depositServiceClient struct {
cc *grpc.ClientConn
}
func NewDepositServiceClient(cc *grpc.ClientConn) DepositServiceClient {
return &depositServiceClient{cc}
}
func (c *depositServiceClient) Deposit(ctx context.Context, in *DepositRequest, opts ...grpc.CallOption) (*DepositResponse, error) {
out := new(DepositResponse)
err := grpc.Invoke(ctx, "/mock.DepositService/Deposit", in, out, c.cc, opts...)
if err != nil {
return nil, err
}
return out, nil
}
// Server API for DepositService service
type DepositServiceServer interface {
Deposit(context.Context, *DepositRequest) (*DepositResponse, error)
}
func RegisterDepositServiceServer(s *grpc.Server, srv DepositServiceServer) {
s.RegisterService(&_DepositService_serviceDesc, srv)
}
func _DepositService_Deposit_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(DepositRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(DepositServiceServer).Deposit(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/mock.DepositService/Deposit",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(DepositServiceServer).Deposit(ctx, req.(*DepositRequest))
}
return interceptor(ctx, in, info, handler)
}
var _DepositService_serviceDesc = grpc.ServiceDesc{
ServiceName: "mock.DepositService",
HandlerType: (*DepositServiceServer)(nil),
Methods: []grpc.MethodDesc{
{
MethodName: "Deposit",
Handler: _DepositService_Deposit_Handler,
},
},
Streams: []grpc.StreamDesc{},
Metadata: "deposit.proto",
}
func init() { proto.RegisterFile("deposit.proto", fileDescriptor0) }
var fileDescriptor0 = []byte{
// 139 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0xe2, 0xe2, 0x4d, 0x49, 0x2d, 0xc8,
0x2f, 0xce, 0x2c, 0xd1, 0x2b, 0x28, 0xca, 0x2f, 0xc9, 0x17, 0x62, 0xc9, 0xcd, 0x4f, 0xce, 0x56,
0xd2, 0xe0, 0xe2, 0x73, 0x81, 0x08, 0x07, 0xa5, 0x16, 0x96, 0xa6, 0x16, 0x97, 0x08, 0x89, 0x71,
0xb1, 0x25, 0xe6, 0xe6, 0x97, 0xe6, 0x95, 0x48, 0x30, 0x2a, 0x30, 0x6a, 0x30, 0x05, 0x41, 0x79,
0x4a, 0x8a, 0x5c, 0xfc, 0x70, 0x95, 0xc5, 0x05, 0xf9, 0x79, 0xc5, 0xa9, 0x42, 0x7c, 0x5c, 0x4c,
0xf9, 0xd9, 0x60, 0x65, 0x1c, 0x41, 0x4c, 0xf9, 0xd9, 0x46, 0x1e, 0x70, 0xc3, 0x82, 0x53, 0x8b,
0xca, 0x32, 0x93, 0x53, 0x85, 0xcc, 0xb8, 0xd8, 0xa1, 0x22, 0x42, 0x22, 0x7a, 0x20, 0x0b, 0xf5,
0x50, 0x6d, 0x93, 0x12, 0x45, 0x13, 0x85, 0x98, 0x9c, 0xc4, 0x06, 0x76, 0xa3, 0x31, 0x20, 0x00,
0x00, 0xff, 0xff, 0x62, 0x37, 0xf2, 0x36, 0xb4, 0x00, 0x00, 0x00,
}

View File

@@ -0,0 +1,15 @@
syntax = "proto3";
package mock;
message DepositRequest {
float amount = 1;
}
message DepositResponse {
bool ok = 1;
}
service DepositService {
rpc Deposit(DepositRequest) returns (DepositResponse);
}

View File

@@ -0,0 +1,19 @@
package mock
import (
"context"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
type DepositServer struct {
}
func (*DepositServer) Deposit(ctx context.Context, req *DepositRequest) (*DepositResponse, error) {
if req.GetAmount() < 0 {
return nil, status.Errorf(codes.InvalidArgument, "cannot deposit %v", req.GetAmount())
}
return &DepositResponse{Ok: true}, nil
}

View File

@@ -11,7 +11,9 @@ type directBuilder struct{}
func (d *directBuilder) Build(target resolver.Target, cc resolver.ClientConn, opts resolver.BuildOptions) (
resolver.Resolver, error) {
var addrs []resolver.Address
endpoints := strings.Split(target.Endpoint, EndpointSep)
endpoints := strings.FieldsFunc(target.Endpoint, func(r rune) bool {
return r == EndpointSepChar
})
for _, val := range subset(endpoints, subsetSize) {
addrs = append(addrs, resolver.Address{

View File

@@ -0,0 +1,52 @@
package resolver
import (
"fmt"
"strconv"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/tal-tech/go-zero/core/lang"
"github.com/tal-tech/go-zero/core/mathx"
"google.golang.org/grpc/resolver"
)
func TestDirectBuilder_Build(t *testing.T) {
tests := []int{
0,
1,
2,
subsetSize / 2,
subsetSize,
subsetSize * 2,
}
for _, test := range tests {
t.Run(strconv.Itoa(test), func(t *testing.T) {
var servers []string
for i := 0; i < test; i++ {
servers = append(servers, fmt.Sprintf("localhost:%d", i))
}
var b directBuilder
cc := new(mockedClientConn)
_, err := b.Build(resolver.Target{
Scheme: DirectScheme,
Endpoint: strings.Join(servers, ","),
}, cc, resolver.BuildOptions{})
assert.Nil(t, err)
size := mathx.MinInt(test, subsetSize)
assert.Equal(t, size, len(cc.state.Addresses))
m := make(map[string]lang.PlaceholderType)
for _, each := range cc.state.Addresses {
m[each.Addr] = lang.Placeholder
}
assert.Equal(t, size, len(m))
})
}
}
func TestDirectBuilder_Scheme(t *testing.T) {
var b directBuilder
assert.Equal(t, DirectScheme, b.Scheme())
}

View File

@@ -11,7 +11,9 @@ type discovBuilder struct{}
func (d *discovBuilder) Build(target resolver.Target, cc resolver.ClientConn, opts resolver.BuildOptions) (
resolver.Resolver, error) {
hosts := strings.Split(target.Authority, EndpointSep)
hosts := strings.FieldsFunc(target.Authority, func(r rune) bool {
return r == EndpointSepChar
})
sub, err := discov.NewSubscriber(hosts, target.Endpoint)
if err != nil {
return nil, err

View File

@@ -1,17 +1,22 @@
package resolver
import "google.golang.org/grpc/resolver"
import (
"fmt"
"google.golang.org/grpc/resolver"
)
const (
DirectScheme = "direct"
DiscovScheme = "discov"
EndpointSep = ","
subsetSize = 32
DirectScheme = "direct"
DiscovScheme = "discov"
EndpointSepChar = ','
subsetSize = 32
)
var (
dirBuilder directBuilder
disBuilder discovBuilder
EndpointSep = fmt.Sprintf("%c", EndpointSepChar)
dirBuilder directBuilder
disBuilder discovBuilder
)
func RegisterResolver() {

View File

@@ -0,0 +1,36 @@
package resolver
import (
"testing"
"google.golang.org/grpc/resolver"
"google.golang.org/grpc/serviceconfig"
)
func TestNopResolver(t *testing.T) {
// make sure ResolveNow & Close don't panic
var r nopResolver
r.ResolveNow(resolver.ResolveNowOptions{})
r.Close()
}
type mockedClientConn struct {
state resolver.State
}
func (m *mockedClientConn) UpdateState(state resolver.State) {
m.state = state
}
func (m *mockedClientConn) ReportError(err error) {
}
func (m *mockedClientConn) NewAddress(addresses []resolver.Address) {
}
func (m *mockedClientConn) NewServiceConfig(serviceConfig string) {
}
func (m *mockedClientConn) ParseServiceConfig(serviceConfigJSON string) *serviceconfig.ParseResult {
return nil
}

View File

@@ -0,0 +1,16 @@
package internal
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/tal-tech/go-zero/core/stat"
)
func TestWithMetrics(t *testing.T) {
metrics := stat.NewMetrics("foo")
opt := WithMetrics(metrics)
var options rpcServerOptions
opt(&options)
assert.Equal(t, metrics, options.metrics)
}

View File

@@ -0,0 +1,53 @@
package internal
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
"github.com/tal-tech/go-zero/core/stat"
"google.golang.org/grpc"
)
func TestBaseRpcServer_AddOptions(t *testing.T) {
metrics := stat.NewMetrics("foo")
server := newBaseRpcServer("foo", metrics)
server.SetName("bar")
var opt grpc.EmptyServerOption
server.AddOptions(opt)
assert.Contains(t, server.options, opt)
}
func TestBaseRpcServer_AddStreamInterceptors(t *testing.T) {
metrics := stat.NewMetrics("foo")
server := newBaseRpcServer("foo", metrics)
server.SetName("bar")
var vals []int
f := func(srv interface{}, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error {
vals = append(vals, 1)
return nil
}
server.AddStreamInterceptors(f)
for _, each := range server.streamInterceptors {
assert.Nil(t, each(nil, nil, nil, nil))
}
assert.ElementsMatch(t, []int{1}, vals)
}
func TestBaseRpcServer_AddUnaryInterceptors(t *testing.T) {
metrics := stat.NewMetrics("foo")
server := newBaseRpcServer("foo", metrics)
server.SetName("bar")
var vals []int
f := func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (
resp interface{}, err error) {
vals = append(vals, 1)
return nil, nil
}
server.AddUnaryInterceptors(f)
for _, each := range server.unaryInterceptors {
_, err := each(context.Background(), nil, nil, nil)
assert.Nil(t, err)
}
assert.ElementsMatch(t, []int{1}, vals)
}

View File

@@ -0,0 +1,200 @@
package serverinterceptors
import (
"context"
"testing"
"github.com/alicebob/miniredis"
"github.com/stretchr/testify/assert"
"github.com/tal-tech/go-zero/core/stores/redis"
"github.com/tal-tech/go-zero/rpcx/internal/auth"
"google.golang.org/grpc"
"google.golang.org/grpc/metadata"
)
func TestStreamAuthorizeInterceptor(t *testing.T) {
tests := []struct {
name string
app string
token string
strict bool
hasError bool
}{
{
name: "strict=false",
strict: false,
hasError: false,
},
{
name: "strict=true",
strict: true,
hasError: true,
},
{
name: "strict=true,with token",
app: "foo",
token: "bar",
strict: true,
hasError: false,
},
{
name: "strict=true,with error token",
app: "foo",
token: "error",
strict: true,
hasError: true,
},
}
r := miniredis.NewMiniRedis()
assert.Nil(t, r.Start())
defer r.Close()
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
store := redis.NewRedis(r.Addr(), redis.NodeType)
if len(test.app) > 0 {
assert.Nil(t, store.Hset("apps", test.app, test.token))
defer store.Hdel("apps", test.app)
}
authenticator, err := auth.NewAuthenticator(store, "apps", test.strict)
assert.Nil(t, err)
interceptor := StreamAuthorizeInterceptor(authenticator)
md := metadata.New(map[string]string{
"app": "foo",
"token": "bar",
})
ctx := metadata.NewIncomingContext(context.Background(), md)
stream := mockedStream{ctx: ctx}
err = interceptor(nil, stream, nil, func(srv interface{}, stream grpc.ServerStream) error {
return nil
})
if test.hasError {
assert.NotNil(t, err)
} else {
assert.Nil(t, err)
}
})
}
}
func TestUnaryAuthorizeInterceptor(t *testing.T) {
tests := []struct {
name string
app string
token string
strict bool
hasError bool
}{
{
name: "strict=false",
strict: false,
hasError: false,
},
{
name: "strict=true",
strict: true,
hasError: true,
},
{
name: "strict=true,with token",
app: "foo",
token: "bar",
strict: true,
hasError: false,
},
{
name: "strict=true,with error token",
app: "foo",
token: "error",
strict: true,
hasError: true,
},
}
r := miniredis.NewMiniRedis()
assert.Nil(t, r.Start())
defer r.Close()
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
store := redis.NewRedis(r.Addr(), redis.NodeType)
if len(test.app) > 0 {
assert.Nil(t, store.Hset("apps", test.app, test.token))
defer store.Hdel("apps", test.app)
}
authenticator, err := auth.NewAuthenticator(store, "apps", test.strict)
assert.Nil(t, err)
interceptor := UnaryAuthorizeInterceptor(authenticator)
md := metadata.New(map[string]string{
"app": "foo",
"token": "bar",
})
ctx := metadata.NewIncomingContext(context.Background(), md)
_, err = interceptor(ctx, nil, nil,
func(ctx context.Context, req interface{}) (interface{}, error) {
return nil, nil
})
if test.hasError {
assert.NotNil(t, err)
} else {
assert.Nil(t, err)
}
if test.strict {
_, err = interceptor(context.Background(), nil, nil,
func(ctx context.Context, req interface{}) (interface{}, error) {
return nil, nil
})
assert.NotNil(t, err)
var md metadata.MD
ctx := metadata.NewIncomingContext(context.Background(), md)
_, err = interceptor(ctx, nil, nil,
func(ctx context.Context, req interface{}) (interface{}, error) {
return nil, nil
})
assert.NotNil(t, err)
md = metadata.New(map[string]string{
"app": "",
"token": "",
})
ctx = metadata.NewIncomingContext(context.Background(), md)
_, err = interceptor(ctx, nil, nil,
func(ctx context.Context, req interface{}) (interface{}, error) {
return nil, nil
})
assert.NotNil(t, err)
}
})
}
}
type mockedStream struct {
ctx context.Context
}
func (m mockedStream) SetHeader(md metadata.MD) error {
return nil
}
func (m mockedStream) SendHeader(md metadata.MD) error {
return nil
}
func (m mockedStream) SetTrailer(md metadata.MD) {
}
func (m mockedStream) Context() context.Context {
return m.ctx
}
func (m mockedStream) SendMsg(v interface{}) error {
return nil
}
func (m mockedStream) RecvMsg(v interface{}) error {
return nil
}

View File

@@ -0,0 +1,31 @@
package serverinterceptors
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
"github.com/tal-tech/go-zero/core/logx"
"google.golang.org/grpc"
)
func init() {
logx.Disable()
}
func TestStreamCrashInterceptor(t *testing.T) {
err := StreamCrashInterceptor(nil, nil, nil, func(
srv interface{}, stream grpc.ServerStream) error {
panic("mock panic")
})
assert.NotNil(t, err)
}
func TestUnaryCrashInterceptor(t *testing.T) {
interceptor := UnaryCrashInterceptor()
_, err := interceptor(context.Background(), nil, nil,
func(ctx context.Context, req interface{}) (interface{}, error) {
panic("mock panic")
})
assert.NotNil(t, err)
}

View File

@@ -33,12 +33,12 @@ var (
)
func UnaryPromMetricInterceptor() grpc.UnaryServerInterceptor {
return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (
interface{}, error) {
startTime := timex.Now()
resp, err := handler(ctx, req)
metricServerReqDur.Observe(int64(timex.Since(startTime)/time.Millisecond), info.FullMethod)
metricServerReqCodeTotal.Inc(info.FullMethod, strconv.Itoa(int(status.Code(err))))
return resp, err
}
}

View File

@@ -0,0 +1,19 @@
package serverinterceptors
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
"google.golang.org/grpc"
)
func TestUnaryPromMetricInterceptor(t *testing.T) {
interceptor := UnaryPromMetricInterceptor()
_, err := interceptor(context.Background(), nil, &grpc.UnaryServerInfo{
FullMethod: "/",
}, func(ctx context.Context, req interface{}) (interface{}, error) {
return nil, nil
})
assert.Nil(t, err)
}

View File

@@ -0,0 +1,77 @@
package serverinterceptors
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
"github.com/tal-tech/go-zero/core/load"
"github.com/tal-tech/go-zero/core/stat"
"google.golang.org/grpc"
)
func TestUnarySheddingInterceptor(t *testing.T) {
tests := []struct {
name string
allow bool
handleErr error
expect error
}{
{
name: "allow",
allow: true,
handleErr: nil,
expect: nil,
},
{
name: "allow",
allow: true,
handleErr: context.DeadlineExceeded,
expect: context.DeadlineExceeded,
},
{
name: "reject",
allow: false,
handleErr: nil,
expect: load.ErrServiceOverloaded,
},
}
for _, test := range tests {
test := test
t.Run(test.name, func(t *testing.T) {
t.Parallel()
shedder := mockedShedder{allow: test.allow}
metrics := stat.NewMetrics("mock")
interceptor := UnarySheddingInterceptor(shedder, metrics)
_, err := interceptor(context.Background(), nil, &grpc.UnaryServerInfo{
FullMethod: "/",
}, func(ctx context.Context, req interface{}) (interface{}, error) {
return nil, test.handleErr
})
assert.Equal(t, test.expect, err)
})
}
}
type mockedShedder struct {
allow bool
}
func (m mockedShedder) Allow() (load.Promise, error) {
if m.allow {
return mockedPromise{}, nil
} else {
return nil, load.ErrServiceOverloaded
}
}
type mockedPromise struct {
}
func (m mockedPromise) Pass() {
}
func (m mockedPromise) Fail() {
}

View File

@@ -0,0 +1,32 @@
package serverinterceptors
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
"github.com/tal-tech/go-zero/core/stat"
"google.golang.org/grpc"
)
func TestUnaryStatInterceptor(t *testing.T) {
metrics := stat.NewMetrics("mock")
interceptor := UnaryStatInterceptor(metrics)
_, err := interceptor(context.Background(), nil, &grpc.UnaryServerInfo{
FullMethod: "/",
}, func(ctx context.Context, req interface{}) (interface{}, error) {
return nil, nil
})
assert.Nil(t, err)
}
func TestUnaryStatInterceptor_crash(t *testing.T) {
metrics := stat.NewMetrics("mock")
interceptor := UnaryStatInterceptor(metrics)
_, err := interceptor(context.Background(), nil, &grpc.UnaryServerInfo{
FullMethod: "/",
}, func(ctx context.Context, req interface{}) (interface{}, error) {
panic("error")
})
assert.NotNil(t, err)
}

View File

@@ -0,0 +1,41 @@
package serverinterceptors
import (
"context"
"sync"
"testing"
"time"
"github.com/stretchr/testify/assert"
"google.golang.org/grpc"
)
func TestUnaryTimeoutInterceptor(t *testing.T) {
interceptor := UnaryTimeoutInterceptor(time.Millisecond * 10)
_, err := interceptor(context.Background(), nil, &grpc.UnaryServerInfo{
FullMethod: "/",
}, func(ctx context.Context, req interface{}) (interface{}, error) {
return nil, nil
})
assert.Nil(t, err)
}
func TestUnaryTimeoutInterceptor_timeout(t *testing.T) {
const timeout = time.Millisecond * 10
interceptor := UnaryTimeoutInterceptor(timeout)
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
defer cancel()
var wg sync.WaitGroup
wg.Add(1)
_, err := interceptor(ctx, nil, &grpc.UnaryServerInfo{
FullMethod: "/",
}, func(ctx context.Context, req interface{}) (interface{}, error) {
defer wg.Done()
tm, ok := ctx.Deadline()
assert.True(t, ok)
assert.True(t, tm.Before(time.Now().Add(timeout+time.Millisecond)))
return nil, nil
})
wg.Wait()
assert.Nil(t, err)
}

View File

@@ -0,0 +1,48 @@
package serverinterceptors
import (
"context"
"sync"
"sync/atomic"
"testing"
"github.com/stretchr/testify/assert"
"github.com/tal-tech/go-zero/core/trace/tracespec"
"google.golang.org/grpc"
"google.golang.org/grpc/metadata"
)
func TestUnaryTracingInterceptor(t *testing.T) {
interceptor := UnaryTracingInterceptor("foo")
var run int32
var wg sync.WaitGroup
wg.Add(1)
_, err := interceptor(context.Background(), nil, &grpc.UnaryServerInfo{
FullMethod: "/",
}, func(ctx context.Context, req interface{}) (interface{}, error) {
defer wg.Done()
atomic.AddInt32(&run, 1)
return nil, nil
})
wg.Wait()
assert.Nil(t, err)
assert.Equal(t, int32(1), atomic.LoadInt32(&run))
}
func TestUnaryTracingInterceptor_GrpcFormat(t *testing.T) {
interceptor := UnaryTracingInterceptor("foo")
var wg sync.WaitGroup
wg.Add(1)
var md metadata.MD
ctx := metadata.NewIncomingContext(context.Background(), md)
_, err := interceptor(ctx, nil, &grpc.UnaryServerInfo{
FullMethod: "/",
}, func(ctx context.Context, req interface{}) (interface{}, error) {
defer wg.Done()
assert.True(t, len(ctx.Value(tracespec.TracingKey).(tracespec.Trace).TraceId()) > 0)
assert.True(t, len(ctx.Value(tracespec.TracingKey).(tracespec.Trace).SpanId()) > 0)
return nil, nil
})
wg.Wait()
assert.Nil(t, err)
}

View File

@@ -8,7 +8,8 @@ import (
)
func BuildDirectTarget(endpoints []string) string {
return fmt.Sprintf("%s:///%s", resolver.DirectScheme, strings.Join(endpoints, resolver.EndpointSep))
return fmt.Sprintf("%s:///%s", resolver.DirectScheme,
strings.Join(endpoints, resolver.EndpointSep))
}
func BuildDiscovTarget(endpoints []string, key string) string {

View File

@@ -0,0 +1,17 @@
package internal
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestBuildDirectTarget(t *testing.T) {
target := BuildDirectTarget([]string{"localhost:123", "localhost:456"})
assert.Equal(t, "direct:///localhost:123,localhost:456", target)
}
func TestBuildDiscovTarget(t *testing.T) {
target := BuildDiscovTarget([]string{"localhost:123", "localhost:456"}, "foo")
assert.Equal(t, "discov://localhost:123,localhost:456/foo", target)
}

View File

@@ -18,7 +18,7 @@ type RpcProxy struct {
lock sync.Mutex
}
func NewRpcProxy(backend string, opts ...internal.ClientOption) *RpcProxy {
func NewProxy(backend string, opts ...internal.ClientOption) *RpcProxy {
return &RpcProxy{
backend: backend,
clients: make(map[string]Client),
@@ -56,5 +56,5 @@ func (p *RpcProxy) TakeConn(ctx context.Context) (*grpc.ClientConn, error) {
return nil, err
}
return val.(*RpcClient).Conn(), nil
return val.(Client).Conn(), nil
}

66
rpcx/proxy_test.go Normal file
View File

@@ -0,0 +1,66 @@
package rpcx
import (
"context"
"fmt"
"testing"
"github.com/stretchr/testify/assert"
"github.com/tal-tech/go-zero/rpcx/internal/mock"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
func TestProxy(t *testing.T) {
tests := []struct {
name string
amount float32
res *mock.DepositResponse
errCode codes.Code
errMsg string
}{
{
"invalid request with negative amount",
-1.11,
nil,
codes.InvalidArgument,
fmt.Sprintf("cannot deposit %v", -1.11),
},
{
"valid request with non negative amount",
0.00,
&mock.DepositResponse{Ok: true},
codes.OK,
"",
},
}
proxy := NewProxy("foo", WithDialOption(grpc.WithInsecure()),
WithDialOption(grpc.WithContextDialer(dialer())))
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
conn, err := proxy.TakeConn(context.Background())
assert.Nil(t, err)
cli := mock.NewDepositServiceClient(conn)
request := &mock.DepositRequest{Amount: tt.amount}
response, err := cli.Deposit(context.Background(), request)
if response != nil {
assert.True(t, len(response.String()) > 0)
if response.GetOk() != tt.res.GetOk() {
t.Error("response: expected", tt.res.GetOk(), "received", response.GetOk())
}
}
if err != nil {
if e, ok := status.FromError(err); ok {
if e.Code() != tt.errCode {
t.Error("error code: expected", codes.InvalidArgument, "received", e.Code())
}
if e.Message() != tt.errMsg {
t.Error("error message: expected", tt.errMsg, "received", e.Message())
}
}
}
})
}
}

View File

@@ -19,29 +19,24 @@ const apiTemplate = `info(
email: {{.gitEmail}}
)
type request struct{
type request struct {
// TODO: add members here and delete this comment
}
type response struct{
type response struct {
// TODO: add members here and delete this comment
}
@server(
port: // TODO: add port here and delete this comment
)
service {{.serviceName}} {
@server(
handler: // TODO: set handler name and delete this comment
)
// TODO: edit the below line
// get /users/id/:userId(request) returns(response)
get /users/id/:userId(request) returns(response)
@server(
handler: // TODO: set handler name and delete this comment
)
// TODO: edit the below line
// post /users/create(request)
post /users/create(request)
}
`

View File

@@ -140,18 +140,7 @@ func createGoModFileIfNeed(dir string) {
panic(err)
}
var tempPath = absDir
var hasGoMod = false
for {
if tempPath == filepath.Dir(tempPath) {
break
}
tempPath = filepath.Dir(tempPath)
if util.FileExists(filepath.Join(tempPath, goModeIdentifier)) {
hasGoMod = true
break
}
}
_, hasGoMod := util.FindGoModPath(dir)
if !hasGoMod {
gopath := os.Getenv("GOPATH")
parent := path.Join(gopath, "src")

View File

@@ -13,9 +13,7 @@ const (
configFile = "config.go"
configTemplate = `package config
import (
{{.authImport}}
)
import {{.authImport}}
type Config struct {
rest.RestConf

View File

@@ -13,15 +13,14 @@ import (
const (
defaultPort = 8888
etcDir = "etc"
etcTemplate = `{
"Name": "{{.serviceName}}",
"Host": "{{.host}}",
"Port": {{.port}}
}`
etcTemplate = `Name: {{.serviceName}}
Host: {{.host}}
Port: {{.port}}
`
)
func genEtc(dir string, api *spec.ApiSpec) error {
fp, created, err := util.MaybeCreateFile(dir, etcDir, fmt.Sprintf("%s.json", api.Service.Name))
fp, created, err := util.MaybeCreateFile(dir, etcDir, fmt.Sprintf("%s.yaml", api.Service.Name))
if err != nil {
return err
}

View File

@@ -4,7 +4,6 @@ import (
"bytes"
"fmt"
"path"
"sort"
"strings"
"text/template"
@@ -25,7 +24,6 @@ import (
func {{.handlerName}}(ctx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
l := logic.{{.logic}}(r.Context(), ctx)
{{.handlerBody}}
}
}
@@ -40,6 +38,7 @@ func {{.handlerName}}(ctx *svc.ServiceContext) http.HandlerFunc {
}
`
hasRespTemplate = `
l := logic.{{.logic}}(r.Context(), ctx)
{{.logicResponse}} l.{{.callee}}({{.req}})
if err != nil {
httpx.Error(w, err)
@@ -85,6 +84,7 @@ func genHandler(dir string, group spec.Group, route spec.Route) error {
var logicBodyBuilder strings.Builder
t := template.Must(template.New("hasRespTemplate").Parse(hasRespTemplate))
if err := t.Execute(&logicBodyBuilder, map[string]string{
"logic": "New" + strings.TrimSuffix(strings.Title(handler), "Handler") + "Logic",
"callee": strings.Title(strings.TrimSuffix(handler, "Handler")),
"req": req,
"logicResponse": logicResponse,
@@ -135,7 +135,6 @@ func doGenToFile(dir, handler string, group spec.Group, route spec.Route, bodyBu
t := template.Must(template.New("handlerTemplate").Parse(handlerTemplate))
buffer := new(bytes.Buffer)
err = t.Execute(buffer, map[string]string{
"logic": "New" + strings.TrimSuffix(strings.Title(handler), "Handler") + "Logic",
"importPackages": genHandlerImports(group, route, parentPkg),
"handlerName": handler,
"handlerBody": strings.TrimSpace(bodyBuilder.String()),
@@ -162,14 +161,13 @@ func genHandlers(dir string, api *spec.ApiSpec) error {
func genHandlerImports(group spec.Group, route spec.Route, parentPkg string) string {
var imports []string
imports = append(imports, fmt.Sprintf("\"%s/rest/httpx\"", vars.ProjectOpenSourceUrl))
imports = append(imports, fmt.Sprintf("\"%s\"", util.JoinPackages(parentPkg, contextDir)))
if len(route.RequestType.Name) > 0 || len(route.ResponseType.Name) > 0 {
imports = append(imports, fmt.Sprintf("\"%s\"", util.JoinPackages(parentPkg, typesDir)))
}
imports = append(imports, fmt.Sprintf("\"%s\"",
util.JoinPackages(parentPkg, getLogicFolderPath(group, route))))
sort.Strings(imports)
imports = append(imports, fmt.Sprintf("\"%s\"", util.JoinPackages(parentPkg, contextDir)))
if len(route.RequestType.Name) > 0 || len(route.ResponseType.Name) > 0 {
imports = append(imports, fmt.Sprintf("\"%s\"\n", util.JoinPackages(parentPkg, typesDir)))
}
imports = append(imports, fmt.Sprintf("\"%s/rest/httpx\"", vars.ProjectOpenSourceUrl))
return strings.Join(imports, "\n\t")
}

View File

@@ -29,7 +29,6 @@ func New{{.logic}}(ctx context.Context, svcCtx *svc.ServiceContext) {{.logic}} {
ctx: ctx,
Logger: logx.WithContext(ctx),
}
// TODO need set model here from svc
}
func (l *{{.logic}}) {{.function}}({{.request}}) {{.responseType}} {
@@ -77,8 +76,9 @@ func genLogicByRoute(dir string, group spec.Group, route spec.Route) error {
returnString := ""
requestString := ""
if len(route.ResponseType.Name) > 0 {
responseString = "(*types." + strings.Title(route.ResponseType.Name) + ", error)"
returnString = "return nil, nil"
resp := strings.Title(route.ResponseType.Name)
responseString = "(*types." + resp + ", error)"
returnString = fmt.Sprintf("return &types.%s{}, nil", resp)
} else {
responseString = "error"
returnString = "return nil"
@@ -98,7 +98,7 @@ func genLogicByRoute(dir string, group spec.Group, route spec.Route) error {
"request": requestString,
})
if err != nil {
return nil
return err
}
formatCode := formatCode(buffer.String())
_, err = fp.WriteString(formatCode)
@@ -120,12 +120,11 @@ func getLogicFolderPath(group spec.Group, route spec.Route) string {
func genLogicImports(route spec.Route, parentPkg string) string {
var imports []string
imports = append(imports, `"context"`)
imports = append(imports, "\n")
imports = append(imports, fmt.Sprintf("\"%s/core/logx\"", vars.ProjectOpenSourceUrl))
if len(route.ResponseType.Name) > 0 || len(route.RequestType.Name) > 0 {
imports = append(imports, fmt.Sprintf("\"%s\"", ctlutil.JoinPackages(parentPkg, typesDir)))
}
imports = append(imports, `"context"`+"\n")
imports = append(imports, fmt.Sprintf("\"%s\"", ctlutil.JoinPackages(parentPkg, contextDir)))
if len(route.ResponseType.Name) > 0 || len(route.RequestType.Name) > 0 {
imports = append(imports, fmt.Sprintf("\"%s\"\n", ctlutil.JoinPackages(parentPkg, typesDir)))
}
imports = append(imports, fmt.Sprintf("\"%s/core/logx\"", vars.ProjectOpenSourceUrl))
return strings.Join(imports, "\n\t")
}

View File

@@ -3,7 +3,6 @@ package gogen
import (
"bytes"
"fmt"
"sort"
"strings"
"text/template"
@@ -21,7 +20,7 @@ import (
{{.importPackages}}
)
var configFile = flag.String("f", "etc/{{.serviceName}}.json", "the config file")
var configFile = flag.String("f", "etc/{{.serviceName}}.yaml", "the config file")
func main() {
flag.Parse()
@@ -73,13 +72,11 @@ func genMain(dir string, api *spec.ApiSpec) error {
}
func genMainImports(parentPkg string) string {
imports := []string{
fmt.Sprintf("\"%s/core/conf\"", vars.ProjectOpenSourceUrl),
fmt.Sprintf("\"%s/rest\"", vars.ProjectOpenSourceUrl),
}
var imports []string
imports = append(imports, fmt.Sprintf("\"%s\"", ctlutil.JoinPackages(parentPkg, configDir)))
imports = append(imports, fmt.Sprintf("\"%s\"", ctlutil.JoinPackages(parentPkg, handlerDir)))
imports = append(imports, fmt.Sprintf("\"%s\"", ctlutil.JoinPackages(parentPkg, contextDir)))
sort.Strings(imports)
imports = append(imports, fmt.Sprintf("\"%s\"\n", ctlutil.JoinPackages(parentPkg, contextDir)))
imports = append(imports, fmt.Sprintf("\"%s/core/conf\"", vars.ProjectOpenSourceUrl))
imports = append(imports, fmt.Sprintf("\"%s/rest\"", vars.ProjectOpenSourceUrl))
return strings.Join(imports, "\n\t")
}

View File

@@ -131,7 +131,6 @@ func genRoutes(dir string, api *spec.ApiSpec) error {
func genRouteImports(parentPkg string, api *spec.ApiSpec) string {
var importSet = collection.NewSet()
importSet.AddStr(fmt.Sprintf("\"%s/rest\"", vars.ProjectOpenSourceUrl))
importSet.AddStr(fmt.Sprintf("\"%s\"", util.JoinPackages(parentPkg, contextDir)))
for _, group := range api.Service.Groups {
for _, route := range group.Routes {
@@ -148,7 +147,9 @@ func genRouteImports(parentPkg string, api *spec.ApiSpec) string {
}
imports := importSet.KeysStr()
sort.Strings(imports)
return strings.Join(imports, "\n\t")
projectSection := strings.Join(imports, "\n\t")
depSection := fmt.Sprintf("\"%s/rest\"", vars.ProjectOpenSourceUrl)
return fmt.Sprintf("%s\n\n\t%s", projectSection, depSection)
}
func getRoutes(api *spec.ApiSpec) ([]group, error) {

View File

@@ -20,10 +20,9 @@ type ServiceContext struct {
Config {{.config}}
}
func NewServiceContext(config {{.config}}) *ServiceContext {
return &ServiceContext{Config: config}
func NewServiceContext(c {{.config}}) *ServiceContext {
return &ServiceContext{Config: c}
}
`
)

View File

@@ -15,8 +15,6 @@ import (
goctlutil "github.com/tal-tech/go-zero/tools/goctl/util"
)
const goModeIdentifier = "go.mod"
func getParentPackage(dir string) (string, error) {
absDir, err := filepath.Abs(dir)
if err != nil {
@@ -24,36 +22,22 @@ func getParentPackage(dir string) (string, error) {
}
absDir = strings.ReplaceAll(absDir, `\`, `/`)
var rootPath string
var tempPath = absDir
var hasGoMod = false
for {
if tempPath == filepath.Dir(tempPath) {
break
}
tempPath = filepath.Dir(tempPath)
if goctlutil.FileExists(filepath.Join(tempPath, goModeIdentifier)) {
tempPath = filepath.Dir(tempPath)
rootPath = absDir[len(tempPath)+1:]
hasGoMod = true
break
}
if tempPath == string(filepath.Separator) {
break
}
var rootPath, hasGoMod = goctlutil.FindGoModPath(dir)
if hasGoMod {
return rootPath, nil
}
if !hasGoMod {
gopath := os.Getenv("GOPATH")
parent := path.Join(gopath, "src")
pos := strings.Index(absDir, parent)
if pos < 0 {
fmt.Printf("%s not in gomod project path, or not in GOPATH of %s directory\n", absDir, gopath)
tempPath = filepath.Dir(absDir)
rootPath = absDir[len(tempPath)+1:]
} else {
rootPath = absDir[len(parent)+1:]
}
gopath := os.Getenv("GOPATH")
parent := path.Join(gopath, "src")
pos := strings.Index(absDir, parent)
if pos < 0 {
fmt.Printf("%s not in go.mod project path, or not in GOPATH of %s directory\n", absDir, gopath)
var tempPath = filepath.Dir(absDir)
rootPath = absDir[len(tempPath)+1:]
} else {
rootPath = absDir[len(parent)+1:]
}
return rootPath, nil
}

View File

@@ -9,17 +9,14 @@ import (
"github.com/tal-tech/go-zero/tools/goctl/api/spec"
)
const (
// struct匹配
typeRegex = `(?m)(?m)(^ *type\s+[a-zA-Z][a-zA-Z0-9_-]+\s+(((struct)\s*?\{[\w\W]*?[^\{]\})|([a-zA-Z][a-zA-Z0-9_-]+)))|(^ *type\s*?\([\w\W]+\}\s*\))`
)
// struct匹配
const typeRegex = `(?m)(?m)(^ *type\s+[a-zA-Z][a-zA-Z0-9_-]+\s+(((struct)\s*?\{[\w\W]*?[^\{]\})|([a-zA-Z][a-zA-Z0-9_-]+)))|(^ *type\s*?\([\w\W]+\}\s*\))`
var (
emptyStrcut = errors.New("struct body not found")
emptyType spec.Type
)
var emptyType spec.Type
func GetType(api *spec.ApiSpec, t string) spec.Type {
for _, tp := range api.Types {
if tp.Name == t {

View File

@@ -10,26 +10,27 @@ import (
"text/template"
"github.com/logrusorgru/aurora"
"github.com/tal-tech/go-zero/tools/goctl/vars"
"github.com/tal-tech/go-zero/tools/goctl/util"
"github.com/urfave/cli"
)
const configTemplate = `package main
import (
"encoding/json"
"io/ioutil"
"os"
"{{.import}}"
"github.com/ghodss/yaml"
)
func main() {
var c config.Config
template, err := json.MarshalIndent(c, "", " ")
template, err := yaml.Marshal(c)
if err != nil {
panic(err)
}
err = ioutil.WriteFile("config.json", template, os.ModePerm)
err = ioutil.WriteFile("config.yaml", template, os.ModePerm)
if err != nil {
panic(err)
}
@@ -41,9 +42,9 @@ func GenConfigCommand(c *cli.Context) error {
if err != nil {
return errors.New("abs failed: " + c.String("path"))
}
xi := strings.Index(path, vars.ProjectName)
if xi <= 0 {
return errors.New("path should the absolute path of config go file")
goModPath, hasFound := util.FindGoModPath(path)
if !hasFound {
return errors.New("go mod not initial")
}
path = strings.TrimSuffix(path, "/config.go")
location := path + "/tmp"
@@ -62,16 +63,28 @@ func GenConfigCommand(c *cli.Context) error {
t := template.Must(template.New("template").Parse(configTemplate))
if err := t.Execute(fp, map[string]string{
"import": path[xi:],
"import": filepath.Dir(goModPath),
}); err != nil {
return err
}
cmd := exec.Command("go", "run", goPath)
_, err = cmd.Output()
gen := exec.Command("go", "run", "config.go")
gen.Dir = filepath.Dir(goPath)
gen.Stderr = os.Stderr
gen.Stdout = os.Stdout
err = gen.Run()
if err != nil {
return err
panic(err)
}
path, err = os.Getwd()
if err != nil {
panic(err)
}
err = os.Rename(filepath.Dir(goPath)+"/config.yaml", path+"/config.yaml")
if err != nil {
panic(err)
}
fmt.Println(aurora.Green("Done."))
return nil
}

View File

@@ -17,7 +17,8 @@ import (
"github.com/tal-tech/go-zero/tools/goctl/configgen"
"github.com/tal-tech/go-zero/tools/goctl/docker"
"github.com/tal-tech/go-zero/tools/goctl/feature"
"github.com/tal-tech/go-zero/tools/goctl/model/sql/command"
model "github.com/tal-tech/go-zero/tools/goctl/model/sql/command"
rpc "github.com/tal-tech/go-zero/tools/goctl/rpc/command"
"github.com/urfave/cli"
)
@@ -189,27 +190,114 @@ var (
Action: docker.DockerCommand,
},
{
Name: "model",
Usage: "generate model code",
Flags: []cli.Flag{
cli.StringFlag{
Name: "src, s",
Usage: "the file path of the ddl source file",
Name: "rpc",
Usage: "generate rpc code",
Subcommands: []cli.Command{
{
Name: "template",
Usage: `generate proto template`,
Flags: []cli.Flag{
cli.StringFlag{
Name: "out, o",
Usage: "the target path of proto",
},
cli.BoolFlag{
Name: "idea",
Usage: "whether the command execution environment is from idea plugin. [option]",
},
},
Action: rpc.RpcTemplate,
},
cli.StringFlag{
Name: "dir, d",
Usage: "the target dir",
},
cli.BoolFlag{
Name: "cache, c",
Usage: "generate code with cache [optional]",
},
cli.BoolFlag{
Name: "idea",
Usage: "for idea plugin [optional]",
{
Name: "proto",
Usage: `generate rpc from proto`,
Flags: []cli.Flag{
cli.StringFlag{
Name: "src, s",
Usage: "the file path of the proto source file",
},
cli.StringFlag{
Name: "dir, d",
Usage: `the target path of the code,default path is "${pwd}". [option]`,
},
cli.StringFlag{
Name: "service, srv",
Usage: `the name of rpc service. [option]`,
},
cli.StringFlag{
Name: "shared",
Usage: `the dir of the shared file,default path is "${pwd}/shared. [option]`,
},
cli.BoolFlag{
Name: "idea",
Usage: "whether the command execution environment is from idea plugin. [option]",
},
},
Action: rpc.Rpc,
},
},
},
{
Name: "model",
Usage: "generate model code",
Subcommands: []cli.Command{
{
Name: "mysql",
Usage: `generate mysql model`,
Subcommands: []cli.Command{
{
Name: "ddl",
Usage: `generate mysql model from ddl`,
Flags: []cli.Flag{
cli.StringFlag{
Name: "src, s",
Usage: "the file path of the ddl source file",
},
cli.StringFlag{
Name: "dir, d",
Usage: "the target dir",
},
cli.BoolFlag{
Name: "cache, c",
Usage: "generate code with cache [optional]",
},
cli.BoolFlag{
Name: "idea",
Usage: "for idea plugin [optional]",
},
},
Action: model.MysqlDDL,
},
{
Name: "datasource",
Usage: `generate model from datasource`,
Flags: []cli.Flag{
cli.StringFlag{
Name: "url",
Usage: `the data source of database,like "root:password@tcp(127.0.0.1:3306)/database`,
},
cli.StringFlag{
Name: "table, t",
Usage: `source table,tables separated by commas,like "user,course`,
},
cli.BoolFlag{
Name: "cache, c",
Usage: "generate code with cache [optional]",
},
cli.StringFlag{
Name: "dir, d",
Usage: "the target dir",
},
cli.BoolFlag{
Name: "idea",
Usage: "for idea plugin [optional]",
},
},
Action: model.MyDataSource,
},
},
},
},
Action: command.Mysql,
},
{
Name: "config",

View File

@@ -0,0 +1,10 @@
# Change log
# 2020-08-20
* 新增支持通过连接数据库生成model
* 支持数据库多表生成
* 优化stringx
# 2020-08-19
* 重构model代码生成逻辑
* 实现从ddl解析表信息生成代码

View File

@@ -4,21 +4,28 @@ goctl model 为go-zero下的工具模块中的组件之一目前支持识别m
# 快速开始
```
$ goctl model -src ./sql/user.sql -dir ./model -c true
```
* 通过ddl生成
详情用法请参考[example](https://github.com/tal-tech/go-zero/tools/goctl/model/sql/example)
```shell script
$ goctl model mysql ddl -src="./sql/user.sql" -dir="./sql/model" -c=true
```
执行上述命令后即可快速生成CURD代码。
执行上述命令后即可快速生成CURD代码。
```
model
│   ├── error.go
│   └── usermodel.go
```
```
model
│   ├── error.go
│   └── usermodel.go
```
* 通过datasource生成
```shell script
$ goctl model mysql datasource -url="user:password@tcp(127.0.0.1:3306)/database" -table="table1,table2" -dir="./model"
```
> 详情用法请参考[example](https://github.com/tal-tech/go-zero/tree/master/tools/goctl/model/sql/example)
> 注意这里的目录结构中有usercoursemodel.go目录在example中我为了体现带cache与不带cache代码的区别因此将sql文件分别使用了独立的sql文件(user.sql&course.sql)在实际项目开发中你可以将ddl建表语句放在一个sql文件中`goctl model`会自动解析并分割最终按照每个ddl建表语句为单位生成独立的go文件。
* 生成代码示例
@@ -174,22 +181,22 @@ model
# 用法
```
$ goctl model -h
$ goctl model mysql -h
```
```
NAME:
goctl model - generate model code
goctl model mysql - generate mysql model"
USAGE:
goctl model [command options] [arguments...]
goctl model mysql command [command options] [arguments...]
COMMANDS:
ddl generate mysql model from ddl"
datasource generate model from datasource"
OPTIONS:
--src value, -s value the file path of the ddl source file
--dir value, -d value the target dir
--cache, -c generate code with cache [optional]
--idea for idea plugin [optional]
--help, -h show help
```
# 生成规则
@@ -198,22 +205,43 @@ OPTIONS:
我们默认用户在建表时会创建createTime、updateTime字段(忽略大小写、下划线命名风格)且默认值均为`CURRENT_TIMESTAMP`而updateTime支持`ON UPDATE CURRENT_TIMESTAMP`,对于这两个字段生成`insert`、`update`时会被移除,不在赋值范畴内,当然,如果你不需要这两个字段那也无大碍。
* 带缓存模式
```
$ goctl model -src {filename} -dir {dir} -cache true
```
* ddl
```shell script
$ goctl model mysql -src={filename} -dir={dir} -cache=true
```
* datasource
```shell script
$ goctl model mysql datasource -url={datasource} -table={tables} -dir={dir} -cache=true
```
目前仅支持redis缓存如果选择带缓存模式即生成的`FindOne(ByXxx)`&`Delete`代码会生成带缓存逻辑的代码目前仅支持单索引字段除全文索引外对于联合索引我们默认认为不需要带缓存且不属于通用型代码因此没有放在代码生成行列如example中user表中的`id`、`name`、`mobile`字段均属于单字段索引。
* 不带缓存模式
```
$ goctl model -src {filename} -dir {dir}
```
* ddl
```shell script
$ goctl model -src={filename} -dir={dir}
```
* datasource
```shell script
$ goctl model mysql datasource -url={datasource} -table={tables} -dir={dir}
```
or
```
$ goctl model -src {filename} -dir {dir} -cache false
```
* ddl
```shell script
$ goctl model -src={filename} -dir={dir} -cache=false
```
* datasource
```shell script
$ goctl model mysql datasource -url={datasource} -table={tables} -dir={dir} -cache=false
```
生成代码仅基本的CURD结构。
# 缓存
@@ -238,10 +266,6 @@ OPTIONS:
# QA
* goctl model支持根据数据库连接后选择表生成代码吗
目前暂时不支持,在后面会向这个方向扩展。
* goctl model除了命令行模式支持插件模式吗
很快支持idea插件。

View File

@@ -1,24 +1,84 @@
package command
import (
"io/ioutil"
"path/filepath"
"strings"
"github.com/tal-tech/go-zero/core/collection"
"github.com/tal-tech/go-zero/core/logx"
"github.com/tal-tech/go-zero/core/stores/sqlx"
"github.com/tal-tech/go-zero/tools/goctl/model/sql/gen"
"github.com/tal-tech/go-zero/tools/goctl/model/sql/model"
"github.com/tal-tech/go-zero/tools/goctl/util/console"
"github.com/urfave/cli"
)
func Mysql(ctx *cli.Context) error {
src := ctx.String("src")
dir := ctx.String("dir")
cache := ctx.Bool("cache")
idea := ctx.Bool("idea")
var log console.Console
if idea {
log = console.NewIdeaConsole()
} else {
log = console.NewColorConsole()
const (
flagSrc = "src"
flagDir = "dir"
flagCache = "cache"
flagIdea = "idea"
flagUrl = "url"
flagTable = "table"
)
func MysqlDDL(ctx *cli.Context) error {
src := ctx.String(flagSrc)
dir := ctx.String(flagDir)
cache := ctx.Bool(flagCache)
idea := ctx.Bool(flagIdea)
log := console.NewConsole(idea)
fileSrc, err := filepath.Abs(src)
if err != nil {
return err
}
generator := gen.NewDefaultGenerator(src, dir, gen.WithConsoleOption(log))
err := generator.Start(cache)
data, err := ioutil.ReadFile(fileSrc)
if err != nil {
return err
}
source := string(data)
generator := gen.NewDefaultGenerator(source, dir, gen.WithConsoleOption(log))
err = generator.Start(cache)
if err != nil {
log.Error("%v", err)
}
return nil
}
func MyDataSource(ctx *cli.Context) error {
url := strings.TrimSpace(ctx.String(flagUrl))
dir := strings.TrimSpace(ctx.String(flagDir))
cache := ctx.Bool(flagCache)
idea := ctx.Bool(flagIdea)
table := strings.TrimSpace(ctx.String(flagTable))
log := console.NewConsole(idea)
if len(url) == 0 {
log.Error("%v", "expected data source of mysql, but is empty")
return nil
}
if len(table) == 0 {
log.Error("%v", "expected table(s), but nothing found")
return nil
}
logx.Disable()
conn := sqlx.NewMysql(url)
m := model.NewDDLModel(conn)
tables := collection.NewSet()
for _, item := range strings.Split(table, ",") {
item = strings.TrimSpace(item)
if len(item) == 0 {
continue
}
tables.AddStr(item)
}
ddl, err := m.ShowDDL(tables.KeysStr()...)
if err != nil {
log.Error("%v", err)
return nil
}
generator := gen.NewDefaultGenerator(strings.Join(ddl, "\n"), dir, gen.WithConsoleOption(log))
err = generator.Start(cache)
if err != nil {
log.Error("%v", err)
}

View File

@@ -1,4 +1,7 @@
#!/bin/bash
# generate usermodel with cache
goctl model -src ./sql/user.sql -dir ./model -c true
# generate model with cache from ddl
goctl model mysql ddl -src="./sql/user.sql" -dir="./sql/model" -c
# generate model with cache from data source
goctl model mysql datasource -url="user:password@tcp(127.0.0.1:3306)/database" -table="table1,table2" -dir="./model"

View File

@@ -5,8 +5,8 @@ import (
"github.com/tal-tech/go-zero/core/collection"
"github.com/tal-tech/go-zero/tools/goctl/model/sql/template"
"github.com/tal-tech/go-zero/tools/goctl/util"
"github.com/tal-tech/go-zero/tools/goctl/util/stringx"
"github.com/tal-tech/go-zero/tools/goctl/util/templatex"
)
func genDelete(table Table, withCache bool) (string, error) {
@@ -27,14 +27,14 @@ func genDelete(table Table, withCache bool) (string, error) {
break
}
}
camel := table.Name.Snake2Camel()
output, err := templatex.With("delete").
camel := table.Name.ToCamel()
output, err := util.With("delete").
Parse(template.Delete).
Execute(map[string]interface{}{
"upperStartCamelObject": camel,
"withCache": withCache,
"containsIndexCache": containsIndexCache,
"lowerStartCamelPrimaryKey": stringx.From(table.PrimaryKey.Name.Snake2Camel()).LowerStart(),
"lowerStartCamelPrimaryKey": stringx.From(table.PrimaryKey.Name.ToCamel()).UnTitle(),
"dataType": table.PrimaryKey.DataType,
"keys": strings.Join(keySet.KeysStr(), "\n"),
"originalPrimaryKey": table.PrimaryKey.Name.Source(),

View File

@@ -2,6 +2,4 @@ package gen
import "errors"
var (
ErrCircleQuery = errors.New("circle query with other fields")
)
var ErrCircleQuery = errors.New("circle query with other fields")

View File

@@ -5,7 +5,7 @@ import (
"github.com/tal-tech/go-zero/tools/goctl/model/sql/parser"
"github.com/tal-tech/go-zero/tools/goctl/model/sql/template"
"github.com/tal-tech/go-zero/tools/goctl/util/templatex"
"github.com/tal-tech/go-zero/tools/goctl/util"
)
func genFields(fields []parser.Field) (string, error) {
@@ -25,10 +25,10 @@ func genField(field parser.Field) (string, error) {
if err != nil {
return "", err
}
output, err := templatex.With("types").
output, err := util.With("types").
Parse(template.Field).
Execute(map[string]interface{}{
"name": field.Name.Snake2Camel(),
"name": field.Name.ToCamel(),
"type": field.DataType,
"tag": tag,
"hasComment": field.Comment != "",

View File

@@ -2,20 +2,20 @@ package gen
import (
"github.com/tal-tech/go-zero/tools/goctl/model/sql/template"
"github.com/tal-tech/go-zero/tools/goctl/util"
"github.com/tal-tech/go-zero/tools/goctl/util/stringx"
"github.com/tal-tech/go-zero/tools/goctl/util/templatex"
)
func genFindOne(table Table, withCache bool) (string, error) {
camel := table.Name.Snake2Camel()
output, err := templatex.With("findOne").
camel := table.Name.ToCamel()
output, err := util.With("findOne").
Parse(template.FindOne).
Execute(map[string]interface{}{
"withCache": withCache,
"upperStartCamelObject": camel,
"lowerStartCamelObject": stringx.From(camel).LowerStart(),
"lowerStartCamelObject": stringx.From(camel).UnTitle(),
"originalPrimaryKey": table.PrimaryKey.Name.Source(),
"lowerStartCamelPrimaryKey": stringx.From(table.PrimaryKey.Name.Snake2Camel()).LowerStart(),
"lowerStartCamelPrimaryKey": stringx.From(table.PrimaryKey.Name.ToCamel()).UnTitle(),
"dataType": table.PrimaryKey.DataType,
"cacheKey": table.CacheKey[table.PrimaryKey.Name.Source()].KeyExpression,
"cacheKeyVariable": table.CacheKey[table.PrimaryKey.Name.Source()].Variable,

View File

@@ -5,30 +5,30 @@ import (
"strings"
"github.com/tal-tech/go-zero/tools/goctl/model/sql/template"
"github.com/tal-tech/go-zero/tools/goctl/util"
"github.com/tal-tech/go-zero/tools/goctl/util/stringx"
"github.com/tal-tech/go-zero/tools/goctl/util/templatex"
)
func genFineOneByField(table Table, withCache bool) (string, error) {
t := templatex.With("findOneByField").Parse(template.FindOneByField)
t := util.With("findOneByField").Parse(template.FindOneByField)
var list []string
camelTableName := table.Name.Snake2Camel()
camelTableName := table.Name.ToCamel()
for _, field := range table.Fields {
if field.IsPrimaryKey || !field.IsKey {
continue
}
camelFieldName := field.Name.Snake2Camel()
camelFieldName := field.Name.ToCamel()
output, err := t.Execute(map[string]interface{}{
"upperStartCamelObject": camelTableName,
"upperField": camelFieldName,
"in": fmt.Sprintf("%s %s", stringx.From(camelFieldName).LowerStart(), field.DataType),
"in": fmt.Sprintf("%s %s", stringx.From(camelFieldName).UnTitle(), field.DataType),
"withCache": withCache,
"cacheKey": table.CacheKey[field.Name.Source()].KeyExpression,
"cacheKeyVariable": table.CacheKey[field.Name.Source()].Variable,
"primaryKeyLeft": table.CacheKey[table.PrimaryKey.Name.Source()].Left,
"lowerStartCamelObject": stringx.From(camelTableName).LowerStart(),
"lowerStartCamelField": stringx.From(camelFieldName).LowerStart(),
"upperStartCamelPrimaryKey": table.PrimaryKey.Name.Snake2Camel(),
"lowerStartCamelObject": stringx.From(camelTableName).UnTitle(),
"lowerStartCamelField": stringx.From(camelFieldName).UnTitle(),
"upperStartCamelPrimaryKey": table.PrimaryKey.Name.ToCamel(),
"originalField": field.Name.Source(),
"originalPrimaryField": table.PrimaryKey.Name.Source(),
})

View File

@@ -12,7 +12,6 @@ import (
"github.com/tal-tech/go-zero/tools/goctl/util"
"github.com/tal-tech/go-zero/tools/goctl/util/console"
"github.com/tal-tech/go-zero/tools/goctl/util/stringx"
"github.com/tal-tech/go-zero/tools/goctl/util/templatex"
)
const (
@@ -23,21 +22,17 @@ const (
type (
defaultGenerator struct {
source string
src string
dir string
console.Console
}
Option func(generator *defaultGenerator)
)
func NewDefaultGenerator(src, dir string, opt ...Option) *defaultGenerator {
if src == "" {
src = pwd
}
func NewDefaultGenerator(source, dir string, opt ...Option) *defaultGenerator {
if dir == "" {
dir = pwd
}
generator := &defaultGenerator{src: src, dir: dir}
generator := &defaultGenerator{source: source, dir: dir}
var optionList []Option
optionList = append(optionList, newDefaultOption())
optionList = append(optionList, opt...)
@@ -60,10 +55,6 @@ func newDefaultOption() Option {
}
func (g *defaultGenerator) Start(withCache bool) error {
fileSrc, err := filepath.Abs(g.src)
if err != nil {
return err
}
dirAbs, err := filepath.Abs(g.dir)
if err != nil {
return err
@@ -72,21 +63,16 @@ func (g *defaultGenerator) Start(withCache bool) error {
if err != nil {
return err
}
data, err := ioutil.ReadFile(fileSrc)
if err != nil {
return err
}
g.source = string(data)
modelList, err := g.genFromDDL(withCache)
if err != nil {
return err
}
for tableName, code := range modelList {
name := fmt.Sprintf("%smodel.go", strings.ToLower(stringx.From(tableName).Snake2Camel()))
name := fmt.Sprintf("%smodel.go", strings.ToLower(stringx.From(tableName).ToCamel()))
filename := filepath.Join(dirAbs, name)
if util.FileExists(filename) {
g.Warning("%s already exists,ignored.", name)
g.Warning("%s already exists, ignored.", name)
continue
}
err = ioutil.WriteFile(filename, []byte(code), os.ModePerm)
@@ -95,7 +81,7 @@ func (g *defaultGenerator) Start(withCache bool) error {
}
}
// generate error file
filename := filepath.Join(dirAbs, "error.go")
filename := filepath.Join(dirAbs, "vars.go")
if !util.FileExists(filename) {
err = ioutil.WriteFile(filename, []byte(template.Error), os.ModePerm)
if err != nil {
@@ -132,7 +118,7 @@ type (
)
func (g *defaultGenerator) genModel(in parser.Table, withCache bool) (string, error) {
t := templatex.With("model").
t := util.With("model").
Parse(template.Model).
GoFmt(true)
@@ -185,7 +171,7 @@ func (g *defaultGenerator) genModel(in parser.Table, withCache bool) (string, er
"types": typesCode,
"new": newCode,
"insert": insertCode,
"find": strings.Join(findCode, "\r\n"),
"find": strings.Join(findCode, "\n"),
"update": updateCode,
"delete": deleteCode,
})

View File

@@ -4,15 +4,15 @@ import (
"strings"
"github.com/tal-tech/go-zero/tools/goctl/model/sql/template"
"github.com/tal-tech/go-zero/tools/goctl/util"
"github.com/tal-tech/go-zero/tools/goctl/util/stringx"
"github.com/tal-tech/go-zero/tools/goctl/util/templatex"
)
func genInsert(table Table, withCache bool) (string, error) {
expressions := make([]string, 0)
expressionValues := make([]string, 0)
for _, filed := range table.Fields {
camel := filed.Name.Snake2Camel()
camel := filed.Name.ToCamel()
if camel == "CreateTime" || camel == "UpdateTime" {
continue
}
@@ -22,13 +22,13 @@ func genInsert(table Table, withCache bool) (string, error) {
expressions = append(expressions, "?")
expressionValues = append(expressionValues, "data."+camel)
}
camel := table.Name.Snake2Camel()
output, err := templatex.With("insert").
camel := table.Name.ToCamel()
output, err := util.With("insert").
Parse(template.Insert).
Execute(map[string]interface{}{
"withCache": withCache,
"upperStartCamelObject": camel,
"lowerStartCamelObject": stringx.From(camel).LowerStart(),
"lowerStartCamelObject": stringx.From(camel).UnTitle(),
"expression": strings.Join(expressions, ", "),
"expressionValues": strings.Join(expressionValues, ", "),
})

View File

@@ -26,14 +26,14 @@ type (
func genCacheKeys(table parser.Table) (map[string]Key, error) {
fields := table.Fields
m := make(map[string]Key)
camelTableName := table.Name.Snake2Camel()
lowerStartCamelTableName := stringx.From(camelTableName).LowerStart()
camelTableName := table.Name.ToCamel()
lowerStartCamelTableName := stringx.From(camelTableName).UnTitle()
for _, field := range fields {
if !field.IsKey {
continue
}
camelFieldName := field.Name.Snake2Camel()
lowerStartCamelFieldName := stringx.From(camelFieldName).LowerStart()
camelFieldName := field.Name.ToCamel()
lowerStartCamelFieldName := stringx.From(camelFieldName).UnTitle()
left := fmt.Sprintf("cache%s%sPrefix", camelTableName, camelFieldName)
right := fmt.Sprintf("cache#%s#%s#", camelTableName, lowerStartCamelFieldName)
variable := fmt.Sprintf("%s%sKey", lowerStartCamelTableName, camelFieldName)

View File

@@ -2,15 +2,15 @@ package gen
import (
"github.com/tal-tech/go-zero/tools/goctl/model/sql/template"
"github.com/tal-tech/go-zero/tools/goctl/util/templatex"
"github.com/tal-tech/go-zero/tools/goctl/util"
)
func genNew(table Table, withCache bool) (string, error) {
output, err := templatex.With("new").
output, err := util.With("new").
Parse(template.New).
Execute(map[string]interface{}{
"withCache": withCache,
"upperStartCamelObject": table.Name.Snake2Camel(),
"upperStartCamelObject": table.Name.ToCamel(),
})
if err != nil {
return "", err

View File

@@ -2,14 +2,14 @@ package gen
import (
"github.com/tal-tech/go-zero/tools/goctl/model/sql/template"
"github.com/tal-tech/go-zero/tools/goctl/util/templatex"
"github.com/tal-tech/go-zero/tools/goctl/util"
)
func genTag(in string) (string, error) {
if in == "" {
return in, nil
}
output, err := templatex.With("tag").
output, err := util.With("tag").
Parse(template.Tag).
Execute(map[string]interface{}{
"field": in,

View File

@@ -2,7 +2,7 @@ package gen
import (
"github.com/tal-tech/go-zero/tools/goctl/model/sql/template"
"github.com/tal-tech/go-zero/tools/goctl/util/templatex"
"github.com/tal-tech/go-zero/tools/goctl/util"
)
func genTypes(table Table, withCache bool) (string, error) {
@@ -11,11 +11,11 @@ func genTypes(table Table, withCache bool) (string, error) {
if err != nil {
return "", err
}
output, err := templatex.With("types").
output, err := util.With("types").
Parse(template.Types).
Execute(map[string]interface{}{
"withCache": withCache,
"upperStartCamelObject": table.Name.Snake2Camel(),
"upperStartCamelObject": table.Name.ToCamel(),
"fields": fieldsString,
})
if err != nil {

View File

@@ -4,14 +4,14 @@ import (
"strings"
"github.com/tal-tech/go-zero/tools/goctl/model/sql/template"
"github.com/tal-tech/go-zero/tools/goctl/util"
"github.com/tal-tech/go-zero/tools/goctl/util/stringx"
"github.com/tal-tech/go-zero/tools/goctl/util/templatex"
)
func genUpdate(table Table, withCache bool) (string, error) {
expressionValues := make([]string, 0)
for _, filed := range table.Fields {
camel := filed.Name.Snake2Camel()
camel := filed.Name.ToCamel()
if camel == "CreateTime" || camel == "UpdateTime" {
continue
}
@@ -20,16 +20,16 @@ func genUpdate(table Table, withCache bool) (string, error) {
}
expressionValues = append(expressionValues, "data."+camel)
}
expressionValues = append(expressionValues, "data."+table.PrimaryKey.Name.Snake2Camel())
camelTableName := table.Name.Snake2Camel()
output, err := templatex.With("update").
expressionValues = append(expressionValues, "data."+table.PrimaryKey.Name.ToCamel())
camelTableName := table.Name.ToCamel()
output, err := util.With("update").
Parse(template.Update).
Execute(map[string]interface{}{
"withCache": withCache,
"upperStartCamelObject": camelTableName,
"primaryCacheKey": table.CacheKey[table.PrimaryKey.Name.Source()].DataKeyExpression,
"primaryKeyVariable": table.CacheKey[table.PrimaryKey.Name.Source()].Variable,
"lowerStartCamelObject": stringx.From(camelTableName).LowerStart(),
"lowerStartCamelObject": stringx.From(camelTableName).UnTitle(),
"originalPrimaryKey": table.PrimaryKey.Name.Source(),
"expressionValues": strings.Join(expressionValues, ", "),
})

View File

@@ -4,8 +4,8 @@ import (
"strings"
"github.com/tal-tech/go-zero/tools/goctl/model/sql/template"
"github.com/tal-tech/go-zero/tools/goctl/util"
"github.com/tal-tech/go-zero/tools/goctl/util/stringx"
"github.com/tal-tech/go-zero/tools/goctl/util/templatex"
)
func genVars(table Table, withCache bool) (string, error) {
@@ -13,14 +13,14 @@ func genVars(table Table, withCache bool) (string, error) {
for _, v := range table.CacheKey {
keys = append(keys, v.VarExpression)
}
camel := table.Name.Snake2Camel()
output, err := templatex.With("var").
camel := table.Name.ToCamel()
output, err := util.With("var").
Parse(template.Vars).
GoFmt(true).
Execute(map[string]interface{}{
"lowerStartCamelObject": stringx.From(camel).LowerStart(),
"lowerStartCamelObject": stringx.From(camel).UnTitle(),
"upperStartCamelObject": camel,
"cacheKeys": strings.Join(keys, "\r\n"),
"cacheKeys": strings.Join(keys, "\n"),
"autoIncrement": table.PrimaryKey.AutoIncrement,
"originalPrimaryKey": table.PrimaryKey.Name.Source(),
"withCache": withCache,

View File

@@ -0,0 +1,33 @@
package model
import (
"github.com/tal-tech/go-zero/core/stores/sqlx"
)
type (
DDLModel struct {
conn sqlx.SqlConn
}
DDL struct {
Table string `db:"Table"`
DDL string `db:"Create Table"`
}
)
func NewDDLModel(conn sqlx.SqlConn) *DDLModel {
return &DDLModel{conn: conn}
}
func (m *DDLModel) ShowDDL(table ...string) ([]string, error) {
var ddl []string
for _, t := range table {
query := `show create table ` + t
var resp DDL
err := m.conn.QueryRow(&resp, query)
if err != nil {
return nil, err
}
ddl = append(ddl, resp.DDL)
}
return ddl, nil
}

View File

@@ -81,7 +81,7 @@ func Parse(ddl string) (*Table, error) {
}
column := index.Columns[0]
columnName := column.Column.String()
camelColumnName := stringx.From(columnName).Snake2Camel()
camelColumnName := stringx.From(columnName).ToCamel()
// by default, createTime|updateTime findOne is not used.
if camelColumnName == "CreateTime" || camelColumnName == "UpdateTime" {
continue

View File

@@ -2,10 +2,11 @@ package template
var Delete = `
func (m *{{.upperStartCamelObject}}Model) Delete({{.lowerStartCamelPrimaryKey}} {{.dataType}}) error {
{{if .withCache}}{{if .containsIndexCache}}data,err:=m.FindOne({{.lowerStartCamelPrimaryKey}})
{{if .withCache}}{{if .containsIndexCache}}_, err:=m.FindOne({{.lowerStartCamelPrimaryKey}})
if err!=nil{
return err
}{{end}}
{{.keys}}
_, err {{if .containsIndexCache}}={{else}}:={{end}} m.Exec(func(conn sqlx.SqlConn) (result sql.Result, err error) {
query := ` + "`" + `delete from ` + "` +" + ` m.table + ` + " `" + ` where {{.originalPrimaryKey}} = ?` + "`" + `

View File

@@ -4,8 +4,5 @@ var Error = `package model
import "github.com/tal-tech/go-zero/core/stores/sqlx"
var (
ErrNotFound = sqlx.ErrNotFound
)
var ErrNotFound = sqlx.ErrNotFound
`

View File

@@ -5,7 +5,6 @@ var (
"database/sql"
"fmt"
"strings"
"time"
"github.com/tal-tech/go-zero/core/stores/cache"
"github.com/tal-tech/go-zero/core/stores/sqlc"

View File

@@ -2,7 +2,7 @@ package template
var Insert = `
func (m *{{.upperStartCamelObject}}Model) Insert(data {{.upperStartCamelObject}}) (sql.Result, error) {
query := ` + "`" + `insert into ` + "`" + ` + m.table + ` + "`(` + " + `{{.lowerStartCamelObject}}RowsExpectAutoSet` + " + `) value ({{.expression}})` " + `
query := ` + "`" + `insert into ` + "`" + ` + m.table + ` + "` (` + " + `{{.lowerStartCamelObject}}RowsExpectAutoSet` + " + `) values ({{.expression}})` " + `
return m.{{if .withCache}}ExecNoCache{{else}}conn.Exec{{end}}(query, {{.expressionValues}})
}
`

View File

@@ -0,0 +1,9 @@
# Change log
# 2020-08-29
* 新增支持windows生成
# 2020-08-27
* 新增支持rpc模板生成
* 新增支持rpc服务生成

140
tools/goctl/rpc/README.md Normal file
View File

@@ -0,0 +1,140 @@
# Rpc Generation
Goctl Rpc是`goctl`脚手架下的一个rpc服务代码生成模块支持proto模板生成和rpc服务代码生成通过此工具生成代码你只需要关注业务逻辑编写而不用去编写一些重复性的代码。这使得我们把精力重心放在业务上从而加快了开发效率且降低了代码出错率。
# 特性
* 简单易用
* 快速提升开发效率
* 出错率低
# 快速开始
### 生成proto模板
```shell script
$ goctl rpc template -o=user.proto
```
```golang
syntax = "proto3";
package remote;
message Request {
// 用户名
string username = 1;
// 用户密码
string password = 2;
}
message Response {
// 用户名称
string name = 1;
// 用户性别
string gender = 2;
}
service User {
// 登录
rpc Login(Request)returns(Response);
}
```
### 生成rpc服务代码
生成user rpc服务
```
$ goctl rpc proto -src=user.proto
```
代码tree
```
user
├── etc
│   └── user.json
├── internal
│   ├── config
│   │   └── config.go
│   ├── handler
│   │   ├── loginhandler.go
│   ├── logic
│   │   └── loginlogic.go
│   └── svc
│   └── servicecontext.go
├── pb
│   └── user.pb.go
├── shared
│   ├── mockusermodel.go
│   ├── types.go
│   └── usermodel.go
├── user.go
└── user.proto
```
# 准备工作
* 安装了go环境
* 安装了protoc&protoc-gen-go并且已经设置环境变量
* mockgen(可选)
# 用法
```shell script
$ goctl rpc proto -h
```
```shell script
NAME:
goctl rpc proto - generate rpc from proto
USAGE:
goctl rpc proto [command options] [arguments...]
OPTIONS:
--src value, -s value the file path of the proto source file
--dir value, -d value the target path of the code,default path is "${pwd}". [option]
--service value, --srv value the name of rpc service. [option]
--shared value the dir of the shared file,default path is "${pwd}/shared. [option]"
--idea whether the command execution environment is from idea plugin. [option]
```
* 参数说明
* --src 必填proto数据源目前暂时支持单个proto文件生成这里不支持不建议外部依赖
* --dir 非必填默认为proto文件所在目录生成代码的目标目录
* --service 服务名称非必填默认为proto文件所在目录名称但是如果proto所在目录为一下结构
```shell script
user
├── cmd
│   └── rpc
│   └── user.proto
```
则服务名称亦为user而非proto所在文件夹名称了这里推荐使用这种结构可以方便在同一个服务名下建立不同类型的服务(api、rpc、mq等),便于代码管理与维护。
* --shared 非必填,默认为$dir(xxx.proto)/sharedrpc client逻辑代码存放目录。
> 注意这里的shared文件夹名称将会是代码中的package名称。
* --idea 非必填是否为idea插件中执行保留字段终端执行可以忽略
# 开发人员需要做什么
关注业务代码编写将重复性、与业务无关的工作交给goctl生成好rpc服务代码后开饭人员仅需要修改
* 服务中的配置文件编写(etc/xx.json、internal/config/config.go)
* 服务中业务逻辑编写(internal/logic/xxlogic.go)
* 服务中资源上下文的编写(internal/svc/servicecontext.go)
# 扩展
对于需要进行rpc mock的开发人员在安装了`mockgen`工具的前提下可以在rpc的shared文件中生成好对应的mock文件。
# 注意事项
* proto不支持暂多文件同时生成
* proto不支持外部依赖包引入message不支持inline
* 目前main文件、shared文件、handler文件会被强制覆盖而和开发人员手动需要编写的则不会覆盖生成这一类在代码头部均有
```shell script
// Code generated by goctl. DO NOT EDIT!
// Source: xxx.proto
```
的标识,请注意不要将也写业务性代码写在里面。

View File

@@ -0,0 +1,22 @@
package command
import (
"github.com/tal-tech/go-zero/tools/goctl/rpc/ctx"
"github.com/tal-tech/go-zero/tools/goctl/rpc/gen"
"github.com/urfave/cli"
)
func Rpc(c *cli.Context) error {
rpcCtx := ctx.MustCreateRpcContextFromCli(c)
generator := gen.NewDefaultRpcGenerator(rpcCtx)
rpcCtx.Must(generator.Generate())
return nil
}
func RpcTemplate(c *cli.Context) error {
out := c.String("out")
idea := c.Bool("idea")
generator := gen.NewRpcTemplate(out, idea)
generator.MustGenerate()
return nil
}

View File

@@ -0,0 +1,94 @@
package ctx
import (
"fmt"
"path/filepath"
"runtime"
"strings"
"github.com/tal-tech/go-zero/core/logx"
"github.com/tal-tech/go-zero/tools/goctl/util"
"github.com/tal-tech/go-zero/tools/goctl/util/console"
"github.com/tal-tech/go-zero/tools/goctl/util/stringx"
"github.com/urfave/cli"
)
const (
flagSrc = "src"
flagDir = "dir"
flagService = "service"
flagIdea = "idea"
)
type RpcContext struct {
ProjectPath string
ProjectName stringx.String
ServiceName stringx.String
CurrentPath string
Module string
ProtoFileSrc string
ProtoSource string
TargetDir string
console.Console
}
func MustCreateRpcContext(protoSrc, targetDir, serviceName string, idea bool) *RpcContext {
log := console.NewConsole(idea)
info, err := prepare(log)
log.Must(err)
if stringx.From(protoSrc).IsEmptyOrSpace() {
log.Fatalln("expected proto source, but nothing found")
}
srcFp, err := filepath.Abs(protoSrc)
log.Must(err)
if !util.FileExists(srcFp) {
log.Fatalln("%s is not exists", srcFp)
}
current := filepath.Dir(srcFp)
if stringx.From(targetDir).IsEmptyOrSpace() {
targetDir = current
}
targetDirFp, err := filepath.Abs(targetDir)
log.Must(err)
if stringx.From(serviceName).IsEmptyOrSpace() {
serviceName = getServiceFromRpcStructure(targetDirFp)
}
serviceNameString := stringx.From(serviceName)
if serviceNameString.IsEmptyOrSpace() {
log.Fatalln("service name is not found")
}
return &RpcContext{
ProjectPath: info.Path,
ProjectName: stringx.From(info.Name),
ServiceName: serviceNameString,
CurrentPath: current,
Module: info.GoMod.Module,
ProtoFileSrc: srcFp,
ProtoSource: filepath.Base(srcFp),
TargetDir: targetDirFp,
Console: log,
}
}
func MustCreateRpcContextFromCli(ctx *cli.Context) *RpcContext {
os := runtime.GOOS
switch os {
case "darwin", "windows":
default:
logx.Must(fmt.Errorf("unexpected os: %s", os))
}
protoSrc := ctx.String(flagSrc)
targetDir := ctx.String(flagDir)
serviceName := ctx.String(flagService)
idea := ctx.Bool(flagIdea)
return MustCreateRpcContext(protoSrc, targetDir, serviceName, idea)
}
func getServiceFromRpcStructure(targetDir string) string {
targetDir = filepath.Clean(targetDir)
suffix := filepath.Join("cmd", "rpc")
return filepath.Base(strings.TrimSuffix(targetDir, suffix))
}

View File

@@ -0,0 +1,121 @@
package ctx
import (
"fmt"
"io/ioutil"
"os"
"os/exec"
"path/filepath"
"regexp"
"strings"
"github.com/tal-tech/go-zero/tools/goctl/rpc/execx"
"github.com/tal-tech/go-zero/tools/goctl/util/console"
)
const (
constGo = "go"
constProtoC = "protoc"
constGoMod = "go env GOMOD"
constGoPath = "go env GOPATH"
constProtoCGenGo = "protoc-gen-go"
)
type (
Project struct {
Path string
Name string
GoMod GoMod
}
GoMod struct {
Module string
}
)
func prepare(log console.Console) (*Project, error) {
log.Info("checking go env...")
_, err := exec.LookPath(constGo)
if err != nil {
return nil, err
}
_, err = exec.LookPath(constProtoC)
if err != nil {
return nil, err
}
_, err = exec.LookPath(constProtoCGenGo)
if err != nil {
return nil, err
}
var (
goMod, module string
goPath string
name, path string
)
ret, err := execx.Run(constGoMod)
if err != nil {
return nil, err
}
goMod = strings.TrimSpace(ret)
ret, err = execx.Run(constGoPath)
if err != nil {
return nil, err
}
goPath = strings.TrimSpace(ret)
src := filepath.Join(goPath, "src")
if len(goMod) > 0 {
path = filepath.Dir(goMod)
name = filepath.Base(path)
data, err := ioutil.ReadFile(goMod)
if err != nil {
return nil, err
}
module, err = matchModule(data)
if err != nil {
return nil, err
}
} else {
pwd, err := os.Getwd()
if err != nil {
return nil, err
}
if !strings.HasPrefix(pwd, src) {
return nil, fmt.Errorf("%s: project is not in go mod and go path", pwd)
}
r := strings.TrimPrefix(pwd, src+string(filepath.Separator))
name = filepath.Dir(r)
if name == "." {
name = r
}
path = filepath.Join(src, name)
module = name
}
return &Project{
Name: name,
Path: path,
GoMod: GoMod{
Module: module,
},
}, nil
}
func matchModule(data []byte) (string, error) {
text := string(data)
re := regexp.MustCompile(`(?m)^\s*module\s+[a-z0-9/\-.]+$`)
matches := re.FindAllString(text, -1)
if len(matches) == 1 {
target := matches[0]
index := strings.Index(target, "module")
return strings.TrimSpace(target[index+6:]), nil
}
return "", nil
}

View File

@@ -0,0 +1,34 @@
package execx
import (
"bytes"
"errors"
"fmt"
"os/exec"
"runtime"
)
func Run(arg string) (string, error) {
goos := runtime.GOOS
var cmd *exec.Cmd
switch goos {
case "darwin":
cmd = exec.Command("sh", "-c", arg)
case "windows":
cmd = exec.Command("cmd.exe", "/c", arg)
default:
return "", fmt.Errorf("unexpected os: %v", goos)
}
dtsout := new(bytes.Buffer)
stderr := new(bytes.Buffer)
cmd.Stdout = dtsout
cmd.Stderr = stderr
err := cmd.Run()
if err != nil {
if stderr.Len() > 0 {
return "", errors.New(stderr.String())
}
return "", err
}
return dtsout.String(), nil
}

View File

@@ -0,0 +1,86 @@
package gen
import (
"github.com/tal-tech/go-zero/tools/goctl/rpc/ctx"
"github.com/tal-tech/go-zero/tools/goctl/rpc/parser"
)
const (
dirTarget = "dirTarget"
dirConfig = "config"
dirEtc = "etc"
dirSvc = "svc"
dirServer = "server"
dirLogic = "logic"
dirPb = "pb"
dirInternal = "internal"
fileConfig = "config.go"
fileServiceContext = "servicecontext.go"
)
type defaultRpcGenerator struct {
dirM map[string]string
Ctx *ctx.RpcContext
ast *parser.PbAst
}
func NewDefaultRpcGenerator(ctx *ctx.RpcContext) *defaultRpcGenerator {
return &defaultRpcGenerator{
Ctx: ctx,
}
}
func (g *defaultRpcGenerator) Generate() (err error) {
g.Ctx.Info("generating code...")
defer func() {
if err == nil {
g.Ctx.Success("Done.")
}
}()
err = g.createDir()
if err != nil {
return
}
err = g.genEtc()
if err != nil {
return
}
err = g.genPb()
if err != nil {
return
}
err = g.genConfig()
if err != nil {
return
}
err = g.genSvc()
if err != nil {
return
}
err = g.genLogic()
if err != nil {
return
}
err = g.genHandler()
if err != nil {
return
}
err = g.genMain()
if err != nil {
return
}
err = g.genCall()
if err != nil {
return
}
return nil
}

View File

@@ -0,0 +1,240 @@
package gen
import (
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
"github.com/tal-tech/go-zero/tools/goctl/rpc/execx"
"github.com/tal-tech/go-zero/tools/goctl/rpc/parser"
"github.com/tal-tech/go-zero/tools/goctl/util"
)
const (
callTemplateText = `{{.head}}
//go:generate mockgen -destination ./{{.name}}_mock.go -package {{.filePackage}} -source $GOFILE
package {{.filePackage}}
import (
"context"
{{.package}}
"github.com/tal-tech/go-zero/core/jsonx"
"github.com/tal-tech/go-zero/rpcx"
)
type (
{{.serviceName}} interface {
{{.interface}}
}
default{{.serviceName}} struct {
cli rpcx.Client
}
)
func New{{.serviceName}}(cli rpcx.Client) {{.serviceName}} {
return &default{{.serviceName}}{
cli: cli,
}
}
{{.functions}}
`
callTemplateTypes = `{{.head}}
package {{.filePackage}}
import "errors"
var errJsonConvert = errors.New("json convert error")
{{.types}}
`
callInterfaceFunctionTemplate = `{{if .hasComment}}{{.comment}}
{{end}}{{.method}}(ctx context.Context,in *{{.pbRequest}}) {{if .hasResponse}}(*{{.pbResponse}},{{end}} error{{if .hasResponse}}){{end}}`
callFunctionTemplate = `
{{if .hasComment}}{{.comment}}{{end}}
func (m *default{{.rpcServiceName}}) {{.method}}(ctx context.Context,in *{{.pbRequest}}) {{if .hasResponse}}(*{{.pbResponse}},{{end}} error{{if .hasResponse}}){{end}} {
var request {{.package}}.{{.pbRequest}}
bts, err := jsonx.Marshal(in)
if err != nil {
return {{if .hasResponse}}nil, {{end}}errJsonConvert
}
err = jsonx.Unmarshal(bts, &request)
if err != nil {
return {{if .hasResponse}}nil, {{end}}errJsonConvert
}
client := {{.package}}.New{{.rpcServiceName}}Client(m.cli.Conn())
{{if .hasResponse}}resp, err := {{else}}_, err = {{end}}client.{{.method}}(ctx, &request)
{{if .hasResponse}}if err != nil{
return nil, err
}
var ret {{.pbResponse}}
bts, err = jsonx.Marshal(resp)
if err != nil{
return nil, errJsonConvert
}
err = jsonx.Unmarshal(bts, &ret)
if err != nil{
return nil, errJsonConvert
}
return &ret, nil{{else}}if err != nil {
return err
}
return nil{{end}}
}
`
)
func (g *defaultRpcGenerator) genCall() error {
file := g.ast
if len(file.Service) == 0 {
return nil
}
if len(file.Service) > 1 {
return fmt.Errorf("we recommend only one service in a proto, currently %d", len(file.Service))
}
typeCode, err := file.GenTypesCode()
if err != nil {
return err
}
service := file.Service[0]
callPath, err := filepath.Abs(service.Name.Lower())
if err != nil {
return err
}
if err = util.MkdirIfNotExist(callPath); err != nil {
return err
}
pbPkg := file.Package
remotePackage := fmt.Sprintf(`%v "%v"`, pbPkg, g.mustGetPackage(dirPb))
filename := filepath.Join(callPath, "types.go")
head := util.GetHead(g.Ctx.ProtoSource)
err = util.With("types").GoFmt(true).Parse(callTemplateTypes).SaveTo(map[string]interface{}{
"head": head,
"filePackage": service.Name.Lower(),
"pbPkg": pbPkg,
"serviceName": g.Ctx.ServiceName.Title(),
"lowerStartServiceName": g.Ctx.ServiceName.UnTitle(),
"types": typeCode,
}, filename, true)
if err != nil {
return err
}
_, err = exec.LookPath("mockgen")
mockGenInstalled := err == nil
filename = filepath.Join(callPath, fmt.Sprintf("%s.go", service.Name.Lower()))
functions, err := g.getFuncs(service)
if err != nil {
return err
}
iFunctions, err := g.getInterfaceFuncs(service)
if err != nil {
return err
}
mockFile := filepath.Join(callPath, fmt.Sprintf("%s_mock.go", service.Name.Lower()))
os.Remove(mockFile)
err = util.With("shared").GoFmt(true).Parse(callTemplateText).SaveTo(map[string]interface{}{
"name": service.Name.Lower(),
"head": head,
"filePackage": service.Name.Lower(),
"pbPkg": pbPkg,
"package": remotePackage,
"serviceName": service.Name.Title(),
"functions": strings.Join(functions, "\n"),
"interface": strings.Join(iFunctions, "\n"),
}, filename, true)
if err != nil {
return err
}
// if mockgen is already installed, it will generate code of gomock for shared files
_, err = exec.LookPath("mockgen")
if mockGenInstalled {
execx.Run(fmt.Sprintf("go generate %s", filename))
}
return nil
}
func (g *defaultRpcGenerator) getFuncs(service *parser.RpcService) ([]string, error) {
file := g.ast
pkgName := file.Package
functions := make([]string, 0)
for _, method := range service.Funcs {
data, found := file.Strcuts[strings.ToLower(method.OutType)]
if found {
found = len(data.Field) > 0
}
var comment string
if len(method.Document) > 0 {
comment = method.Document[0]
}
buffer, err := util.With("sharedFn").Parse(callFunctionTemplate).Execute(map[string]interface{}{
"rpcServiceName": service.Name.Title(),
"method": method.Name.Title(),
"package": pkgName,
"pbRequest": method.InType,
"pbResponse": method.OutType,
"hasResponse": found,
"hasComment": len(method.Document) > 0,
"comment": comment,
})
if err != nil {
return nil, err
}
functions = append(functions, buffer.String())
}
return functions, nil
}
func (g *defaultRpcGenerator) getInterfaceFuncs(service *parser.RpcService) ([]string, error) {
file := g.ast
functions := make([]string, 0)
for _, method := range service.Funcs {
data, found := file.Strcuts[strings.ToLower(method.OutType)]
if found {
found = len(data.Field) > 0
}
var comment string
if len(method.Document) > 0 {
comment = method.Document[0]
}
buffer, err := util.With("interfaceFn").Parse(callInterfaceFunctionTemplate).Execute(
map[string]interface{}{
"hasComment": len(method.Document) > 0,
"comment": comment,
"method": method.Name.Title(),
"pbRequest": method.InType,
"pbResponse": method.OutType,
"hasResponse": found,
})
if err != nil {
return nil, err
}
functions = append(functions, buffer.String())
}
return functions, nil
}

View File

@@ -0,0 +1,27 @@
package gen
import (
"io/ioutil"
"os"
"path/filepath"
"github.com/tal-tech/go-zero/tools/goctl/util"
)
const configTemplate = `package config
import "github.com/tal-tech/go-zero/rpcx"
type Config struct {
rpcx.RpcServerConf
}
`
func (g *defaultRpcGenerator) genConfig() error {
configPath := g.dirM[dirConfig]
fileName := filepath.Join(configPath, fileConfig)
if util.FileExists(fileName) {
return nil
}
return ioutil.WriteFile(fileName, []byte(configTemplate), os.ModePerm)
}

Some files were not shown because too many files have changed in this diff Show More