mirror of
https://github.com/zeromicro/go-zero.git
synced 2026-05-13 01:40:00 +08:00
Compare commits
64 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
89f3712347 | ||
|
|
af7acdd843 | ||
|
|
7ffa3349a9 | ||
|
|
f03862c378 | ||
|
|
fe3e70a60f | ||
|
|
36174ba5cc | ||
|
|
7b17b3604a | ||
|
|
eb40c2731d | ||
|
|
618bec5075 | ||
|
|
5821b7324e | ||
|
|
befdaab542 | ||
|
|
431be8ed9d | ||
|
|
3c688c319e | ||
|
|
59ffa75c00 | ||
|
|
09340e82a7 | ||
|
|
6c4a4be5d2 | ||
|
|
6e3d99e869 | ||
|
|
0f97b2019a | ||
|
|
0cf4ed46a1 | ||
|
|
3affe62ae4 | ||
|
|
0734bbcab3 | ||
|
|
f411178a4f | ||
|
|
72132ce399 | ||
|
|
db16115037 | ||
|
|
71bbf91a63 | ||
|
|
69ccc61cfe | ||
|
|
a94cf653f0 | ||
|
|
77e23ad65d | ||
|
|
38806e7237 | ||
|
|
a987d12237 | ||
|
|
33208e6ef6 | ||
|
|
5d8a3c07cd | ||
|
|
1c24e71568 | ||
|
|
229544f3ca | ||
|
|
c575fa7f95 | ||
|
|
fe2252184a | ||
|
|
1a8014c704 | ||
|
|
30e52707ae | ||
|
|
73b61e09ed | ||
|
|
9b8595a85e | ||
|
|
015e284515 | ||
|
|
456b395860 | ||
|
|
f3c367a323 | ||
|
|
a32028c4fb | ||
|
|
b4572fa064 | ||
|
|
ccbabf6f58 | ||
|
|
5989444227 | ||
|
|
dc286a03f5 | ||
|
|
b82c02ed16 | ||
|
|
59ba4ecc5b | ||
|
|
5e7b514ae2 | ||
|
|
2b1466e41e | ||
|
|
9c9f80518f | ||
|
|
25973d6b59 | ||
|
|
6237d01948 | ||
|
|
49316b113e | ||
|
|
6a673e8cb0 | ||
|
|
0efa28ddbd | ||
|
|
0b6a13fe84 | ||
|
|
11aa6668e8 | ||
|
|
267a283328 | ||
|
|
2d8366b30e | ||
|
|
db83843558 | ||
|
|
50565c9765 |
@@ -1,3 +1,4 @@
|
||||
ignore:
|
||||
- "example/*"
|
||||
- "tools/*"
|
||||
- "doc"
|
||||
- "example"
|
||||
- "tools"
|
||||
@@ -1,36 +0,0 @@
|
||||
run:
|
||||
# concurrency: 6
|
||||
timeout: 5m
|
||||
skip-dirs:
|
||||
- core
|
||||
- doc
|
||||
- example
|
||||
- rest
|
||||
- rpcx
|
||||
- tools
|
||||
|
||||
|
||||
linters:
|
||||
disable-all: true
|
||||
enable:
|
||||
- bodyclose
|
||||
- deadcode
|
||||
- errcheck
|
||||
- gosimple
|
||||
- govet
|
||||
- ineffassign
|
||||
- staticcheck
|
||||
- structcheck
|
||||
- typecheck
|
||||
- unused
|
||||
- varcheck
|
||||
# - dupl
|
||||
|
||||
|
||||
linters-settings:
|
||||
|
||||
issues:
|
||||
exclude-rules:
|
||||
- linters:
|
||||
- staticcheck
|
||||
text: 'SA1019: (baseresponse.BoolResponse|oldresponse.FormatBadRequestResponse|oldresponse.FormatResponse)|SA5008: unknown JSON option ("optional"|"default=|"range=|"options=)'
|
||||
@@ -213,7 +213,10 @@ func TestTimingWheel_SetTimer(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
test := test
|
||||
t.Run(stringx.RandId(), func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var count int32
|
||||
ticker := timex.NewFakeTicker()
|
||||
tick := func() {
|
||||
@@ -291,7 +294,10 @@ func TestTimingWheel_SetAndMoveThenStart(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
test := test
|
||||
t.Run(stringx.RandId(), func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var count int32
|
||||
ticker := timex.NewFakeTicker()
|
||||
tick := func() {
|
||||
@@ -376,7 +382,10 @@ func TestTimingWheel_SetAndMoveTwice(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
test := test
|
||||
t.Run(stringx.RandId(), func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var count int32
|
||||
ticker := timex.NewFakeTicker()
|
||||
tick := func() {
|
||||
@@ -454,7 +463,10 @@ func TestTimingWheel_ElapsedAndSet(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
test := test
|
||||
t.Run(stringx.RandId(), func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var count int32
|
||||
ticker := timex.NewFakeTicker()
|
||||
tick := func() {
|
||||
@@ -542,7 +554,10 @@ func TestTimingWheel_ElapsedAndSetThenMove(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
test := test
|
||||
t.Run(stringx.RandId(), func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var count int32
|
||||
ticker := timex.NewFakeTicker()
|
||||
tick := func() {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: discovery
|
||||
name: discov
|
||||
@@ -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
|
||||
@@ -4,9 +4,8 @@ import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/tal-tech/go-zero/core/fs"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/tal-tech/go-zero/core/fs"
|
||||
)
|
||||
|
||||
const (
|
||||
|
||||
@@ -213,6 +213,7 @@ func Infof(format string, v ...interface{}) {
|
||||
func Must(err error) {
|
||||
if err != nil {
|
||||
msg := formatWithCaller(err.Error(), 3)
|
||||
log.Print(msg)
|
||||
output(severeLog, levelFatal, msg)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
@@ -22,19 +22,30 @@ var (
|
||||
cores uint64
|
||||
)
|
||||
|
||||
// if /proc not present, ignore the cpu calcuation, like wsl linux
|
||||
func init() {
|
||||
cpus, err := perCpuUsage()
|
||||
logx.Must(err)
|
||||
cores = uint64(len(cpus))
|
||||
if err != nil {
|
||||
logx.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
cores = uint64(len(cpus))
|
||||
sets, err := cpuSets()
|
||||
logx.Must(err)
|
||||
if err != nil {
|
||||
logx.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
quota = float64(len(sets))
|
||||
cq, err := cpuQuota()
|
||||
if err == nil {
|
||||
if cq != -1 {
|
||||
period, err := cpuPeriod()
|
||||
logx.Must(err)
|
||||
if err != nil {
|
||||
logx.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
limit := float64(cq) / float64(period)
|
||||
if limit < quota {
|
||||
@@ -44,10 +55,16 @@ func init() {
|
||||
}
|
||||
|
||||
preSystem, err = systemCpuUsage()
|
||||
logx.Must(err)
|
||||
if err != nil {
|
||||
logx.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
preTotal, err = totalCpuUsage()
|
||||
logx.Must(err)
|
||||
if err != nil {
|
||||
logx.Error(err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func RefreshCpu() uint64 {
|
||||
|
||||
@@ -195,6 +195,13 @@ ts需要指定webapi所在目录
|
||||
goctl api dart -api user/user.api -dir ./src
|
||||
```
|
||||
|
||||
## 根据mysql ddl或者datasource生成model文件
|
||||
|
||||
```shell script
|
||||
$ goctl model mysql -src={filename} -dir={dir} -cache={true|false}
|
||||
```
|
||||
详情参考[model文档](https://github.com/tal-tech/go-zero/blob/master/tools/goctl/model/sql/README.MD)
|
||||
|
||||
## 根据定义好的简单go文件生成mongo代码文件(仅限golang使用)
|
||||
```shell
|
||||
goctl model mongo -src {{yourDir}}/xiao/service/xhb/user/model/usermodel.go -cache yes
|
||||
@@ -218,7 +225,7 @@ type User struct {
|
||||
o是改字段需要生产的操作函数 可以取得get,find,set 分别表示生成返回单个对象的查询方法,返回多个对象的查询方法,设置该字段方法
|
||||
生成的目标文件会覆盖该简单go文件
|
||||
|
||||
## goctl rpc生成
|
||||
## goctl rpc生成(业务剥离中,暂未开放)
|
||||
|
||||
命令 `goctl rpc proto -proto ${proto} -service ${serviceName} -project ${projectName} -dir ${directory} -shared ${shared}`
|
||||
如: `goctl rpc proto -proto test.proto -service test -project xjy -dir .`
|
||||
|
||||
BIN
doc/images/architecture.png
Normal file
BIN
doc/images/architecture.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 333 KiB |
BIN
doc/images/shorturl-arch.png
Normal file
BIN
doc/images/shorturl-arch.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 126 KiB |
BIN
doc/images/shorturl-benchmark.png
Normal file
BIN
doc/images/shorturl-benchmark.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 111 KiB |
@@ -10,23 +10,28 @@
|
||||
|
||||
## 2. 关键词替换
|
||||
|
||||
支持关键词重叠,自动选用最长的关键词,代码示例如下:
|
||||
|
||||
```go
|
||||
replacer := stringx.NewReplacer(map[string]string{
|
||||
"PHP": "PPT",
|
||||
"世界上": "吹牛",
|
||||
"日本": "法国",
|
||||
"日本的首都": "东京",
|
||||
"东京": "日本的首都",
|
||||
})
|
||||
fmt.Println(replacer.Replace("PHP是世界上最好的语言!"))
|
||||
fmt.Println(replacer.Replace("日本的首都是东京"))
|
||||
```
|
||||
|
||||
可以得到:
|
||||
```
|
||||
PPT是吹牛最好的语言!
|
||||
东京是日本的首都
|
||||
```
|
||||
|
||||
示例代码见`example/stringx/replace/replace.go`
|
||||
|
||||
## 3. 查找敏感词
|
||||
|
||||
代码示例如下:
|
||||
|
||||
```go
|
||||
filter := stringx.NewTrie([]string{
|
||||
"AV演员",
|
||||
@@ -47,6 +52,8 @@ fmt.Println(keywords)
|
||||
|
||||
## 4. 敏感词过滤
|
||||
|
||||
代码示例如下:
|
||||
|
||||
```go
|
||||
filter := stringx.NewTrie([]string{
|
||||
"AV演员",
|
||||
|
||||
507
doc/shorturl.md
Normal file
507
doc/shorturl.md
Normal 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来生成压测请求
|
||||
|
||||

|
||||
|
||||
可以看出在我的MacBook Pro上能达到3万+的qps。
|
||||
|
||||
## 11. 总结
|
||||
|
||||
我们一直强调**工具大于约定和文档**。
|
||||
|
||||
go-zero不只是一个框架,更是一个建立在框架+工具基础上的,简化和规范了整个微服务构建的技术体系。
|
||||
|
||||
我们在保持简单的同时也尽可能把微服务治理的复杂度封装到了框架内部,极大的降低了开发人员的心智负担,使得业务开发得以快速推进。
|
||||
|
||||
通过go-zero+goctl生成的代码,包含了微服务治理的各种组件,包括:并发控制、自适应熔断、自适应降载、自动缓存控制等,可以轻松部署以承载巨大访问量。
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -8,8 +8,9 @@ import (
|
||||
|
||||
func main() {
|
||||
replacer := stringx.NewReplacer(map[string]string{
|
||||
"PHP": "PPT",
|
||||
"世界上": "吹牛",
|
||||
"日本": "法国",
|
||||
"日本的首都": "东京",
|
||||
"东京": "日本的首都",
|
||||
})
|
||||
fmt.Println(replacer.Replace("PHP是世界上最好的语言!"))
|
||||
fmt.Println(replacer.Replace("日本的首都是东京"))
|
||||
}
|
||||
|
||||
3
go.mod
3
go.mod
@@ -8,6 +8,7 @@ require (
|
||||
github.com/alicebob/miniredis v2.5.0+incompatible
|
||||
github.com/dchest/siphash v1.2.1
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible
|
||||
github.com/dsymonds/gotoc v0.0.0-20160928043926-5aebcfc91819
|
||||
github.com/fatih/color v1.9.0 // indirect
|
||||
github.com/frankban/quicktest v1.7.2 // indirect
|
||||
github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8
|
||||
@@ -55,7 +56,7 @@ require (
|
||||
golang.org/x/tools v0.0.0-20200410132612-ae9902aceb98 // indirect
|
||||
google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f // indirect
|
||||
google.golang.org/grpc v1.29.1
|
||||
google.golang.org/protobuf v1.25.0 // indirect
|
||||
google.golang.org/protobuf v1.25.0
|
||||
gopkg.in/cheggaaa/pb.v1 v1.0.28
|
||||
gopkg.in/yaml.v2 v2.2.8
|
||||
honnef.co/go/tools v0.0.1-2020.1.4 // indirect
|
||||
|
||||
2
go.sum
2
go.sum
@@ -48,6 +48,8 @@ github.com/dchest/siphash v1.2.1 h1:4cLinnzVJDKxTCl9B01807Yiy+W7ZzVHj/KIroQRvT4=
|
||||
github.com/dchest/siphash v1.2.1/go.mod h1:q+IRvb2gOSrUnYoPqHiyHXS0FOBBOdl6tONBlVnOnt4=
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
|
||||
github.com/dsymonds/gotoc v0.0.0-20160928043926-5aebcfc91819 h1:9778zj477h/VauD8kHbOtbytW2KGQefJ/wUGE5w+mzw=
|
||||
github.com/dsymonds/gotoc v0.0.0-20160928043926-5aebcfc91819/go.mod h1:MvzMVHq8BH2Ji/o8TGDocVA70byvLrAgFTxkEnmjO4Y=
|
||||
github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4 h1:qk/FSDDxo05wdJH28W+p5yivv7LuLYLRXPPD8KQCtZs=
|
||||
github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
|
||||
87
readme.md
87
readme.md
@@ -6,6 +6,23 @@
|
||||
[](https://github.com/tal-tech/go-zero)
|
||||
[](https://opensource.org/licenses/MIT)
|
||||
|
||||
## 0. go-zero介绍
|
||||
|
||||
go-zero是一个集成了各种工程实践的web和rpc框架。通过弹性设计保障了大并发服务端的稳定性,经受了充分的实战检验。
|
||||
|
||||
go-zero 包含极简的 API 定义和生成工具 goctl,可以根据定义的 api 文件一键生成 Go, iOS, Android, Kotlin, Dart, TypeScript, JavaScript 代码,并可直接运行。
|
||||
|
||||
使用go-zero的好处:
|
||||
|
||||
* 轻松获得支撑千万日活服务的稳定性
|
||||
* 内建级联超时控制、限流、自适应熔断、自适应降载等微服务治理能力,无需配置和额外代码
|
||||
* 微服务治理中间件可无缝集成到其它现有框架使用
|
||||
* 极简的API描述,一键生成各端代码
|
||||
* 自动校验客户端请求参数合法性
|
||||
* 大量微服务治理和并发工具包
|
||||
|
||||
<img src="doc/images/architecture.png" alt="架构图" width="1500" />
|
||||
|
||||
## 1. go-zero框架背景
|
||||
|
||||
18年初,晓黑板后端在经过频繁的宕机后,决定从`Java+MongoDB`的单体架构迁移到微服务架构,经过仔细思考和对比,我们决定:
|
||||
@@ -57,47 +74,38 @@ go-zero是一个集成了各种工程实践的包含web和rpc框架,有如下
|
||||
|
||||

|
||||
|
||||
## 4. go-zero框架收益
|
||||
|
||||
* 保障大并发服务端的稳定性,经受了充分的实战检验
|
||||
* 极简的API定义
|
||||
* 一键生成Go, iOS, Android, Dart, TypeScript, JavaScript代码,并可直接运行
|
||||
* 服务端自动校验参数合法性
|
||||
|
||||
## 5. go-zero近期开发计划
|
||||
## 4. go-zero近期开发计划
|
||||
|
||||
* 自动生成API mock server,便于客户端开发
|
||||
* 自动生成服务端功能测试
|
||||
|
||||
## 6. Installation
|
||||
## 5. Installation
|
||||
|
||||
1. 在项目目录下通过如下命令安装:
|
||||
在项目目录下通过如下命令安装:
|
||||
|
||||
```shell
|
||||
go get -u github.com/tal-tech/go-zero
|
||||
```
|
||||
|
||||
## 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
|
||||
|
||||

|
||||
|
||||
[测试代码见这里](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社区群🤝
|
||||
|
||||
@@ -209,6 +209,6 @@ func (s *engine) use(middleware Middleware) {
|
||||
|
||||
func convertMiddleware(ware Middleware) func(http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(ware(next.ServeHTTP))
|
||||
return ware(next.ServeHTTP)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package httpx
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
@@ -17,6 +18,24 @@ func init() {
|
||||
logx.Disable()
|
||||
}
|
||||
|
||||
func TestError(t *testing.T) {
|
||||
const body = "foo"
|
||||
w := tracedResponseWriter{
|
||||
headers: make(map[string][]string),
|
||||
}
|
||||
Error(&w, errors.New(body))
|
||||
assert.Equal(t, http.StatusBadRequest, w.code)
|
||||
assert.Equal(t, body, strings.TrimSpace(w.builder.String()))
|
||||
}
|
||||
|
||||
func TestOk(t *testing.T) {
|
||||
w := tracedResponseWriter{
|
||||
headers: make(map[string][]string),
|
||||
}
|
||||
Ok(&w)
|
||||
assert.Equal(t, http.StatusOK, w.code)
|
||||
}
|
||||
|
||||
func TestOkJson(t *testing.T) {
|
||||
w := tracedResponseWriter{
|
||||
headers: make(map[string][]string),
|
||||
|
||||
101
rpcx/client_test.go
Normal file
101
rpcx/client_test.go
Normal file
@@ -0,0 +1,101 @@
|
||||
package rpcx
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/tal-tech/go-zero/core/logx"
|
||||
"github.com/tal-tech/go-zero/rpcx/internal/mock"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
"google.golang.org/grpc/test/bufconn"
|
||||
)
|
||||
|
||||
func init() {
|
||||
logx.Disable()
|
||||
}
|
||||
|
||||
func dialer() func(context.Context, string) (net.Conn, error) {
|
||||
listener := bufconn.Listen(1024 * 1024)
|
||||
server := grpc.NewServer()
|
||||
mock.RegisterDepositServiceServer(server, &mock.DepositServer{})
|
||||
|
||||
go func() {
|
||||
if err := server.Serve(listener); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}()
|
||||
|
||||
return func(context.Context, string) (net.Conn, error) {
|
||||
return listener.Dial()
|
||||
}
|
||||
}
|
||||
|
||||
func TestDepositServer_Deposit(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
amount float32
|
||||
res *mock.DepositResponse
|
||||
errCode codes.Code
|
||||
errMsg string
|
||||
}{
|
||||
{
|
||||
"invalid request with negative amount",
|
||||
-1.11,
|
||||
nil,
|
||||
codes.InvalidArgument,
|
||||
fmt.Sprintf("cannot deposit %v", -1.11),
|
||||
},
|
||||
{
|
||||
"valid request with non negative amount",
|
||||
0.00,
|
||||
&mock.DepositResponse{Ok: true},
|
||||
codes.OK,
|
||||
"",
|
||||
},
|
||||
}
|
||||
|
||||
directClient := MustNewClient(RpcClientConf{
|
||||
Endpoints: []string{"foo"},
|
||||
App: "foo",
|
||||
Token: "bar",
|
||||
Timeout: 1000,
|
||||
}, WithDialOption(grpc.WithInsecure()), WithDialOption(grpc.WithContextDialer(dialer())))
|
||||
targetClient, err := NewClientWithTarget("foo", WithDialOption(grpc.WithInsecure()),
|
||||
WithDialOption(grpc.WithContextDialer(dialer())))
|
||||
assert.Nil(t, err)
|
||||
clients := []Client{
|
||||
directClient,
|
||||
targetClient,
|
||||
}
|
||||
for _, tt := range tests {
|
||||
for _, client := range clients {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
cli := mock.NewDepositServiceClient(client.Conn())
|
||||
request := &mock.DepositRequest{Amount: tt.amount}
|
||||
response, err := cli.Deposit(context.Background(), request)
|
||||
if response != nil {
|
||||
assert.True(t, len(response.String()) > 0)
|
||||
if response.GetOk() != tt.res.GetOk() {
|
||||
t.Error("response: expected", tt.res.GetOk(), "received", response.GetOk())
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
if e, ok := status.FromError(err); ok {
|
||||
if e.Code() != tt.errCode {
|
||||
t.Error("error code: expected", codes.InvalidArgument, "received", e.Code())
|
||||
}
|
||||
if e.Message() != tt.errMsg {
|
||||
t.Error("error message: expected", tt.errMsg, "received", e.Message())
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
42
rpcx/config_test.go
Normal file
42
rpcx/config_test.go
Normal file
@@ -0,0 +1,42 @@
|
||||
package rpcx
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/tal-tech/go-zero/core/discov"
|
||||
"github.com/tal-tech/go-zero/core/service"
|
||||
"github.com/tal-tech/go-zero/core/stores/redis"
|
||||
)
|
||||
|
||||
func TestRpcClientConf(t *testing.T) {
|
||||
conf := NewDirectClientConf([]string{"localhost:1234"}, "foo", "bar")
|
||||
assert.True(t, conf.HasCredential())
|
||||
conf = NewEtcdClientConf([]string{"localhost:1234", "localhost:5678"}, "key", "foo", "bar")
|
||||
assert.True(t, conf.HasCredential())
|
||||
}
|
||||
|
||||
func TestRpcServerConf(t *testing.T) {
|
||||
conf := RpcServerConf{
|
||||
ServiceConf: service.ServiceConf{},
|
||||
ListenOn: "",
|
||||
Etcd: discov.EtcdConf{
|
||||
Hosts: []string{"localhost:1234"},
|
||||
Key: "key",
|
||||
},
|
||||
Auth: true,
|
||||
Redis: redis.RedisKeyConf{
|
||||
RedisConf: redis.RedisConf{
|
||||
Type: redis.NodeType,
|
||||
},
|
||||
Key: "foo",
|
||||
},
|
||||
StrictControl: false,
|
||||
Timeout: 0,
|
||||
CpuThreshold: 0,
|
||||
}
|
||||
assert.True(t, conf.HasEtcd())
|
||||
assert.NotNil(t, conf.Validate())
|
||||
conf.Redis.Host = "localhost:5678"
|
||||
assert.Nil(t, conf.Validate())
|
||||
}
|
||||
62
rpcx/internal/auth/credential_test.go
Normal file
62
rpcx/internal/auth/credential_test.go
Normal file
@@ -0,0 +1,62 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"google.golang.org/grpc/metadata"
|
||||
)
|
||||
|
||||
func TestParseCredential(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
withNil bool
|
||||
withEmptyMd bool
|
||||
app string
|
||||
token string
|
||||
}{
|
||||
{
|
||||
name: "nil",
|
||||
withNil: true,
|
||||
},
|
||||
{
|
||||
name: "empty md",
|
||||
withEmptyMd: true,
|
||||
},
|
||||
{
|
||||
name: "empty",
|
||||
},
|
||||
{
|
||||
name: "valid",
|
||||
app: "foo",
|
||||
token: "bar",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
test := test
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var ctx context.Context
|
||||
if test.withNil {
|
||||
ctx = context.Background()
|
||||
} else if test.withEmptyMd {
|
||||
ctx = metadata.NewIncomingContext(context.Background(), metadata.MD{})
|
||||
} else {
|
||||
md := metadata.New(map[string]string{
|
||||
"app": test.app,
|
||||
"token": test.token,
|
||||
})
|
||||
ctx = metadata.NewIncomingContext(context.Background(), md)
|
||||
}
|
||||
cred := ParseCredential(ctx)
|
||||
assert.False(t, cred.RequireTransportSecurity())
|
||||
m, err := cred.GetRequestMetadata(context.Background())
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, test.app, m[appKey])
|
||||
assert.Equal(t, test.token, m[tokenKey])
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,9 @@ package p2c
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
@@ -33,19 +35,31 @@ func TestP2cPicker_Pick(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
candidates int
|
||||
threshold float64
|
||||
}{
|
||||
{
|
||||
name: "single",
|
||||
candidates: 1,
|
||||
threshold: 0.9,
|
||||
},
|
||||
{
|
||||
name: "two",
|
||||
candidates: 2,
|
||||
threshold: 0.5,
|
||||
},
|
||||
{
|
||||
name: "multiple",
|
||||
candidates: 100,
|
||||
threshold: 0.95,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
test := test
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
const total = 10000
|
||||
builder := new(p2cPickerBuilder)
|
||||
ready := make(map[resolver.Address]balancer.SubConn)
|
||||
for i := 0; i < test.candidates; i++ {
|
||||
@@ -55,7 +69,9 @@ func TestP2cPicker_Pick(t *testing.T) {
|
||||
}
|
||||
|
||||
picker := builder.Build(ready)
|
||||
for i := 0; i < 10000; i++ {
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(total)
|
||||
for i := 0; i < total; i++ {
|
||||
_, done, err := picker.Pick(context.Background(), balancer.PickInfo{
|
||||
FullMethodName: "/",
|
||||
Ctx: context.Background(),
|
||||
@@ -64,11 +80,16 @@ func TestP2cPicker_Pick(t *testing.T) {
|
||||
if i%100 == 0 {
|
||||
err = status.Error(codes.DeadlineExceeded, "deadline")
|
||||
}
|
||||
done(balancer.DoneInfo{
|
||||
Err: err,
|
||||
})
|
||||
go func() {
|
||||
runtime.Gosched()
|
||||
done(balancer.DoneInfo{
|
||||
Err: err,
|
||||
})
|
||||
wg.Done()
|
||||
}()
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
dist := make(map[interface{}]int)
|
||||
conns := picker.(*p2cPicker).conns
|
||||
for _, conn := range conns {
|
||||
@@ -76,7 +97,8 @@ func TestP2cPicker_Pick(t *testing.T) {
|
||||
}
|
||||
|
||||
entropy := mathx.CalcEntropy(dist)
|
||||
assert.True(t, entropy > .95, fmt.Sprintf("entropy is %f, less than .95", entropy))
|
||||
assert.True(t, entropy > test.threshold, fmt.Sprintf("entropy is %f, less than %f",
|
||||
entropy, test.threshold))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
123
rpcx/internal/chainclientinterceptors_test.go
Normal file
123
rpcx/internal/chainclientinterceptors_test.go
Normal file
@@ -0,0 +1,123 @@
|
||||
package internal
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
func TestWithStreamClientInterceptors(t *testing.T) {
|
||||
opts := WithStreamClientInterceptors()
|
||||
assert.NotNil(t, opts)
|
||||
}
|
||||
|
||||
func TestWithUnaryClientInterceptors(t *testing.T) {
|
||||
opts := WithUnaryClientInterceptors()
|
||||
assert.NotNil(t, opts)
|
||||
}
|
||||
|
||||
func TestChainStreamClientInterceptors_zero(t *testing.T) {
|
||||
var vals []int
|
||||
interceptors := chainStreamClientInterceptors()
|
||||
_, err := interceptors(context.Background(), nil, new(grpc.ClientConn), "/foo",
|
||||
func(ctx context.Context, desc *grpc.StreamDesc, cc *grpc.ClientConn, method string,
|
||||
opts ...grpc.CallOption) (grpc.ClientStream, error) {
|
||||
vals = append(vals, 1)
|
||||
return nil, nil
|
||||
})
|
||||
assert.Nil(t, err)
|
||||
assert.ElementsMatch(t, []int{1}, vals)
|
||||
}
|
||||
|
||||
func TestChainStreamClientInterceptors_one(t *testing.T) {
|
||||
var vals []int
|
||||
interceptors := chainStreamClientInterceptors(func(ctx context.Context, desc *grpc.StreamDesc,
|
||||
cc *grpc.ClientConn, method string, streamer grpc.Streamer, opts ...grpc.CallOption) (
|
||||
grpc.ClientStream, error) {
|
||||
vals = append(vals, 1)
|
||||
return streamer(ctx, desc, cc, method, opts...)
|
||||
})
|
||||
_, err := interceptors(context.Background(), nil, new(grpc.ClientConn), "/foo",
|
||||
func(ctx context.Context, desc *grpc.StreamDesc, cc *grpc.ClientConn, method string,
|
||||
opts ...grpc.CallOption) (grpc.ClientStream, error) {
|
||||
vals = append(vals, 2)
|
||||
return nil, nil
|
||||
})
|
||||
assert.Nil(t, err)
|
||||
assert.ElementsMatch(t, []int{1, 2}, vals)
|
||||
}
|
||||
|
||||
func TestChainStreamClientInterceptors_more(t *testing.T) {
|
||||
var vals []int
|
||||
interceptors := chainStreamClientInterceptors(func(ctx context.Context, desc *grpc.StreamDesc,
|
||||
cc *grpc.ClientConn, method string, streamer grpc.Streamer, opts ...grpc.CallOption) (
|
||||
grpc.ClientStream, error) {
|
||||
vals = append(vals, 1)
|
||||
return streamer(ctx, desc, cc, method, opts...)
|
||||
}, func(ctx context.Context, desc *grpc.StreamDesc, cc *grpc.ClientConn, method string,
|
||||
streamer grpc.Streamer, opts ...grpc.CallOption) (grpc.ClientStream, error) {
|
||||
vals = append(vals, 2)
|
||||
return streamer(ctx, desc, cc, method, opts...)
|
||||
})
|
||||
_, err := interceptors(context.Background(), nil, new(grpc.ClientConn), "/foo",
|
||||
func(ctx context.Context, desc *grpc.StreamDesc, cc *grpc.ClientConn, method string,
|
||||
opts ...grpc.CallOption) (grpc.ClientStream, error) {
|
||||
vals = append(vals, 3)
|
||||
return nil, nil
|
||||
})
|
||||
assert.Nil(t, err)
|
||||
assert.ElementsMatch(t, []int{1, 2, 3}, vals)
|
||||
}
|
||||
|
||||
func TestWithUnaryClientInterceptors_zero(t *testing.T) {
|
||||
var vals []int
|
||||
interceptors := chainUnaryClientInterceptors()
|
||||
err := interceptors(context.Background(), "/foo", nil, nil, new(grpc.ClientConn),
|
||||
func(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn,
|
||||
opts ...grpc.CallOption) error {
|
||||
vals = append(vals, 1)
|
||||
return nil
|
||||
})
|
||||
assert.Nil(t, err)
|
||||
assert.ElementsMatch(t, []int{1}, vals)
|
||||
}
|
||||
|
||||
func TestWithUnaryClientInterceptors_one(t *testing.T) {
|
||||
var vals []int
|
||||
interceptors := chainUnaryClientInterceptors(func(ctx context.Context, method string, req,
|
||||
reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
|
||||
vals = append(vals, 1)
|
||||
return invoker(ctx, method, req, reply, cc, opts...)
|
||||
})
|
||||
err := interceptors(context.Background(), "/foo", nil, nil, new(grpc.ClientConn),
|
||||
func(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn,
|
||||
opts ...grpc.CallOption) error {
|
||||
vals = append(vals, 2)
|
||||
return nil
|
||||
})
|
||||
assert.Nil(t, err)
|
||||
assert.ElementsMatch(t, []int{1, 2}, vals)
|
||||
}
|
||||
|
||||
func TestWithUnaryClientInterceptors_more(t *testing.T) {
|
||||
var vals []int
|
||||
interceptors := chainUnaryClientInterceptors(func(ctx context.Context, method string, req,
|
||||
reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
|
||||
vals = append(vals, 1)
|
||||
return invoker(ctx, method, req, reply, cc, opts...)
|
||||
}, func(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn,
|
||||
invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
|
||||
vals = append(vals, 2)
|
||||
return invoker(ctx, method, req, reply, cc, opts...)
|
||||
})
|
||||
err := interceptors(context.Background(), "/foo", nil, nil, new(grpc.ClientConn),
|
||||
func(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn,
|
||||
opts ...grpc.CallOption) error {
|
||||
vals = append(vals, 3)
|
||||
return nil
|
||||
})
|
||||
assert.Nil(t, err)
|
||||
assert.ElementsMatch(t, []int{1, 2, 3}, vals)
|
||||
}
|
||||
111
rpcx/internal/chainserverinterceptors_test.go
Normal file
111
rpcx/internal/chainserverinterceptors_test.go
Normal file
@@ -0,0 +1,111 @@
|
||||
package internal
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
func TestWithStreamServerInterceptors(t *testing.T) {
|
||||
opts := WithStreamServerInterceptors()
|
||||
assert.NotNil(t, opts)
|
||||
}
|
||||
|
||||
func TestWithUnaryServerInterceptors(t *testing.T) {
|
||||
opts := WithUnaryServerInterceptors()
|
||||
assert.NotNil(t, opts)
|
||||
}
|
||||
|
||||
func TestChainStreamServerInterceptors_zero(t *testing.T) {
|
||||
var vals []int
|
||||
interceptors := chainStreamServerInterceptors()
|
||||
err := interceptors(nil, nil, nil, func(srv interface{}, stream grpc.ServerStream) error {
|
||||
vals = append(vals, 1)
|
||||
return nil
|
||||
})
|
||||
assert.Nil(t, err)
|
||||
assert.ElementsMatch(t, []int{1}, vals)
|
||||
}
|
||||
|
||||
func TestChainStreamServerInterceptors_one(t *testing.T) {
|
||||
var vals []int
|
||||
interceptors := chainStreamServerInterceptors(func(srv interface{}, ss grpc.ServerStream,
|
||||
info *grpc.StreamServerInfo, handler grpc.StreamHandler) error {
|
||||
vals = append(vals, 1)
|
||||
return handler(srv, ss)
|
||||
})
|
||||
err := interceptors(nil, nil, nil, func(srv interface{}, stream grpc.ServerStream) error {
|
||||
vals = append(vals, 2)
|
||||
return nil
|
||||
})
|
||||
assert.Nil(t, err)
|
||||
assert.ElementsMatch(t, []int{1, 2}, vals)
|
||||
}
|
||||
|
||||
func TestChainStreamServerInterceptors_more(t *testing.T) {
|
||||
var vals []int
|
||||
interceptors := chainStreamServerInterceptors(func(srv interface{}, ss grpc.ServerStream,
|
||||
info *grpc.StreamServerInfo, handler grpc.StreamHandler) error {
|
||||
vals = append(vals, 1)
|
||||
return handler(srv, ss)
|
||||
}, func(srv interface{}, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error {
|
||||
vals = append(vals, 2)
|
||||
return handler(srv, ss)
|
||||
})
|
||||
err := interceptors(nil, nil, nil, func(srv interface{}, stream grpc.ServerStream) error {
|
||||
vals = append(vals, 3)
|
||||
return nil
|
||||
})
|
||||
assert.Nil(t, err)
|
||||
assert.ElementsMatch(t, []int{1, 2, 3}, vals)
|
||||
}
|
||||
|
||||
func TestChainUnaryServerInterceptors_zero(t *testing.T) {
|
||||
var vals []int
|
||||
interceptors := chainUnaryServerInterceptors()
|
||||
_, err := interceptors(context.Background(), nil, nil,
|
||||
func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
vals = append(vals, 1)
|
||||
return nil, nil
|
||||
})
|
||||
assert.Nil(t, err)
|
||||
assert.ElementsMatch(t, []int{1}, vals)
|
||||
}
|
||||
|
||||
func TestChainUnaryServerInterceptors_one(t *testing.T) {
|
||||
var vals []int
|
||||
interceptors := chainUnaryServerInterceptors(func(ctx context.Context, req interface{},
|
||||
info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) {
|
||||
vals = append(vals, 1)
|
||||
return handler(ctx, req)
|
||||
})
|
||||
_, err := interceptors(context.Background(), nil, nil,
|
||||
func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
vals = append(vals, 2)
|
||||
return nil, nil
|
||||
})
|
||||
assert.Nil(t, err)
|
||||
assert.ElementsMatch(t, []int{1, 2}, vals)
|
||||
}
|
||||
|
||||
func TestChainUnaryServerInterceptors_more(t *testing.T) {
|
||||
var vals []int
|
||||
interceptors := chainUnaryServerInterceptors(func(ctx context.Context, req interface{},
|
||||
info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) {
|
||||
vals = append(vals, 1)
|
||||
return handler(ctx, req)
|
||||
}, func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo,
|
||||
handler grpc.UnaryHandler) (resp interface{}, err error) {
|
||||
vals = append(vals, 2)
|
||||
return handler(ctx, req)
|
||||
})
|
||||
_, err := interceptors(context.Background(), nil, nil,
|
||||
func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
vals = append(vals, 3)
|
||||
return nil, nil
|
||||
})
|
||||
assert.Nil(t, err)
|
||||
assert.ElementsMatch(t, []int{1, 2, 3}, vals)
|
||||
}
|
||||
30
rpcx/internal/client_test.go
Normal file
30
rpcx/internal/client_test.go
Normal file
@@ -0,0 +1,30 @@
|
||||
package internal
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
func TestWithDialOption(t *testing.T) {
|
||||
var options ClientOptions
|
||||
agent := grpc.WithUserAgent("chrome")
|
||||
opt := WithDialOption(agent)
|
||||
opt(&options)
|
||||
assert.Contains(t, options.DialOptions, agent)
|
||||
}
|
||||
|
||||
func TestWithTimeout(t *testing.T) {
|
||||
var options ClientOptions
|
||||
opt := WithTimeout(time.Second)
|
||||
opt(&options)
|
||||
assert.Equal(t, time.Second, options.Timeout)
|
||||
}
|
||||
|
||||
func TestBuildDialOptions(t *testing.T) {
|
||||
agent := grpc.WithUserAgent("chrome")
|
||||
opts := buildDialOptions(WithDialOption(agent))
|
||||
assert.Contains(t, opts, agent)
|
||||
}
|
||||
@@ -1,12 +1,15 @@
|
||||
package clientinterceptors
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/tal-tech/go-zero/core/breaker"
|
||||
"github.com/tal-tech/go-zero/core/stat"
|
||||
rcodes "github.com/tal-tech/go-zero/rpcx/internal/codes"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
)
|
||||
@@ -49,3 +52,30 @@ func TestBreakerInterceptorDeadlineExceeded(t *testing.T) {
|
||||
assert.True(t, errs[err] > 0)
|
||||
assert.True(t, errs[breaker.ErrServiceUnavailable] > 0)
|
||||
}
|
||||
|
||||
func TestBreakerInterceptor(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
err error
|
||||
}{
|
||||
{
|
||||
name: "nil",
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
name: "with error",
|
||||
err: errors.New("mock"),
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
cc := new(grpc.ClientConn)
|
||||
err := BreakerInterceptor(context.Background(), "/foo", nil, nil, cc,
|
||||
func(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn,
|
||||
opts ...grpc.CallOption) error {
|
||||
return test.err
|
||||
})
|
||||
assert.Equal(t, test.err, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
37
rpcx/internal/clientinterceptors/durationinterceptor_test.go
Normal file
37
rpcx/internal/clientinterceptors/durationinterceptor_test.go
Normal file
@@ -0,0 +1,37 @@
|
||||
package clientinterceptors
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
func TestDurationInterceptor(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
err error
|
||||
}{
|
||||
{
|
||||
name: "nil",
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
name: "with error",
|
||||
err: errors.New("mock"),
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
cc := new(grpc.ClientConn)
|
||||
err := DurationInterceptor(context.Background(), "/foo", nil, nil, cc,
|
||||
func(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn,
|
||||
opts ...grpc.CallOption) error {
|
||||
return test.err
|
||||
})
|
||||
assert.Equal(t, test.err, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package clientinterceptors
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
func TestPromMetricInterceptor(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
err error
|
||||
}{
|
||||
{
|
||||
name: "nil",
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
name: "with error",
|
||||
err: errors.New("mock"),
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
cc := new(grpc.ClientConn)
|
||||
err := PromMetricInterceptor(context.Background(), "/foo", nil, nil, cc,
|
||||
func(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn,
|
||||
opts ...grpc.CallOption) error {
|
||||
return test.err
|
||||
})
|
||||
assert.Equal(t, test.err, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
50
rpcx/internal/clientinterceptors/timeoutinterceptor_test.go
Normal file
50
rpcx/internal/clientinterceptors/timeoutinterceptor_test.go
Normal file
@@ -0,0 +1,50 @@
|
||||
package clientinterceptors
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strconv"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
func TestTimeoutInterceptor(t *testing.T) {
|
||||
timeouts := []time.Duration{0, time.Millisecond * 10}
|
||||
for _, timeout := range timeouts {
|
||||
t.Run(strconv.FormatInt(int64(timeout), 10), func(t *testing.T) {
|
||||
interceptor := TimeoutInterceptor(timeout)
|
||||
cc := new(grpc.ClientConn)
|
||||
err := interceptor(context.Background(), "/foo", nil, nil, cc,
|
||||
func(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn,
|
||||
opts ...grpc.CallOption) error {
|
||||
return nil
|
||||
},
|
||||
)
|
||||
assert.Nil(t, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestTimeoutInterceptor_timeout(t *testing.T) {
|
||||
const timeout = time.Millisecond * 10
|
||||
interceptor := TimeoutInterceptor(timeout)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
|
||||
defer cancel()
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(1)
|
||||
cc := new(grpc.ClientConn)
|
||||
err := interceptor(ctx, "/foo", nil, nil, cc,
|
||||
func(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn,
|
||||
opts ...grpc.CallOption) error {
|
||||
defer wg.Done()
|
||||
tm, ok := ctx.Deadline()
|
||||
assert.True(t, ok)
|
||||
assert.True(t, tm.Before(time.Now().Add(timeout+time.Millisecond)))
|
||||
return nil
|
||||
})
|
||||
wg.Wait()
|
||||
assert.Nil(t, err)
|
||||
}
|
||||
53
rpcx/internal/clientinterceptors/tracinginterceptor_test.go
Normal file
53
rpcx/internal/clientinterceptors/tracinginterceptor_test.go
Normal file
@@ -0,0 +1,53 @@
|
||||
package clientinterceptors
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/tal-tech/go-zero/core/trace"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/metadata"
|
||||
)
|
||||
|
||||
func TestTracingInterceptor(t *testing.T) {
|
||||
var run int32
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(1)
|
||||
cc := new(grpc.ClientConn)
|
||||
err := TracingInterceptor(context.Background(), "/foo", nil, nil, cc,
|
||||
func(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn,
|
||||
opts ...grpc.CallOption) error {
|
||||
defer wg.Done()
|
||||
atomic.AddInt32(&run, 1)
|
||||
return nil
|
||||
})
|
||||
wg.Wait()
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, int32(1), atomic.LoadInt32(&run))
|
||||
}
|
||||
|
||||
func TestTracingInterceptor_GrpcFormat(t *testing.T) {
|
||||
var run int32
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(1)
|
||||
md := metadata.New(map[string]string{
|
||||
"foo": "bar",
|
||||
})
|
||||
carrier, err := trace.Inject(trace.GrpcFormat, md)
|
||||
assert.Nil(t, err)
|
||||
ctx, _ := trace.StartServerSpan(context.Background(), carrier, "user", "/foo")
|
||||
cc := new(grpc.ClientConn)
|
||||
err = TracingInterceptor(ctx, "/foo", nil, nil, cc,
|
||||
func(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn,
|
||||
opts ...grpc.CallOption) error {
|
||||
defer wg.Done()
|
||||
atomic.AddInt32(&run, 1)
|
||||
return nil
|
||||
})
|
||||
wg.Wait()
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, int32(1), atomic.LoadInt32(&run))
|
||||
}
|
||||
159
rpcx/internal/mock/deposit.pb.go
Normal file
159
rpcx/internal/mock/deposit.pb.go
Normal file
@@ -0,0 +1,159 @@
|
||||
// Code generated by protoc-gen-go.
|
||||
// source: deposit.proto
|
||||
// DO NOT EDIT!
|
||||
|
||||
/*
|
||||
Package mock is a generated protocol buffer package.
|
||||
|
||||
It is generated from these files:
|
||||
deposit.proto
|
||||
|
||||
It has these top-level messages:
|
||||
DepositRequest
|
||||
DepositResponse
|
||||
*/
|
||||
package mock
|
||||
|
||||
import proto "github.com/golang/protobuf/proto"
|
||||
import fmt "fmt"
|
||||
import math "math"
|
||||
|
||||
import (
|
||||
context "golang.org/x/net/context"
|
||||
grpc "google.golang.org/grpc"
|
||||
)
|
||||
|
||||
// Reference imports to suppress errors if they are not otherwise used.
|
||||
var _ = proto.Marshal
|
||||
var _ = fmt.Errorf
|
||||
var _ = math.Inf
|
||||
|
||||
// This is a compile-time assertion to ensure that this generated file
|
||||
// is compatible with the proto package it is being compiled against.
|
||||
// A compilation error at this line likely means your copy of the
|
||||
// proto package needs to be updated.
|
||||
const _ = proto.ProtoPackageIsVersion2 // please upgrade the proto package
|
||||
|
||||
type DepositRequest struct {
|
||||
Amount float32 `protobuf:"fixed32,1,opt,name=amount" json:"amount,omitempty"`
|
||||
}
|
||||
|
||||
func (m *DepositRequest) Reset() { *m = DepositRequest{} }
|
||||
func (m *DepositRequest) String() string { return proto.CompactTextString(m) }
|
||||
func (*DepositRequest) ProtoMessage() {}
|
||||
func (*DepositRequest) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{0} }
|
||||
|
||||
func (m *DepositRequest) GetAmount() float32 {
|
||||
if m != nil {
|
||||
return m.Amount
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
type DepositResponse struct {
|
||||
Ok bool `protobuf:"varint,1,opt,name=ok" json:"ok,omitempty"`
|
||||
}
|
||||
|
||||
func (m *DepositResponse) Reset() { *m = DepositResponse{} }
|
||||
func (m *DepositResponse) String() string { return proto.CompactTextString(m) }
|
||||
func (*DepositResponse) ProtoMessage() {}
|
||||
func (*DepositResponse) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{1} }
|
||||
|
||||
func (m *DepositResponse) GetOk() bool {
|
||||
if m != nil {
|
||||
return m.Ok
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func init() {
|
||||
proto.RegisterType((*DepositRequest)(nil), "mock.DepositRequest")
|
||||
proto.RegisterType((*DepositResponse)(nil), "mock.DepositResponse")
|
||||
}
|
||||
|
||||
// Reference imports to suppress errors if they are not otherwise used.
|
||||
var _ context.Context
|
||||
var _ grpc.ClientConn
|
||||
|
||||
// This is a compile-time assertion to ensure that this generated file
|
||||
// is compatible with the grpc package it is being compiled against.
|
||||
const _ = grpc.SupportPackageIsVersion4
|
||||
|
||||
// Client API for DepositService service
|
||||
|
||||
type DepositServiceClient interface {
|
||||
Deposit(ctx context.Context, in *DepositRequest, opts ...grpc.CallOption) (*DepositResponse, error)
|
||||
}
|
||||
|
||||
type depositServiceClient struct {
|
||||
cc *grpc.ClientConn
|
||||
}
|
||||
|
||||
func NewDepositServiceClient(cc *grpc.ClientConn) DepositServiceClient {
|
||||
return &depositServiceClient{cc}
|
||||
}
|
||||
|
||||
func (c *depositServiceClient) Deposit(ctx context.Context, in *DepositRequest, opts ...grpc.CallOption) (*DepositResponse, error) {
|
||||
out := new(DepositResponse)
|
||||
err := grpc.Invoke(ctx, "/mock.DepositService/Deposit", in, out, c.cc, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// Server API for DepositService service
|
||||
|
||||
type DepositServiceServer interface {
|
||||
Deposit(context.Context, *DepositRequest) (*DepositResponse, error)
|
||||
}
|
||||
|
||||
func RegisterDepositServiceServer(s *grpc.Server, srv DepositServiceServer) {
|
||||
s.RegisterService(&_DepositService_serviceDesc, srv)
|
||||
}
|
||||
|
||||
func _DepositService_Deposit_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(DepositRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(DepositServiceServer).Deposit(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: "/mock.DepositService/Deposit",
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(DepositServiceServer).Deposit(ctx, req.(*DepositRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
var _DepositService_serviceDesc = grpc.ServiceDesc{
|
||||
ServiceName: "mock.DepositService",
|
||||
HandlerType: (*DepositServiceServer)(nil),
|
||||
Methods: []grpc.MethodDesc{
|
||||
{
|
||||
MethodName: "Deposit",
|
||||
Handler: _DepositService_Deposit_Handler,
|
||||
},
|
||||
},
|
||||
Streams: []grpc.StreamDesc{},
|
||||
Metadata: "deposit.proto",
|
||||
}
|
||||
|
||||
func init() { proto.RegisterFile("deposit.proto", fileDescriptor0) }
|
||||
|
||||
var fileDescriptor0 = []byte{
|
||||
// 139 bytes of a gzipped FileDescriptorProto
|
||||
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0xe2, 0xe2, 0x4d, 0x49, 0x2d, 0xc8,
|
||||
0x2f, 0xce, 0x2c, 0xd1, 0x2b, 0x28, 0xca, 0x2f, 0xc9, 0x17, 0x62, 0xc9, 0xcd, 0x4f, 0xce, 0x56,
|
||||
0xd2, 0xe0, 0xe2, 0x73, 0x81, 0x08, 0x07, 0xa5, 0x16, 0x96, 0xa6, 0x16, 0x97, 0x08, 0x89, 0x71,
|
||||
0xb1, 0x25, 0xe6, 0xe6, 0x97, 0xe6, 0x95, 0x48, 0x30, 0x2a, 0x30, 0x6a, 0x30, 0x05, 0x41, 0x79,
|
||||
0x4a, 0x8a, 0x5c, 0xfc, 0x70, 0x95, 0xc5, 0x05, 0xf9, 0x79, 0xc5, 0xa9, 0x42, 0x7c, 0x5c, 0x4c,
|
||||
0xf9, 0xd9, 0x60, 0x65, 0x1c, 0x41, 0x4c, 0xf9, 0xd9, 0x46, 0x1e, 0x70, 0xc3, 0x82, 0x53, 0x8b,
|
||||
0xca, 0x32, 0x93, 0x53, 0x85, 0xcc, 0xb8, 0xd8, 0xa1, 0x22, 0x42, 0x22, 0x7a, 0x20, 0x0b, 0xf5,
|
||||
0x50, 0x6d, 0x93, 0x12, 0x45, 0x13, 0x85, 0x98, 0x9c, 0xc4, 0x06, 0x76, 0xa3, 0x31, 0x20, 0x00,
|
||||
0x00, 0xff, 0xff, 0x62, 0x37, 0xf2, 0x36, 0xb4, 0x00, 0x00, 0x00,
|
||||
}
|
||||
15
rpcx/internal/mock/deposit.proto
Normal file
15
rpcx/internal/mock/deposit.proto
Normal file
@@ -0,0 +1,15 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package mock;
|
||||
|
||||
message DepositRequest {
|
||||
float amount = 1;
|
||||
}
|
||||
|
||||
message DepositResponse {
|
||||
bool ok = 1;
|
||||
}
|
||||
|
||||
service DepositService {
|
||||
rpc Deposit(DepositRequest) returns (DepositResponse);
|
||||
}
|
||||
19
rpcx/internal/mock/depositserver.go
Normal file
19
rpcx/internal/mock/depositserver.go
Normal file
@@ -0,0 +1,19 @@
|
||||
package mock
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
type DepositServer struct {
|
||||
}
|
||||
|
||||
func (*DepositServer) Deposit(ctx context.Context, req *DepositRequest) (*DepositResponse, error) {
|
||||
if req.GetAmount() < 0 {
|
||||
return nil, status.Errorf(codes.InvalidArgument, "cannot deposit %v", req.GetAmount())
|
||||
}
|
||||
|
||||
return &DepositResponse{Ok: true}, nil
|
||||
}
|
||||
@@ -11,7 +11,9 @@ type directBuilder struct{}
|
||||
func (d *directBuilder) Build(target resolver.Target, cc resolver.ClientConn, opts resolver.BuildOptions) (
|
||||
resolver.Resolver, error) {
|
||||
var addrs []resolver.Address
|
||||
endpoints := strings.Split(target.Endpoint, EndpointSep)
|
||||
endpoints := strings.FieldsFunc(target.Endpoint, func(r rune) bool {
|
||||
return r == EndpointSepChar
|
||||
})
|
||||
|
||||
for _, val := range subset(endpoints, subsetSize) {
|
||||
addrs = append(addrs, resolver.Address{
|
||||
|
||||
52
rpcx/internal/resolver/directbuilder_test.go
Normal file
52
rpcx/internal/resolver/directbuilder_test.go
Normal file
@@ -0,0 +1,52 @@
|
||||
package resolver
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/tal-tech/go-zero/core/lang"
|
||||
"github.com/tal-tech/go-zero/core/mathx"
|
||||
"google.golang.org/grpc/resolver"
|
||||
)
|
||||
|
||||
func TestDirectBuilder_Build(t *testing.T) {
|
||||
tests := []int{
|
||||
0,
|
||||
1,
|
||||
2,
|
||||
subsetSize / 2,
|
||||
subsetSize,
|
||||
subsetSize * 2,
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(strconv.Itoa(test), func(t *testing.T) {
|
||||
var servers []string
|
||||
for i := 0; i < test; i++ {
|
||||
servers = append(servers, fmt.Sprintf("localhost:%d", i))
|
||||
}
|
||||
var b directBuilder
|
||||
cc := new(mockedClientConn)
|
||||
_, err := b.Build(resolver.Target{
|
||||
Scheme: DirectScheme,
|
||||
Endpoint: strings.Join(servers, ","),
|
||||
}, cc, resolver.BuildOptions{})
|
||||
assert.Nil(t, err)
|
||||
size := mathx.MinInt(test, subsetSize)
|
||||
assert.Equal(t, size, len(cc.state.Addresses))
|
||||
m := make(map[string]lang.PlaceholderType)
|
||||
for _, each := range cc.state.Addresses {
|
||||
m[each.Addr] = lang.Placeholder
|
||||
}
|
||||
assert.Equal(t, size, len(m))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDirectBuilder_Scheme(t *testing.T) {
|
||||
var b directBuilder
|
||||
assert.Equal(t, DirectScheme, b.Scheme())
|
||||
}
|
||||
@@ -11,7 +11,9 @@ type discovBuilder struct{}
|
||||
|
||||
func (d *discovBuilder) Build(target resolver.Target, cc resolver.ClientConn, opts resolver.BuildOptions) (
|
||||
resolver.Resolver, error) {
|
||||
hosts := strings.Split(target.Authority, EndpointSep)
|
||||
hosts := strings.FieldsFunc(target.Authority, func(r rune) bool {
|
||||
return r == EndpointSepChar
|
||||
})
|
||||
sub, err := discov.NewSubscriber(hosts, target.Endpoint)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -1,17 +1,22 @@
|
||||
package resolver
|
||||
|
||||
import "google.golang.org/grpc/resolver"
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"google.golang.org/grpc/resolver"
|
||||
)
|
||||
|
||||
const (
|
||||
DirectScheme = "direct"
|
||||
DiscovScheme = "discov"
|
||||
EndpointSep = ","
|
||||
subsetSize = 32
|
||||
DirectScheme = "direct"
|
||||
DiscovScheme = "discov"
|
||||
EndpointSepChar = ','
|
||||
subsetSize = 32
|
||||
)
|
||||
|
||||
var (
|
||||
dirBuilder directBuilder
|
||||
disBuilder discovBuilder
|
||||
EndpointSep = fmt.Sprintf("%c", EndpointSepChar)
|
||||
dirBuilder directBuilder
|
||||
disBuilder discovBuilder
|
||||
)
|
||||
|
||||
func RegisterResolver() {
|
||||
|
||||
36
rpcx/internal/resolver/resolver_test.go
Normal file
36
rpcx/internal/resolver/resolver_test.go
Normal file
@@ -0,0 +1,36 @@
|
||||
package resolver
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"google.golang.org/grpc/resolver"
|
||||
"google.golang.org/grpc/serviceconfig"
|
||||
)
|
||||
|
||||
func TestNopResolver(t *testing.T) {
|
||||
// make sure ResolveNow & Close don't panic
|
||||
var r nopResolver
|
||||
r.ResolveNow(resolver.ResolveNowOptions{})
|
||||
r.Close()
|
||||
}
|
||||
|
||||
type mockedClientConn struct {
|
||||
state resolver.State
|
||||
}
|
||||
|
||||
func (m *mockedClientConn) UpdateState(state resolver.State) {
|
||||
m.state = state
|
||||
}
|
||||
|
||||
func (m *mockedClientConn) ReportError(err error) {
|
||||
}
|
||||
|
||||
func (m *mockedClientConn) NewAddress(addresses []resolver.Address) {
|
||||
}
|
||||
|
||||
func (m *mockedClientConn) NewServiceConfig(serviceConfig string) {
|
||||
}
|
||||
|
||||
func (m *mockedClientConn) ParseServiceConfig(serviceConfigJSON string) *serviceconfig.ParseResult {
|
||||
return nil
|
||||
}
|
||||
16
rpcx/internal/rpcserver_test.go
Normal file
16
rpcx/internal/rpcserver_test.go
Normal file
@@ -0,0 +1,16 @@
|
||||
package internal
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/tal-tech/go-zero/core/stat"
|
||||
)
|
||||
|
||||
func TestWithMetrics(t *testing.T) {
|
||||
metrics := stat.NewMetrics("foo")
|
||||
opt := WithMetrics(metrics)
|
||||
var options rpcServerOptions
|
||||
opt(&options)
|
||||
assert.Equal(t, metrics, options.metrics)
|
||||
}
|
||||
53
rpcx/internal/server_test.go
Normal file
53
rpcx/internal/server_test.go
Normal file
@@ -0,0 +1,53 @@
|
||||
package internal
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/tal-tech/go-zero/core/stat"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
func TestBaseRpcServer_AddOptions(t *testing.T) {
|
||||
metrics := stat.NewMetrics("foo")
|
||||
server := newBaseRpcServer("foo", metrics)
|
||||
server.SetName("bar")
|
||||
var opt grpc.EmptyServerOption
|
||||
server.AddOptions(opt)
|
||||
assert.Contains(t, server.options, opt)
|
||||
}
|
||||
|
||||
func TestBaseRpcServer_AddStreamInterceptors(t *testing.T) {
|
||||
metrics := stat.NewMetrics("foo")
|
||||
server := newBaseRpcServer("foo", metrics)
|
||||
server.SetName("bar")
|
||||
var vals []int
|
||||
f := func(srv interface{}, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error {
|
||||
vals = append(vals, 1)
|
||||
return nil
|
||||
}
|
||||
server.AddStreamInterceptors(f)
|
||||
for _, each := range server.streamInterceptors {
|
||||
assert.Nil(t, each(nil, nil, nil, nil))
|
||||
}
|
||||
assert.ElementsMatch(t, []int{1}, vals)
|
||||
}
|
||||
|
||||
func TestBaseRpcServer_AddUnaryInterceptors(t *testing.T) {
|
||||
metrics := stat.NewMetrics("foo")
|
||||
server := newBaseRpcServer("foo", metrics)
|
||||
server.SetName("bar")
|
||||
var vals []int
|
||||
f := func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (
|
||||
resp interface{}, err error) {
|
||||
vals = append(vals, 1)
|
||||
return nil, nil
|
||||
}
|
||||
server.AddUnaryInterceptors(f)
|
||||
for _, each := range server.unaryInterceptors {
|
||||
_, err := each(context.Background(), nil, nil, nil)
|
||||
assert.Nil(t, err)
|
||||
}
|
||||
assert.ElementsMatch(t, []int{1}, vals)
|
||||
}
|
||||
200
rpcx/internal/serverinterceptors/authinterceptor_test.go
Normal file
200
rpcx/internal/serverinterceptors/authinterceptor_test.go
Normal file
@@ -0,0 +1,200 @@
|
||||
package serverinterceptors
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/alicebob/miniredis"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/tal-tech/go-zero/core/stores/redis"
|
||||
"github.com/tal-tech/go-zero/rpcx/internal/auth"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/metadata"
|
||||
)
|
||||
|
||||
func TestStreamAuthorizeInterceptor(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
app string
|
||||
token string
|
||||
strict bool
|
||||
hasError bool
|
||||
}{
|
||||
{
|
||||
name: "strict=false",
|
||||
strict: false,
|
||||
hasError: false,
|
||||
},
|
||||
{
|
||||
name: "strict=true",
|
||||
strict: true,
|
||||
hasError: true,
|
||||
},
|
||||
{
|
||||
name: "strict=true,with token",
|
||||
app: "foo",
|
||||
token: "bar",
|
||||
strict: true,
|
||||
hasError: false,
|
||||
},
|
||||
{
|
||||
name: "strict=true,with error token",
|
||||
app: "foo",
|
||||
token: "error",
|
||||
strict: true,
|
||||
hasError: true,
|
||||
},
|
||||
}
|
||||
|
||||
r := miniredis.NewMiniRedis()
|
||||
assert.Nil(t, r.Start())
|
||||
defer r.Close()
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
store := redis.NewRedis(r.Addr(), redis.NodeType)
|
||||
if len(test.app) > 0 {
|
||||
assert.Nil(t, store.Hset("apps", test.app, test.token))
|
||||
defer store.Hdel("apps", test.app)
|
||||
}
|
||||
|
||||
authenticator, err := auth.NewAuthenticator(store, "apps", test.strict)
|
||||
assert.Nil(t, err)
|
||||
interceptor := StreamAuthorizeInterceptor(authenticator)
|
||||
md := metadata.New(map[string]string{
|
||||
"app": "foo",
|
||||
"token": "bar",
|
||||
})
|
||||
ctx := metadata.NewIncomingContext(context.Background(), md)
|
||||
stream := mockedStream{ctx: ctx}
|
||||
err = interceptor(nil, stream, nil, func(srv interface{}, stream grpc.ServerStream) error {
|
||||
return nil
|
||||
})
|
||||
if test.hasError {
|
||||
assert.NotNil(t, err)
|
||||
} else {
|
||||
assert.Nil(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnaryAuthorizeInterceptor(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
app string
|
||||
token string
|
||||
strict bool
|
||||
hasError bool
|
||||
}{
|
||||
{
|
||||
name: "strict=false",
|
||||
strict: false,
|
||||
hasError: false,
|
||||
},
|
||||
{
|
||||
name: "strict=true",
|
||||
strict: true,
|
||||
hasError: true,
|
||||
},
|
||||
{
|
||||
name: "strict=true,with token",
|
||||
app: "foo",
|
||||
token: "bar",
|
||||
strict: true,
|
||||
hasError: false,
|
||||
},
|
||||
{
|
||||
name: "strict=true,with error token",
|
||||
app: "foo",
|
||||
token: "error",
|
||||
strict: true,
|
||||
hasError: true,
|
||||
},
|
||||
}
|
||||
|
||||
r := miniredis.NewMiniRedis()
|
||||
assert.Nil(t, r.Start())
|
||||
defer r.Close()
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
store := redis.NewRedis(r.Addr(), redis.NodeType)
|
||||
if len(test.app) > 0 {
|
||||
assert.Nil(t, store.Hset("apps", test.app, test.token))
|
||||
defer store.Hdel("apps", test.app)
|
||||
}
|
||||
|
||||
authenticator, err := auth.NewAuthenticator(store, "apps", test.strict)
|
||||
assert.Nil(t, err)
|
||||
interceptor := UnaryAuthorizeInterceptor(authenticator)
|
||||
md := metadata.New(map[string]string{
|
||||
"app": "foo",
|
||||
"token": "bar",
|
||||
})
|
||||
ctx := metadata.NewIncomingContext(context.Background(), md)
|
||||
_, err = interceptor(ctx, nil, nil,
|
||||
func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return nil, nil
|
||||
})
|
||||
if test.hasError {
|
||||
assert.NotNil(t, err)
|
||||
} else {
|
||||
assert.Nil(t, err)
|
||||
}
|
||||
if test.strict {
|
||||
_, err = interceptor(context.Background(), nil, nil,
|
||||
func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return nil, nil
|
||||
})
|
||||
assert.NotNil(t, err)
|
||||
|
||||
var md metadata.MD
|
||||
ctx := metadata.NewIncomingContext(context.Background(), md)
|
||||
_, err = interceptor(ctx, nil, nil,
|
||||
func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return nil, nil
|
||||
})
|
||||
assert.NotNil(t, err)
|
||||
|
||||
md = metadata.New(map[string]string{
|
||||
"app": "",
|
||||
"token": "",
|
||||
})
|
||||
ctx = metadata.NewIncomingContext(context.Background(), md)
|
||||
_, err = interceptor(ctx, nil, nil,
|
||||
func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return nil, nil
|
||||
})
|
||||
assert.NotNil(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type mockedStream struct {
|
||||
ctx context.Context
|
||||
}
|
||||
|
||||
func (m mockedStream) SetHeader(md metadata.MD) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m mockedStream) SendHeader(md metadata.MD) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m mockedStream) SetTrailer(md metadata.MD) {
|
||||
}
|
||||
|
||||
func (m mockedStream) Context() context.Context {
|
||||
return m.ctx
|
||||
}
|
||||
|
||||
func (m mockedStream) SendMsg(v interface{}) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m mockedStream) RecvMsg(v interface{}) error {
|
||||
return nil
|
||||
}
|
||||
31
rpcx/internal/serverinterceptors/crashinterceptor_test.go
Normal file
31
rpcx/internal/serverinterceptors/crashinterceptor_test.go
Normal file
@@ -0,0 +1,31 @@
|
||||
package serverinterceptors
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/tal-tech/go-zero/core/logx"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
func init() {
|
||||
logx.Disable()
|
||||
}
|
||||
|
||||
func TestStreamCrashInterceptor(t *testing.T) {
|
||||
err := StreamCrashInterceptor(nil, nil, nil, func(
|
||||
srv interface{}, stream grpc.ServerStream) error {
|
||||
panic("mock panic")
|
||||
})
|
||||
assert.NotNil(t, err)
|
||||
}
|
||||
|
||||
func TestUnaryCrashInterceptor(t *testing.T) {
|
||||
interceptor := UnaryCrashInterceptor()
|
||||
_, err := interceptor(context.Background(), nil, nil,
|
||||
func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
panic("mock panic")
|
||||
})
|
||||
assert.NotNil(t, err)
|
||||
}
|
||||
@@ -33,12 +33,12 @@ var (
|
||||
)
|
||||
|
||||
func UnaryPromMetricInterceptor() grpc.UnaryServerInterceptor {
|
||||
return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
|
||||
return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (
|
||||
interface{}, error) {
|
||||
startTime := timex.Now()
|
||||
resp, err := handler(ctx, req)
|
||||
metricServerReqDur.Observe(int64(timex.Since(startTime)/time.Millisecond), info.FullMethod)
|
||||
metricServerReqCodeTotal.Inc(info.FullMethod, strconv.Itoa(int(status.Code(err))))
|
||||
return resp, err
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
package serverinterceptors
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
func TestUnaryPromMetricInterceptor(t *testing.T) {
|
||||
interceptor := UnaryPromMetricInterceptor()
|
||||
_, err := interceptor(context.Background(), nil, &grpc.UnaryServerInfo{
|
||||
FullMethod: "/",
|
||||
}, func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return nil, nil
|
||||
})
|
||||
assert.Nil(t, err)
|
||||
}
|
||||
77
rpcx/internal/serverinterceptors/sheddinginterceptor_test.go
Normal file
77
rpcx/internal/serverinterceptors/sheddinginterceptor_test.go
Normal file
@@ -0,0 +1,77 @@
|
||||
package serverinterceptors
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/tal-tech/go-zero/core/load"
|
||||
"github.com/tal-tech/go-zero/core/stat"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
func TestUnarySheddingInterceptor(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
allow bool
|
||||
handleErr error
|
||||
expect error
|
||||
}{
|
||||
{
|
||||
name: "allow",
|
||||
allow: true,
|
||||
handleErr: nil,
|
||||
expect: nil,
|
||||
},
|
||||
{
|
||||
name: "allow",
|
||||
allow: true,
|
||||
handleErr: context.DeadlineExceeded,
|
||||
expect: context.DeadlineExceeded,
|
||||
},
|
||||
{
|
||||
name: "reject",
|
||||
allow: false,
|
||||
handleErr: nil,
|
||||
expect: load.ErrServiceOverloaded,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
test := test
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
shedder := mockedShedder{allow: test.allow}
|
||||
metrics := stat.NewMetrics("mock")
|
||||
interceptor := UnarySheddingInterceptor(shedder, metrics)
|
||||
_, err := interceptor(context.Background(), nil, &grpc.UnaryServerInfo{
|
||||
FullMethod: "/",
|
||||
}, func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return nil, test.handleErr
|
||||
})
|
||||
assert.Equal(t, test.expect, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type mockedShedder struct {
|
||||
allow bool
|
||||
}
|
||||
|
||||
func (m mockedShedder) Allow() (load.Promise, error) {
|
||||
if m.allow {
|
||||
return mockedPromise{}, nil
|
||||
} else {
|
||||
return nil, load.ErrServiceOverloaded
|
||||
}
|
||||
}
|
||||
|
||||
type mockedPromise struct {
|
||||
}
|
||||
|
||||
func (m mockedPromise) Pass() {
|
||||
}
|
||||
|
||||
func (m mockedPromise) Fail() {
|
||||
}
|
||||
32
rpcx/internal/serverinterceptors/statinterceptor_test.go
Normal file
32
rpcx/internal/serverinterceptors/statinterceptor_test.go
Normal file
@@ -0,0 +1,32 @@
|
||||
package serverinterceptors
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/tal-tech/go-zero/core/stat"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
func TestUnaryStatInterceptor(t *testing.T) {
|
||||
metrics := stat.NewMetrics("mock")
|
||||
interceptor := UnaryStatInterceptor(metrics)
|
||||
_, err := interceptor(context.Background(), nil, &grpc.UnaryServerInfo{
|
||||
FullMethod: "/",
|
||||
}, func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return nil, nil
|
||||
})
|
||||
assert.Nil(t, err)
|
||||
}
|
||||
|
||||
func TestUnaryStatInterceptor_crash(t *testing.T) {
|
||||
metrics := stat.NewMetrics("mock")
|
||||
interceptor := UnaryStatInterceptor(metrics)
|
||||
_, err := interceptor(context.Background(), nil, &grpc.UnaryServerInfo{
|
||||
FullMethod: "/",
|
||||
}, func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
panic("error")
|
||||
})
|
||||
assert.NotNil(t, err)
|
||||
}
|
||||
41
rpcx/internal/serverinterceptors/timeoutinterceptor_test.go
Normal file
41
rpcx/internal/serverinterceptors/timeoutinterceptor_test.go
Normal file
@@ -0,0 +1,41 @@
|
||||
package serverinterceptors
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
func TestUnaryTimeoutInterceptor(t *testing.T) {
|
||||
interceptor := UnaryTimeoutInterceptor(time.Millisecond * 10)
|
||||
_, err := interceptor(context.Background(), nil, &grpc.UnaryServerInfo{
|
||||
FullMethod: "/",
|
||||
}, func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return nil, nil
|
||||
})
|
||||
assert.Nil(t, err)
|
||||
}
|
||||
|
||||
func TestUnaryTimeoutInterceptor_timeout(t *testing.T) {
|
||||
const timeout = time.Millisecond * 10
|
||||
interceptor := UnaryTimeoutInterceptor(timeout)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
|
||||
defer cancel()
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(1)
|
||||
_, err := interceptor(ctx, nil, &grpc.UnaryServerInfo{
|
||||
FullMethod: "/",
|
||||
}, func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
defer wg.Done()
|
||||
tm, ok := ctx.Deadline()
|
||||
assert.True(t, ok)
|
||||
assert.True(t, tm.Before(time.Now().Add(timeout+time.Millisecond)))
|
||||
return nil, nil
|
||||
})
|
||||
wg.Wait()
|
||||
assert.Nil(t, err)
|
||||
}
|
||||
48
rpcx/internal/serverinterceptors/tracinginterceptor_test.go
Normal file
48
rpcx/internal/serverinterceptors/tracinginterceptor_test.go
Normal file
@@ -0,0 +1,48 @@
|
||||
package serverinterceptors
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/tal-tech/go-zero/core/trace/tracespec"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/metadata"
|
||||
)
|
||||
|
||||
func TestUnaryTracingInterceptor(t *testing.T) {
|
||||
interceptor := UnaryTracingInterceptor("foo")
|
||||
var run int32
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(1)
|
||||
_, err := interceptor(context.Background(), nil, &grpc.UnaryServerInfo{
|
||||
FullMethod: "/",
|
||||
}, func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
defer wg.Done()
|
||||
atomic.AddInt32(&run, 1)
|
||||
return nil, nil
|
||||
})
|
||||
wg.Wait()
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, int32(1), atomic.LoadInt32(&run))
|
||||
}
|
||||
|
||||
func TestUnaryTracingInterceptor_GrpcFormat(t *testing.T) {
|
||||
interceptor := UnaryTracingInterceptor("foo")
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(1)
|
||||
var md metadata.MD
|
||||
ctx := metadata.NewIncomingContext(context.Background(), md)
|
||||
_, err := interceptor(ctx, nil, &grpc.UnaryServerInfo{
|
||||
FullMethod: "/",
|
||||
}, func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
defer wg.Done()
|
||||
assert.True(t, len(ctx.Value(tracespec.TracingKey).(tracespec.Trace).TraceId()) > 0)
|
||||
assert.True(t, len(ctx.Value(tracespec.TracingKey).(tracespec.Trace).SpanId()) > 0)
|
||||
return nil, nil
|
||||
})
|
||||
wg.Wait()
|
||||
assert.Nil(t, err)
|
||||
}
|
||||
@@ -8,7 +8,8 @@ import (
|
||||
)
|
||||
|
||||
func BuildDirectTarget(endpoints []string) string {
|
||||
return fmt.Sprintf("%s:///%s", resolver.DirectScheme, strings.Join(endpoints, resolver.EndpointSep))
|
||||
return fmt.Sprintf("%s:///%s", resolver.DirectScheme,
|
||||
strings.Join(endpoints, resolver.EndpointSep))
|
||||
}
|
||||
|
||||
func BuildDiscovTarget(endpoints []string, key string) string {
|
||||
|
||||
17
rpcx/internal/target_test.go
Normal file
17
rpcx/internal/target_test.go
Normal file
@@ -0,0 +1,17 @@
|
||||
package internal
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestBuildDirectTarget(t *testing.T) {
|
||||
target := BuildDirectTarget([]string{"localhost:123", "localhost:456"})
|
||||
assert.Equal(t, "direct:///localhost:123,localhost:456", target)
|
||||
}
|
||||
|
||||
func TestBuildDiscovTarget(t *testing.T) {
|
||||
target := BuildDiscovTarget([]string{"localhost:123", "localhost:456"}, "foo")
|
||||
assert.Equal(t, "discov://localhost:123,localhost:456/foo", target)
|
||||
}
|
||||
@@ -18,7 +18,7 @@ type RpcProxy struct {
|
||||
lock sync.Mutex
|
||||
}
|
||||
|
||||
func NewRpcProxy(backend string, opts ...internal.ClientOption) *RpcProxy {
|
||||
func NewProxy(backend string, opts ...internal.ClientOption) *RpcProxy {
|
||||
return &RpcProxy{
|
||||
backend: backend,
|
||||
clients: make(map[string]Client),
|
||||
@@ -56,5 +56,5 @@ func (p *RpcProxy) TakeConn(ctx context.Context) (*grpc.ClientConn, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return val.(*RpcClient).Conn(), nil
|
||||
return val.(Client).Conn(), nil
|
||||
}
|
||||
|
||||
66
rpcx/proxy_test.go
Normal file
66
rpcx/proxy_test.go
Normal file
@@ -0,0 +1,66 @@
|
||||
package rpcx
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/tal-tech/go-zero/rpcx/internal/mock"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
func TestProxy(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
amount float32
|
||||
res *mock.DepositResponse
|
||||
errCode codes.Code
|
||||
errMsg string
|
||||
}{
|
||||
{
|
||||
"invalid request with negative amount",
|
||||
-1.11,
|
||||
nil,
|
||||
codes.InvalidArgument,
|
||||
fmt.Sprintf("cannot deposit %v", -1.11),
|
||||
},
|
||||
{
|
||||
"valid request with non negative amount",
|
||||
0.00,
|
||||
&mock.DepositResponse{Ok: true},
|
||||
codes.OK,
|
||||
"",
|
||||
},
|
||||
}
|
||||
|
||||
proxy := NewProxy("foo", WithDialOption(grpc.WithInsecure()),
|
||||
WithDialOption(grpc.WithContextDialer(dialer())))
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
conn, err := proxy.TakeConn(context.Background())
|
||||
assert.Nil(t, err)
|
||||
cli := mock.NewDepositServiceClient(conn)
|
||||
request := &mock.DepositRequest{Amount: tt.amount}
|
||||
response, err := cli.Deposit(context.Background(), request)
|
||||
if response != nil {
|
||||
assert.True(t, len(response.String()) > 0)
|
||||
if response.GetOk() != tt.res.GetOk() {
|
||||
t.Error("response: expected", tt.res.GetOk(), "received", response.GetOk())
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
if e, ok := status.FromError(err); ok {
|
||||
if e.Code() != tt.errCode {
|
||||
t.Error("error code: expected", codes.InvalidArgument, "received", e.Code())
|
||||
}
|
||||
if e.Message() != tt.errMsg {
|
||||
t.Error("error message: expected", tt.errMsg, "received", e.Message())
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
`
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -13,9 +13,7 @@ const (
|
||||
configFile = "config.go"
|
||||
configTemplate = `package config
|
||||
|
||||
import (
|
||||
{{.authImport}}
|
||||
)
|
||||
import {{.authImport}}
|
||||
|
||||
type Config struct {
|
||||
rest.RestConf
|
||||
|
||||
@@ -13,15 +13,14 @@ import (
|
||||
const (
|
||||
defaultPort = 8888
|
||||
etcDir = "etc"
|
||||
etcTemplate = `{
|
||||
"Name": "{{.serviceName}}",
|
||||
"Host": "{{.host}}",
|
||||
"Port": {{.port}}
|
||||
}`
|
||||
etcTemplate = `Name: {{.serviceName}}
|
||||
Host: {{.host}}
|
||||
Port: {{.port}}
|
||||
`
|
||||
)
|
||||
|
||||
func genEtc(dir string, api *spec.ApiSpec) error {
|
||||
fp, created, err := util.MaybeCreateFile(dir, etcDir, fmt.Sprintf("%s.json", api.Service.Name))
|
||||
fp, created, err := util.MaybeCreateFile(dir, etcDir, fmt.Sprintf("%s.yaml", api.Service.Name))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"path"
|
||||
"sort"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
@@ -25,7 +24,6 @@ import (
|
||||
|
||||
func {{.handlerName}}(ctx *svc.ServiceContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
l := logic.{{.logic}}(r.Context(), ctx)
|
||||
{{.handlerBody}}
|
||||
}
|
||||
}
|
||||
@@ -40,6 +38,7 @@ func {{.handlerName}}(ctx *svc.ServiceContext) http.HandlerFunc {
|
||||
}
|
||||
`
|
||||
hasRespTemplate = `
|
||||
l := logic.{{.logic}}(r.Context(), ctx)
|
||||
{{.logicResponse}} l.{{.callee}}({{.req}})
|
||||
if err != nil {
|
||||
httpx.Error(w, err)
|
||||
@@ -85,6 +84,7 @@ func genHandler(dir string, group spec.Group, route spec.Route) error {
|
||||
var logicBodyBuilder strings.Builder
|
||||
t := template.Must(template.New("hasRespTemplate").Parse(hasRespTemplate))
|
||||
if err := t.Execute(&logicBodyBuilder, map[string]string{
|
||||
"logic": "New" + strings.TrimSuffix(strings.Title(handler), "Handler") + "Logic",
|
||||
"callee": strings.Title(strings.TrimSuffix(handler, "Handler")),
|
||||
"req": req,
|
||||
"logicResponse": logicResponse,
|
||||
@@ -135,7 +135,6 @@ func doGenToFile(dir, handler string, group spec.Group, route spec.Route, bodyBu
|
||||
t := template.Must(template.New("handlerTemplate").Parse(handlerTemplate))
|
||||
buffer := new(bytes.Buffer)
|
||||
err = t.Execute(buffer, map[string]string{
|
||||
"logic": "New" + strings.TrimSuffix(strings.Title(handler), "Handler") + "Logic",
|
||||
"importPackages": genHandlerImports(group, route, parentPkg),
|
||||
"handlerName": handler,
|
||||
"handlerBody": strings.TrimSpace(bodyBuilder.String()),
|
||||
@@ -162,14 +161,13 @@ func genHandlers(dir string, api *spec.ApiSpec) error {
|
||||
|
||||
func genHandlerImports(group spec.Group, route spec.Route, parentPkg string) string {
|
||||
var imports []string
|
||||
imports = append(imports, fmt.Sprintf("\"%s/rest/httpx\"", vars.ProjectOpenSourceUrl))
|
||||
imports = append(imports, fmt.Sprintf("\"%s\"", util.JoinPackages(parentPkg, contextDir)))
|
||||
if len(route.RequestType.Name) > 0 || len(route.ResponseType.Name) > 0 {
|
||||
imports = append(imports, fmt.Sprintf("\"%s\"", util.JoinPackages(parentPkg, typesDir)))
|
||||
}
|
||||
imports = append(imports, fmt.Sprintf("\"%s\"",
|
||||
util.JoinPackages(parentPkg, getLogicFolderPath(group, route))))
|
||||
sort.Strings(imports)
|
||||
imports = append(imports, fmt.Sprintf("\"%s\"", util.JoinPackages(parentPkg, contextDir)))
|
||||
if len(route.RequestType.Name) > 0 || len(route.ResponseType.Name) > 0 {
|
||||
imports = append(imports, fmt.Sprintf("\"%s\"\n", util.JoinPackages(parentPkg, typesDir)))
|
||||
}
|
||||
imports = append(imports, fmt.Sprintf("\"%s/rest/httpx\"", vars.ProjectOpenSourceUrl))
|
||||
|
||||
return strings.Join(imports, "\n\t")
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ package gogen
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
@@ -21,7 +20,7 @@ import (
|
||||
{{.importPackages}}
|
||||
)
|
||||
|
||||
var configFile = flag.String("f", "etc/{{.serviceName}}.json", "the config file")
|
||||
var configFile = flag.String("f", "etc/{{.serviceName}}.yaml", "the config file")
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
@@ -73,13 +72,11 @@ func genMain(dir string, api *spec.ApiSpec) error {
|
||||
}
|
||||
|
||||
func genMainImports(parentPkg string) string {
|
||||
imports := []string{
|
||||
fmt.Sprintf("\"%s/core/conf\"", vars.ProjectOpenSourceUrl),
|
||||
fmt.Sprintf("\"%s/rest\"", vars.ProjectOpenSourceUrl),
|
||||
}
|
||||
var imports []string
|
||||
imports = append(imports, fmt.Sprintf("\"%s\"", ctlutil.JoinPackages(parentPkg, configDir)))
|
||||
imports = append(imports, fmt.Sprintf("\"%s\"", ctlutil.JoinPackages(parentPkg, handlerDir)))
|
||||
imports = append(imports, fmt.Sprintf("\"%s\"", ctlutil.JoinPackages(parentPkg, contextDir)))
|
||||
sort.Strings(imports)
|
||||
imports = append(imports, fmt.Sprintf("\"%s\"\n", ctlutil.JoinPackages(parentPkg, contextDir)))
|
||||
imports = append(imports, fmt.Sprintf("\"%s/core/conf\"", vars.ProjectOpenSourceUrl))
|
||||
imports = append(imports, fmt.Sprintf("\"%s/rest\"", vars.ProjectOpenSourceUrl))
|
||||
return strings.Join(imports, "\n\t")
|
||||
}
|
||||
|
||||
@@ -131,7 +131,6 @@ func genRoutes(dir string, api *spec.ApiSpec) error {
|
||||
|
||||
func genRouteImports(parentPkg string, api *spec.ApiSpec) string {
|
||||
var importSet = collection.NewSet()
|
||||
importSet.AddStr(fmt.Sprintf("\"%s/rest\"", vars.ProjectOpenSourceUrl))
|
||||
importSet.AddStr(fmt.Sprintf("\"%s\"", util.JoinPackages(parentPkg, contextDir)))
|
||||
for _, group := range api.Service.Groups {
|
||||
for _, route := range group.Routes {
|
||||
@@ -148,7 +147,9 @@ func genRouteImports(parentPkg string, api *spec.ApiSpec) string {
|
||||
}
|
||||
imports := importSet.KeysStr()
|
||||
sort.Strings(imports)
|
||||
return strings.Join(imports, "\n\t")
|
||||
projectSection := strings.Join(imports, "\n\t")
|
||||
depSection := fmt.Sprintf("\"%s/rest\"", vars.ProjectOpenSourceUrl)
|
||||
return fmt.Sprintf("%s\n\n\t%s", projectSection, depSection)
|
||||
}
|
||||
|
||||
func getRoutes(api *spec.ApiSpec) ([]group, error) {
|
||||
|
||||
@@ -20,10 +20,9 @@ type ServiceContext struct {
|
||||
Config {{.config}}
|
||||
}
|
||||
|
||||
func NewServiceContext(config {{.config}}) *ServiceContext {
|
||||
return &ServiceContext{Config: config}
|
||||
func NewServiceContext(c {{.config}}) *ServiceContext {
|
||||
return &ServiceContext{Config: c}
|
||||
}
|
||||
|
||||
`
|
||||
)
|
||||
|
||||
@@ -15,8 +15,6 @@ import (
|
||||
goctlutil "github.com/tal-tech/go-zero/tools/goctl/util"
|
||||
)
|
||||
|
||||
const goModeIdentifier = "go.mod"
|
||||
|
||||
func getParentPackage(dir string) (string, error) {
|
||||
absDir, err := filepath.Abs(dir)
|
||||
if err != nil {
|
||||
@@ -24,36 +22,22 @@ func getParentPackage(dir string) (string, error) {
|
||||
}
|
||||
|
||||
absDir = strings.ReplaceAll(absDir, `\`, `/`)
|
||||
var rootPath string
|
||||
var tempPath = absDir
|
||||
var hasGoMod = false
|
||||
for {
|
||||
if tempPath == filepath.Dir(tempPath) {
|
||||
break
|
||||
}
|
||||
tempPath = filepath.Dir(tempPath)
|
||||
if goctlutil.FileExists(filepath.Join(tempPath, goModeIdentifier)) {
|
||||
tempPath = filepath.Dir(tempPath)
|
||||
rootPath = absDir[len(tempPath)+1:]
|
||||
hasGoMod = true
|
||||
break
|
||||
}
|
||||
if tempPath == string(filepath.Separator) {
|
||||
break
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -9,17 +9,14 @@ import (
|
||||
"github.com/tal-tech/go-zero/tools/goctl/api/spec"
|
||||
)
|
||||
|
||||
const (
|
||||
// struct匹配
|
||||
typeRegex = `(?m)(?m)(^ *type\s+[a-zA-Z][a-zA-Z0-9_-]+\s+(((struct)\s*?\{[\w\W]*?[^\{]\})|([a-zA-Z][a-zA-Z0-9_-]+)))|(^ *type\s*?\([\w\W]+\}\s*\))`
|
||||
)
|
||||
// struct匹配
|
||||
const typeRegex = `(?m)(?m)(^ *type\s+[a-zA-Z][a-zA-Z0-9_-]+\s+(((struct)\s*?\{[\w\W]*?[^\{]\})|([a-zA-Z][a-zA-Z0-9_-]+)))|(^ *type\s*?\([\w\W]+\}\s*\))`
|
||||
|
||||
var (
|
||||
emptyStrcut = errors.New("struct body not found")
|
||||
emptyType spec.Type
|
||||
)
|
||||
|
||||
var emptyType spec.Type
|
||||
|
||||
func GetType(api *spec.ApiSpec, t string) spec.Type {
|
||||
for _, tp := range api.Types {
|
||||
if tp.Name == t {
|
||||
|
||||
@@ -10,26 +10,27 @@ import (
|
||||
"text/template"
|
||||
|
||||
"github.com/logrusorgru/aurora"
|
||||
"github.com/tal-tech/go-zero/tools/goctl/vars"
|
||||
"github.com/tal-tech/go-zero/tools/goctl/util"
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
|
||||
const configTemplate = `package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"{{.import}}"
|
||||
|
||||
"github.com/ghodss/yaml"
|
||||
)
|
||||
|
||||
func main() {
|
||||
var c config.Config
|
||||
template, err := json.MarshalIndent(c, "", " ")
|
||||
template, err := yaml.Marshal(c)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
err = ioutil.WriteFile("config.json", template, os.ModePerm)
|
||||
err = ioutil.WriteFile("config.yaml", template, os.ModePerm)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
@@ -41,9 +42,9 @@ func GenConfigCommand(c *cli.Context) error {
|
||||
if err != nil {
|
||||
return errors.New("abs failed: " + c.String("path"))
|
||||
}
|
||||
xi := strings.Index(path, vars.ProjectName)
|
||||
if xi <= 0 {
|
||||
return errors.New("path should the absolute path of config go file")
|
||||
goModPath, hasFound := util.FindGoModPath(path)
|
||||
if !hasFound {
|
||||
return errors.New("go mod not initial")
|
||||
}
|
||||
path = strings.TrimSuffix(path, "/config.go")
|
||||
location := path + "/tmp"
|
||||
@@ -62,16 +63,28 @@ func GenConfigCommand(c *cli.Context) error {
|
||||
|
||||
t := template.Must(template.New("template").Parse(configTemplate))
|
||||
if err := t.Execute(fp, map[string]string{
|
||||
"import": path[xi:],
|
||||
"import": filepath.Dir(goModPath),
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd := exec.Command("go", "run", goPath)
|
||||
_, err = cmd.Output()
|
||||
gen := exec.Command("go", "run", "config.go")
|
||||
gen.Dir = filepath.Dir(goPath)
|
||||
gen.Stderr = os.Stderr
|
||||
gen.Stdout = os.Stdout
|
||||
err = gen.Run()
|
||||
if err != nil {
|
||||
return err
|
||||
panic(err)
|
||||
}
|
||||
path, err = os.Getwd()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
err = os.Rename(filepath.Dir(goPath)+"/config.yaml", path+"/config.yaml")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
fmt.Println(aurora.Green("Done."))
|
||||
return nil
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
|
||||
26
tools/goctl/gen/filepath.go
Normal file
26
tools/goctl/gen/filepath.go
Normal 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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
`
|
||||
)
|
||||
|
||||
@@ -17,7 +17,8 @@ import (
|
||||
"github.com/tal-tech/go-zero/tools/goctl/configgen"
|
||||
"github.com/tal-tech/go-zero/tools/goctl/docker"
|
||||
"github.com/tal-tech/go-zero/tools/goctl/feature"
|
||||
"github.com/tal-tech/go-zero/tools/goctl/model/sql/command"
|
||||
model "github.com/tal-tech/go-zero/tools/goctl/model/sql/command"
|
||||
rpc "github.com/tal-tech/go-zero/tools/goctl/rpc/command"
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
|
||||
@@ -189,27 +190,114 @@ var (
|
||||
Action: docker.DockerCommand,
|
||||
},
|
||||
{
|
||||
Name: "model",
|
||||
Usage: "generate model code",
|
||||
Flags: []cli.Flag{
|
||||
cli.StringFlag{
|
||||
Name: "src, s",
|
||||
Usage: "the file path of the ddl source file",
|
||||
Name: "rpc",
|
||||
Usage: "generate rpc code",
|
||||
Subcommands: []cli.Command{
|
||||
{
|
||||
Name: "template",
|
||||
Usage: `generate proto template`,
|
||||
Flags: []cli.Flag{
|
||||
cli.StringFlag{
|
||||
Name: "out, o",
|
||||
Usage: "the target path of proto",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "idea",
|
||||
Usage: "whether the command execution environment is from idea plugin. [option]",
|
||||
},
|
||||
},
|
||||
Action: rpc.RpcTemplate,
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "dir, d",
|
||||
Usage: "the target dir",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "cache, c",
|
||||
Usage: "generate code with cache [optional]",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "idea",
|
||||
Usage: "for idea plugin [optional]",
|
||||
{
|
||||
Name: "proto",
|
||||
Usage: `generate rpc from proto`,
|
||||
Flags: []cli.Flag{
|
||||
cli.StringFlag{
|
||||
Name: "src, s",
|
||||
Usage: "the file path of the proto source file",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "dir, d",
|
||||
Usage: `the target path of the code,default path is "${pwd}". [option]`,
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "service, srv",
|
||||
Usage: `the name of rpc service. [option]`,
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "shared",
|
||||
Usage: `the dir of the shared file,default path is "${pwd}/shared. [option]`,
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "idea",
|
||||
Usage: "whether the command execution environment is from idea plugin. [option]",
|
||||
},
|
||||
},
|
||||
Action: rpc.Rpc,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "model",
|
||||
Usage: "generate model code",
|
||||
Subcommands: []cli.Command{
|
||||
{
|
||||
Name: "mysql",
|
||||
Usage: `generate mysql model`,
|
||||
Subcommands: []cli.Command{
|
||||
{
|
||||
Name: "ddl",
|
||||
Usage: `generate mysql model from ddl`,
|
||||
Flags: []cli.Flag{
|
||||
cli.StringFlag{
|
||||
Name: "src, s",
|
||||
Usage: "the file path of the ddl source file",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "dir, d",
|
||||
Usage: "the target dir",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "cache, c",
|
||||
Usage: "generate code with cache [optional]",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "idea",
|
||||
Usage: "for idea plugin [optional]",
|
||||
},
|
||||
},
|
||||
Action: model.MysqlDDL,
|
||||
},
|
||||
{
|
||||
Name: "datasource",
|
||||
Usage: `generate model from datasource`,
|
||||
Flags: []cli.Flag{
|
||||
cli.StringFlag{
|
||||
Name: "url",
|
||||
Usage: `the data source of database,like "root:password@tcp(127.0.0.1:3306)/database`,
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "table, t",
|
||||
Usage: `source table,tables separated by commas,like "user,course`,
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "cache, c",
|
||||
Usage: "generate code with cache [optional]",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "dir, d",
|
||||
Usage: "the target dir",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "idea",
|
||||
Usage: "for idea plugin [optional]",
|
||||
},
|
||||
},
|
||||
Action: model.MyDataSource,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Action: command.Mysql,
|
||||
},
|
||||
{
|
||||
Name: "config",
|
||||
|
||||
10
tools/goctl/model/sql/CHANGELOG.md
Normal file
10
tools/goctl/model/sql/CHANGELOG.md
Normal file
@@ -0,0 +1,10 @@
|
||||
# Change log
|
||||
|
||||
# 2020-08-20
|
||||
* 新增支持通过连接数据库生成model
|
||||
* 支持数据库多表生成
|
||||
* 优化stringx
|
||||
|
||||
# 2020-08-19
|
||||
* 重构model代码生成逻辑
|
||||
* 实现从ddl解析表信息生成代码
|
||||
@@ -4,21 +4,28 @@ goctl model 为go-zero下的工具模块中的组件之一,目前支持识别m
|
||||
|
||||
# 快速开始
|
||||
|
||||
```
|
||||
$ goctl model -src ./sql/user.sql -dir ./model -c true
|
||||
```
|
||||
* 通过ddl生成
|
||||
|
||||
详情用法请参考[example](https://github.com/tal-tech/go-zero/tools/goctl/model/sql/example)
|
||||
```shell script
|
||||
$ goctl model mysql ddl -src="./sql/user.sql" -dir="./sql/model" -c=true
|
||||
```
|
||||
|
||||
执行上述命令后即可快速生成CURD代码。
|
||||
执行上述命令后即可快速生成CURD代码。
|
||||
|
||||
```
|
||||
model
|
||||
│ ├── error.go
|
||||
│ └── usermodel.go
|
||||
```
|
||||
```
|
||||
model
|
||||
│ ├── error.go
|
||||
│ └── usermodel.go
|
||||
```
|
||||
* 通过datasource生成
|
||||
|
||||
```shell script
|
||||
$ goctl model mysql datasource -url="user:password@tcp(127.0.0.1:3306)/database" -table="table1,table2" -dir="./model"
|
||||
```
|
||||
|
||||
|
||||
> 详情用法请参考[example](https://github.com/tal-tech/go-zero/tree/master/tools/goctl/model/sql/example)
|
||||
|
||||
> 注意:这里的目录结构中有usercoursemodel.go目录,在example中我为了体现带cache与不带cache代码的区别,因此将sql文件分别使用了独立的sql文件(user.sql&course.sql),在实际项目开发中你可以将ddl建表语句放在一个sql文件中,`goctl model`会自动解析并分割,最终按照每个ddl建表语句为单位生成独立的go文件。
|
||||
|
||||
* 生成代码示例
|
||||
|
||||
@@ -174,22 +181,22 @@ model
|
||||
# 用法
|
||||
|
||||
```
|
||||
$ goctl model -h
|
||||
$ goctl model mysql -h
|
||||
```
|
||||
|
||||
```
|
||||
NAME:
|
||||
goctl model - generate model code
|
||||
goctl model mysql - generate mysql model"
|
||||
|
||||
USAGE:
|
||||
goctl model [command options] [arguments...]
|
||||
goctl model mysql command [command options] [arguments...]
|
||||
|
||||
COMMANDS:
|
||||
ddl generate mysql model from ddl"
|
||||
datasource generate model from datasource"
|
||||
|
||||
OPTIONS:
|
||||
--src value, -s value the file path of the ddl source file
|
||||
--dir value, -d value the target dir
|
||||
--cache, -c generate code with cache [optional]
|
||||
--idea for idea plugin [optional]
|
||||
|
||||
--help, -h show help
|
||||
```
|
||||
|
||||
# 生成规则
|
||||
@@ -198,22 +205,43 @@ OPTIONS:
|
||||
|
||||
我们默认用户在建表时会创建createTime、updateTime字段(忽略大小写、下划线命名风格)且默认值均为`CURRENT_TIMESTAMP`,而updateTime支持`ON UPDATE CURRENT_TIMESTAMP`,对于这两个字段生成`insert`、`update`时会被移除,不在赋值范畴内,当然,如果你不需要这两个字段那也无大碍。
|
||||
* 带缓存模式
|
||||
|
||||
```
|
||||
$ goctl model -src {filename} -dir {dir} -cache true
|
||||
```
|
||||
* ddl
|
||||
|
||||
```shell script
|
||||
$ goctl model mysql -src={filename} -dir={dir} -cache=true
|
||||
```
|
||||
* datasource
|
||||
|
||||
```shell script
|
||||
$ goctl model mysql datasource -url={datasource} -table={tables} -dir={dir} -cache=true
|
||||
```
|
||||
|
||||
目前仅支持redis缓存,如果选择带缓存模式,即生成的`FindOne(ByXxx)`&`Delete`代码会生成带缓存逻辑的代码,目前仅支持单索引字段(除全文索引外),对于联合索引我们默认认为不需要带缓存,且不属于通用型代码,因此没有放在代码生成行列,如example中user表中的`id`、`name`、`mobile`字段均属于单字段索引。
|
||||
|
||||
* 不带缓存模式
|
||||
|
||||
```
|
||||
$ goctl model -src {filename} -dir {dir}
|
||||
```
|
||||
* ddl
|
||||
|
||||
```shell script
|
||||
$ goctl model -src={filename} -dir={dir}
|
||||
```
|
||||
* datasource
|
||||
|
||||
```shell script
|
||||
$ goctl model mysql datasource -url={datasource} -table={tables} -dir={dir}
|
||||
```
|
||||
or
|
||||
```
|
||||
$ goctl model -src {filename} -dir {dir} -cache false
|
||||
```
|
||||
* ddl
|
||||
|
||||
```shell script
|
||||
$ goctl model -src={filename} -dir={dir} -cache=false
|
||||
```
|
||||
* datasource
|
||||
|
||||
```shell script
|
||||
$ goctl model mysql datasource -url={datasource} -table={tables} -dir={dir} -cache=false
|
||||
```
|
||||
|
||||
生成代码仅基本的CURD结构。
|
||||
|
||||
# 缓存
|
||||
@@ -238,10 +266,6 @@ OPTIONS:
|
||||
|
||||
# QA
|
||||
|
||||
* goctl model支持根据数据库连接后选择表生成代码吗?
|
||||
|
||||
目前暂时不支持,在后面会向这个方向扩展。
|
||||
|
||||
* goctl model除了命令行模式,支持插件模式吗?
|
||||
|
||||
很快支持idea插件。
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user