mirror of
https://github.com/zeromicro/go-zero.git
synced 2026-05-11 08:50:00 +08:00
Compare commits
49 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6c4a4be5d2 | ||
|
|
6e3d99e869 | ||
|
|
0f97b2019a | ||
|
|
0cf4ed46a1 | ||
|
|
3affe62ae4 | ||
|
|
0734bbcab3 | ||
|
|
f411178a4f | ||
|
|
72132ce399 | ||
|
|
db16115037 | ||
|
|
71bbf91a63 | ||
|
|
69ccc61cfe | ||
|
|
a94cf653f0 | ||
|
|
77e23ad65d | ||
|
|
38806e7237 | ||
|
|
a987d12237 | ||
|
|
33208e6ef6 | ||
|
|
5d8a3c07cd | ||
|
|
1c24e71568 | ||
|
|
229544f3ca | ||
|
|
c575fa7f95 | ||
|
|
fe2252184a | ||
|
|
1a8014c704 | ||
|
|
30e52707ae | ||
|
|
73b61e09ed | ||
|
|
9b8595a85e | ||
|
|
015e284515 | ||
|
|
456b395860 | ||
|
|
f3c367a323 | ||
|
|
a32028c4fb | ||
|
|
b4572fa064 | ||
|
|
ccbabf6f58 | ||
|
|
5989444227 | ||
|
|
dc286a03f5 | ||
|
|
b82c02ed16 | ||
|
|
59ba4ecc5b | ||
|
|
5e7b514ae2 | ||
|
|
2b1466e41e | ||
|
|
9c9f80518f | ||
|
|
25973d6b59 | ||
|
|
6237d01948 | ||
|
|
49316b113e | ||
|
|
6a673e8cb0 | ||
|
|
0efa28ddbd | ||
|
|
0b6a13fe84 | ||
|
|
11aa6668e8 | ||
|
|
267a283328 | ||
|
|
2d8366b30e | ||
|
|
db83843558 | ||
|
|
50565c9765 |
@@ -1,3 +1,4 @@
|
||||
ignore:
|
||||
- "example/*"
|
||||
- "tools/*"
|
||||
- "doc"
|
||||
- "example"
|
||||
- "tools"
|
||||
@@ -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=)'
|
||||
@@ -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() {
|
||||
|
||||
4
core/discov/kubernetes/discov-namespace.yaml
Normal file
4
core/discov/kubernetes/discov-namespace.yaml
Normal file
@@ -0,0 +1,4 @@
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: discov
|
||||
368
core/discov/kubernetes/etcd.yaml
Normal file
368
core/discov/kubernetes/etcd.yaml
Normal 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
|
||||
@@ -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 (
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
BIN
doc/images/architecture.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 333 KiB |
BIN
doc/images/shorturl-arch.png
Normal file
BIN
doc/images/shorturl-arch.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 140 KiB |
BIN
doc/images/shorturl-benchmark.png
Normal file
BIN
doc/images/shorturl-benchmark.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 111 KiB |
@@ -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
647
doc/shorturl.md
Normal 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来生成压测请求
|
||||
|
||||

|
||||
|
||||
可以看出在我的MacBook Pro上能达到3万+的qps。
|
||||
|
||||
## 11. 总结
|
||||
|
||||
我们一直强调**工具大于约定和文档**。
|
||||
|
||||
go-zero不只是一个框架,更是一个建立在框架+工具基础上的,简化和规范了整个微服务构建的技术体系。
|
||||
|
||||
我们在保持简单的同时也尽可能把微服务治理的复杂度封装到了框架内部,极大的降低了开发人员的心智负担,使得业务开发得以快速推进。
|
||||
|
||||
通过go-zero+goctl生成的代码,包含了微服务治理的各种组件,包括:并发控制、自适应熔断、自适应降载、自动缓存控制等,可以轻松部署以承载巨大访问量。
|
||||
@@ -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()
|
||||
|
||||
@@ -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
3
go.mod
@@ -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
2
go.sum
@@ -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=
|
||||
|
||||
53
readme.md
53
readme.md
@@ -6,6 +6,23 @@
|
||||
[](https://github.com/tal-tech/go-zero)
|
||||
[](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框架,有如下
|
||||
|
||||

|
||||
|
||||
## 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
|
||||
|
||||

|
||||
|
||||
[测试代码见这里](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社区群🤝
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
101
rpcx/client_test.go
Normal 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
42
rpcx/config_test.go
Normal 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())
|
||||
}
|
||||
62
rpcx/internal/auth/credential_test.go
Normal file
62
rpcx/internal/auth/credential_test.go
Normal 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])
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
123
rpcx/internal/chainclientinterceptors_test.go
Normal file
123
rpcx/internal/chainclientinterceptors_test.go
Normal 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)
|
||||
}
|
||||
111
rpcx/internal/chainserverinterceptors_test.go
Normal file
111
rpcx/internal/chainserverinterceptors_test.go
Normal 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)
|
||||
}
|
||||
30
rpcx/internal/client_test.go
Normal file
30
rpcx/internal/client_test.go
Normal 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)
|
||||
}
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
37
rpcx/internal/clientinterceptors/durationinterceptor_test.go
Normal file
37
rpcx/internal/clientinterceptors/durationinterceptor_test.go
Normal 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)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
50
rpcx/internal/clientinterceptors/timeoutinterceptor_test.go
Normal file
50
rpcx/internal/clientinterceptors/timeoutinterceptor_test.go
Normal 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)
|
||||
}
|
||||
53
rpcx/internal/clientinterceptors/tracinginterceptor_test.go
Normal file
53
rpcx/internal/clientinterceptors/tracinginterceptor_test.go
Normal 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))
|
||||
}
|
||||
159
rpcx/internal/mock/deposit.pb.go
Normal file
159
rpcx/internal/mock/deposit.pb.go
Normal 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,
|
||||
}
|
||||
15
rpcx/internal/mock/deposit.proto
Normal file
15
rpcx/internal/mock/deposit.proto
Normal 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);
|
||||
}
|
||||
19
rpcx/internal/mock/depositserver.go
Normal file
19
rpcx/internal/mock/depositserver.go
Normal 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
|
||||
}
|
||||
@@ -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{
|
||||
|
||||
52
rpcx/internal/resolver/directbuilder_test.go
Normal file
52
rpcx/internal/resolver/directbuilder_test.go
Normal 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())
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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() {
|
||||
|
||||
36
rpcx/internal/resolver/resolver_test.go
Normal file
36
rpcx/internal/resolver/resolver_test.go
Normal 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
|
||||
}
|
||||
16
rpcx/internal/rpcserver_test.go
Normal file
16
rpcx/internal/rpcserver_test.go
Normal 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)
|
||||
}
|
||||
53
rpcx/internal/server_test.go
Normal file
53
rpcx/internal/server_test.go
Normal 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)
|
||||
}
|
||||
200
rpcx/internal/serverinterceptors/authinterceptor_test.go
Normal file
200
rpcx/internal/serverinterceptors/authinterceptor_test.go
Normal 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
|
||||
}
|
||||
31
rpcx/internal/serverinterceptors/crashinterceptor_test.go
Normal file
31
rpcx/internal/serverinterceptors/crashinterceptor_test.go
Normal 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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
77
rpcx/internal/serverinterceptors/sheddinginterceptor_test.go
Normal file
77
rpcx/internal/serverinterceptors/sheddinginterceptor_test.go
Normal 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() {
|
||||
}
|
||||
32
rpcx/internal/serverinterceptors/statinterceptor_test.go
Normal file
32
rpcx/internal/serverinterceptors/statinterceptor_test.go
Normal 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)
|
||||
}
|
||||
41
rpcx/internal/serverinterceptors/timeoutinterceptor_test.go
Normal file
41
rpcx/internal/serverinterceptors/timeoutinterceptor_test.go
Normal 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)
|
||||
}
|
||||
48
rpcx/internal/serverinterceptors/tracinginterceptor_test.go
Normal file
48
rpcx/internal/serverinterceptors/tracinginterceptor_test.go
Normal 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)
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
17
rpcx/internal/target_test.go
Normal file
17
rpcx/internal/target_test.go
Normal 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)
|
||||
}
|
||||
@@ -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
66
rpcx/proxy_test.go
Normal 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())
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
`
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -13,9 +13,7 @@ const (
|
||||
configFile = "config.go"
|
||||
configTemplate = `package config
|
||||
|
||||
import (
|
||||
{{.authImport}}
|
||||
)
|
||||
import {{.authImport}}
|
||||
|
||||
type Config struct {
|
||||
rest.RestConf
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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}
|
||||
}
|
||||
|
||||
`
|
||||
)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
10
tools/goctl/model/sql/CHANGELOG.md
Normal file
10
tools/goctl/model/sql/CHANGELOG.md
Normal file
@@ -0,0 +1,10 @@
|
||||
# Change log
|
||||
|
||||
# 2020-08-20
|
||||
* 新增支持通过连接数据库生成model
|
||||
* 支持数据库多表生成
|
||||
* 优化stringx
|
||||
|
||||
# 2020-08-19
|
||||
* 重构model代码生成逻辑
|
||||
* 实现从ddl解析表信息生成代码
|
||||
@@ -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插件。
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
@@ -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(),
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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 != "",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(),
|
||||
})
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
@@ -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, ", "),
|
||||
})
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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, ", "),
|
||||
})
|
||||
|
||||
@@ -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,
|
||||
|
||||
33
tools/goctl/model/sql/model/ddlmodel.go
Normal file
33
tools/goctl/model/sql/model/ddlmodel.go
Normal 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
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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}} = ?` + "`" + `
|
||||
|
||||
@@ -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
|
||||
`
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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}})
|
||||
}
|
||||
`
|
||||
|
||||
9
tools/goctl/rpc/CHANGELOG.md
Normal file
9
tools/goctl/rpc/CHANGELOG.md
Normal file
@@ -0,0 +1,9 @@
|
||||
# Change log
|
||||
|
||||
# 2020-08-29
|
||||
* 新增支持windows生成
|
||||
|
||||
# 2020-08-27
|
||||
* 新增支持rpc模板生成
|
||||
* 新增支持rpc服务生成
|
||||
|
||||
140
tools/goctl/rpc/README.md
Normal file
140
tools/goctl/rpc/README.md
Normal 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)/shared,rpc 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
|
||||
```
|
||||
的标识,请注意不要将也写业务性代码写在里面。
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
22
tools/goctl/rpc/command/command.go
Normal file
22
tools/goctl/rpc/command/command.go
Normal 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
|
||||
}
|
||||
94
tools/goctl/rpc/ctx/ctx.go
Normal file
94
tools/goctl/rpc/ctx/ctx.go
Normal 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))
|
||||
}
|
||||
121
tools/goctl/rpc/ctx/project.go
Normal file
121
tools/goctl/rpc/ctx/project.go
Normal 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
|
||||
}
|
||||
34
tools/goctl/rpc/execx/execx.go
Normal file
34
tools/goctl/rpc/execx/execx.go
Normal 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
|
||||
}
|
||||
86
tools/goctl/rpc/gen/gen.go
Normal file
86
tools/goctl/rpc/gen/gen.go
Normal 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
|
||||
}
|
||||
240
tools/goctl/rpc/gen/gencall.go
Normal file
240
tools/goctl/rpc/gen/gencall.go
Normal 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
|
||||
}
|
||||
27
tools/goctl/rpc/gen/genconfig.go
Normal file
27
tools/goctl/rpc/gen/genconfig.go
Normal 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
Reference in New Issue
Block a user