Compare commits

...

64 Commits

Author SHA1 Message Date
kingxt
89f3712347 remove no need empty line (#29)
* rebase upstream

* rebase

* trim no need line

* trim no need line

* trim no need line

* trim no need line

Co-authored-by: kingxt <dream4kingxt@163.com>
2020-09-01 11:08:19 +08:00
kevin
af7acdd843 fix doc errors 2020-09-01 10:26:16 +08:00
kevin
7ffa3349a9 update readme 2020-08-31 22:45:55 +08:00
kevin
f03862c378 update docs 2020-08-31 22:37:43 +08:00
kevin
fe3e70a60f update shorturl doc 2020-08-31 20:52:29 +08:00
kevin
36174ba5cc make svcCtx as a member for better code generation 2020-08-31 12:32:13 +08:00
kevin
7b17b3604a fix dockerfile generation 2020-08-31 12:27:38 +08:00
kevin
eb40c2731d remove files 2020-08-30 23:56:51 +08:00
kevin
618bec5075 remove makefile generation 2020-08-30 23:52:51 +08:00
kevin
5821b7324e update readme 2020-08-30 23:34:57 +08:00
kevin
befdaab542 update goctl makefile 2020-08-30 18:22:46 +08:00
kevin
431be8ed9d make goctl work on linux 2020-08-30 16:37:34 +08:00
kevin
3c688c319e update shorturl doc 2020-08-29 23:42:42 +08:00
kevin
59ffa75c00 fix typo in doc 2020-08-29 23:33:34 +08:00
kevin
09340e82a7 fix doc error 2020-08-29 22:51:48 +08:00
kevin
6c4a4be5d2 update shorturl doc 2020-08-29 20:27:52 +08:00
kevin
6e3d99e869 reorg imports 2020-08-29 14:31:51 +08:00
Keson
0f97b2019a rpc generation support windows (#28)
* add execute files

* add protoc-osx

* add rpc generation

* add rpc generation

* add: rpc template generation

* update usage

* fixed env prepare for project in go path

* optimize gomod cache

* add README.md

* format error

* reactor templatex.go

* remove waste code

* update project.go & README.md

* update project.go & README.md

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

* add protoc-osx

* add rpc generation

* add rpc generation

* add: rpc template generation

* update usage

* fixed env prepare for project in go path

* optimize gomod cache

* add README.md

* format error

* reactor templatex.go

* remove waste code

* update project.go & README.md

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

* add protoc-osx

* add rpc generation

* add rpc generation

* add: rpc template generation

* update usage

* fixed env prepare for project in go path

* optimize gomod cache

* add README.md

* format error

* reactor templatex.go

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

* format

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

* fix bug

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

* new: generate model from data source

* add change log md

* update model doc

* update  doc

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,16 +1,16 @@
apiVersion: v1
kind: Service
metadata:
name: discov
namespace: discovery
name: etcd
namespace: discov
spec:
ports:
- name: discov-port
- name: etcd-port
port: 2379
protocol: TCP
targetPort: 2379
selector:
app: discov
app: etcd
---
@@ -18,30 +18,30 @@ apiVersion: v1
kind: Pod
metadata:
labels:
app: discov
discov_node: discov0
name: discov0
namespace: discovery
app: etcd
etcd_node: etcd0
name: etcd0
namespace: discov
spec:
containers:
- command:
- /usr/local/bin/etcd
- --name
- discov0
- etcd0
- --initial-advertise-peer-urls
- http://discov0:2380
- 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://discov0:2379
- http://etcd0:2379
- --initial-cluster
- discov0=http://discov0:2380,discov1=http://discov1:2380,discov2=http://discov2:2380,discov3=http://discov3:2380,discov4=http://discov4:2380
- 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: registry-vpc.cn-hangzhou.aliyuncs.com/xapp/etcd:latest
name: discov0
image: quay.io/coreos/etcd:latest
name: etcd0
ports:
- containerPort: 2379
name: client
@@ -49,8 +49,6 @@ spec:
- containerPort: 2380
name: server
protocol: TCP
imagePullSecrets:
- name: aliyun
affinity:
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
@@ -59,7 +57,7 @@ spec:
- key: app
operator: In
values:
- discov
- etcd
topologyKey: "kubernetes.io/hostname"
restartPolicy: Always
@@ -69,9 +67,9 @@ apiVersion: v1
kind: Service
metadata:
labels:
discov_node: discov0
name: discov0
namespace: discovery
etcd_node: etcd0
name: etcd0
namespace: discov
spec:
ports:
- name: client
@@ -83,7 +81,7 @@ spec:
protocol: TCP
targetPort: 2380
selector:
discov_node: discov0
etcd_node: etcd0
---
@@ -91,30 +89,30 @@ apiVersion: v1
kind: Pod
metadata:
labels:
app: discov
discov_node: discov1
name: discov1
namespace: discovery
app: etcd
etcd_node: etcd1
name: etcd1
namespace: discov
spec:
containers:
- command:
- /usr/local/bin/etcd
- --name
- discov1
- etcd1
- --initial-advertise-peer-urls
- http://discov1:2380
- 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://discov1:2379
- http://etcd1:2379
- --initial-cluster
- discov0=http://discov0:2380,discov1=http://discov1:2380,discov2=http://discov2:2380,discov3=http://discov3:2380,discov4=http://discov4:2380
- 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: registry-vpc.cn-hangzhou.aliyuncs.com/xapp/etcd:latest
name: discov1
image: quay.io/coreos/etcd:latest
name: etcd1
ports:
- containerPort: 2379
name: client
@@ -122,8 +120,6 @@ spec:
- containerPort: 2380
name: server
protocol: TCP
imagePullSecrets:
- name: aliyun
affinity:
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
@@ -132,7 +128,7 @@ spec:
- key: app
operator: In
values:
- discov
- etcd
topologyKey: "kubernetes.io/hostname"
restartPolicy: Always
@@ -142,9 +138,9 @@ apiVersion: v1
kind: Service
metadata:
labels:
discov_node: discov1
name: discov1
namespace: discovery
etcd_node: etcd1
name: etcd1
namespace: discov
spec:
ports:
- name: client
@@ -156,7 +152,7 @@ spec:
protocol: TCP
targetPort: 2380
selector:
discov_node: discov1
etcd_node: etcd1
---
@@ -164,30 +160,30 @@ apiVersion: v1
kind: Pod
metadata:
labels:
app: discov
discov_node: discov2
name: discov2
namespace: discovery
app: etcd
etcd_node: etcd2
name: etcd2
namespace: discov
spec:
containers:
- command:
- /usr/local/bin/etcd
- --name
- discov2
- etcd2
- --initial-advertise-peer-urls
- http://discov2:2380
- 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://discov2:2379
- http://etcd2:2379
- --initial-cluster
- discov0=http://discov0:2380,discov1=http://discov1:2380,discov2=http://discov2:2380,discov3=http://discov3:2380,discov4=http://discov4:2380
- 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: registry-vpc.cn-hangzhou.aliyuncs.com/xapp/etcd:latest
name: discov2
image: quay.io/coreos/etcd:latest
name: etcd2
ports:
- containerPort: 2379
name: client
@@ -195,8 +191,6 @@ spec:
- containerPort: 2380
name: server
protocol: TCP
imagePullSecrets:
- name: aliyun
affinity:
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
@@ -205,7 +199,7 @@ spec:
- key: app
operator: In
values:
- discov
- etcd
topologyKey: "kubernetes.io/hostname"
restartPolicy: Always
@@ -215,9 +209,9 @@ apiVersion: v1
kind: Service
metadata:
labels:
discov_node: discov2
name: discov2
namespace: discovery
etcd_node: etcd2
name: etcd2
namespace: discov
spec:
ports:
- name: client
@@ -229,7 +223,7 @@ spec:
protocol: TCP
targetPort: 2380
selector:
discov_node: discov2
etcd_node: etcd2
---
@@ -237,30 +231,30 @@ apiVersion: v1
kind: Pod
metadata:
labels:
app: discov
discov_node: discov3
name: discov3
namespace: discovery
app: etcd
etcd_node: etcd3
name: etcd3
namespace: discov
spec:
containers:
- command:
- /usr/local/bin/etcd
- --name
- discov3
- etcd3
- --initial-advertise-peer-urls
- http://discov3:2380
- 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://discov3:2379
- http://etcd3:2379
- --initial-cluster
- discov0=http://discov0:2380,discov1=http://discov1:2380,discov2=http://discov2:2380,discov3=http://discov3:2380,discov4=http://discov4:2380
- 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: registry-vpc.cn-hangzhou.aliyuncs.com/xapp/etcd:latest
name: discov3
image: quay.io/coreos/etcd:latest
name: etcd3
ports:
- containerPort: 2379
name: client
@@ -268,8 +262,6 @@ spec:
- containerPort: 2380
name: server
protocol: TCP
imagePullSecrets:
- name: aliyun
affinity:
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
@@ -278,7 +270,7 @@ spec:
- key: app
operator: In
values:
- discov
- etcd
topologyKey: "kubernetes.io/hostname"
restartPolicy: Always
@@ -288,9 +280,9 @@ apiVersion: v1
kind: Service
metadata:
labels:
discov_node: discov3
name: discov3
namespace: discovery
etcd_node: etcd3
name: etcd3
namespace: discov
spec:
ports:
- name: client
@@ -302,7 +294,7 @@ spec:
protocol: TCP
targetPort: 2380
selector:
discov_node: discov3
etcd_node: etcd3
---
@@ -310,30 +302,30 @@ apiVersion: v1
kind: Pod
metadata:
labels:
app: discov
discov_node: discov4
name: discov4
namespace: discovery
app: etcd
etcd_node: etcd4
name: etcd4
namespace: discov
spec:
containers:
- command:
- /usr/local/bin/etcd
- --name
- discov4
- etcd4
- --initial-advertise-peer-urls
- http://discov4:2380
- 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://discov4:2379
- http://etcd4:2379
- --initial-cluster
- discov0=http://discov0:2380,discov1=http://discov1:2380,discov2=http://discov2:2380,discov3=http://discov3:2380,discov4=http://discov4:2380
- 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: registry-vpc.cn-hangzhou.aliyuncs.com/xapp/etcd:latest
name: discov4
image: quay.io/coreos/etcd:latest
name: etcd4
ports:
- containerPort: 2379
name: client
@@ -341,8 +333,6 @@ spec:
- containerPort: 2380
name: server
protocol: TCP
imagePullSecrets:
- name: aliyun
affinity:
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
@@ -351,7 +341,7 @@ spec:
- key: app
operator: In
values:
- discov
- etcd
topologyKey: "kubernetes.io/hostname"
restartPolicy: Always
@@ -361,9 +351,9 @@ apiVersion: v1
kind: Service
metadata:
labels:
discov_node: discov4
name: discov4
namespace: discovery
etcd_node: etcd4
name: etcd4
namespace: discov
spec:
ports:
- name: client
@@ -375,4 +365,4 @@ spec:
protocol: TCP
targetPort: 2380
selector:
discov_node: discov4
etcd_node: etcd4

View File

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

View File

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

View File

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

View File

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

BIN
doc/images/architecture.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 333 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 126 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 111 KiB

View File

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

507
doc/shorturl.md Normal file
View File

@@ -0,0 +1,507 @@
# 快速构建高并发微服务
## 0. 为什么说做好微服务很难?
要想做好微服务,我们需要理解和掌握的知识点非常多,从几个维度上来说:
* 基本功能层面
1. 并发控制&限流,避免服务被突发流量击垮
2. 服务注册与服务发现,确保能够动态侦测增减的节点
3. 负载均衡,需要根据节点承受能力分发流量
4. 超时控制,避免对已超时请求做无用功
5. 熔断设计,快速失败,保障故障节点的恢复能力
* 高阶功能层面
1. 请求认证,确保每个用户只能访问自己的数据
2. 链路追踪,用于理解整个系统和快速定位特定请求的问题
3. 日志,用于数据收集和问题定位
4. 可观测性,没有度量就没有优化
对于其中每一点,我们都需要用很长的篇幅来讲述其原理和实现,那么对我们后端开发者来说,要想把这些知识点都掌握并落实到业务系统里,难度是非常大的,不过我们可以依赖已经被大流量验证过的框架体系。[go-zero微服务框架](https://github.com/tal-tech/go-zero)就是为此而生。
另外,我们始终秉承**工具大于约定和文档**的理念。我们希望尽可能减少开发人员的心智负担,把精力都投入到产生业务价值的代码上,减少重复代码的编写,所以我们开发了`goctl`工具。
下面我通过短链微服务来演示通过[go-zero](https://github.com/tal-tech/go-zero)快速的创建微服务的流程,走完一遍,你就会发现:原来编写微服务如此简单!
## 1. 什么是短链服务?
短链服务就是将长的URL网址通过程序计算等方式转换为简短的网址字符串。
写此短链服务是为了从整体上演示go-zero构建完整微服务的过程算法和实现细节尽可能简化了所以这不是一个高阶的短链服务。
## 2. 短链微服务架构图
<img src="images/shorturl-arch.png" alt="架构图" width="800" />
* 这里只用了`Transform RPC`一个微服务并不是说API Gateway只能调用一个微服务只是为了最简演示API Gateway如何调用RPC微服务而已
* 在真正项目里要尽可能每个微服务使用自己的数据库,数据边界要清晰
## 3. 准备工作
* 安装etcd, mysql, redis
* 安装goctl工具
```shell
export GO111MODULE=on export GOPROXY=https://goproxy.cn/,direct go get github.com/tal-tech/go-zero/tools/goctl
```
* 创建工作目录`shorturl`
* 在`shorturl`目录下执行`go mod init shorturl`初始化`go.mod`
## 4. 编写API Gateway代码
* 通过goctl生成`api/shorturl.api`并编辑,为了简洁,去除了文件开头的`info`,代码如下:
```go
type (
expandReq struct {
shorten string `form:"shorten"`
}
expandResp struct {
url string `json:"url"`
}
)
type (
shortenReq struct {
url string `form:"url"`
}
shortenResp struct {
shorten string `json:"shorten"`
}
)
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
│   ├── 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.api
│   └── shorturl.go // main入口定义
├── go.mod
└── go.sum
```
* 启动API Gateway服务默认侦听在8888端口
```shell
go run shorturl.go -f 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生成客户端代码给客户端同学并行开发了支持多种语言详见文档
## 5. 编写transform rpc服务
* 在`rpc/transform`目录下编写`transform.proto`文件
可以通过命令生成proto文件模板
```shell
goctl rpc template -o transform.proto
```
修改后文件内容如下:
```protobuf
syntax = "proto3";
package transform;
message expandReq {
string shorten = 1;
}
message expandResp {
string url = 1;
}
message shortenReq {
string url = 1;
}
message shortenResp {
string shorten = 1;
}
service transformer {
rpc expand(expandReq) returns(expandResp);
rpc shorten(shortenReq) returns(shortenResp);
}
```
* 用`goctl`生成rpc代码在`rpc/transform`目录下执行命令
```shell
goctl rpc proto -src transform.proto
```
文件结构如下:
```
rpc/transform
├── etc
│   └── transform.yaml // 配置文件
├── internal
│   ├── config
│   │   └── config.go // 配置定义
│   ├── logic
│   │   ├── expandlogic.go // expand业务逻辑在这里实现
│   │   └── shortenlogic.go // shorten业务逻辑在这里实现
│   ├── server
│   │   └── transformerserver.go // 调用入口, 不需要修改
│   └── svc
│   └── servicecontext.go // 定义ServiceContext传递依赖
├── pb
│   └── transform.pb.go
├── transform.go // rpc服务main函数
├── transform.proto
└── transformer
├── transformer.go // 提供了外部调用方法,无需修改
├── transformer_mock.go // mock方法测试用
└── types.go // request/response结构体定义
```
直接可以运行,如下:
```shell
$ go run transform.go -f etc/transform.yaml
Starting rpc server at 127.0.0.1:8080...
```
`etc/transform.yaml`文件里可以修改侦听端口等配置
## 6. 修改API Gateway代码调用transform rpc服务
* 修改配置文件`shorturl-api.yaml`,增加如下内容
```yaml
Transform:
Etcd:
Hosts:
- localhost:2379
Key: transform.rpc
```
通过etcd自动去发现可用的transform服务
* 修改`internal/config/config.go`如下增加transform服务依赖
```go
type Config struct {
rest.RestConf
Transform rpcx.RpcClientConf // 手动代码
}
```
* 修改`internal/svc/servicecontext.go`,如下:
```go
type ServiceContext struct {
Config config.Config
Transformer rpcx.Client // 手动代码
}
func NewServiceContext(c config.Config) *ServiceContext {
return &ServiceContext{
Config: c,
Transformer: rpcx.MustNewClient(c.Transform), // 手动代码
}
}
```
通过ServiceContext在不同业务逻辑之间传递依赖
* 修改`internal/logic/expandlogic.go`里的`Expand`方法,如下:
```go
func (l *ExpandLogic) Expand(req types.ExpandReq) (*types.ExpandResp, error) {
// 手动代码开始
trans := transformer.NewTransformer(l.svcCtx.Transformer)
resp, err := trans.Expand(l.ctx, &transformer.ExpandReq{
Shorten: req.Shorten,
})
if err != nil {
return nil, err
}
return &types.ExpandResp{
Url: resp.Url,
}, nil
// 手动代码结束
}
```
通过调用`transformer`的`Expand`方法实现短链恢复到url
* 修改`internal/logic/shortenlogic.go`,如下:
```go
func (l *ShortenLogic) Shorten(req types.ShortenReq) (*types.ShortenResp, error) {
// 手动代码开始
trans := transformer.NewTransformer(l.svcCtx.Transformer)
resp, err := trans.Shorten(l.ctx, &transformer.ShortenReq{
Url: req.Url,
})
if err != nil {
return nil, err
}
return &types.ShortenResp{
Shorten: resp.Shorten,
}, nil
// 手动代码结束
}
```
通过调用`transformer`的`Shorten`方法实现url到短链的变换
至此API Gateway修改完成虽然贴的代码多但是期中修改的是很少的一部分为了方便理解上下文我贴了完整代码接下来处理CRUD+cache
## 7. 定义数据库表结构并生成CRUD+cache代码
* shorturl下创建`rpc/transform/model`目录:`mkdir -p rpc/transform/model`
* 在rpc/transform/model目录下编写创建shorturl表的sql文件`shorturl.sql`,如下:
```sql
CREATE TABLE `shorturl`
(
`shorten` varchar(255) NOT NULL COMMENT 'shorten key',
`url` varchar(255) NOT NULL COMMENT 'original url',
PRIMARY KEY(`shorten`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
```
* 创建DB和table
```sql
create database gozero;
```
```sql
source shorturl.sql;
```
* 在`rpc/transform/model`目录下执行如下命令生成CRUD+cache代码`-c`表示使用`redis cache`
```shell
goctl model mysql ddl -c -src shorturl.sql -dir .
```
也可以用`datasource`命令代替`ddl`来指定数据库链接直接从schema生成
生成后的文件结构如下:
```
rpc/transform/model
├── shorturl.sql
├── shorturlmodel.go // CRUD+cache代码
└── vars.go // 定义常量和变量
```
## 8. 修改shorten/expand rpc代码调用crud+cache代码
* 修改`rpc/transform/etc/transform.yaml`,增加如下内容:
```yaml
DataSource: root:@tcp(localhost:3306)/gozero
Table: shorturl
Cache:
- Host: localhost:6379
```
可以使用多个redis作为cache支持redis单点或者redis集群
* 修改`rpc/transform/internal/config.go`,如下:
```go
type Config struct {
rpcx.RpcServerConf
DataSource string // 手动代码
Table string // 手动代码
Cache cache.CacheConf // 手动代码
}
```
增加了mysql和redis cache配置
* 修改`rpc/transform/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/transform/internal/logic/expandlogic.go`,如下:
```go
func (l *ExpandLogic) Expand(in *expand.ExpandReq) (*expand.ExpandResp, error) {
// 手动代码开始
res, err := l.svcCtx.Model.FindOne(in.Shorten)
if err != nil {
return nil, err
}
return &transform.ExpandResp{
Url: res.Url,
}, nil
// 手动代码结束
}
```
* 修改`rpc/shorten/internal/logic/shortenlogic.go`,如下:
```go
func (l *ShortenLogic) Shorten(in *shorten.ShortenReq) (*shorten.ShortenResp, error) {
// 手动代码开始,生成短链接
key := hash.Md5Hex([]byte(in.Url))[:6]
_, err := l.svcCtx.Model.Insert(model.Shorturl{
Shorten: key,
Url: in.Url,
})
if err != nil {
return nil, err
}
return &transform.ShortenResp{
Shorten: 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
{"shorten":"f35b2a"}
```
* expand api调用
```shell
curl -i "http://localhost:8888/expand?shorten=f35b2a"
```
返回如下:
```http
HTTP/1.1 200 OK
Content-Type: application/json
Date: Sat, 29 Aug 2020 10:51:53 GMT
Content-Length: 34
{"url":"http://www.xiaoheiban.cn"}
```
## 10. Benchmark
因为写入依赖于mysql的写入速度就相当于压mysql了所以压测只测试了expand接口相当于从mysql里读取并利用缓存shorten.lua里随机从db里获取了100个热key来生成压测请求
![Benchmark](images/shorturl-benchmark.png)
可以看出在我的MacBook Pro上能达到3万+的qps。
## 11. 总结
我们一直强调**工具大于约定和文档**。
go-zero不只是一个框架更是一个建立在框架+工具基础上的,简化和规范了整个微服务构建的技术体系。
我们在保持简单的同时也尽可能把微服务治理的复杂度封装到了框架内部,极大的降低了开发人员的心智负担,使得业务开发得以快速推进。
通过go-zero+goctl生成的代码包含了微服务治理的各种组件包括并发控制、自适应熔断、自适应降载、自动缓存控制等可以轻松部署以承载巨大访问量。

View File

@@ -1,13 +0,0 @@
version := v$(shell /bin/date "+%y%m%d%H%M%S")
build:
#docker pull alpine
#docker pull golang:alpine
cd $(GOPATH)/src/zero && docker build -t registry.cn-hangzhou.aliyuncs.com/xapp/etcdmon:$(version) . -f example/etcd/demo/Dockerfile
#docker image prune --filter label=stage=gobuilder -f
push: build
docker push registry.cn-hangzhou.aliyuncs.com/xapp/etcdmon:$(version)
deploy: push
kubectl -n xx-xiaoheiban set image deployment/etcdmon-deployment etcdmon=registry-vpc.cn-hangzhou.aliyuncs.com/xapp/etcdmon:$(version)

View File

@@ -1,15 +0,0 @@
apiVersion: v1
kind: Pod
metadata:
name: etcdmon
namespace: discov
spec:
containers:
- name: etcdmon
image: registry-vpc.cn-hangzhou.aliyuncs.com/xapp/etcdmon:v200620093045
imagePullPolicy: Always
env:
- name: ETCD_CLUSTER
value: etcd.discov:2379
imagePullSecrets:
- name: aliyun

View File

@@ -1,11 +0,0 @@
version := v$(shell /bin/date "+%y%m%d%H%M%S")
build:
cd $(GOPATH)/src/zero && docker build -t registry.cn-hangzhou.aliyuncs.com/xapp/pub:$(version) . -f example/etcd/pub/Dockerfile
docker image prune --filter label=stage=gobuilder -f
push: build
docker push registry.cn-hangzhou.aliyuncs.com/xapp/pub:$(version)
deploy: push
kubectl -n adhoc set image deployment/pub-deployment pub=registry-vpc.cn-hangzhou.aliyuncs.com/xapp/pub:$(version)

View File

@@ -1,26 +0,0 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: pub-deployment
namespace: adhoc
labels:
app: pub
spec:
replicas: 1
selector:
matchLabels:
app: pub
template:
metadata:
labels:
app: pub
spec:
containers:
- name: pub
image: registry-vpc.cn-hangzhou.aliyuncs.com/xapp/pub:v200213172101
command:
- /app/pub
- -v
- ccc
imagePullSecrets:
- name: aliyun

View File

@@ -1,11 +0,0 @@
version := v$(shell /bin/date "+%y%m%d%H%M%S")
build:
cd $(GOPATH)/src/zero && docker build -t registry.cn-hangzhou.aliyuncs.com/xapp/sub:$(version) . -f example/etcd/sub/Dockerfile
docker image prune --filter label=stage=gobuilder -f
push: build
docker push registry.cn-hangzhou.aliyuncs.com/xapp/sub:$(version)
deploy: push
kubectl -n adhoc set image deployment/sub-deployment sub=registry-vpc.cn-hangzhou.aliyuncs.com/xapp/sub:$(version)

View File

@@ -1,16 +0,0 @@
apiVersion: v1
kind: Pod
metadata:
labels:
app: sub
name: sub
namespace: adhoc
spec:
containers:
- command:
- /app/sub
image: registry-vpc.cn-hangzhou.aliyuncs.com/xapp/sub:v200213220509
name: sub
imagePullSecrets:
- name: aliyun
restartPolicy: Always

View File

@@ -1,11 +0,0 @@
version := v$(shell /bin/date "+%y%m%d%H%M%S")
build:
docker pull alpine
cd $(GOPATH)/src/zero && docker build -t registry.cn-hangzhou.aliyuncs.com/xapp/graceful:$(version) . -f example/graceful/dns/api/Dockerfile
push: build
docker push registry.cn-hangzhou.aliyuncs.com/xapp/graceful:$(version)
deploy: push
kubectl -n kevin set image deployment/graceful-deployment graceful=registry-vpc.cn-hangzhou.aliyuncs.com/xapp/graceful:$(version)

View File

@@ -1,42 +0,0 @@
apiVersion: v1
kind: Service
metadata:
name: graceful
namespace: kevin
spec:
selector:
app: graceful
type: ClusterIP
ports:
- name: graceful-port
port: 3333
targetPort: 8888
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: graceful-deployment
namespace: kevin
labels:
app: graceful
spec:
replicas: 3
selector:
matchLabels:
app: graceful
template:
metadata:
labels:
app: graceful
spec:
containers:
- name: graceful
image: registry-vpc.cn-hangzhou.aliyuncs.com/xapp/graceful:v191022133857
imagePullPolicy: Always
ports:
- containerPort: 8888
imagePullSecrets:
- name: aliyun

View File

@@ -1,11 +0,0 @@
version := v$(shell /bin/date "+%y%m%d%H%M%S")
build:
docker pull alpine
cd $(GOPATH)/src/zero && docker build -t registry.cn-hangzhou.aliyuncs.com/xapp/gracefulrpc:$(version) . -f example/graceful/dns/rpc/Dockerfile
push: build
docker push registry.cn-hangzhou.aliyuncs.com/xapp/gracefulrpc:$(version)
deploy: push
kubectl -n kevin set image deployment/gracefulrpc-deployment gracefulrpc=registry-vpc.cn-hangzhou.aliyuncs.com/xapp/gracefulrpc:$(version)

View File

@@ -1,46 +0,0 @@
apiVersion: v1
kind: Service
metadata:
name: gracefulrpc
namespace: kevin
spec:
selector:
app: gracefulrpc
type: ClusterIP
clusterIP: None
ports:
- name: gracefulrpc-port
port: 3456
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: gracefulrpc-deployment
namespace: kevin
labels:
app: gracefulrpc
spec:
replicas: 3
selector:
matchLabels:
app: gracefulrpc
template:
metadata:
labels:
app: gracefulrpc
spec:
containers:
- name: gracefulrpc
image: registry-vpc.cn-hangzhou.aliyuncs.com/xapp/gracefulrpc:v191022143425
imagePullPolicy: Always
ports:
- containerPort: 3456
env:
- name: POD_IP
valueFrom:
fieldRef:
fieldPath: status.podIP
imagePullSecrets:
- name: aliyun

View File

@@ -1,13 +0,0 @@
version := v$(shell /bin/date "+%y%m%d%H%M%S")
build:
docker pull alpine
docker pull golang:alpine
cd $(GOPATH)/src/zero && docker build -t registry.cn-hangzhou.aliyuncs.com/xapp/graceful:$(version) . -f example/graceful/etcd/api/Dockerfile
docker image prune --filter label=stage=gobuilder -f
push: build
docker push registry.cn-hangzhou.aliyuncs.com/xapp/graceful:$(version)
deploy: push
kubectl -n kevin set image deployment/graceful-deployment graceful=registry-vpc.cn-hangzhou.aliyuncs.com/xapp/graceful:$(version)

View File

@@ -1,42 +0,0 @@
apiVersion: v1
kind: Service
metadata:
name: graceful
namespace: kevin
spec:
selector:
app: graceful
type: ClusterIP
ports:
- name: graceful-port
port: 3333
targetPort: 8888
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: graceful-deployment
namespace: kevin
labels:
app: graceful
spec:
replicas: 3
selector:
matchLabels:
app: graceful
template:
metadata:
labels:
app: graceful
spec:
containers:
- name: graceful
image: registry-vpc.cn-hangzhou.aliyuncs.com/xapp/graceful:v191031145905
imagePullPolicy: Always
ports:
- containerPort: 8888
imagePullSecrets:
- name: aliyun

View File

@@ -1,13 +0,0 @@
version := v$(shell /bin/date "+%y%m%d%H%M%S")
build:
docker pull alpine
docker pull golang:alpine
cd $(GOPATH)/src/zero && docker build -t registry.cn-hangzhou.aliyuncs.com/xapp/gracefulrpc:$(version) . -f example/graceful/etcd/rpc/Dockerfile
docker image prune --filter label=stage=gobuilder -f
push: build
docker push registry.cn-hangzhou.aliyuncs.com/xapp/gracefulrpc:$(version)
deploy: push
kubectl -n kevin set image deployment/gracefulrpc-deployment gracefulrpc=registry-vpc.cn-hangzhou.aliyuncs.com/xapp/gracefulrpc:$(version)

View File

@@ -1,30 +0,0 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: gracefulrpc-deployment
namespace: kevin
labels:
app: gracefulrpc
spec:
replicas: 9
selector:
matchLabels:
app: gracefulrpc
template:
metadata:
labels:
app: gracefulrpc
spec:
containers:
- name: gracefulrpc
image: registry-vpc.cn-hangzhou.aliyuncs.com/xapp/gracefulrpc:v191031144304
imagePullPolicy: Always
ports:
- containerPort: 3456
env:
- name: POD_IP
valueFrom:
fieldRef:
fieldPath: status.podIP
imagePullSecrets:
- name: aliyun

View File

@@ -1,41 +0,0 @@
apiVersion: v1
kind: Service
metadata:
name: gracefulrpc
namespace: kevin
spec:
selector:
app: gracefulrpc
type: ClusterIP
clusterIP: None
ports:
- name: gracefulrpc-port
port: 3456
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: gracefulrpc-deployment
namespace: kevin
labels:
app: gracefulrpc
spec:
replicas: 9
selector:
matchLabels:
app: gracefulrpc
template:
metadata:
labels:
app: gracefulrpc
spec:
containers:
- name: gracefulrpc
image: registry-vpc.cn-hangzhou.aliyuncs.com/xapp/gracefulrpc:v191031144304
imagePullPolicy: Always
ports:
- containerPort: 3456
imagePullSecrets:
- name: aliyun

View File

@@ -1,25 +0,0 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: gracefulrpc-deployment
namespace: kevin
labels:
app: gracefulrpc
spec:
replicas: 9
selector:
matchLabels:
app: gracefulrpc
template:
metadata:
labels:
app: gracefulrpc
spec:
containers:
- name: gracefulrpc
image: registry-vpc.cn-hangzhou.aliyuncs.com/xapp/gracefulrpc:v191031144304
imagePullPolicy: Always
ports:
- containerPort: 3456
imagePullSecrets:
- name: aliyun

View File

@@ -1,13 +0,0 @@
version := v1
build:
cd $(GOPATH)/src/zero && docker build -t registry.cn-hangzhou.aliyuncs.com/xapp/shedding:$(version) . -f example/load/simulate/cpu/Dockerfile
push: build
docker push registry.cn-hangzhou.aliyuncs.com/xapp/shedding:$(version)
deploy: push
kubectl apply -f shedding.yaml
clean:
kubectl delete -f shedding.yaml

View File

@@ -1,17 +0,0 @@
apiVersion: v1
kind: Pod
metadata:
name: shedding
namespace: adhoc
spec:
containers:
- name: shedding
image: registry-vpc.cn-hangzhou.aliyuncs.com/xapp/shedding:v1
imagePullPolicy: Always
resources:
requests:
cpu: 1000m
limits:
cpu: 1000m
imagePullSecrets:
- name: aliyun

View File

@@ -1,10 +0,0 @@
version := v$(shell /bin/date "+%y%m%d%H%M%S")
build:
cd $(GOPATH)/src/zero && docker build -t registry.cn-hangzhou.aliyuncs.com/xapp/unarydirect:$(version) . -f example/rpc/client/direct/Dockerfile
push: build
docker push registry.cn-hangzhou.aliyuncs.com/xapp/unarydirect:$(version)
deploy: push
kubectl -n adhoc set image deployment/unarydirect-deployment unarydirect=registry-vpc.cn-hangzhou.aliyuncs.com/xapp/unarydirect:$(version)

View File

@@ -1,23 +0,0 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: unarydirect-deployment
namespace: adhoc
labels:
app: unarydirect
spec:
replicas: 1
selector:
matchLabels:
app: unarydirect
template:
metadata:
labels:
app: unarydirect
spec:
containers:
- name: unarydirect
image: registry-vpc.cn-hangzhou.aliyuncs.com/xapp/unarydirect:v1
imagePullPolicy: Always
imagePullSecrets:
- name: aliyun

View File

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

View File

@@ -1,46 +0,0 @@
apiVersion: v1
kind: Service
metadata:
name: unaryproxy
namespace: kevin
spec:
selector:
app: unaryproxy
ports:
- name: unaryproxy-port
port: 3456
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: unaryproxy-deployment
namespace: kevin
labels:
app: unaryproxy
spec:
replicas: 3
selector:
matchLabels:
app: unaryproxy
template:
metadata:
labels:
app: unaryproxy
spec:
containers:
- name: unaryproxy
image: registry-vpc.cn-hangzhou.aliyuncs.com/xapp/unaryproxy:v1
imagePullPolicy: Always
ports:
- containerPort: 3456
volumeMounts:
- name: timezone
mountPath: /etc/localtime
imagePullSecrets:
- name: aliyun
volumes:
- name: timezone
hostPath:
path: /usr/share/zoneinfo/Asia/Shanghai

View File

@@ -1,11 +0,0 @@
version := v1
build:
cd $(GOPATH)/src/zero && docker build -t registry.cn-hangzhou.aliyuncs.com/xapp/unaryserver:$(version) . -f example/rpc/server/unary/Dockerfile
docker image prune --filter label=stage=gobuilder -f
push: build
docker push registry.cn-hangzhou.aliyuncs.com/xapp/unaryserver:$(version)
deploy: push
kubectl -n adhoc set image deployment/unaryserver-deployment unaryserver=registry-vpc.cn-hangzhou.aliyuncs.com/xapp/unaryserver:$(version)

View File

@@ -1,25 +0,0 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: unaryserver-deployment
namespace: adhoc
labels:
app: unaryserver
spec:
replicas: 3
selector:
matchLabels:
app: unaryserver
template:
metadata:
labels:
app: unaryserver
spec:
containers:
- name: unaryserver
image: registry-vpc.cn-hangzhou.aliyuncs.com/xapp/unaryserver:v1
imagePullPolicy: Always
ports:
- containerPort: 3456
imagePullSecrets:
- name: aliyun

View File

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

3
go.mod
View File

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

2
go.sum
View File

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

View File

@@ -6,6 +6,23 @@
[![Release](https://img.shields.io/github/v/release/tal-tech/go-zero.svg?style=flat-square)](https://github.com/tal-tech/go-zero)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
## 0. go-zero介绍
go-zero是一个集成了各种工程实践的web和rpc框架。通过弹性设计保障了大并发服务端的稳定性经受了充分的实战检验。
go-zero 包含极简的 API 定义和生成工具 goctl可以根据定义的 api 文件一键生成 Go, iOS, Android, Kotlin, Dart, TypeScript, JavaScript 代码,并可直接运行。
使用go-zero的好处
* 轻松获得支撑千万日活服务的稳定性
* 内建级联超时控制、限流、自适应熔断、自适应降载等微服务治理能力,无需配置和额外代码
* 微服务治理中间件可无缝集成到其它现有框架使用
* 极简的API描述一键生成各端代码
* 自动校验客户端请求参数合法性
* 大量微服务治理和并发工具包
<img src="doc/images/architecture.png" alt="架构图" width="1500" />
## 1. go-zero框架背景
18年初晓黑板后端在经过频繁的宕机后决定从`Java+MongoDB`的单体架构迁移到微服务架构,经过仔细思考和对比,我们决定:
@@ -57,47 +74,38 @@ go-zero是一个集成了各种工程实践的包含web和rpc框架有如下
![弹性设计](doc/images/resilience.jpg)
## 4. go-zero框架收益
* 保障大并发服务端的稳定性,经受了充分的实战检验
* 极简的API定义
* 一键生成Go, iOS, Android, Dart, TypeScript, JavaScript代码并可直接运行
* 服务端自动校验参数合法性
## 5. go-zero近期开发计划
## 4. go-zero近期开发计划
* 自动生成API mock server便于客户端开发
* 自动生成服务端功能测试
## 6. Installation
## 5. Installation
1. 在项目目录下通过如下命令安装:
在项目目录下通过如下命令安装:
```shell
go get -u github.com/tal-tech/go-zero
```
## 6. Quick Start
0. 完整示例请查看
[快速构建高并发微服务](doc/shorturl.md)
1. 安装goctl工具
```shell
go get -u github.com/tal-tech/go-zero
export GO111MODULE=on export GOPROXY=https://goproxy.cn/,direct go get github.com/tal-tech/go-zero/tools/goctl
```
2. 代码里导入go-zero
```go
import "github.com/tal-tech/go-zero"
```
## 7. Quick Start
1. 编译goctl工具
```shell
go build tools/goctl/goctl.go
```
把goctl放到$PATH的目录下确保goctl可执行
确保goctl可执行
2. 定义API文件比如greet.api可以在vs code里安装`goctl`插件支持api语法
```go
type Request struct {
Name string `path:"name"`
Name string `path:"name,options=you|me"` // 框架自动验证请求参数是否合法
}
type Response struct {
@@ -129,7 +137,7 @@ go-zero是一个集成了各种工程实践的包含web和rpc框架有如下
```
├── greet
│   ├── etc
│   │   └── greet-api.json // 配置文件
│   │   └── greet-api.yaml // 配置文件
│   ├── greet.go // main文件
│   └── internal
│   ├── config
@@ -146,18 +154,24 @@ go-zero是一个集成了各种工程实践的包含web和rpc框架有如下
└── greet.api // api描述文件
```
生成的代码可以直接运行:
```shell
cd greet
go run greet.go -f etc/greet-api.json
go run greet.go -f etc/greet-api.yaml
```
默认侦听在8888端口可以在配置文件里修改可以通过curl请求
```shell
➜ go-zero git:(master) curl -w "\ncode: %{http_code}\n" http://localhost:8888/greet/from/kevin
{"code":0}
code: 200
curl -i http://localhost:8888/greet/from/you
```
返回如下:
```http
HTTP/1.1 200 OK
Date: Sun, 30 Aug 2020 15:32:35 GMT
Content-Length: 0
```
编写业务代码:
@@ -173,17 +187,18 @@ go-zero是一个集成了各种工程实践的包含web和rpc框架有如下
...
```
## 8. Benchmark
## 7. Benchmark
![benchmark](doc/images/benchmark.png)
[测试代码见这里](https://github.com/smallnest/go-web-framework-benchmark)
## 9. 文档
## 8. 文档 (逐步完善中)
* [快速构建高并发微服务](doc/shorturl.md)
* [goctl使用帮助](doc/goctl.md)
* [关键字替换和敏感词过滤工具](doc/keywords.md)
## 10. 微信交流群
## 9. 微信交流群
添加我的微信kevwan请注明go-zero我拉进go-zero社区群🤝

View File

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

View File

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

101
rpcx/client_test.go Normal file
View File

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

42
rpcx/config_test.go Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

66
rpcx/proxy_test.go Normal file
View File

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

View File

@@ -2,3 +2,9 @@ version := $(shell /bin/date "+%Y-%m-%d %H:%M")
build:
go build -ldflags="-s -w" -ldflags="-X 'main.BuildTime=$(version)'" goctl.go && upx goctl
mac:
GOOS=darwin go build -ldflags="-s -w" -ldflags="-X 'main.BuildTime=$(version)'" -o goctl-darwin goctl.go && upx goctl-darwin
win:
GOOS=windows go build -ldflags="-s -w" -ldflags="-X 'main.BuildTime=$(version)'" -o goctl.exe goctl.go && upx goctl.exe
linux:
GOOS=linux go build -ldflags="-s -w" -ldflags="-X 'main.BuildTime=$(version)'" -o goctl-linux goctl.go && upx goctl-linux

View File

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

View File

@@ -98,6 +98,7 @@ func ApiFormat(path string, printToConsole bool) error {
_, err := fmt.Print(result)
return err
}
result = strings.TrimSpace(result)
return ioutil.WriteFile(path, []byte(result), os.ModePerm)
}

View File

@@ -140,35 +140,27 @@ 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 {
return
}
if !hasGoMod {
gopath := os.Getenv("GOPATH")
parent := path.Join(gopath, "src")
pos := strings.Index(absDir, parent)
if pos < 0 {
moduleName := absDir[len(filepath.Dir(absDir))+1:]
cmd := exec.Command("go", "mod", "init", moduleName)
cmd.Dir = dir
var stdout, stderr bytes.Buffer
cmd.Stdout = &stdout
cmd.Stderr = &stderr
err := cmd.Run()
if err != nil {
fmt.Println(err.Error())
}
outStr, errStr := string(stdout.Bytes()), string(stderr.Bytes())
fmt.Printf(outStr + "\n" + errStr)
}
gopath := os.Getenv("GOPATH")
parent := path.Join(gopath, "src")
pos := strings.Index(absDir, parent)
if pos >= 0 {
return
}
moduleName := absDir[len(filepath.Dir(absDir))+1:]
cmd := exec.Command("go", "mod", "init", moduleName)
cmd.Dir = dir
var stdout, stderr bytes.Buffer
cmd.Stdout = &stdout
cmd.Stderr = &stderr
if err = cmd.Run(); err != nil {
fmt.Println(err.Error())
}
outStr, errStr := string(stdout.Bytes()), string(stderr.Bytes())
fmt.Printf(outStr + "\n" + errStr)
}

View File

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

View File

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

View File

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

View File

@@ -20,19 +20,22 @@ import (
)
type {{.logic}} struct {
ctx context.Context
ctx context.Context
svcCtx *svc.ServiceContext
logx.Logger
}
func New{{.logic}}(ctx context.Context, svcCtx *svc.ServiceContext) {{.logic}} {
return {{.logic}}{
ctx: ctx,
svcCtx: svcCtx,
Logger: logx.WithContext(ctx),
}
// TODO need set model here from svc
}
func (l *{{.logic}}) {{.function}}({{.request}}) {{.responseType}} {
// todo: add your logic here and delete this line
{{.returnString}}
}
`
@@ -77,8 +80,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 +102,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 +124,11 @@ func getLogicFolderPath(group spec.Group, route spec.Route) string {
func genLogicImports(route spec.Route, parentPkg string) string {
var imports []string
imports = append(imports, `"context"`)
imports = append(imports, "\n")
imports = append(imports, fmt.Sprintf("\"%s/core/logx\"", vars.ProjectOpenSourceUrl))
if len(route.ResponseType.Name) > 0 || len(route.RequestType.Name) > 0 {
imports = append(imports, fmt.Sprintf("\"%s\"", ctlutil.JoinPackages(parentPkg, typesDir)))
}
imports = append(imports, `"context"`+"\n")
imports = append(imports, fmt.Sprintf("\"%s\"", ctlutil.JoinPackages(parentPkg, contextDir)))
if len(route.ResponseType.Name) > 0 || len(route.RequestType.Name) > 0 {
imports = append(imports, fmt.Sprintf("\"%s\"\n", ctlutil.JoinPackages(parentPkg, typesDir)))
}
imports = append(imports, fmt.Sprintf("\"%s/core/logx\"", vars.ProjectOpenSourceUrl))
return strings.Join(imports, "\n\t")
}

View File

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

View File

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

View File

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

View File

@@ -15,8 +15,6 @@ import (
goctlutil "github.com/tal-tech/go-zero/tools/goctl/util"
)
const goModeIdentifier = "go.mod"
func getParentPackage(dir string) (string, error) {
absDir, err := filepath.Abs(dir)
if err != nil {
@@ -24,36 +22,22 @@ func getParentPackage(dir string) (string, error) {
}
absDir = strings.ReplaceAll(absDir, `\`, `/`)
var rootPath string
var tempPath = absDir
var hasGoMod = false
for {
if tempPath == filepath.Dir(tempPath) {
break
}
tempPath = filepath.Dir(tempPath)
if goctlutil.FileExists(filepath.Join(tempPath, goModeIdentifier)) {
tempPath = filepath.Dir(tempPath)
rootPath = absDir[len(tempPath)+1:]
hasGoMod = true
break
}
if tempPath == string(filepath.Separator) {
break
}
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)
tempPath := filepath.Dir(absDir)
rootPath = absDir[len(tempPath)+1:]
} else {
rootPath = absDir[len(parent)+1:]
}
return rootPath, nil
}
@@ -77,7 +61,7 @@ func writeProperty(writer io.Writer, name, tp, tag, comment string, indent int)
}
func getAuths(api *spec.ApiSpec) []string {
var authNames = collection.NewSet()
authNames := collection.NewSet()
for _, g := range api.Service.Groups {
if value, ok := util.GetAnnotationValue(g.Annotations, "server", "jwt"); ok {
authNames.Add(value)
@@ -94,5 +78,6 @@ func formatCode(code string) string {
if err != nil {
return code
}
return string(ret)
}

View File

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

View File

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

View File

@@ -9,14 +9,9 @@ import (
func DockerCommand(c *cli.Context) error {
goFile := c.String("go")
namespace := c.String("namespace")
if len(goFile) == 0 || len(namespace) == 0 {
return errors.New("-go and -namespace can't be empty")
if len(goFile) == 0 {
return errors.New("-go can't be empty")
}
if err := gen.GenerateDockerfile(goFile, "-f", "etc/config.json"); err != nil {
return err
}
return gen.GenerateMakefile(goFile, namespace)
return gen.GenerateDockerfile(goFile, "-f", "etc/config.yaml")
}

View File

@@ -1,6 +1,7 @@
package gen
import (
"path/filepath"
"strings"
"text/template"
@@ -9,11 +10,16 @@ import (
)
func GenerateDockerfile(goFile string, args ...string) error {
relPath, err := util.PathFromGoSrc()
projPath, err := getFilePath(filepath.Dir(goFile))
if err != nil {
return err
}
pos := strings.IndexByte(projPath, '/')
if pos >= 0 {
projPath = projPath[pos+1:]
}
out, err := util.CreateIfNotExist("Dockerfile")
if err != nil {
return err
@@ -28,7 +34,7 @@ func GenerateDockerfile(goFile string, args ...string) error {
t := template.Must(template.New("dockerfile").Parse(dockerTemplate))
return t.Execute(out, map[string]string{
"projectName": vars.ProjectName,
"goRelPath": relPath,
"goRelPath": projPath,
"goFile": goFile,
"exeFile": util.FileNameWithoutExt(goFile),
"argument": builder.String(),

View File

@@ -0,0 +1,26 @@
package gen
import (
"errors"
"os"
"path/filepath"
"github.com/tal-tech/go-zero/tools/goctl/util"
)
func getFilePath(file string) (string, error) {
wd, err := os.Getwd()
if err != nil {
return "", err
}
projPath, ok := util.FindGoModPath(filepath.Join(wd, file))
if !ok {
projPath, err = util.PathFromGoSrc()
if err != nil {
return "", errors.New("no go.mod found, or not in GOPATH")
}
}
return projPath, nil
}

View File

@@ -1,52 +0,0 @@
package gen
import (
"strings"
"text/template"
"github.com/tal-tech/go-zero/tools/goctl/util"
)
func GenerateMakefile(goFile, namespace string) error {
relPath, err := util.PathFromGoSrc()
if err != nil {
return err
}
movePath, err := getMovePath()
if err != nil {
return err
}
out, err := util.CreateIfNotExist("Makefile")
if err != nil {
return err
}
defer out.Close()
t := template.Must(template.New("makefile").Parse(makefileTemplate))
return t.Execute(out, map[string]string{
"rootRelPath": movePath,
"relPath": relPath,
"exeFile": util.FileNameWithoutExt(goFile),
"namespace": namespace,
})
}
func getMovePath() (string, error) {
relPath, err := util.PathFromGoSrc()
if err != nil {
return "", err
}
var builder strings.Builder
for range strings.Split(relPath, "/") {
builder.WriteString("../")
}
if move := builder.String(); len(move) == 0 {
return ".", nil
} else {
return move, nil
}
}

View File

@@ -1,7 +1,6 @@
package gen
const (
dockerTemplate = `FROM golang:alpine AS builder
const dockerTemplate = `FROM golang:alpine AS builder
LABEL stage=gobuilder
@@ -26,19 +25,3 @@ COPY --from=builder /app/{{.exeFile}} /app/{{.exeFile}}
CMD ["./{{.exeFile}}"{{.argument}}]
`
makefileTemplate = `version := v$(shell /bin/date "+%y%m%d%H%M%S")
build:
docker pull alpine
docker pull golang:alpine
cd $(GOPATH)/src/xiao && docker build -t registry.cn-hangzhou.aliyuncs.com/xapp/{{.exeFile}}:$(version) . -f {{.relPath}}/Dockerfile
docker image prune --filter label=stage=gobuilder -f
push: build
docker push registry.cn-hangzhou.aliyuncs.com/xapp/{{.exeFile}}:$(version)
deploy: push
kubectl -n {{.namespace}} set image deployment/{{.exeFile}}-deployment {{.exeFile}}=registry-vpc.cn-hangzhou.aliyuncs.com/xapp/{{.exeFile}}:$(version)
`
)

View File

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

View File

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

View File

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

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