Compare commits
30 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0b1ee79d3a | ||
|
|
26e16107ce | ||
|
|
1e5e9d63bd | ||
|
|
f994e1df1a | ||
|
|
b5dcadda78 | ||
|
|
df37597ac3 | ||
|
|
68335ada54 | ||
|
|
ecdae2477e | ||
|
|
a561884fcf | ||
|
|
a50bcb90a6 | ||
|
|
e6f8e0e8c3 | ||
|
|
598ff6d0fc | ||
|
|
9a57993e83 | ||
|
|
ee45b0a459 | ||
|
|
2896ef1a49 | ||
|
|
05df86436f | ||
|
|
fb22589cf5 | ||
|
|
a8fb010333 | ||
|
|
8cc09244a0 | ||
|
|
21e811887c | ||
|
|
7f0ec14704 | ||
|
|
d12e9fa2d7 | ||
|
|
ce5961a7d0 | ||
|
|
e1d942a799 | ||
|
|
754e631dc4 | ||
|
|
72aeac3fa9 | ||
|
|
1c3c8f4bbc | ||
|
|
17e6cfb7a9 | ||
|
|
0d151c17f8 | ||
|
|
52990550fb |
1
.github/workflows/go.yml
vendored
@@ -7,7 +7,6 @@ on:
|
||||
branches: [ master ]
|
||||
|
||||
jobs:
|
||||
|
||||
build:
|
||||
name: Build
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
6
.markdownlint.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"MD010": false,
|
||||
"MD013": false,
|
||||
"MD033": false,
|
||||
"MD034": false
|
||||
}
|
||||
@@ -5,12 +5,12 @@ import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/tal-tech/go-zero/core/mathx"
|
||||
"github.com/tal-tech/go-zero/core/proc"
|
||||
"github.com/tal-tech/go-zero/core/stat"
|
||||
"github.com/tal-tech/go-zero/core/stringx"
|
||||
"github.com/tal-tech/go-zero/core/timex"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -195,23 +195,23 @@ type errorWindow struct {
|
||||
|
||||
func (ew *errorWindow) add(reason string) {
|
||||
ew.lock.Lock()
|
||||
ew.reasons[ew.index] = fmt.Sprintf("%s %s", time.Now().Format(timeFormat), reason)
|
||||
ew.reasons[ew.index] = fmt.Sprintf("%s %s", timex.Time().Format(timeFormat), reason)
|
||||
ew.index = (ew.index + 1) % numHistoryReasons
|
||||
ew.count = mathx.MinInt(ew.count+1, numHistoryReasons)
|
||||
ew.lock.Unlock()
|
||||
}
|
||||
|
||||
func (ew *errorWindow) String() string {
|
||||
var builder strings.Builder
|
||||
var reasons []string
|
||||
|
||||
ew.lock.Lock()
|
||||
for i := ew.index + ew.count - 1; i >= ew.index; i-- {
|
||||
builder.WriteString(ew.reasons[i%numHistoryReasons])
|
||||
builder.WriteByte('\n')
|
||||
// reverse order
|
||||
for i := ew.index - 1; i >= ew.index-ew.count; i-- {
|
||||
reasons = append(reasons, ew.reasons[(i+numHistoryReasons)%numHistoryReasons])
|
||||
}
|
||||
ew.lock.Unlock()
|
||||
|
||||
return builder.String()
|
||||
return strings.Join(reasons, "\n")
|
||||
}
|
||||
|
||||
type promiseWithReason struct {
|
||||
|
||||
@@ -2,7 +2,9 @@ package breaker
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
@@ -33,6 +35,84 @@ func TestLogReason(t *testing.T) {
|
||||
assert.Equal(t, numHistoryReasons, errs.count)
|
||||
}
|
||||
|
||||
func TestErrorWindow(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
reasons []string
|
||||
}{
|
||||
{
|
||||
name: "no error",
|
||||
},
|
||||
{
|
||||
name: "one error",
|
||||
reasons: []string{"foo"},
|
||||
},
|
||||
{
|
||||
name: "two errors",
|
||||
reasons: []string{"foo", "bar"},
|
||||
},
|
||||
{
|
||||
name: "five errors",
|
||||
reasons: []string{"first", "second", "third", "fourth", "fifth"},
|
||||
},
|
||||
{
|
||||
name: "six errors",
|
||||
reasons: []string{"first", "second", "third", "fourth", "fifth", "sixth"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
var ew errorWindow
|
||||
for _, reason := range test.reasons {
|
||||
ew.add(reason)
|
||||
}
|
||||
var reasons []string
|
||||
if len(test.reasons) > numHistoryReasons {
|
||||
reasons = test.reasons[len(test.reasons)-numHistoryReasons:]
|
||||
} else {
|
||||
reasons = test.reasons
|
||||
}
|
||||
for _, reason := range reasons {
|
||||
assert.True(t, strings.Contains(ew.String(), reason), fmt.Sprintf("actual: %s", ew.String()))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPromiseWithReason(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
reason string
|
||||
expect string
|
||||
}{
|
||||
{
|
||||
name: "success",
|
||||
},
|
||||
{
|
||||
name: "success",
|
||||
reason: "fail",
|
||||
expect: "fail",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
promise := promiseWithReason{
|
||||
promise: new(mockedPromise),
|
||||
errWin: new(errorWindow),
|
||||
}
|
||||
if len(test.reason) == 0 {
|
||||
promise.Accept()
|
||||
} else {
|
||||
promise.Reject(test.reason)
|
||||
}
|
||||
|
||||
assert.True(t, strings.Contains(promise.errWin.String(), test.expect))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkGoogleBreaker(b *testing.B) {
|
||||
br := NewBreaker()
|
||||
for i := 0; i < b.N; i++ {
|
||||
@@ -41,3 +121,12 @@ func BenchmarkGoogleBreaker(b *testing.B) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type mockedPromise struct {
|
||||
}
|
||||
|
||||
func (m *mockedPromise) Accept() {
|
||||
}
|
||||
|
||||
func (m *mockedPromise) Reject() {
|
||||
}
|
||||
|
||||
@@ -79,7 +79,7 @@ func Map(generate GenerateFunc, mapper MapFunc, opts ...Option) chan interface{}
|
||||
collector := make(chan interface{}, options.workers)
|
||||
done := syncx.NewDoneChan()
|
||||
|
||||
go mapDispatcher(mapper, source, collector, done.Done(), options.workers)
|
||||
go executeMappers(mapper, source, collector, done.Done(), options.workers)
|
||||
|
||||
return collector
|
||||
}
|
||||
@@ -124,8 +124,12 @@ func MapReduceWithSource(source <-chan interface{}, mapper MapperFunc, reducer R
|
||||
}
|
||||
}()
|
||||
reducer(collector, writer, cancel)
|
||||
drain(collector)
|
||||
}()
|
||||
go mapperDispatcher(mapper, source, collector, done.Done(), cancel, options.workers)
|
||||
|
||||
go executeMappers(func(item interface{}, w Writer) {
|
||||
mapper(item, w, cancel)
|
||||
}, source, collector, done.Done(), options.workers)
|
||||
|
||||
value, ok := <-output
|
||||
if err := retErr.Load(); err != nil {
|
||||
@@ -140,6 +144,7 @@ func MapReduceWithSource(source <-chan interface{}, mapper MapperFunc, reducer R
|
||||
func MapReduceVoid(generator GenerateFunc, mapper MapperFunc, reducer VoidReducerFunc, opts ...Option) error {
|
||||
_, err := MapReduce(generator, mapper, func(input <-chan interface{}, writer Writer, cancel func(error)) {
|
||||
reducer(input, cancel)
|
||||
drain(input)
|
||||
// We need to write a placeholder to let MapReduce to continue on reducer done,
|
||||
// otherwise, all goroutines are waiting. The placeholder will be discarded by MapReduce.
|
||||
writer.Write(lang.Placeholder)
|
||||
@@ -224,20 +229,6 @@ func executeMappers(mapper MapFunc, input <-chan interface{}, collector chan<- i
|
||||
}
|
||||
}
|
||||
|
||||
func mapDispatcher(mapper MapFunc, input <-chan interface{}, collector chan<- interface{},
|
||||
done <-chan lang.PlaceholderType, workers int) {
|
||||
executeMappers(func(item interface{}, writer Writer) {
|
||||
mapper(item, writer)
|
||||
}, input, collector, done, workers)
|
||||
}
|
||||
|
||||
func mapperDispatcher(mapper MapperFunc, input <-chan interface{}, collector chan<- interface{},
|
||||
done <-chan lang.PlaceholderType, cancel func(error), workers int) {
|
||||
executeMappers(func(item interface{}, writer Writer) {
|
||||
mapper(item, writer, cancel)
|
||||
}, input, collector, done, workers)
|
||||
}
|
||||
|
||||
func newOptions() *mapReduceOptions {
|
||||
return &mapReduceOptions{
|
||||
workers: defaultWorkers,
|
||||
|
||||
@@ -32,7 +32,7 @@ func AddWrapUpListener(fn func()) (waitForCalled func()) {
|
||||
return wrapUpListeners.addListener(fn)
|
||||
}
|
||||
|
||||
func SetTimeoutToForceQuit(duration time.Duration) {
|
||||
func SetTimeToForceQuit(duration time.Duration) {
|
||||
delayTimeBeforeForceQuit = duration
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
English | [简体中文](bookstore.md)
|
||||
|
||||
# Rapid development of microservices - multiple RPCs
|
||||
|
||||
## 0. Why building microservices are so difficult?
|
||||
English | [简体中文](bookstore.md)
|
||||
|
||||
## 0. Why building microservices are so difficult
|
||||
|
||||
To build a well working microservice, we need lots of knowledges from different aspects.
|
||||
|
||||
@@ -25,7 +25,7 @@ As well, we always adhere to the idea that **prefer tools over conventions and d
|
||||
|
||||
Let’s take the shorturl microservice as a quick example to demonstrate how to quickly create microservices by using [go-zero](https://github.com/tal-tech/go-zero). After finishing this tutorial, you’ll find that it’s so easy to write microservices!
|
||||
|
||||
## 1. What is a bookstore service?
|
||||
## 1. What is a bookstore service
|
||||
|
||||
For simplicity, the bookstore service only contains two functionalities, adding books and quering prices.
|
||||
|
||||
@@ -71,7 +71,7 @@ And now, let’s walk through the complete flow of quickly create a microservice
|
||||
|
||||
* use goctl to generate `api/bookstore.api`
|
||||
|
||||
```
|
||||
```Plain Text
|
||||
goctl api -o bookstore.api
|
||||
```
|
||||
|
||||
@@ -128,7 +128,7 @@ And now, let’s walk through the complete flow of quickly create a microservice
|
||||
|
||||
the generated file structure looks like:
|
||||
|
||||
```
|
||||
```Plain Text
|
||||
api
|
||||
├── bookstore.api // api definition
|
||||
├── bookstore.go // main entrance
|
||||
@@ -188,13 +188,13 @@ And now, let’s walk through the complete flow of quickly create a microservice
|
||||
* under directory `rpc/add` create `add.proto` file
|
||||
|
||||
```shell
|
||||
goctl rpc template -o add.proto
|
||||
goctl rpc template -o add.proto
|
||||
```
|
||||
|
||||
edit the file and make the code looks like:
|
||||
|
||||
```protobuf
|
||||
syntax = "proto3";
|
||||
syntax = "proto3";
|
||||
|
||||
package add;
|
||||
|
||||
@@ -220,7 +220,7 @@ syntax = "proto3";
|
||||
|
||||
the generated file structure looks like:
|
||||
|
||||
```
|
||||
```Plain Text
|
||||
rpc/add
|
||||
├── add.go // rpc main entrance
|
||||
├── add.proto // rpc definition
|
||||
@@ -242,7 +242,6 @@ syntax = "proto3";
|
||||
└── pb
|
||||
└── add.pb.go
|
||||
```
|
||||
|
||||
|
||||
just run it, looks like:
|
||||
|
||||
@@ -258,13 +257,13 @@ you can change the listening port in file `etc/add.yaml`.
|
||||
* under directory `rpc/check` create `check.proto` file
|
||||
|
||||
```shell
|
||||
goctl rpc template -o check.proto
|
||||
goctl rpc template -o check.proto
|
||||
```
|
||||
|
||||
edit the file and make the code looks like:
|
||||
|
||||
```protobuf
|
||||
syntax = "proto3";
|
||||
syntax = "proto3";
|
||||
|
||||
package check;
|
||||
|
||||
@@ -290,7 +289,7 @@ syntax = "proto3";
|
||||
|
||||
the generated file structure looks like:
|
||||
|
||||
```
|
||||
```Plain Text
|
||||
rpc/check
|
||||
├── check.go // rpc main entrance
|
||||
├── check.proto // rpc definition
|
||||
@@ -315,7 +314,7 @@ syntax = "proto3";
|
||||
|
||||
you can change the listening port in `etc/check.yaml`.
|
||||
|
||||
we need to change the port in `etc/check.yaml` to `8081`, because `8080 ` is used by `add` service.
|
||||
we need to change the port in `etc/check.yaml` to `8081`, because `8080` is used by `add` service.
|
||||
|
||||
just run it, looks like:
|
||||
|
||||
@@ -348,8 +347,8 @@ syntax = "proto3";
|
||||
```go
|
||||
type Config struct {
|
||||
rest.RestConf
|
||||
Add rpcx.RpcClientConf // manual code
|
||||
Check rpcx.RpcClientConf // manual code
|
||||
Add zrpc.RpcClientConf // manual code
|
||||
Check zrpc.RpcClientConf // manual code
|
||||
}
|
||||
```
|
||||
|
||||
@@ -365,8 +364,8 @@ syntax = "proto3";
|
||||
func NewServiceContext(c config.Config) *ServiceContext {
|
||||
return &ServiceContext{
|
||||
Config: c,
|
||||
Adder: adder.NewAdder(rpcx.MustNewClient(c.Add)), // manual code
|
||||
Checker: checker.NewChecker(rpcx.MustNewClient(c.Check)), // manual code
|
||||
Adder: adder.NewAdder(zrpc.MustNewClient(c.Add)), // manual code
|
||||
Checker: checker.NewChecker(zrpc.MustNewClient(c.Check)), // manual code
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -444,7 +443,7 @@ Till now, we’ve done the modification of API Gateway. All the manually added c
|
||||
source book.sql;
|
||||
```
|
||||
|
||||
* under the directory `rpc/model execute the following command to genrate CRUD+cache code, `-c` means using `redis cache`
|
||||
* under the directory `rpc/model` execute the following command to genrate CRUD+cache code, `-c` means using `redis cache`
|
||||
|
||||
```shell
|
||||
goctl model mysql ddl -c -src book.sql -dir .
|
||||
@@ -454,7 +453,7 @@ Till now, we’ve done the modification of API Gateway. All the manually added c
|
||||
|
||||
the generated file structure looks like:
|
||||
|
||||
```
|
||||
```Plain Text
|
||||
rpc/model
|
||||
├── bookstore.sql
|
||||
├── bookstoremodel.go // CRUD+cache code
|
||||
@@ -478,7 +477,7 @@ Till now, we’ve done the modification of API Gateway. All the manually added c
|
||||
|
||||
```go
|
||||
type Config struct {
|
||||
rpcx.RpcServerConf
|
||||
zrpc.RpcServerConf
|
||||
DataSource string // manual code
|
||||
Table string // manual code
|
||||
Cache cache.CacheConf // manual code
|
||||
@@ -614,4 +613,4 @@ We not only keep the framework simple, but also encapsulate the complexity into
|
||||
|
||||
For the generated code by goctl, lots of microservice components are included, like concurrency control, adaptive circuit breaker, adaptive load shedding, auto cache control etc. And it’s easy to deal with the busy sites.
|
||||
|
||||
If you have any ideas that can help us to improve the productivity, tell me any time! 👏
|
||||
If you have any ideas that can help us to improve the productivity, tell me any time! 👏
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
[English](bookstore-en.md) | 简体中文
|
||||
|
||||
# 快速构建微服务-多RPC版
|
||||
|
||||
## 0. 为什么说做好微服务很难?
|
||||
[English](bookstore-en.md) | 简体中文
|
||||
|
||||
## 0. 为什么说做好微服务很难
|
||||
|
||||
要想做好微服务,我们需要理解和掌握的知识点非常多,从几个维度上来说:
|
||||
|
||||
@@ -71,7 +71,7 @@
|
||||
|
||||
* 在`bookstore/api`目录下通过goctl生成`api/bookstore.api`:
|
||||
|
||||
```
|
||||
```bash
|
||||
goctl api -o bookstore.api
|
||||
```
|
||||
|
||||
@@ -128,7 +128,7 @@
|
||||
|
||||
生成的文件结构如下:
|
||||
|
||||
```
|
||||
```Plain Text
|
||||
api
|
||||
├── bookstore.api // api定义
|
||||
├── bookstore.go // main入口定义
|
||||
@@ -222,7 +222,7 @@
|
||||
|
||||
文件结构如下:
|
||||
|
||||
```
|
||||
```Plain Text
|
||||
rpc/add
|
||||
├── add.go // rpc服务main函数
|
||||
├── add.proto // rpc接口定义
|
||||
@@ -244,7 +244,6 @@
|
||||
└── pb
|
||||
└── add.pb.go
|
||||
```
|
||||
|
||||
|
||||
直接可以运行,如下:
|
||||
|
||||
@@ -294,7 +293,7 @@
|
||||
|
||||
文件结构如下:
|
||||
|
||||
```
|
||||
```Plain Text
|
||||
rpc/check
|
||||
├── check.go // rpc服务main函数
|
||||
├── check.proto // rpc接口定义
|
||||
@@ -350,8 +349,8 @@
|
||||
```go
|
||||
type Config struct {
|
||||
rest.RestConf
|
||||
Add rpcx.RpcClientConf // 手动代码
|
||||
Check rpcx.RpcClientConf // 手动代码
|
||||
Add zrpc.RpcClientConf // 手动代码
|
||||
Check zrpc.RpcClientConf // 手动代码
|
||||
}
|
||||
```
|
||||
|
||||
@@ -367,8 +366,8 @@
|
||||
func NewServiceContext(c config.Config) *ServiceContext {
|
||||
return &ServiceContext{
|
||||
Config: c,
|
||||
Adder: adder.NewAdder(rpcx.MustNewClient(c.Add)), // 手动代码
|
||||
Checker: checker.NewChecker(rpcx.MustNewClient(c.Check)), // 手动代码
|
||||
Adder: adder.NewAdder(zrpc.MustNewClient(c.Add)), // 手动代码
|
||||
Checker: checker.NewChecker(zrpc.MustNewClient(c.Check)), // 手动代码
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -454,7 +453,7 @@
|
||||
|
||||
生成后的文件结构如下:
|
||||
|
||||
```
|
||||
```Plain Text
|
||||
rpc/model
|
||||
├── bookstore.sql
|
||||
├── bookstoremodel.go // CRUD+cache代码
|
||||
@@ -478,7 +477,7 @@
|
||||
|
||||
```go
|
||||
type Config struct {
|
||||
rpcx.RpcServerConf
|
||||
zrpc.RpcServerConf
|
||||
DataSource string // 手动代码
|
||||
Table string // 手动代码
|
||||
Cache cache.CacheConf // 手动代码
|
||||
@@ -615,4 +614,3 @@ go-zero不只是一个框架,更是一个建立在框架+工具基础上的,
|
||||
通过go-zero+goctl生成的代码,包含了微服务治理的各种组件,包括:并发控制、自适应熔断、自适应降载、自动缓存控制等,可以轻松部署以承载巨大访问量。
|
||||
|
||||
有任何好的提升工程效率的想法,随时欢迎交流!👏
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
# 熔断机制设计
|
||||
|
||||
## 设计目的
|
||||
* 依赖的服务出现大规模故障时,调用方应该尽可能少调用,降低故障服务的压力,使之尽快恢复服务
|
||||
|
||||
* 依赖的服务出现大规模故障时,调用方应该尽可能少调用,降低故障服务的压力,使之尽快恢复服务
|
||||
|
||||
111
doc/collection.md
Normal file
@@ -0,0 +1,111 @@
|
||||
# 通过 collection.Cache 进行缓存
|
||||
|
||||
go-zero微服务框架中提供了许多开箱即用的工具,好的工具不仅能提升服务的性能而且还能提升代码的鲁棒性避免出错,实现代码风格的统一方便他人阅读等等,本系列文章将分别介绍go-zero框架中工具的使用及其实现原理
|
||||
|
||||
## 进程内缓存工具[collection.Cache](https://github.com/tal-tech/go-zero/tree/master/core/collection/cache.go)
|
||||
|
||||
在做服务器开发的时候,相信都会遇到使用缓存的情况,go-zero 提供的简单的缓存封装 **collection.Cache**,简单使用方式如下
|
||||
|
||||
```go
|
||||
// 初始化 cache,其中 WithLimit 可以指定最大缓存的数量
|
||||
c, err := collection.NewCache(time.Minute, collection.WithLimit(10000))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// 设置缓存
|
||||
c.Set("key", user)
|
||||
|
||||
// 获取缓存,ok:是否存在
|
||||
v, ok := c.Get("key")
|
||||
|
||||
// 删除缓存
|
||||
c.Del("key")
|
||||
|
||||
// 获取缓存,如果 key 不存在的,则会调用 func 去生成缓存
|
||||
v, err := c.Take("key", func() (interface{}, error) {
|
||||
return user, nil
|
||||
})
|
||||
```
|
||||
|
||||
cache 实现的建的功能包括
|
||||
|
||||
* 缓存自动失效,可以指定过期时间
|
||||
* 缓存大小限制,可以指定缓存个数
|
||||
* 缓存增删改
|
||||
* 缓存命中率统计
|
||||
* 并发安全
|
||||
* 缓存击穿
|
||||
|
||||
实现原理:
|
||||
Cache 自动失效,是采用 TimingWheel(https://github.com/tal-tech/go-zero/blob/master/core/collection/timingwheel.go) 进行管理的
|
||||
|
||||
``` go
|
||||
timingWheel, err := NewTimingWheel(time.Second, slots, func(k, v interface{}) {
|
||||
key, ok := k.(string)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
cache.Del(key)
|
||||
})
|
||||
```
|
||||
|
||||
Cache 大小限制,是采用 LRU 淘汰策略,在新增缓存的时候会去检查是否已经超出过限制,具体代码在 keyLru 中实现
|
||||
|
||||
``` go
|
||||
func (klru *keyLru) add(key string) {
|
||||
if elem, ok := klru.elements[key]; ok {
|
||||
klru.evicts.MoveToFront(elem)
|
||||
return
|
||||
}
|
||||
|
||||
// Add new item
|
||||
elem := klru.evicts.PushFront(key)
|
||||
klru.elements[key] = elem
|
||||
|
||||
// Verify size not exceeded
|
||||
if klru.evicts.Len() > klru.limit {
|
||||
klru.removeOldest()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Cache 的命中率统计,是在代码中实现 cacheStat,在缓存命中丢失的时候自动统计,并且会定时打印使用的命中率, qps 等状态.
|
||||
|
||||
打印的具体效果如下
|
||||
|
||||
```go
|
||||
cache(proc) - qpm: 2, hit_ratio: 50.0%, elements: 0, hit: 1, miss: 1
|
||||
```
|
||||
|
||||
缓存击穿包含是使用 syncx.SharedCalls(https://github.com/tal-tech/go-zero/blob/master/core/syncx/sharedcalls.go) 进行实现的,就是将同时请求同一个 key 的请求, 关于 sharedcalls 后续会继续补充。 相关具体实现是在:
|
||||
|
||||
```go
|
||||
func (c *Cache) Take(key string, fetch func() (interface{}, error)) (interface{}, error) {
|
||||
val, fresh, err := c.barrier.DoEx(key, func() (interface{}, error) {
|
||||
v, e := fetch()
|
||||
if e != nil {
|
||||
return nil, e
|
||||
}
|
||||
|
||||
c.Set(key, v)
|
||||
return v, nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if fresh {
|
||||
c.stats.IncrementMiss()
|
||||
return val, nil
|
||||
} else {
|
||||
// got the result from previous ongoing query
|
||||
c.stats.IncrementHit()
|
||||
}
|
||||
|
||||
return val, nil
|
||||
}
|
||||
```
|
||||
|
||||
本文主要介绍了go-zero框架中的 Cache 工具,在实际的项目中非常实用。用好工具对于提升服务性能和开发效率都有很大的帮助,希望本篇文章能给大家带来一些收获。
|
||||
272
doc/goctl-model-sql.md
Normal file
@@ -0,0 +1,272 @@
|
||||
# Goctl Model
|
||||
|
||||
goctl model 为go-zero下的工具模块中的组件之一,目前支持识别mysql ddl进行model层代码生成,通过命令行或者idea插件(即将支持)可以有选择地生成带redis cache或者不带redis cache的代码逻辑。
|
||||
|
||||
## 快速开始
|
||||
|
||||
* 通过ddl生成
|
||||
|
||||
```shell script
|
||||
goctl model mysql ddl -src="./sql/user.sql" -dir="./sql/model" -c=true
|
||||
```
|
||||
|
||||
执行上述命令后即可快速生成CURD代码。
|
||||
|
||||
```Plain Text
|
||||
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"
|
||||
```
|
||||
|
||||
* 生成代码示例
|
||||
|
||||
``` go
|
||||
package model
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/tal-tech/go-zero/core/stores/cache"
|
||||
"github.com/tal-tech/go-zero/core/stores/sqlc"
|
||||
"github.com/tal-tech/go-zero/core/stores/sqlx"
|
||||
"github.com/tal-tech/go-zero/core/stringx"
|
||||
"github.com/tal-tech/go-zero/tools/goctl/model/sql/builderx"
|
||||
)
|
||||
|
||||
var (
|
||||
userFieldNames = builderx.FieldNames(&User{})
|
||||
userRows = strings.Join(userFieldNames, ",")
|
||||
userRowsExpectAutoSet = strings.Join(stringx.Remove(userFieldNames, "id", "create_time", "update_time"), ",")
|
||||
userRowsWithPlaceHolder = strings.Join(stringx.Remove(userFieldNames, "id", "create_time", "update_time"), "=?,") + "=?"
|
||||
|
||||
cacheUserMobilePrefix = "cache#User#mobile#"
|
||||
cacheUserIdPrefix = "cache#User#id#"
|
||||
cacheUserNamePrefix = "cache#User#name#"
|
||||
)
|
||||
|
||||
type (
|
||||
UserModel struct {
|
||||
sqlc.CachedConn
|
||||
table string
|
||||
}
|
||||
|
||||
User struct {
|
||||
Id int64 `db:"id"`
|
||||
Name string `db:"name"` // 用户名称
|
||||
Password string `db:"password"` // 用户密码
|
||||
Mobile string `db:"mobile"` // 手机号
|
||||
Gender string `db:"gender"` // 男|女|未公开
|
||||
Nickname string `db:"nickname"` // 用户昵称
|
||||
CreateTime time.Time `db:"create_time"`
|
||||
UpdateTime time.Time `db:"update_time"`
|
||||
}
|
||||
)
|
||||
|
||||
func NewUserModel(conn sqlx.SqlConn, c cache.CacheConf, table string) *UserModel {
|
||||
return &UserModel{
|
||||
CachedConn: sqlc.NewConn(conn, c),
|
||||
table: table,
|
||||
}
|
||||
}
|
||||
|
||||
func (m *UserModel) Insert(data User) (sql.Result, error) {
|
||||
query := `insert into ` + m.table + `(` + userRowsExpectAutoSet + `) value (?, ?, ?, ?, ?)`
|
||||
return m.ExecNoCache(query, data.Name, data.Password, data.Mobile, data.Gender, data.Nickname)
|
||||
}
|
||||
|
||||
func (m *UserModel) FindOne(id int64) (*User, error) {
|
||||
userIdKey := fmt.Sprintf("%s%v", cacheUserIdPrefix, id)
|
||||
var resp User
|
||||
err := m.QueryRow(&resp, userIdKey, func(conn sqlx.SqlConn, v interface{}) error {
|
||||
query := `select ` + userRows + ` from ` + m.table + ` where id = ? limit 1`
|
||||
return conn.QueryRow(v, query, id)
|
||||
})
|
||||
switch err {
|
||||
case nil:
|
||||
return &resp, nil
|
||||
case sqlc.ErrNotFound:
|
||||
return nil, ErrNotFound
|
||||
default:
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
func (m *UserModel) FindOneByName(name string) (*User, error) {
|
||||
userNameKey := fmt.Sprintf("%s%v", cacheUserNamePrefix, name)
|
||||
var resp User
|
||||
err := m.QueryRowIndex(&resp, userNameKey, func(primary interface{}) string {
|
||||
return fmt.Sprintf("%s%v", cacheUserIdPrefix, primary)
|
||||
}, func(conn sqlx.SqlConn, v interface{}) (i interface{}, e error) {
|
||||
query := `select ` + userRows + ` from ` + m.table + ` where name = ? limit 1`
|
||||
if err := conn.QueryRow(&resp, query, name); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return resp.Id, nil
|
||||
}, func(conn sqlx.SqlConn, v, primary interface{}) error {
|
||||
query := `select ` + userRows + ` from ` + m.table + ` where id = ? limit 1`
|
||||
return conn.QueryRow(v, query, primary)
|
||||
})
|
||||
switch err {
|
||||
case nil:
|
||||
return &resp, nil
|
||||
case sqlc.ErrNotFound:
|
||||
return nil, ErrNotFound
|
||||
default:
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
func (m *UserModel) FindOneByMobile(mobile string) (*User, error) {
|
||||
userMobileKey := fmt.Sprintf("%s%v", cacheUserMobilePrefix, mobile)
|
||||
var resp User
|
||||
err := m.QueryRowIndex(&resp, userMobileKey, func(primary interface{}) string {
|
||||
return fmt.Sprintf("%s%v", cacheUserIdPrefix, primary)
|
||||
}, func(conn sqlx.SqlConn, v interface{}) (i interface{}, e error) {
|
||||
query := `select ` + userRows + ` from ` + m.table + ` where mobile = ? limit 1`
|
||||
if err := conn.QueryRow(&resp, query, mobile); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return resp.Id, nil
|
||||
}, func(conn sqlx.SqlConn, v, primary interface{}) error {
|
||||
query := `select ` + userRows + ` from ` + m.table + ` where id = ? limit 1`
|
||||
return conn.QueryRow(v, query, primary)
|
||||
})
|
||||
switch err {
|
||||
case nil:
|
||||
return &resp, nil
|
||||
case sqlc.ErrNotFound:
|
||||
return nil, ErrNotFound
|
||||
default:
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
func (m *UserModel) Update(data User) error {
|
||||
userIdKey := fmt.Sprintf("%s%v", cacheUserIdPrefix, data.Id)
|
||||
_, err := m.Exec(func(conn sqlx.SqlConn) (result sql.Result, err error) {
|
||||
query := `update ` + m.table + ` set ` + userRowsWithPlaceHolder + ` where id = ?`
|
||||
return conn.Exec(query, data.Name, data.Password, data.Mobile, data.Gender, data.Nickname, data.Id)
|
||||
}, userIdKey)
|
||||
return err
|
||||
}
|
||||
|
||||
func (m *UserModel) Delete(id int64) error {
|
||||
data, err := m.FindOne(id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
userIdKey := fmt.Sprintf("%s%v", cacheUserIdPrefix, id)
|
||||
userNameKey := fmt.Sprintf("%s%v", cacheUserNamePrefix, data.Name)
|
||||
userMobileKey := fmt.Sprintf("%s%v", cacheUserMobilePrefix, data.Mobile)
|
||||
_, err = m.Exec(func(conn sqlx.SqlConn) (result sql.Result, err error) {
|
||||
query := `delete from ` + m.table + ` where id = ?`
|
||||
return conn.Exec(query, id)
|
||||
}, userIdKey, userNameKey, userMobileKey)
|
||||
return err
|
||||
}
|
||||
```
|
||||
|
||||
### 用法
|
||||
|
||||
```Plain Text
|
||||
goctl model mysql -h
|
||||
```
|
||||
|
||||
```Plain Text
|
||||
NAME:
|
||||
goctl model mysql - generate mysql model"
|
||||
|
||||
USAGE:
|
||||
goctl model mysql command [command options] [arguments...]
|
||||
|
||||
COMMANDS:
|
||||
ddl generate mysql model from ddl"
|
||||
datasource generate model from datasource"
|
||||
|
||||
OPTIONS:
|
||||
--help, -h show help
|
||||
```
|
||||
|
||||
## 生成规则
|
||||
|
||||
* 默认规则
|
||||
|
||||
我们默认用户在建表时会创建createTime、updateTime字段(忽略大小写、下划线命名风格)且默认值均为`CURRENT_TIMESTAMP`,而updateTime支持`ON UPDATE CURRENT_TIMESTAMP`,对于这两个字段生成`insert`、`update`时会被移除,不在赋值范畴内,当然,如果你不需要这两个字段那也无大碍。
|
||||
* 带缓存模式
|
||||
* 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`字段均属于单字段索引。
|
||||
|
||||
* 不带缓存模式
|
||||
|
||||
* ddl
|
||||
|
||||
```shell script
|
||||
goctl model -src={filename} -dir={dir}
|
||||
```
|
||||
|
||||
* datasource
|
||||
|
||||
```shell script
|
||||
goctl model mysql datasource -url={datasource} -table={tables} -dir={dir}
|
||||
```
|
||||
|
||||
or
|
||||
* 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结构。
|
||||
|
||||
## 缓存
|
||||
|
||||
对于缓存这一块我选择用一问一答的形式进行罗列。我想这样能够更清晰的描述model中缓存的功能。
|
||||
|
||||
* 缓存会缓存哪些信息?
|
||||
|
||||
对于主键字段缓存,会缓存整个结构体信息,而对于单索引字段(除全文索引)则缓存主键字段值。
|
||||
|
||||
* 数据有更新(`update`)操作会清空缓存吗?
|
||||
|
||||
会,但仅清空主键缓存的信息,why?这里就不做详细赘述了。
|
||||
|
||||
* 为什么不按照单索引字段生成`updateByXxx`和`deleteByXxx`的代码?
|
||||
|
||||
理论上是没任何问题,但是我们认为,对于model层的数据操作均是以整个结构体为单位,包括查询,我不建议只查询某部分字段(不反对),否则我们的缓存就没有意义了。
|
||||
|
||||
* 为什么不支持`findPageLimit`、`findAll`这么模式代码生层?
|
||||
|
||||
目前,我认为除了基本的CURD外,其他的代码均属于<i>业务型</i>代码,这个我觉得开发人员根据业务需要进行编写更好。
|
||||
|
||||
## QA
|
||||
|
||||
* goctl model除了命令行模式,支持插件模式吗?
|
||||
|
||||
很快支持idea插件。
|
||||
238
doc/goctl-rpc.md
Normal file
@@ -0,0 +1,238 @@
|
||||
# Rpc Generation
|
||||
|
||||
Goctl Rpc是`goctl`脚手架下的一个rpc服务代码生成模块,支持proto模板生成和rpc服务代码生成,通过此工具生成代码你只需要关注业务逻辑编写而不用去编写一些重复性的代码。这使得我们把精力重心放在业务上,从而加快了开发效率且降低了代码出错率。
|
||||
|
||||
## 特性
|
||||
|
||||
* 简单易用
|
||||
* 快速提升开发效率
|
||||
* 出错率低
|
||||
|
||||
## 快速开始
|
||||
|
||||
### 方式一:快速生成greet服务
|
||||
|
||||
通过命令 `goctl rpc new ${servieName}`生成
|
||||
|
||||
如生成greet rpc服务:
|
||||
|
||||
```shell script
|
||||
goctl rpc new greet
|
||||
```
|
||||
|
||||
执行后代码结构如下:
|
||||
|
||||
```golang
|
||||
└── greet
|
||||
├── etc
|
||||
│ └── greet.yaml
|
||||
├── go.mod
|
||||
├── go.sum
|
||||
├── greet
|
||||
│ ├── greet.go
|
||||
│ ├── greet_mock.go
|
||||
│ └── types.go
|
||||
├── greet.go
|
||||
├── greet.proto
|
||||
├── internal
|
||||
│ ├── config
|
||||
│ │ └── config.go
|
||||
│ ├── logic
|
||||
│ │ └── pinglogic.go
|
||||
│ ├── server
|
||||
│ │ └── greetserver.go
|
||||
│ └── svc
|
||||
│ └── servicecontext.go
|
||||
└── pb
|
||||
└── greet.pb.go
|
||||
```
|
||||
|
||||
rpc一键生成常见问题解决见 <a href="#常见问题解决">常见问题解决</a>
|
||||
|
||||
### 方式二:通过指定proto生成rpc服务
|
||||
|
||||
* 生成proto模板
|
||||
|
||||
```shell script
|
||||
goctl rpc template -o=user.proto
|
||||
```
|
||||
|
||||
```golang
|
||||
syntax = "proto3";
|
||||
|
||||
package remote;
|
||||
|
||||
message Request {
|
||||
// 用户名
|
||||
string username = 1;
|
||||
// 用户密码
|
||||
string password = 2;
|
||||
}
|
||||
|
||||
message Response {
|
||||
// 用户名称
|
||||
string name = 1;
|
||||
// 用户性别
|
||||
string gender = 2;
|
||||
}
|
||||
|
||||
service User {
|
||||
// 登录
|
||||
rpc Login(Request)returns(Response);
|
||||
}
|
||||
```
|
||||
|
||||
* 生成rpc服务代码
|
||||
|
||||
```shell script
|
||||
goctl rpc proto -src=user.proto
|
||||
```
|
||||
|
||||
代码tree
|
||||
|
||||
```Plain Text
|
||||
user
|
||||
├── etc
|
||||
│ └── user.json
|
||||
├── internal
|
||||
│ ├── config
|
||||
│ │ └── config.go
|
||||
│ ├── handler
|
||||
│ │ ├── loginhandler.go
|
||||
│ ├── logic
|
||||
│ │ └── loginlogic.go
|
||||
│ └── svc
|
||||
│ └── servicecontext.go
|
||||
├── pb
|
||||
│ └── user.pb.go
|
||||
├── shared
|
||||
│ ├── mockusermodel.go
|
||||
│ ├── types.go
|
||||
│ └── usermodel.go
|
||||
├── user.go
|
||||
└── user.proto
|
||||
|
||||
```
|
||||
|
||||
## 准备工作
|
||||
|
||||
* 安装了go环境
|
||||
* 安装了protoc&protoc-gen-go,并且已经设置环境变量
|
||||
* mockgen(可选,将移除)
|
||||
* 更多问题请见 <a href="#注意事项">注意事项</a>
|
||||
|
||||
## 用法
|
||||
|
||||
### rpc服务生成用法
|
||||
|
||||
```shell script
|
||||
goctl rpc proto -h
|
||||
```
|
||||
|
||||
```shell script
|
||||
NAME:
|
||||
goctl rpc proto - generate rpc from proto
|
||||
|
||||
USAGE:
|
||||
goctl rpc proto [command options] [arguments...]
|
||||
|
||||
OPTIONS:
|
||||
--src value, -s value the file path of the proto source file
|
||||
--dir value, -d value the target path of the code,default path is "${pwd}". [option]
|
||||
--service value, --srv value the name of rpc service. [option]
|
||||
--shared[已废弃] value the dir of the shared file,default path is "${pwd}/shared. [option]"
|
||||
--idea whether the command execution environment is from idea plugin. [option]
|
||||
|
||||
```
|
||||
|
||||
### 参数说明
|
||||
|
||||
* --src 必填,proto数据源,目前暂时支持单个proto文件生成,这里不支持(不建议)外部依赖
|
||||
* --dir 非必填,默认为proto文件所在目录,生成代码的目标目录
|
||||
* --service 服务名称,非必填,默认为proto文件所在目录名称,但是,如果proto所在目录为一下结构:
|
||||
|
||||
```shell script
|
||||
user
|
||||
├── cmd
|
||||
│ └── rpc
|
||||
│ └── user.proto
|
||||
```
|
||||
|
||||
则服务名称亦为user,而非proto所在文件夹名称了,这里推荐使用这种结构,可以方便在同一个服务名下建立不同类型的服务(api、rpc、mq等),便于代码管理与维护。
|
||||
* --shared[⚠️已废弃] 非必填,默认为$dir(xxx.proto)/shared,rpc client逻辑代码存放目录。
|
||||
|
||||
> 注意:这里的shared文件夹名称将会是代码中的package名称。
|
||||
|
||||
* --idea 非必填,是否为idea插件中执行,保留字段,终端执行可以忽略
|
||||
|
||||
## 开发人员需要做什么
|
||||
|
||||
关注业务代码编写,将重复性、与业务无关的工作交给goctl,生成好rpc服务代码后,开饭人员仅需要修改
|
||||
|
||||
* 服务中的配置文件编写(etc/xx.json、internal/config/config.go)
|
||||
* 服务中业务逻辑编写(internal/logic/xxlogic.go)
|
||||
* 服务中资源上下文的编写(internal/svc/servicecontext.go)
|
||||
|
||||
## 扩展
|
||||
|
||||
对于需要进行rpc mock的开发人员,在安装了`mockgen`工具的前提下可以在rpc的shared文件中生成好对应的mock文件。
|
||||
|
||||
## 注意事项
|
||||
|
||||
* `google.golang.org/grpc`需要降级到v1.26.0,且protoc-gen-go版本不能高于v1.3.2(see [https://github.com/grpc/grpc-go/issues/3347](https://github.com/grpc/grpc-go/issues/3347))即
|
||||
|
||||
```shell script
|
||||
replace google.golang.org/grpc => google.golang.org/grpc v1.26.0
|
||||
```
|
||||
|
||||
* proto不支持暂多文件同时生成
|
||||
* proto不支持外部依赖包引入,message不支持inline
|
||||
* 目前main文件、shared文件、handler文件会被强制覆盖,而和开发人员手动需要编写的则不会覆盖生成,这一类在代码头部均有
|
||||
|
||||
```shell script
|
||||
// Code generated by goctl. DO NOT EDIT!
|
||||
// Source: xxx.proto
|
||||
```
|
||||
|
||||
的标识,请注意不要将也写业务性代码写在里面。
|
||||
|
||||
## 常见问题解决(go mod工程)
|
||||
|
||||
* 错误一:
|
||||
|
||||
```golang
|
||||
pb/xx.pb.go:220:7: undefined: grpc.ClientConnInterface
|
||||
pb/xx.pb.go:224:11: undefined: grpc.SupportPackageIsVersion6
|
||||
pb/xx.pb.go:234:5: undefined: grpc.ClientConnInterface
|
||||
pb/xx.pb.go:237:24: undefined: grpc.ClientConnInterface
|
||||
```
|
||||
|
||||
解决方法:请将`protoc-gen-go`版本降至v1.3.2及一下
|
||||
|
||||
* 错误二:
|
||||
|
||||
```golang
|
||||
|
||||
# go.etcd.io/etcd/clientv3/balancer/picker
|
||||
../../../go/pkg/mod/go.etcd.io/etcd@v0.0.0-20200402134248-51bdeb39e698/clientv3/balancer/picker/err.go:25:9: cannot use &errPicker literal (type *errPicker) as type Picker in return argument:*errPicker does not implement Picker (wrong type for Pick method)
|
||||
have Pick(context.Context, balancer.PickInfo) (balancer.SubConn, func(balancer.DoneInfo), error)
|
||||
want Pick(balancer.PickInfo) (balancer.PickResult, error)
|
||||
../../../go/pkg/mod/go.etcd.io/etcd@v0.0.0-20200402134248-51bdeb39e698/clientv3/balancer/picker/roundrobin_balanced.go:33:9: cannot use &rrBalanced literal (type *rrBalanced) as type Picker in return argument:
|
||||
*rrBalanced does not implement Picker (wrong type for Pick method)
|
||||
have Pick(context.Context, balancer.PickInfo) (balancer.SubConn, func(balancer.DoneInfo), error)
|
||||
want Pick(balancer.PickInfo) (balancer.PickResult, error)
|
||||
#github.com/tal-tech/go-zero/zrpc/internal/balancer/p2c
|
||||
../../../go/pkg/mod/github.com/tal-tech/go-zero@v1.0.12/zrpc/internal/balancer/p2c/p2c.go:41:32: not enough arguments in call to base.NewBalancerBuilder
|
||||
have (string, *p2cPickerBuilder)
|
||||
want (string, base.PickerBuilder, base.Config)
|
||||
../../../go/pkg/mod/github.com/tal-tech/go-zero@v1.0.12/zrpc/internal/balancer/p2c/p2c.go:58:9: cannot use &p2cPicker literal (type *p2cPicker) as type balancer.Picker in return argument:
|
||||
*p2cPicker does not implement balancer.Picker (wrong type for Pick method)
|
||||
have Pick(context.Context, balancer.PickInfo) (balancer.SubConn, func(balancer.DoneInfo), error)
|
||||
want Pick(balancer.PickInfo) (balancer.PickResult, error)
|
||||
```
|
||||
|
||||
解决方法:
|
||||
|
||||
```golang
|
||||
replace google.golang.org/grpc => google.golang.org/grpc v1.26.0
|
||||
```
|
||||
88
doc/goctl.md
@@ -1,33 +1,40 @@
|
||||
# goctl使用说明
|
||||
# goctl使用
|
||||
|
||||
## goctl用途
|
||||
|
||||
* 定义api请求
|
||||
* 根据定义的api自动生成golang(后端), java(iOS & Android), typescript(web & 晓程序),dart(flutter)
|
||||
* 生成MySQL CURD+Cache
|
||||
* 生成MongoDB CURD+Cache
|
||||
* 生成MongoDB CURD+Cache
|
||||
|
||||
## goctl使用说明
|
||||
|
||||
### 快速生成服务
|
||||
|
||||
* api: goctl api new xxxx
|
||||
* rpc: goctl rpc new xxxx
|
||||
|
||||
#### goctl参数说明
|
||||
|
||||
`goctl api [go/java/ts] [-api user/user.api] [-dir ./src]`
|
||||
|
||||
> api 后面接生成的语言,现支持go/java/typescript
|
||||
|
||||
>
|
||||
> -api 自定义api所在路径
|
||||
|
||||
>
|
||||
> -dir 自定义生成目录
|
||||
|
||||
#### 保持goctl总是最新版
|
||||
|
||||
第一次运行会在~/.goctl里增加下面两行:
|
||||
|
||||
```
|
||||
```Plain Text
|
||||
url = http://47.97.184.41:7777/
|
||||
```
|
||||
|
||||
#### API 语法说明
|
||||
|
||||
```
|
||||
``` golang
|
||||
info(
|
||||
title: doc title
|
||||
desc: >
|
||||
@@ -123,7 +130,9 @@ service user-api {
|
||||
)
|
||||
head /api/ping()
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
1. info部分:描述了api基本信息,比如Auth,api是哪个用途。
|
||||
2. type部分:type类型声明和golang语法兼容。
|
||||
3. service部分:service代表一组服务,一个服务可以由多组名称相同的service组成,可以针对每一组service配置jwt和auth认证,另外通过folder属性可以指定service生成所在子目录。
|
||||
@@ -132,6 +141,7 @@ service user-api {
|
||||
请求getRequest里面的属性赋值,getResponse为返回的结构体,这两个类型都定义在2描述的类型中。
|
||||
|
||||
#### api vscode插件
|
||||
|
||||
开发者可以在vscode中搜索goctl的api插件,它提供了api语法高亮,语法检测和格式化相关功能。
|
||||
|
||||
1. 支持语法高亮和类型导航。
|
||||
@@ -143,7 +153,7 @@ service user-api {
|
||||
命令如下:
|
||||
`goctl api go -api user/user.api -dir user`
|
||||
|
||||
```
|
||||
```Plain Text
|
||||
|
||||
.
|
||||
├── internal
|
||||
@@ -173,17 +183,20 @@ service user-api {
|
||||
└── user.go
|
||||
|
||||
```
|
||||
|
||||
生成的代码可以直接跑,有几个地方需要改:
|
||||
|
||||
* 在`servicecontext.go`里面增加需要传递给logic的一些资源,比如mysql, redis,rpc等
|
||||
* 在定义的get/post/put/delete等请求的handler和logic里增加处理业务逻辑的代码
|
||||
* 在`servicecontext.go`里面增加需要传递给logic的一些资源,比如mysql, redis,rpc等
|
||||
* 在定义的get/post/put/delete等请求的handler和logic里增加处理业务逻辑的代码
|
||||
|
||||
#### 根据定义好的api文件生成java代码
|
||||
|
||||
```shell
|
||||
goctl api java -api user/user.api -dir ./src
|
||||
```
|
||||
|
||||
#### 根据定义好的api文件生成typescript代码
|
||||
|
||||
```shell
|
||||
goctl api ts -api user/user.api -dir ./src -webapi ***
|
||||
|
||||
@@ -191,6 +204,7 @@ ts需要指定webapi所在目录
|
||||
```
|
||||
|
||||
#### 根据定义好的api文件生成Dart代码
|
||||
|
||||
```shell
|
||||
goctl api dart -api user/user.api -dir ./src
|
||||
```
|
||||
@@ -198,32 +212,37 @@ 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}
|
||||
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
|
||||
|
||||
-src需要提供简单的usermodel.go文件,里面只需要提供一个结构体即可
|
||||
-cache 控制是否需要缓存 yes=需要 no=不需要
|
||||
src 示例代码如下
|
||||
```
|
||||
|
||||
* src需要提供简单的usermodel.go文件,里面只需要提供一个结构体即可
|
||||
* cache 控制是否需要缓存 yes=需要 no=不需要
|
||||
|
||||
src 示例代码如下
|
||||
|
||||
```go
|
||||
package model
|
||||
|
||||
type User struct {
|
||||
Name string `o:"find,get,set" c:"姓名"`
|
||||
Age int `o:"find,get,set" c:"年纪"`
|
||||
School string `c:"学校"`
|
||||
}
|
||||
package model
|
||||
|
||||
type User struct {
|
||||
Name string `o:"find,get,set" c:"姓名"`
|
||||
Age int `o:"find,get,set" c:"年纪"`
|
||||
School string `c:"学校"`
|
||||
}
|
||||
```
|
||||
结构体中不需要提供Id,CreateTime,UpdateTime三个字段,会自动生成
|
||||
结构体中每个tag有两个可选标签 c 和 o
|
||||
c是改字段的注释
|
||||
o是改字段需要生产的操作函数 可以取得get,find,set 分别表示生成返回单个对象的查询方法,返回多个对象的查询方法,设置该字段方法
|
||||
生成的目标文件会覆盖该简单go文件
|
||||
|
||||
结构体中不需要提供Id,CreateTime,UpdateTime三个字段,会自动生成
|
||||
结构体中每个tag有两个可选标签 c 和 o
|
||||
c 是改字段的注释
|
||||
o 是改字段需要生产的操作函数 可以取得get,find,set 分别表示生成返回单个对象的查询方法,返回多个对象的查询方法,设置该字段方法
|
||||
生成的目标文件会覆盖该简单go文件
|
||||
|
||||
## goctl rpc生成(业务剥离中,暂未开放)
|
||||
|
||||
@@ -232,15 +251,15 @@ type User struct {
|
||||
|
||||
参数说明:
|
||||
|
||||
- ${proto}: proto文件
|
||||
- ${serviceName}: rpc服务名称
|
||||
- ${projectName}: 所属项目,如xjy,xhb,crm,hera,具体查看help,主要为了根据不同项目服务往redis注册key,可选
|
||||
- ${directory}: 输出目录
|
||||
- ${shared}: shared文件生成目录,可选,默认为${pwd}/shared
|
||||
* ${proto}: proto文件
|
||||
* ${serviceName}: rpc服务名称
|
||||
* ${projectName}: 所属项目,如xjy,xhb,crm,hera,具体查看help,主要为了根据不同项目服务往redis注册key,可选
|
||||
* ${directory}: 输出目录
|
||||
* ${shared}: shared文件生成目录,可选,默认为${pwd}/shared
|
||||
|
||||
生成目录结构示例:
|
||||
生成目录结构示例:
|
||||
|
||||
``` go
|
||||
```Plain Text
|
||||
.
|
||||
├── shared [示例目录,可自己指定,强制覆盖更新]
|
||||
│ └── contentservicemodel.go
|
||||
@@ -276,4 +295,5 @@ type User struct {
|
||||
│ └── test.go [强制覆盖更新]
|
||||
└── test.proto
|
||||
```
|
||||
- 注意 :目前rpc目录生成的proto文件暂不支持import外部proto文件
|
||||
|
||||
注意 :目前rpc目录生成的proto文件暂不支持import外部proto文件
|
||||
|
||||
BIN
doc/images/datasource.png
Normal file
|
After Width: | Height: | Size: 72 KiB |
BIN
doc/images/panel.png
Normal file
|
After Width: | Height: | Size: 457 KiB |
BIN
doc/images/prom_up.png
Normal file
|
After Width: | Height: | Size: 109 KiB |
BIN
doc/images/prometheus.png
Normal file
|
After Width: | Height: | Size: 95 KiB |
BIN
doc/images/qps.png
Normal file
|
After Width: | Height: | Size: 121 KiB |
BIN
doc/images/qps_panel.png
Normal file
|
After Width: | Height: | Size: 99 KiB |
BIN
doc/images/variables.png
Normal file
|
After Width: | Height: | Size: 90 KiB |
|
Before Width: | Height: | Size: 148 KiB After Width: | Height: | Size: 191 KiB |
@@ -22,7 +22,8 @@ fmt.Println(replacer.Replace("日本的首都是东京"))
|
||||
```
|
||||
|
||||
可以得到:
|
||||
```
|
||||
|
||||
```Plain Text
|
||||
东京是日本的首都
|
||||
```
|
||||
|
||||
@@ -46,7 +47,7 @@ fmt.Println(keywords)
|
||||
|
||||
可以得到:
|
||||
|
||||
```
|
||||
```Plain Text
|
||||
[苍井空 日本AV女优 AV演员色情 AV AV演员]
|
||||
```
|
||||
|
||||
@@ -70,7 +71,7 @@ fmt.Println(found)
|
||||
|
||||
可以得到:
|
||||
|
||||
```
|
||||
```Plain Text
|
||||
日本????兼电视、电影演员。?????女优是xx出道, ??????们最精彩的表演是??????表演
|
||||
[苍井空 日本AV女优 AV演员色情 AV AV演员]
|
||||
true
|
||||
@@ -83,4 +84,3 @@ true
|
||||
| Sentences | Keywords | Regex | go-zero |
|
||||
| --------- | -------- | -------- | ------- |
|
||||
| 10000 | 10000 | 16min10s | 27.2ms |
|
||||
|
||||
|
||||
@@ -1,23 +1,26 @@
|
||||
# 服务自适应降载保护设计
|
||||
|
||||
## 设计目的
|
||||
|
||||
* 保证系统不被过量请求拖垮
|
||||
* 在保证系统稳定的前提下,尽可能提供更高的吞吐量
|
||||
|
||||
## 设计考虑因素
|
||||
|
||||
* 如何衡量系统负载
|
||||
* 是否处于虚机或容器内,需要读取cgroup相关负载
|
||||
* 用1000m表示100%CPU,推荐使用800m表示系统高负载
|
||||
* 是否处于虚机或容器内,需要读取cgroup相关负载
|
||||
* 用1000m表示100%CPU,推荐使用800m表示系统高负载
|
||||
* 尽可能小的Overhead,不显著增加RT
|
||||
* 不考虑服务本身所依赖的DB或者缓存系统问题,这类问题通过熔断机制来解决
|
||||
|
||||
## 机制设计
|
||||
|
||||
* 计算CPU负载时使用滑动平均来降低CPU负载抖动带来的不稳定,关于滑动平均见参考资料
|
||||
* 滑动平均就是取之前连续N次值的近似平均,N取值可以通过超参beta来决定
|
||||
* 当CPU负载大于指定值时触发降载保护机制
|
||||
* 滑动平均就是取之前连续N次值的近似平均,N取值可以通过超参beta来决定
|
||||
* 当CPU负载大于指定值时触发降载保护机制
|
||||
* 时间窗口机制,用滑动窗口机制来记录之前时间窗口内的QPS和RT(response time)
|
||||
* 滑动窗口使用5秒钟50个桶的方式,每个桶保存100ms时间内的请求,循环利用,最新的覆盖最老的
|
||||
* 计算maxQPS和minRT时需要过滤掉最新的时间没有用完的桶,防止此桶内只有极少数请求,并且RT处于低概率的极小值,所以计算maxQPS和minRT时按照上面的50个桶的参数只会算49个
|
||||
* 滑动窗口使用5秒钟50个桶的方式,每个桶保存100ms时间内的请求,循环利用,最新的覆盖最老的
|
||||
* 计算maxQPS和minRT时需要过滤掉最新的时间没有用完的桶,防止此桶内只有极少数请求,并且RT处于低概率的极小值,所以计算maxQPS和minRT时按照上面的50个桶的参数只会算49个
|
||||
* 满足以下所有条件则拒绝该请求
|
||||
1. 当前CPU负载超过预设阈值,或者上次拒绝时间到现在不超过1秒(冷却期)。冷却期是为了不能让负载刚下来就马上增加压力导致立马又上去的来回抖动
|
||||
2. `averageFlying > max(1, QPS*minRT/1e3)`
|
||||
@@ -27,7 +30,7 @@
|
||||
1. 请求增加后更新一次averageFlying,见图中橙色曲线
|
||||
2. 请求结束后更新一次averageFlying,见图中绿色曲线
|
||||
3. 请求增加后更新一次averageFlying,请求结束后更新一次averageFlying
|
||||
|
||||
|
||||
我们使用的是第二种,这样可以更好的防止抖动,如图:
|
||||

|
||||
* QPS = maxPass * bucketsPerSecond
|
||||
@@ -36,11 +39,13 @@
|
||||
* 1e3表示1000毫秒,minRT单位也是毫秒,QPS*minRT/1e3得到的就是平均每个时间点有多少并发请求
|
||||
|
||||
## 降载的使用
|
||||
|
||||
* 已经在ngin和rpcx框架里增加了可选激活配置
|
||||
* CpuThreshold,如果把值设置为大于0的值,则激活该服务的自动降载机制
|
||||
* 如果请求被drop,那么错误日志里会有`dropreq`关键字
|
||||
* CpuThreshold,如果把值设置为大于0的值,则激活该服务的自动降载机制
|
||||
* 如果请求被drop,那么错误日志里会有`dropreq`关键字
|
||||
|
||||
## 参考资料
|
||||
|
||||
* [滑动平均](https://www.cnblogs.com/wuliytTaotao/p/9479958.html)
|
||||
* [Sentinel自适应限流](https://github.com/alibaba/Sentinel/wiki/%E7%B3%BB%E7%BB%9F%E8%87%AA%E9%80%82%E5%BA%94%E9%99%90%E6%B5%81)
|
||||
* [Kratos自适应限流保护](https://github.com/bilibili/kratos/blob/master/doc/wiki-cn/ratelimit.md)
|
||||
* [Kratos自适应限流保护](https://github.com/bilibili/kratos/blob/master/doc/wiki-cn/ratelimit.md)
|
||||
|
||||
131
doc/mapping.md
Normal file
@@ -0,0 +1,131 @@
|
||||
# 文本序列化和反序列化
|
||||
|
||||
go-zero针对文本的序列化和反序列化主要在三个地方使用
|
||||
|
||||
* http api请求体的反序列化
|
||||
* http api返回体的序列化
|
||||
* 配置文件的反序列化
|
||||
|
||||
本文假定读者已经定义过api文件以及修改过配置文件,如不熟悉,可参照
|
||||
|
||||
* [快速构建高并发微服务](shorturl.md)
|
||||
* [快速构建高并发微服务](bookstore.md)
|
||||
|
||||
## 1. http api请求体的反序列化
|
||||
|
||||
在反序列化的过程中的针对请求数据的`数据格式`以及`数据校验`需求,go-zero实现了自己的一套反序列化机制
|
||||
|
||||
### 1.1 `数据格式`以订单order.api文件为例
|
||||
|
||||
```go
|
||||
type (
|
||||
createOrderReq struct {
|
||||
token string `path:"token"` // 用户token
|
||||
productId string `json:"productId"` // 商品ID
|
||||
num int `json:"num"` // 商品数量
|
||||
}
|
||||
createOrderRes struct {
|
||||
success bool `json:"success"` // 是否成功
|
||||
}
|
||||
findOrderReq struct {
|
||||
token string `path:"token"` // 用户token
|
||||
page int `form:"page"` // 页数
|
||||
pageSize int8 `form:"pageSize"` // 页大小
|
||||
}
|
||||
findOrderRes struct {
|
||||
orderInfo []orderInfo `json:"orderInfo"` // 商品ID
|
||||
}
|
||||
orderInfo struct {
|
||||
productId string `json:"productId"` // 商品ID
|
||||
productName string `json:"productName"` // 商品名称
|
||||
num int `json:"num"` // 商品数量
|
||||
}
|
||||
deleteOrderReq struct {
|
||||
id string `path:"id"`
|
||||
}
|
||||
deleteOrderRes struct {
|
||||
success bool `json:"success"` // 是否成功
|
||||
}
|
||||
)
|
||||
|
||||
service order {
|
||||
@doc(
|
||||
summary: 创建订单
|
||||
)
|
||||
@server(
|
||||
handler: CreateOrderHandler
|
||||
)
|
||||
post /order/add/:token(createOrderReq) returns(createOrderRes)
|
||||
|
||||
@doc(
|
||||
summary: 获取订单
|
||||
)
|
||||
@server(
|
||||
handler: FindOrderHandler
|
||||
)
|
||||
get /order/find/:token(findOrderReq) returns(findOrderRes)
|
||||
|
||||
@doc(
|
||||
summary: 删除订单
|
||||
)
|
||||
@server(
|
||||
handler: DeleteOrderHandler
|
||||
)
|
||||
delete /order/:id(deleteOrderReq) returns(deleteOrderRes)
|
||||
}
|
||||
```
|
||||
|
||||
http api请求体的反序列化的tag有三种:
|
||||
|
||||
* `path`:http url 路径中参数反序列化
|
||||
* `/order/add/1234567`会解析出来token为1234567
|
||||
* `form`:http form表单反序列化,需要 header头添加 Content-Type: multipart/form-data
|
||||
* `/order/find/1234567?page=1&pageSize=20`会解析出来token为1234567,page为1,pageSize为20
|
||||
|
||||
* `json`:http request json body反序列化,需要 header头添加 Content-Type: application/json
|
||||
* `{"productId":"321","num":1}`会解析出来productId为321,num为1
|
||||
|
||||
### 1.2 `数据校验`以用户user.api文件为例
|
||||
|
||||
```go
|
||||
type (
|
||||
createUserReq struct {
|
||||
age int8 `json:"age,default=20,range=(12:100]"` // 年龄
|
||||
name string `json:"name"` // 名字
|
||||
alias string `json:"alias,optional"` // 别名
|
||||
sex string `json:"sex,options=male|female"` // 性别
|
||||
avatar string `json:"avatar,default=default.png"` // 头像
|
||||
}
|
||||
createUserRes struct {
|
||||
success bool `json:"success"` // 是否成功
|
||||
}
|
||||
)
|
||||
|
||||
service user {
|
||||
@doc(
|
||||
summary: 创建订单
|
||||
)
|
||||
@server(
|
||||
handler: CreateUserHandler
|
||||
)
|
||||
post /user/add(createUserReq) returns(createUserRes)
|
||||
}
|
||||
```
|
||||
|
||||
数据校验有很多种方式,包括以下但不限:
|
||||
|
||||
* `age`:默认不输入为20,输入则取值范围为(12:100],前开后闭
|
||||
* `name`:必填,不可为空
|
||||
* `alias`:选填,可为空
|
||||
* `sex`:必填,取值为`male`或`female`
|
||||
* `avatar`:选填,默认为`default.png`
|
||||
|
||||
更多详情参见[unmarshaler_test.go](../core/mapping/unmarshaler_test.go)
|
||||
|
||||
## 2. http api返回体的序列化
|
||||
|
||||
* 使用官方默认的`encoding/json`包序列化,在此不再累赘
|
||||
|
||||
## 3. 配置文件的反序列化
|
||||
|
||||
* `配置文件的反序列化`和`http api请求体的反序列化`使用同一套解析规则,可参照`http api请求体的反序列化`
|
||||
@@ -24,7 +24,7 @@ MapReduce主要有三个参数,第一个参数为generate用以生产数据,
|
||||
|
||||
场景一: 某些功能的结果往往需要依赖多个服务,比如商品详情的结果往往会依赖用户服务、库存服务、订单服务等等,一般被依赖的服务都是以rpc的形式对外提供,为了降低依赖的耗时我们往往需要对依赖做并行处理
|
||||
|
||||
```
|
||||
```go
|
||||
func productDetail(uid, pid int64) (*ProductDetail, error) {
|
||||
var pd ProductDetail
|
||||
err := mr.Finish(func() (err error) {
|
||||
@@ -42,15 +42,16 @@ func productDetail(uid, pid int64) (*ProductDetail, error) {
|
||||
log.Printf("product detail error: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
||||
return &pd, nil
|
||||
}
|
||||
```
|
||||
|
||||
该示例中返回商品详情依赖了多个服务获取数据,因此做并发的依赖处理,对接口的性能有很大的提升
|
||||
|
||||
场景二: 很多时候我们需要对一批数据进行处理,比如对一批用户id,效验每个用户的合法性并且效验过程中有一个出错就认为效验失败,返回的结果为效验合法的用户id
|
||||
|
||||
```
|
||||
```go
|
||||
func checkLegal(uids []int64) ([]int64, error) {
|
||||
r, err := mr.MapReduce(func(source chan<- interface{}) {
|
||||
for _, uid := range uids {
|
||||
@@ -85,9 +86,11 @@ func check(uid int64) (bool, error) {
|
||||
return true, nil
|
||||
}
|
||||
```
|
||||
|
||||
该示例中,如果check过程出现错误则通过cancel方法结束效验过程,并返回error整个效验过程结束,如果某个uid效验结果为false则最终结果不返回该uid
|
||||
|
||||
**MapReduce使用注意事项**
|
||||
***MapReduce使用注意事项***
|
||||
|
||||
* mapper和reducer中都可以调用cancel,参数为error,调用后立即返回,返回结果为nil, error
|
||||
* mapper中如果不调用writer.Write则item最终不会被reducer聚合
|
||||
* reducer中如果不调用writer.Wirte则返回结果为nil, ErrReduceNoOutput
|
||||
@@ -96,12 +99,13 @@ func check(uid int64) (bool, error) {
|
||||
***实现原理分析:***
|
||||
|
||||
MapReduce中首先通过buildSource方法通过执行generate(参数为无缓冲channel)产生数据,并返回无缓冲的channel,mapper会从该channel中读取数据
|
||||
```
|
||||
|
||||
```go
|
||||
func buildSource(generate GenerateFunc) chan interface{} {
|
||||
source := make(chan interface{})
|
||||
go func() {
|
||||
defer close(source)
|
||||
generate(source)
|
||||
generate(source)
|
||||
}()
|
||||
|
||||
return source
|
||||
@@ -109,7 +113,8 @@ func buildSource(generate GenerateFunc) chan interface{} {
|
||||
```
|
||||
|
||||
在MapReduceWithSource方法中定义了cancel方法,mapper和reducer中都可以调用该方法,调用后主线程收到close信号会立马返回
|
||||
```
|
||||
|
||||
```go
|
||||
cancel := once(func(err error) {
|
||||
if err != nil {
|
||||
retErr.Set(err)
|
||||
@@ -125,7 +130,8 @@ cancel := once(func(err error) {
|
||||
```
|
||||
|
||||
在mapperDispatcher方法中调用了executeMappers,executeMappers消费buildSource产生的数据,每一个item都会起一个goroutine单独处理,默认最大并发数为16,可以通过WithWorkers进行设置
|
||||
```
|
||||
|
||||
```go
|
||||
var wg sync.WaitGroup
|
||||
defer func() {
|
||||
wg.Wait() // 保证所有的item都处理完成
|
||||
@@ -159,7 +165,8 @@ for {
|
||||
```
|
||||
|
||||
reducer单goroutine对数mapper写入collector的数据进行处理,如果reducer中没有手动调用writer.Write则最终会执行finish方法对output进行close避免死锁
|
||||
```
|
||||
|
||||
```go
|
||||
go func() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
@@ -172,13 +179,12 @@ go func() {
|
||||
}()
|
||||
```
|
||||
|
||||
在该工具包中还提供了许多针对不同业务场景的方法,实现原理与MapReduce大同小异,感兴趣的同学可以查看源码学习
|
||||
在该工具包中还提供了许多针对不同业务场景的方法,实现原理与MapReduce大同小异,感兴趣的同学可以查看源码学习
|
||||
|
||||
* MapReduceVoid 功能和MapReduce类似但没有结果返回只返回error
|
||||
* Finish 处理固定数量的依赖,返回error,有一个error立即返回
|
||||
* FinishVoid 和Finish方法功能类似,没有返回值
|
||||
* Map 只做generate和mapper处理,返回channel
|
||||
* MapVoid 和Map功能类似,无返回
|
||||
|
||||
|
||||
本文主要介绍了go-zero框架中的MapReduce工具,在实际的项目中非常实用。用好工具对于提升服务性能和开发效率都有很大的帮助,希望本篇文章能给大家带来一些收获。
|
||||
|
||||
|
||||
112
doc/metric.md
Normal file
@@ -0,0 +1,112 @@
|
||||
# 基于prometheus的微服务指标监控
|
||||
|
||||
服务上线后我们往往需要对服务进行监控,以便能及早发现问题并做针对性的优化,监控又可分为多种形式,比如日志监控,调用链监控,指标监控等等。而通过指标监控能清晰的观察出服务指标的变化趋势,了解服务的运行状态,对于保证服务稳定起着非常重要的作用
|
||||
|
||||
[prometheus](https://prometheus.io/)是一个开源的系统监控和告警工具,支持强大的查询语言PromQL允许用户实时选择和汇聚时间序列数据,时间序列数据是服务端通过HTTP协议主动拉取获得,也可以通过中间网关来推送时间序列数据,可以通过静态配置文件或服务发现来获取监控目标
|
||||
|
||||
## Prometheus 的架构
|
||||
|
||||
Prometheus 的整体架构以及生态系统组件如下图所示:
|
||||
|
||||

|
||||
|
||||
Prometheus Server直接从监控目标中或者间接通过推送网关来拉取监控指标,它在本地存储所有抓取到样本数据,并对此数据执行一系列规则,以汇总和记录现有数据的新时间序列或生成告警。可以通过 [Grafana](https://grafana.com/) 或者其他工具来实现监控数据的可视化
|
||||
|
||||
## go-zero基于prometheus的服务指标监控
|
||||
|
||||
[go-zero](https://github.com/tal-tech/go-zero) 框架中集成了基于prometheus的服务指标监控,下面我们通过go-zero官方的示例[shorturl](https://github.com/tal-tech/go-zero/blob/master/doc/shorturl.md)来演示是如何对服务指标进行收集监控的:
|
||||
|
||||
- 第一步需要先安装Prometheus,安装步骤请参考[官方文档](https://prometheus.io/)
|
||||
- go-zero默认不开启prometheus监控,开启方式很简单,只需要在shorturl-api.yaml文件中增加配置如下,其中Host为Prometheus Server地址为必填配置,Port端口不填默认9091,Path为用来拉取指标的路径默认为/metrics
|
||||
|
||||
```go
|
||||
Prometheus:
|
||||
Host: 127.0.0.1
|
||||
Port: 9091
|
||||
Path: /metrics
|
||||
```
|
||||
|
||||
- 编辑prometheus的配置文件prometheus.yml,添加如下配置,并创建targets.json
|
||||
|
||||
```go
|
||||
- job_name: 'file_ds'
|
||||
file_sd_configs:
|
||||
- files:
|
||||
- targets.json
|
||||
```
|
||||
|
||||
- 编辑targets.json文件,其中targets为shorturl配置的目标地址,并添加了几个默认的标签
|
||||
|
||||
```go
|
||||
[
|
||||
{
|
||||
"targets": ["127.0.0.1:9091"],
|
||||
"labels": {
|
||||
"job": "shorturl-api",
|
||||
"app": "shorturl-api",
|
||||
"env": "test",
|
||||
"instance": "127.0.0.1:8888"
|
||||
}
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
- 启动prometheus服务,默认侦听在9090端口
|
||||
|
||||
```go
|
||||
prometheus --config.file=prometheus.yml
|
||||
```
|
||||
|
||||
- 在浏览器输入http://127.0.0.1:9090/,然后点击Status -> Targets即可看到状态为Up的Job,并且Lables栏可以看到我们配置的默认的标签
|
||||
|
||||

|
||||
|
||||
通过以上几个步骤我们完成了prometheus对shorturl服务的指标监控收集的配置工作,为了演示简单我们进行了手动的配置,在实际的生产环境中一般采用定时更新配置文件或者服务发现的方式来配置监控目标,篇幅有限这里不展开讲解,感兴趣的同学请自行查看相关文档
|
||||
|
||||
## go-zero监控的指标类型
|
||||
|
||||
go-zero中目前在http的中间件和rpc的拦截器中添加了对请求指标的监控。
|
||||
|
||||
主要从请求耗时和请求错误两个维度,请求耗时采用了Histogram指标类型定义了多个Buckets方便进行分位统计,请求错误采用了Counter类型,并在http metric中添加了path标签rpc metric中添加了method标签以便进行细分监控。
|
||||
|
||||
接下来演示如何查看监控指标:
|
||||
|
||||
首先在命令行多次执行如下命令
|
||||
|
||||
```go
|
||||
curl -i "http://localhost:8888/shorten?url=http://www.xiaoheiban.cn"
|
||||
```
|
||||
|
||||
打开Prometheus切换到Graph界面,在输入框中输入{path="/shorten"}指令,即可查看监控指标,如下图
|
||||
|
||||

|
||||
|
||||
我们通过PromQL语法查询过滤path为/shorten的指标,结果中显示了指标名以及指标数值,其中http_server_requests_code_total指标中code值为http的状态码,200表明请求成功,http_server_requests_duration_ms_bucket中对不同bucket结果分别进行了统计,还可以看到所有的指标中都添加了我们配置的默认指标
|
||||
|
||||
Console界面主要展示了查询的指标结果,Graph界面为我们提供了简单的图形化的展示界面,在实际的生产环境中我们一般使用Grafana做图形化的展示
|
||||
|
||||
## grafana可视化界面
|
||||
|
||||
[grafana](https://grafana.com/)是一款可视化工具,功能强大,支持多种数据来源Prometheus、Elasticsearch、Graphite等,安装比较简单请参考[官方文档](https://grafana.com/docs/grafana/latest/),grafana默认端口3000,安装好后再浏览器输入http://localhost:3000/,默认账号和密码都为admin
|
||||
|
||||
下面演示如何基于以上指标进行可视化界面的绘制:
|
||||
|
||||
- 点击左侧边栏Configuration->Data Source->Add data source进行数据源添加,其中HTTP的URL为数据源的地址
|
||||
|
||||

|
||||
|
||||
- 点击左侧边栏添加dashboard,然后添加Variables方便针对不同的标签进行过滤筛选比如添加app变量用来过滤不同的服务
|
||||
|
||||

|
||||
|
||||
- 进入dashboard点击右上角Add panel添加面板,以path维度统计接口的qps
|
||||
|
||||

|
||||
|
||||
- 最终的效果如下所示,可以通过服务名称过滤不同的服务,面板展示了path为/shorten的qps变化趋势
|
||||
|
||||

|
||||
|
||||
## 总结
|
||||
|
||||
以上演示了go-zero中基于prometheus+grafana服务指标监控的简单流程,生产环境中可以根据实际的场景做不同维度的监控分析。现在go-zero的监控指标主要还是针对http和rpc,这对于服务的整体监控显然还是不足的,比如容器资源的监控,依赖的mysql、redis等资源的监控,以及自定义的指标监控等等,go-zero在这方面后续还会持续优化。希望这篇文章能够给您带来帮助
|
||||
@@ -1,6 +1,6 @@
|
||||
# PeriodicalExecutor设计
|
||||
|
||||
# 添加任务
|
||||
## 添加任务
|
||||
|
||||
* 当前没有未执行的任务
|
||||
* 添加并启动定时器
|
||||
@@ -9,7 +9,7 @@
|
||||
* 如到,执行所有缓存任务
|
||||
* 未到,只添加
|
||||
|
||||
# 定时器到期
|
||||
## 定时器到期
|
||||
|
||||
* 清除并执行所有缓存任务
|
||||
* 再等待N个定时周期,如果等待过程中一直没有新任务,则退出
|
||||
* 再等待N个定时周期,如果等待过程中一直没有新任务,则退出
|
||||
|
||||
167
doc/sharedcalls.md
Normal file
@@ -0,0 +1,167 @@
|
||||
# 防止缓存击穿之进程内共享调用
|
||||
|
||||
go-zero微服务框架中提供了许多开箱即用的工具,好的工具不仅能提升服务的性能而且还能提升代码的鲁棒性避免出错,实现代码风格的统一方便他人阅读等等。
|
||||
|
||||
本文主要讲述进程内共享调用神器[SharedCalls](https://github.com/tal-tech/go-zero/blob/master/core/syncx/sharedcalls.go)。
|
||||
|
||||
## 使用场景
|
||||
|
||||
并发场景下,可能会有多个线程(协程)同时请求同一份资源,如果每个请求都要走一遍资源的请求过程,除了比较低效之外,还会对资源服务造成并发的压力。举一个具体例子,比如缓存失效,多个请求同时到达某服务请求某资源,该资源在缓存中已经失效,此时这些请求会继续访问DB做查询,会引起数据库压力瞬间增大。而使用SharedCalls可以使得同时多个请求只需要发起一次拿结果的调用,其他请求"坐享其成",这种设计有效减少了资源服务的并发压力,可以有效防止缓存击穿。
|
||||
|
||||
高并发场景下,当某个热点key缓存失效后,多个请求会同时从数据库加载该资源,并保存到缓存,如果不做防范,可能会导致数据库被直接打死。针对这种场景,go-zero框架中已经提供了实现,具体可参看[sqlc](https://github.com/tal-tech/go-zero/blob/master/core/stores/sqlc/cachedsql.go)和[mongoc](https://github.com/tal-tech/go-zero/blob/master/core/stores/mongoc/cachedcollection.go)等实现代码。
|
||||
|
||||
为了简化演示代码,我们通过多个线程同时去获取一个id来模拟缓存的场景。如下:
|
||||
|
||||
```go
|
||||
func main() {
|
||||
const round = 5
|
||||
var wg sync.WaitGroup
|
||||
barrier := syncx.NewSharedCalls()
|
||||
|
||||
wg.Add(round)
|
||||
for i := 0; i < round; i++ {
|
||||
// 多个线程同时执行
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
// 可以看到,多个线程在同一个key上去请求资源,获取资源的实际函数只会被调用一次
|
||||
val, err := barrier.Do("once", func() (interface{}, error) {
|
||||
// sleep 1秒,为了让多个线程同时取once这个key上的数据
|
||||
time.Sleep(time.Second)
|
||||
// 生成了一个随机的id
|
||||
return stringx.RandId(), nil
|
||||
})
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
} else {
|
||||
fmt.Println(val)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
}
|
||||
```
|
||||
|
||||
运行,打印结果为:
|
||||
|
||||
```
|
||||
837c577b1008a0db
|
||||
837c577b1008a0db
|
||||
837c577b1008a0db
|
||||
837c577b1008a0db
|
||||
837c577b1008a0db
|
||||
```
|
||||
|
||||
可以看出,只要是同一个key上的同时发起的请求,都会共享同一个结果,对获取DB数据进缓存等场景特别有用,可以有效防止缓存击穿。
|
||||
|
||||
## 关键源码分析
|
||||
|
||||
- SharedCalls interface提供了Do和DoEx两种方法的抽象
|
||||
|
||||
```go
|
||||
// SharedCalls接口提供了Do和DoEx两种方法
|
||||
type SharedCalls interface {
|
||||
Do(key string, fn func() (interface{}, error)) (interface{}, error)
|
||||
DoEx(key string, fn func() (interface{}, error)) (interface{}, bool, error)
|
||||
}
|
||||
```
|
||||
|
||||
- SharedCalls interface的具体实现sharedGroup
|
||||
|
||||
```go
|
||||
// call代表对指定资源的一次请求
|
||||
type call struct {
|
||||
wg sync.WaitGroup // 用于协调各个请求goroutine之间的资源共享
|
||||
val interface{} // 用于保存请求的返回值
|
||||
err error // 用于保存请求过程中发生的错误
|
||||
}
|
||||
|
||||
type sharedGroup struct {
|
||||
calls map[string]*call
|
||||
lock sync.Mutex
|
||||
}
|
||||
```
|
||||
|
||||
- sharedGroup的Do方法
|
||||
|
||||
- key参数:可以理解为资源的唯一标识。
|
||||
- fn参数:真正获取资源的方法。
|
||||
- 处理过程分析:
|
||||
|
||||
```go
|
||||
// 当多个请求同时使用Do方法请求资源时
|
||||
func (g *sharedGroup) Do(key string, fn func() (interface{}, error)) (interface{}, error) {
|
||||
// 先申请加锁
|
||||
g.lock.Lock()
|
||||
|
||||
// 根据key,获取对应的call结果,并用变量c保存
|
||||
if c, ok := g.calls[key]; ok {
|
||||
// 拿到call以后,释放锁,此处call可能还没有实际数据,只是一个空的内存占位
|
||||
g.lock.Unlock()
|
||||
// 调用wg.Wait,判断是否有其他goroutine正在申请资源,如果阻塞,说明有其他goroutine正在获取资源
|
||||
c.wg.Wait()
|
||||
// 当wg.Wait不再阻塞,表示资源获取已经结束,可以直接返回结果
|
||||
return c.val, c.err
|
||||
}
|
||||
|
||||
// 没有拿到结果,则调用makeCall方法去获取资源,注意此处仍然是锁住的,可以保证只有一个goroutine可以调用makecall
|
||||
c := g.makeCall(key, fn)
|
||||
// 返回调用结果
|
||||
return c.val, c.err
|
||||
}
|
||||
```
|
||||
|
||||
- sharedGroup的DoEx方法
|
||||
|
||||
- 和Do方法类似,只是返回值中增加了布尔值表示值是调用makeCall方法直接获取的,还是取的共享成果
|
||||
|
||||
```go
|
||||
func (g *sharedGroup) DoEx(key string, fn func() (interface{}, error)) (val interface{}, fresh bool, err error) {
|
||||
g.lock.Lock()
|
||||
if c, ok := g.calls[key]; ok {
|
||||
g.lock.Unlock()
|
||||
c.wg.Wait()
|
||||
return c.val, false, c.err
|
||||
}
|
||||
|
||||
c := g.makeCall(key, fn)
|
||||
return c.val, true, c.err
|
||||
}
|
||||
```
|
||||
|
||||
- sharedGroup的makeCall方法
|
||||
|
||||
- 该方法由Do和DoEx方法调用,是真正发起资源请求的方法。
|
||||
|
||||
```go
|
||||
// 进入makeCall的一定只有一个goroutine,因为要拿锁锁住的
|
||||
func (g *sharedGroup) makeCall(key string, fn func() (interface{}, error)) *call {
|
||||
// 创建call结构,用于保存本次请求的结果
|
||||
c := new(call)
|
||||
// wg加1,用于通知其他请求资源的goroutine等待本次资源获取的结束
|
||||
c.wg.Add(1)
|
||||
// 将用于保存结果的call放入map中,以供其他goroutine获取
|
||||
g.calls[key] = c
|
||||
// 释放锁,这样其他请求的goroutine才能获取call的内存占位
|
||||
g.lock.Unlock()
|
||||
|
||||
defer func() {
|
||||
// delete key first, done later. can't reverse the order, because if reverse,
|
||||
// another Do call might wg.Wait() without get notified with wg.Done()
|
||||
g.lock.Lock()
|
||||
delete(g.calls, key)
|
||||
g.lock.Unlock()
|
||||
|
||||
// 调用wg.Done,通知其他goroutine可以返回结果,这样本批次所有请求完成结果的共享
|
||||
c.wg.Done()
|
||||
}()
|
||||
|
||||
// 调用fn方法,将结果填入变量c中
|
||||
c.val, c.err = fn()
|
||||
return c
|
||||
}
|
||||
```
|
||||
|
||||
## 最后
|
||||
|
||||
本文主要介绍了go-zero框架中的 SharedCalls工具,对其应用场景和关键代码做了简单的梳理,希望本篇文章能给大家带来一些收获。
|
||||
@@ -1,8 +1,8 @@
|
||||
English | [简体中文](shorturl.md)
|
||||
|
||||
# Rapid development of microservices
|
||||
|
||||
## 0. Why building microservices are so difficult?
|
||||
English | [简体中文](shorturl.md)
|
||||
|
||||
## 0. Why building microservices are so difficult
|
||||
|
||||
To build a well working microservice, we need lots of knowledges from different aspects.
|
||||
|
||||
@@ -25,7 +25,7 @@ As well, we always adhere to the idea that **prefer tools over conventions and d
|
||||
|
||||
Let’s take the shorturl microservice as a quick example to demonstrate how to quickly create microservices by using [go-zero](https://github.com/tal-tech/go-zero). After finishing this tutorial, you’ll find that it’s so easy to write microservices!
|
||||
|
||||
## 1. What is a shorturl service?
|
||||
## 1. What is a shorturl service
|
||||
|
||||
A shorturl service is that it converts a long url into a short one, by well designed algorithms.
|
||||
|
||||
@@ -129,7 +129,7 @@ And now, let’s walk through the complete flow of quickly create a microservice
|
||||
|
||||
the generated file structure looks like:
|
||||
|
||||
```
|
||||
```Plain Text
|
||||
.
|
||||
├── api
|
||||
│ ├── etc
|
||||
@@ -232,7 +232,7 @@ And now, let’s walk through the complete flow of quickly create a microservice
|
||||
|
||||
the generated file structure looks like:
|
||||
|
||||
```
|
||||
```Plain Text
|
||||
rpc/transform
|
||||
├── etc
|
||||
│ └── transform.yaml // configuration file
|
||||
@@ -284,7 +284,7 @@ And now, let’s walk through the complete flow of quickly create a microservice
|
||||
```go
|
||||
type Config struct {
|
||||
rest.RestConf
|
||||
Transform rpcx.RpcClientConf // manual code
|
||||
Transform zrpc.RpcClientConf // manual code
|
||||
}
|
||||
```
|
||||
|
||||
@@ -299,7 +299,7 @@ And now, let’s walk through the complete flow of quickly create a microservice
|
||||
func NewServiceContext(c config.Config) *ServiceContext {
|
||||
return &ServiceContext{
|
||||
Config: c,
|
||||
Transformer: transformer.NewTransformer(rpcx.MustNewClient(c.Transform)), // manual code
|
||||
Transformer: transformer.NewTransformer(zrpc.MustNewClient(c.Transform)), // manual code
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -375,7 +375,7 @@ Till now, we’ve done the modification of API Gateway. All the manually added c
|
||||
source shorturl.sql;
|
||||
```
|
||||
|
||||
* under the directory `rpc/transform/model execute the following command to genrate CRUD+cache code, `-c` means using `redis cache`
|
||||
* under the directory `rpc/transform/model` execute the following command to genrate CRUD+cache code, `-c` means using `redis cache`
|
||||
|
||||
```shell
|
||||
goctl model mysql ddl -c -src shorturl.sql -dir .
|
||||
@@ -385,7 +385,7 @@ Till now, we’ve done the modification of API Gateway. All the manually added c
|
||||
|
||||
the generated file structure looks like:
|
||||
|
||||
```
|
||||
```Plain Text
|
||||
rpc/transform/model
|
||||
├── shorturl.sql
|
||||
├── shorturlmodel.go // CRUD+cache code
|
||||
@@ -409,7 +409,7 @@ Till now, we’ve done the modification of API Gateway. All the manually added c
|
||||
|
||||
```go
|
||||
type Config struct {
|
||||
rpcx.RpcServerConf
|
||||
zrpc.RpcServerConf
|
||||
DataSource string // manual code
|
||||
Table string // manual code
|
||||
Cache cache.CacheConf // manual code
|
||||
@@ -533,4 +533,3 @@ We not only keep the framework simple, but also encapsulate the complexity into
|
||||
For the generated code by goctl, lots of microservice components are included, like concurrency control, adaptive circuit breaker, adaptive load shedding, auto cache control etc. And it’s easy to deal with the busy sites.
|
||||
|
||||
If you have any ideas that can help us to improve the productivity, tell me any time! 👏
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
[English](shorturl-en.md) | 简体中文
|
||||
|
||||
# 快速构建高并发微服务
|
||||
|
||||
## 0. 为什么说做好微服务很难?
|
||||
[English](shorturl-en.md) | 简体中文
|
||||
|
||||
## 0. 为什么说做好微服务很难
|
||||
|
||||
要想做好微服务,我们需要理解和掌握的知识点非常多,从几个维度上来说:
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
|
||||
下面我通过短链微服务来演示通过[go-zero](https://github.com/tal-tech/go-zero)快速的创建微服务的流程,走完一遍,你就会发现:原来编写微服务如此简单!
|
||||
|
||||
## 1. 什么是短链服务?
|
||||
## 1. 什么是短链服务
|
||||
|
||||
短链服务就是将长的URL网址,通过程序计算等方式,转换为简短的网址字符串。
|
||||
|
||||
@@ -123,7 +123,7 @@
|
||||
|
||||
生成的文件结构如下:
|
||||
|
||||
```
|
||||
```Plain Text
|
||||
.
|
||||
├── api
|
||||
│ ├── etc
|
||||
@@ -228,7 +228,7 @@
|
||||
|
||||
文件结构如下:
|
||||
|
||||
```
|
||||
```Plain Text
|
||||
rpc/transform
|
||||
├── etc
|
||||
│ └── transform.yaml // 配置文件
|
||||
@@ -280,7 +280,7 @@
|
||||
```go
|
||||
type Config struct {
|
||||
rest.RestConf
|
||||
Transform rpcx.RpcClientConf // 手动代码
|
||||
Transform zrpc.RpcClientConf // 手动代码
|
||||
}
|
||||
```
|
||||
|
||||
@@ -295,7 +295,7 @@
|
||||
func NewServiceContext(c config.Config) *ServiceContext {
|
||||
return &ServiceContext{
|
||||
Config: c,
|
||||
Transformer: transformer.NewTransformer(rpcx.MustNewClient(c.Transform)), // 手动代码
|
||||
Transformer: transformer.NewTransformer(zrpc.MustNewClient(c.Transform)), // 手动代码
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -320,7 +320,6 @@
|
||||
// 手动代码结束
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
通过调用`transformer`的`Expand`方法实现短链恢复到url
|
||||
|
||||
@@ -342,7 +341,6 @@
|
||||
// 手动代码结束
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
通过调用`transformer`的`Shorten`方法实现url到短链的变换
|
||||
|
||||
@@ -383,7 +381,7 @@
|
||||
|
||||
生成后的文件结构如下:
|
||||
|
||||
```
|
||||
```Plain Text
|
||||
rpc/transform/model
|
||||
├── shorturl.sql
|
||||
├── shorturlmodel.go // CRUD+cache代码
|
||||
@@ -407,7 +405,7 @@
|
||||
|
||||
```go
|
||||
type Config struct {
|
||||
rpcx.RpcServerConf
|
||||
zrpc.RpcServerConf
|
||||
DataSource string // 手动代码
|
||||
Table string // 手动代码
|
||||
Cache cache.CacheConf // 手动代码
|
||||
@@ -531,4 +529,3 @@ go-zero不只是一个框架,更是一个建立在框架+工具基础上的,
|
||||
通过go-zero+goctl生成的代码,包含了微服务治理的各种组件,包括:并发控制、自适应熔断、自适应降载、自动缓存控制等,可以轻松部署以承载巨大访问量。
|
||||
|
||||
有任何好的提升工程效率的想法,随时欢迎交流!👏
|
||||
|
||||
|
||||
@@ -21,4 +21,3 @@
|
||||
* 通过Primary到DB查询行记录,并写入缓存
|
||||
* 有Primary到行记录的缓存
|
||||
* 直接返回缓存结果
|
||||
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
|
||||
"bookstore/api/internal/config"
|
||||
"bookstore/api/internal/handler"
|
||||
"bookstore/api/internal/svc"
|
||||
"flag"
|
||||
|
||||
"github.com/tal-tech/go-zero/core/conf"
|
||||
"github.com/tal-tech/go-zero/rest"
|
||||
@@ -23,5 +25,7 @@ func main() {
|
||||
defer server.Stop()
|
||||
|
||||
handler.RegisterHandlers(server, ctx)
|
||||
|
||||
fmt.Printf("Starting server at %s:%d...\n", c.Host, c.Port)
|
||||
server.Start()
|
||||
}
|
||||
|
||||
@@ -2,11 +2,11 @@ package config
|
||||
|
||||
import (
|
||||
"github.com/tal-tech/go-zero/rest"
|
||||
"github.com/tal-tech/go-zero/rpcx"
|
||||
"github.com/tal-tech/go-zero/zrpc"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
rest.RestConf
|
||||
Add rpcx.RpcClientConf
|
||||
Check rpcx.RpcClientConf
|
||||
}
|
||||
Add zrpc.RpcClientConf
|
||||
Check zrpc.RpcClientConf
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
"bookstore/rpc/add/adder"
|
||||
"bookstore/rpc/check/checker"
|
||||
|
||||
"github.com/tal-tech/go-zero/rpcx"
|
||||
"github.com/tal-tech/go-zero/zrpc"
|
||||
)
|
||||
|
||||
type ServiceContext struct {
|
||||
@@ -17,7 +17,7 @@ type ServiceContext struct {
|
||||
func NewServiceContext(c config.Config) *ServiceContext {
|
||||
return &ServiceContext{
|
||||
Config: c,
|
||||
Adder: adder.NewAdder(rpcx.MustNewClient(c.Add)),
|
||||
Checker: checker.NewChecker(rpcx.MustNewClient(c.Check)),
|
||||
Adder: adder.NewAdder(zrpc.MustNewClient(c.Add)),
|
||||
Checker: checker.NewChecker(zrpc.MustNewClient(c.Check)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,16 +4,17 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
|
||||
"bookstore/rpc/add/internal/config"
|
||||
"bookstore/rpc/add/internal/server"
|
||||
"bookstore/rpc/add/internal/svc"
|
||||
add "bookstore/rpc/add/pb"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"github.com/tal-tech/go-zero/core/conf"
|
||||
"github.com/tal-tech/go-zero/rpcx"
|
||||
"github.com/tal-tech/go-zero/core/logx"
|
||||
"github.com/tal-tech/go-zero/zrpc"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
@@ -27,12 +28,10 @@ func main() {
|
||||
ctx := svc.NewServiceContext(c)
|
||||
adderSrv := server.NewAdderServer(ctx)
|
||||
|
||||
s, err := rpcx.NewServer(c.RpcServerConf, func(grpcServer *grpc.Server) {
|
||||
s, err := zrpc.NewServer(c.RpcServerConf, func(grpcServer *grpc.Server) {
|
||||
add.RegisterAdderServer(grpcServer, adderSrv)
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
logx.Must(err)
|
||||
|
||||
fmt.Printf("Starting rpc server at %s...\n", c.ListenOn)
|
||||
s.Start()
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
"context"
|
||||
|
||||
"github.com/tal-tech/go-zero/core/jsonx"
|
||||
"github.com/tal-tech/go-zero/rpcx"
|
||||
"github.com/tal-tech/go-zero/zrpc"
|
||||
)
|
||||
|
||||
type (
|
||||
@@ -19,11 +19,11 @@ type (
|
||||
}
|
||||
|
||||
defaultAdder struct {
|
||||
cli rpcx.Client
|
||||
cli zrpc.Client
|
||||
}
|
||||
)
|
||||
|
||||
func NewAdder(cli rpcx.Client) Adder {
|
||||
func NewAdder(cli zrpc.Client) Adder {
|
||||
return &defaultAdder{
|
||||
cli: cli,
|
||||
}
|
||||
|
||||
@@ -2,12 +2,12 @@ package config
|
||||
|
||||
import (
|
||||
"github.com/tal-tech/go-zero/core/stores/cache"
|
||||
"github.com/tal-tech/go-zero/rpcx"
|
||||
"github.com/tal-tech/go-zero/zrpc"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
rpcx.RpcServerConf
|
||||
zrpc.RpcServerConf
|
||||
DataSource string
|
||||
Table string
|
||||
Cache cache.CacheConf
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,16 +4,17 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
|
||||
"bookstore/rpc/check/internal/config"
|
||||
"bookstore/rpc/check/internal/server"
|
||||
"bookstore/rpc/check/internal/svc"
|
||||
check "bookstore/rpc/check/pb"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"github.com/tal-tech/go-zero/core/conf"
|
||||
"github.com/tal-tech/go-zero/rpcx"
|
||||
"github.com/tal-tech/go-zero/core/logx"
|
||||
"github.com/tal-tech/go-zero/zrpc"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
@@ -27,12 +28,10 @@ func main() {
|
||||
ctx := svc.NewServiceContext(c)
|
||||
checkerSrv := server.NewCheckerServer(ctx)
|
||||
|
||||
s, err := rpcx.NewServer(c.RpcServerConf, func(grpcServer *grpc.Server) {
|
||||
s, err := zrpc.NewServer(c.RpcServerConf, func(grpcServer *grpc.Server) {
|
||||
check.RegisterCheckerServer(grpcServer, checkerSrv)
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
logx.Must(err)
|
||||
|
||||
fmt.Printf("Starting rpc server at %s...\n", c.ListenOn)
|
||||
s.Start()
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
"context"
|
||||
|
||||
"github.com/tal-tech/go-zero/core/jsonx"
|
||||
"github.com/tal-tech/go-zero/rpcx"
|
||||
"github.com/tal-tech/go-zero/zrpc"
|
||||
)
|
||||
|
||||
type (
|
||||
@@ -19,11 +19,11 @@ type (
|
||||
}
|
||||
|
||||
defaultChecker struct {
|
||||
cli rpcx.Client
|
||||
cli zrpc.Client
|
||||
}
|
||||
)
|
||||
|
||||
func NewChecker(cli rpcx.Client) Checker {
|
||||
func NewChecker(cli zrpc.Client) Checker {
|
||||
return &defaultChecker{
|
||||
cli: cli,
|
||||
}
|
||||
|
||||
@@ -2,12 +2,12 @@ package config
|
||||
|
||||
import (
|
||||
"github.com/tal-tech/go-zero/core/stores/cache"
|
||||
"github.com/tal-tech/go-zero/rpcx"
|
||||
"github.com/tal-tech/go-zero/zrpc"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
rpcx.RpcServerConf
|
||||
zrpc.RpcServerConf
|
||||
DataSource string
|
||||
Table string
|
||||
Cache cache.CacheConf
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,10 +2,10 @@ package config
|
||||
|
||||
import (
|
||||
"github.com/tal-tech/go-zero/rest"
|
||||
"github.com/tal-tech/go-zero/rpcx"
|
||||
"github.com/tal-tech/go-zero/zrpc"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
rest.RestConf
|
||||
Rpc rpcx.RpcClientConf
|
||||
Rpc zrpc.RpcClientConf
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
"github.com/tal-tech/go-zero/example/graceful/dns/api/handler"
|
||||
"github.com/tal-tech/go-zero/example/graceful/dns/api/svc"
|
||||
"github.com/tal-tech/go-zero/rest"
|
||||
"github.com/tal-tech/go-zero/rpcx"
|
||||
"github.com/tal-tech/go-zero/zrpc"
|
||||
)
|
||||
|
||||
var configFile = flag.String("f", "etc/graceful-api.json", "the config file")
|
||||
@@ -19,7 +19,7 @@ func main() {
|
||||
var c config.Config
|
||||
conf.MustLoad(*configFile, &c)
|
||||
|
||||
client := rpcx.MustNewClient(c.Rpc)
|
||||
client := zrpc.MustNewClient(c.Rpc)
|
||||
ctx := &svc.ServiceContext{
|
||||
Client: client,
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package svc
|
||||
|
||||
import "github.com/tal-tech/go-zero/rpcx"
|
||||
import "github.com/tal-tech/go-zero/zrpc"
|
||||
|
||||
type ServiceContext struct {
|
||||
Client rpcx.Client
|
||||
Client zrpc.Client
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
|
||||
"github.com/tal-tech/go-zero/core/conf"
|
||||
"github.com/tal-tech/go-zero/example/graceful/dns/rpc/graceful"
|
||||
"github.com/tal-tech/go-zero/rpcx"
|
||||
"github.com/tal-tech/go-zero/zrpc"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
@@ -39,10 +39,10 @@ func (gs *GracefulServer) Grace(ctx context.Context, req *graceful.Request) (*gr
|
||||
func main() {
|
||||
flag.Parse()
|
||||
|
||||
var c rpcx.RpcServerConf
|
||||
var c zrpc.RpcServerConf
|
||||
conf.MustLoad(*configFile, &c)
|
||||
|
||||
server := rpcx.MustNewServer(c, func(grpcServer *grpc.Server) {
|
||||
server := zrpc.MustNewServer(c, func(grpcServer *grpc.Server) {
|
||||
graceful.RegisterGraceServiceServer(grpcServer, NewGracefulServer())
|
||||
})
|
||||
defer server.Stop()
|
||||
|
||||
@@ -2,10 +2,10 @@ package config
|
||||
|
||||
import (
|
||||
"github.com/tal-tech/go-zero/rest"
|
||||
"github.com/tal-tech/go-zero/rpcx"
|
||||
"github.com/tal-tech/go-zero/zrpc"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
rest.RestConf
|
||||
Rpc rpcx.RpcClientConf
|
||||
Rpc zrpc.RpcClientConf
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
"Rpc": {
|
||||
"Etcd": {
|
||||
"Hosts": ["etcd.discov:2379"],
|
||||
"Key": "rpcx"
|
||||
"Key": "zrpc"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
"github.com/tal-tech/go-zero/example/graceful/etcd/api/handler"
|
||||
"github.com/tal-tech/go-zero/example/graceful/etcd/api/svc"
|
||||
"github.com/tal-tech/go-zero/rest"
|
||||
"github.com/tal-tech/go-zero/rpcx"
|
||||
"github.com/tal-tech/go-zero/zrpc"
|
||||
)
|
||||
|
||||
var configFile = flag.String("f", "etc/graceful-api.json", "the config file")
|
||||
@@ -19,7 +19,7 @@ func main() {
|
||||
var c config.Config
|
||||
conf.MustLoad(*configFile, &c)
|
||||
|
||||
client := rpcx.MustNewClient(c.Rpc)
|
||||
client := zrpc.MustNewClient(c.Rpc)
|
||||
ctx := &svc.ServiceContext{
|
||||
Client: client,
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package svc
|
||||
|
||||
import "github.com/tal-tech/go-zero/rpcx"
|
||||
import "github.com/tal-tech/go-zero/zrpc"
|
||||
|
||||
type ServiceContext struct {
|
||||
Client rpcx.Client
|
||||
Client zrpc.Client
|
||||
}
|
||||
|
||||
@@ -3,6 +3,6 @@
|
||||
"ListenOn": "0.0.0.0:3456",
|
||||
"Etcd": {
|
||||
"Hosts": ["etcd.discov:2379"],
|
||||
"Key": "rpcx"
|
||||
"Key": "zrpc"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
|
||||
"github.com/tal-tech/go-zero/core/conf"
|
||||
"github.com/tal-tech/go-zero/example/graceful/etcd/rpc/graceful"
|
||||
"github.com/tal-tech/go-zero/rpcx"
|
||||
"github.com/tal-tech/go-zero/zrpc"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
@@ -39,10 +39,10 @@ func (gs *GracefulServer) Grace(ctx context.Context, req *graceful.Request) (*gr
|
||||
func main() {
|
||||
flag.Parse()
|
||||
|
||||
var c rpcx.RpcServerConf
|
||||
var c zrpc.RpcServerConf
|
||||
conf.MustLoad(*configFile, &c)
|
||||
|
||||
server := rpcx.MustNewServer(c, func(grpcServer *grpc.Server) {
|
||||
server := zrpc.MustNewServer(c, func(grpcServer *grpc.Server) {
|
||||
graceful.RegisterGraceServiceServer(grpcServer, NewGracefulServer())
|
||||
})
|
||||
defer server.Stop()
|
||||
|
||||
@@ -25,4 +25,3 @@
|
||||
`watch -n 5 kubectl top pod -n adhoc`
|
||||
|
||||
可以看到`kubectl`报告的`CPU`使用率,两者进行对比,即可知道是否准确
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
|
||||
"github.com/tal-tech/go-zero/core/discov"
|
||||
"github.com/tal-tech/go-zero/example/rpc/remote/unary"
|
||||
"github.com/tal-tech/go-zero/rpcx"
|
||||
"github.com/tal-tech/go-zero/zrpc"
|
||||
)
|
||||
|
||||
const timeFormat = "15:04:05"
|
||||
@@ -16,10 +16,10 @@ const timeFormat = "15:04:05"
|
||||
func main() {
|
||||
flag.Parse()
|
||||
|
||||
client := rpcx.MustNewClient(rpcx.RpcClientConf{
|
||||
client := zrpc.MustNewClient(zrpc.RpcClientConf{
|
||||
Etcd: discov.EtcdConf{
|
||||
Hosts: []string{"localhost:2379"},
|
||||
Key: "rpcx",
|
||||
Key: "zrpc",
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
|
||||
"github.com/tal-tech/go-zero/core/discov"
|
||||
"github.com/tal-tech/go-zero/example/rpc/remote/unary"
|
||||
"github.com/tal-tech/go-zero/rpcx"
|
||||
"github.com/tal-tech/go-zero/zrpc"
|
||||
)
|
||||
|
||||
var lb = flag.String("t", "direct", "the load balancer type")
|
||||
@@ -17,20 +17,20 @@ var lb = flag.String("t", "direct", "the load balancer type")
|
||||
func main() {
|
||||
flag.Parse()
|
||||
|
||||
var cli rpcx.Client
|
||||
var cli zrpc.Client
|
||||
switch *lb {
|
||||
case "direct":
|
||||
cli = rpcx.MustNewClient(rpcx.RpcClientConf{
|
||||
cli = zrpc.MustNewClient(zrpc.RpcClientConf{
|
||||
Endpoints: []string{
|
||||
"localhost:3456",
|
||||
"localhost:3457",
|
||||
},
|
||||
})
|
||||
case "discov":
|
||||
cli = rpcx.MustNewClient(rpcx.RpcClientConf{
|
||||
cli = zrpc.MustNewClient(zrpc.RpcClientConf{
|
||||
Etcd: discov.EtcdConf{
|
||||
Hosts: []string{"localhost:2379"},
|
||||
Key: "rpcx",
|
||||
Key: "zrpc",
|
||||
},
|
||||
})
|
||||
default:
|
||||
|
||||
@@ -8,17 +8,17 @@ import (
|
||||
|
||||
"github.com/tal-tech/go-zero/core/discov"
|
||||
"github.com/tal-tech/go-zero/example/rpc/remote/stream"
|
||||
"github.com/tal-tech/go-zero/rpcx"
|
||||
"github.com/tal-tech/go-zero/zrpc"
|
||||
)
|
||||
|
||||
const name = "kevin"
|
||||
|
||||
var key = flag.String("key", "rpcx", "the key on etcd")
|
||||
var key = flag.String("key", "zrpc", "the key on etcd")
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
|
||||
client, err := rpcx.NewClientNoAuth(discov.EtcdConf{
|
||||
client, err := zrpc.NewClientNoAuth(discov.EtcdConf{
|
||||
Hosts: []string{"localhost:2379"},
|
||||
Key: *key,
|
||||
})
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
|
||||
"github.com/tal-tech/go-zero/core/conf"
|
||||
"github.com/tal-tech/go-zero/example/rpc/remote/unary"
|
||||
"github.com/tal-tech/go-zero/rpcx"
|
||||
"github.com/tal-tech/go-zero/zrpc"
|
||||
)
|
||||
|
||||
var configFile = flag.String("f", "config.json", "the config file")
|
||||
@@ -16,9 +16,9 @@ var configFile = flag.String("f", "config.json", "the config file")
|
||||
func main() {
|
||||
flag.Parse()
|
||||
|
||||
var c rpcx.RpcClientConf
|
||||
var c zrpc.RpcClientConf
|
||||
conf.MustLoad(*configFile, &c)
|
||||
client := rpcx.MustNewClient(c)
|
||||
client := zrpc.MustNewClient(c)
|
||||
ticker := time.NewTicker(time.Millisecond * 500)
|
||||
defer ticker.Stop()
|
||||
for {
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
"github.com/tal-tech/go-zero/core/logx"
|
||||
"github.com/tal-tech/go-zero/core/service"
|
||||
"github.com/tal-tech/go-zero/example/rpc/remote/unary"
|
||||
"github.com/tal-tech/go-zero/rpcx"
|
||||
"github.com/tal-tech/go-zero/zrpc"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
@@ -17,7 +17,7 @@ var (
|
||||
)
|
||||
|
||||
type GreetServer struct {
|
||||
*rpcx.RpcProxy
|
||||
*zrpc.RpcProxy
|
||||
}
|
||||
|
||||
func (s *GreetServer) Greet(ctx context.Context, req *unary.Request) (*unary.Response, error) {
|
||||
@@ -33,7 +33,7 @@ func (s *GreetServer) Greet(ctx context.Context, req *unary.Request) (*unary.Res
|
||||
func main() {
|
||||
flag.Parse()
|
||||
|
||||
proxy := rpcx.MustNewServer(rpcx.RpcServerConf{
|
||||
proxy := zrpc.MustNewServer(zrpc.RpcServerConf{
|
||||
ServiceConf: service.ServiceConf{
|
||||
Log: logx.LogConf{
|
||||
Mode: "console",
|
||||
@@ -42,7 +42,7 @@ func main() {
|
||||
ListenOn: *listen,
|
||||
}, func(grpcServer *grpc.Server) {
|
||||
unary.RegisterGreeterServer(grpcServer, &GreetServer{
|
||||
RpcProxy: rpcx.NewProxy(*server),
|
||||
RpcProxy: zrpc.NewProxy(*server),
|
||||
})
|
||||
})
|
||||
proxy.Start()
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
"Hosts": [
|
||||
"localhost:2379"
|
||||
],
|
||||
"Key": "rpcx"
|
||||
"Key": "zrpc"
|
||||
},
|
||||
"Redis": {
|
||||
"Host": "localhost:6379",
|
||||
|
||||
@@ -6,7 +6,7 @@ import (
|
||||
|
||||
"github.com/tal-tech/go-zero/core/conf"
|
||||
"github.com/tal-tech/go-zero/example/rpc/remote/stream"
|
||||
"github.com/tal-tech/go-zero/rpcx"
|
||||
"github.com/tal-tech/go-zero/zrpc"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
@@ -39,10 +39,10 @@ func (gs StreamGreetServer) Greet(s stream.StreamGreeter_GreetServer) error {
|
||||
}
|
||||
|
||||
func main() {
|
||||
var c rpcx.RpcServerConf
|
||||
var c zrpc.RpcServerConf
|
||||
conf.MustLoad("etc/config.json", &c)
|
||||
|
||||
server := rpcx.MustNewServer(c, func(grpcServer *grpc.Server) {
|
||||
server := zrpc.MustNewServer(c, func(grpcServer *grpc.Server) {
|
||||
stream.RegisterStreamGreeterServer(grpcServer, StreamGreetServer(0))
|
||||
})
|
||||
server.Start()
|
||||
|
||||
@@ -8,6 +8,6 @@
|
||||
"Hosts": [
|
||||
"localhost:2379"
|
||||
],
|
||||
"Key": "rpcx"
|
||||
"Key": "zrpc"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,6 @@
|
||||
"Hosts": [
|
||||
"localhost:2379"
|
||||
],
|
||||
"Key": "rpcx"
|
||||
"Key": "zrpc"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
"Hosts": [
|
||||
"etcd.discov:2379"
|
||||
],
|
||||
"Key": "rpcx"
|
||||
"Key": "zrpc"
|
||||
},
|
||||
"Timeout": 500
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
|
||||
"github.com/tal-tech/go-zero/core/conf"
|
||||
"github.com/tal-tech/go-zero/example/rpc/remote/unary"
|
||||
"github.com/tal-tech/go-zero/rpcx"
|
||||
"github.com/tal-tech/go-zero/zrpc"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
@@ -44,10 +44,10 @@ func (gs *GreetServer) Greet(ctx context.Context, req *unary.Request) (*unary.Re
|
||||
func main() {
|
||||
flag.Parse()
|
||||
|
||||
var c rpcx.RpcServerConf
|
||||
var c zrpc.RpcServerConf
|
||||
conf.MustLoad(*configFile, &c)
|
||||
|
||||
server := rpcx.MustNewServer(c, func(grpcServer *grpc.Server) {
|
||||
server := zrpc.MustNewServer(c, func(grpcServer *grpc.Server) {
|
||||
unary.RegisterGreeterServer(grpcServer, NewGreetServer())
|
||||
})
|
||||
server.Start()
|
||||
|
||||
@@ -2,10 +2,10 @@ package config
|
||||
|
||||
import (
|
||||
"github.com/tal-tech/go-zero/rest"
|
||||
"github.com/tal-tech/go-zero/rpcx"
|
||||
"github.com/tal-tech/go-zero/zrpc"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
rest.RestConf
|
||||
Transform rpcx.RpcClientConf
|
||||
Transform zrpc.RpcClientConf
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import (
|
||||
"shorturl/api/internal/config"
|
||||
"shorturl/rpc/transform/transformer"
|
||||
|
||||
"github.com/tal-tech/go-zero/rpcx"
|
||||
"github.com/tal-tech/go-zero/zrpc"
|
||||
)
|
||||
|
||||
type ServiceContext struct {
|
||||
@@ -15,6 +15,6 @@ type ServiceContext struct {
|
||||
func NewServiceContext(c config.Config) *ServiceContext {
|
||||
return &ServiceContext{
|
||||
Config: c,
|
||||
Transformer: transformer.NewTransformer(rpcx.MustNewClient(c.Transform)),
|
||||
Transformer: transformer.NewTransformer(zrpc.MustNewClient(c.Transform)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,11 +2,11 @@ package config
|
||||
|
||||
import (
|
||||
"github.com/tal-tech/go-zero/core/stores/cache"
|
||||
"github.com/tal-tech/go-zero/rpcx"
|
||||
"github.com/tal-tech/go-zero/zrpc"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
rpcx.RpcServerConf
|
||||
zrpc.RpcServerConf
|
||||
DataSource string
|
||||
Table string
|
||||
Cache cache.CacheConf
|
||||
|
||||
@@ -14,7 +14,7 @@ import (
|
||||
transform "shorturl/rpc/transform/pb"
|
||||
|
||||
"github.com/tal-tech/go-zero/core/conf"
|
||||
"github.com/tal-tech/go-zero/rpcx"
|
||||
"github.com/tal-tech/go-zero/zrpc"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
@@ -28,7 +28,7 @@ func main() {
|
||||
ctx := svc.NewServiceContext(c)
|
||||
transformerSrv := server.NewTransformerServer(ctx)
|
||||
|
||||
s, err := rpcx.NewServer(c.RpcServerConf, func(grpcServer *grpc.Server) {
|
||||
s, err := zrpc.NewServer(c.RpcServerConf, func(grpcServer *grpc.Server) {
|
||||
transform.RegisterTransformerServer(grpcServer, transformerSrv)
|
||||
})
|
||||
if err != nil {
|
||||
|
||||
@@ -11,7 +11,7 @@ import (
|
||||
transform "shorturl/rpc/transform/pb"
|
||||
|
||||
"github.com/tal-tech/go-zero/core/jsonx"
|
||||
"github.com/tal-tech/go-zero/rpcx"
|
||||
"github.com/tal-tech/go-zero/zrpc"
|
||||
)
|
||||
|
||||
type (
|
||||
@@ -21,11 +21,11 @@ type (
|
||||
}
|
||||
|
||||
defaultTransformer struct {
|
||||
cli rpcx.Client
|
||||
cli zrpc.Client
|
||||
}
|
||||
)
|
||||
|
||||
func NewTransformer(cli rpcx.Client) Transformer {
|
||||
func NewTransformer(cli zrpc.Client) Transformer {
|
||||
return &defaultTransformer{
|
||||
cli: cli,
|
||||
}
|
||||
|
||||
34
example/syncx/sharedcalls/main.go
Normal file
@@ -0,0 +1,34 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/tal-tech/go-zero/core/stringx"
|
||||
"github.com/tal-tech/go-zero/core/syncx"
|
||||
)
|
||||
|
||||
func main() {
|
||||
const round = 5
|
||||
var wg sync.WaitGroup
|
||||
barrier := syncx.NewSharedCalls()
|
||||
|
||||
wg.Add(round)
|
||||
for i := 0; i < round; i++ {
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
val, err := barrier.Do("once", func() (interface{}, error) {
|
||||
time.Sleep(time.Second)
|
||||
return stringx.RandId(), nil
|
||||
})
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
} else {
|
||||
fmt.Println(val)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
}
|
||||
@@ -1,3 +1,13 @@
|
||||
{
|
||||
"Server": "localhost:3456"
|
||||
"Name": "edge-api",
|
||||
"Host": "0.0.0.0",
|
||||
"Port": 3456,
|
||||
"Portal": {
|
||||
"Etcd": {
|
||||
"Hosts": [
|
||||
"localhost:2379"
|
||||
],
|
||||
"Key": "portal"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,14 +10,19 @@ import (
|
||||
"github.com/tal-tech/go-zero/example/tracing/remote/portal"
|
||||
"github.com/tal-tech/go-zero/rest"
|
||||
"github.com/tal-tech/go-zero/rest/httpx"
|
||||
"github.com/tal-tech/go-zero/rpcx"
|
||||
"github.com/tal-tech/go-zero/zrpc"
|
||||
)
|
||||
|
||||
var (
|
||||
configFile = flag.String("f", "config.json", "the config file")
|
||||
client rpcx.Client
|
||||
client zrpc.Client
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
rest.RestConf
|
||||
Portal zrpc.RpcClientConf
|
||||
}
|
||||
|
||||
func handle(w http.ResponseWriter, r *http.Request) {
|
||||
conn := client.Conn()
|
||||
greet := portal.NewPortalClient(conn)
|
||||
@@ -34,16 +39,16 @@ func handle(w http.ResponseWriter, r *http.Request) {
|
||||
func main() {
|
||||
flag.Parse()
|
||||
|
||||
var c rpcx.RpcClientConf
|
||||
var c Config
|
||||
conf.MustLoad(*configFile, &c)
|
||||
client = rpcx.MustNewClient(c)
|
||||
client = zrpc.MustNewClient(c.Portal)
|
||||
engine := rest.MustNewServer(rest.RestConf{
|
||||
ServiceConf: service.ServiceConf{
|
||||
Log: logx.LogConf{
|
||||
Mode: "console",
|
||||
},
|
||||
},
|
||||
Port: 3333,
|
||||
Port: c.Port,
|
||||
})
|
||||
defer engine.Stop()
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
"github.com/tal-tech/go-zero/core/conf"
|
||||
"github.com/tal-tech/go-zero/example/tracing/remote/portal"
|
||||
"github.com/tal-tech/go-zero/example/tracing/remote/user"
|
||||
"github.com/tal-tech/go-zero/rpcx"
|
||||
"github.com/tal-tech/go-zero/zrpc"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
@@ -15,16 +15,16 @@ var configFile = flag.String("f", "etc/config.json", "the config file")
|
||||
|
||||
type (
|
||||
Config struct {
|
||||
rpcx.RpcServerConf
|
||||
UserRpc rpcx.RpcClientConf
|
||||
zrpc.RpcServerConf
|
||||
UserRpc zrpc.RpcClientConf
|
||||
}
|
||||
|
||||
PortalServer struct {
|
||||
userRpc rpcx.Client
|
||||
userRpc zrpc.Client
|
||||
}
|
||||
)
|
||||
|
||||
func NewPortalServer(client rpcx.Client) *PortalServer {
|
||||
func NewPortalServer(client zrpc.Client) *PortalServer {
|
||||
return &PortalServer{
|
||||
userRpc: client,
|
||||
}
|
||||
@@ -53,8 +53,8 @@ func main() {
|
||||
var c Config
|
||||
conf.MustLoad(*configFile, &c)
|
||||
|
||||
client := rpcx.MustNewClient(c.UserRpc)
|
||||
server := rpcx.MustNewServer(c.RpcServerConf, func(grpcServer *grpc.Server) {
|
||||
client := zrpc.MustNewClient(c.UserRpc)
|
||||
server := zrpc.MustNewServer(c.RpcServerConf, func(grpcServer *grpc.Server) {
|
||||
portal.RegisterPortalServer(grpcServer, NewPortalServer(client))
|
||||
})
|
||||
server.Start()
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
|
||||
"github.com/tal-tech/go-zero/core/conf"
|
||||
"github.com/tal-tech/go-zero/example/tracing/remote/user"
|
||||
"github.com/tal-tech/go-zero/rpcx"
|
||||
"github.com/tal-tech/go-zero/zrpc"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
@@ -44,10 +44,10 @@ func (gs *UserServer) GetGrade(ctx context.Context, req *user.UserRequest) (*use
|
||||
func main() {
|
||||
flag.Parse()
|
||||
|
||||
var c rpcx.RpcServerConf
|
||||
var c zrpc.RpcServerConf
|
||||
conf.MustLoad(*configFile, &c)
|
||||
|
||||
server := rpcx.MustNewServer(c, func(grpcServer *grpc.Server) {
|
||||
server := zrpc.MustNewServer(c, func(grpcServer *grpc.Server) {
|
||||
user.RegisterUserServer(grpcServer, NewUserServer())
|
||||
})
|
||||
server.Start()
|
||||
|
||||
20
readme-en.md
@@ -1,14 +1,14 @@
|
||||
English | [简体中文](readme.md)
|
||||
|
||||
# go-zero
|
||||
|
||||
English | [简体中文](readme.md)
|
||||
|
||||
[](https://github.com/tal-tech/go-zero/actions)
|
||||
[](https://codecov.io/gh/tal-tech/go-zero)
|
||||
[](https://goreportcard.com/report/github.com/tal-tech/go-zero)
|
||||
[](https://github.com/tal-tech/go-zero)
|
||||
[](https://opensource.org/licenses/MIT)
|
||||
|
||||
## 0. what is go-zero?
|
||||
## 0. what is go-zero
|
||||
|
||||
go-zero is a web and rpc framework that with lots of engineering practices builtin. It’s born to ensure the stability of the busy services with resilience design, and has been serving sites with tens of millions users for years.
|
||||
|
||||
@@ -99,7 +99,7 @@ go get -u github.com/tal-tech/go-zero
|
||||
|
||||
1. install goctl
|
||||
|
||||
`goctl`can be read as `go control`. `goctl means not to be controlled by code, instead, we control it. The inside `go` is not `golang`. At the very beginning, I was expecting it to help us improve the productivity, and make our lives easier.
|
||||
`goctl`can be read as `go control`. `goctl` means not to be controlled by code, instead, we control it. The inside `go` is not `golang`. At the very beginning, I was expecting it to help us improve the productivity, and make our lives easier.
|
||||
|
||||
```shell
|
||||
GO111MODULE=on go get -u github.com/tal-tech/go-zero/tools/goctl
|
||||
@@ -113,11 +113,11 @@ go get -u github.com/tal-tech/go-zero
|
||||
type Request struct {
|
||||
Name string `path:"name,options=you|me"` // parameters are auto validated
|
||||
}
|
||||
|
||||
|
||||
type Response struct {
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
|
||||
service greet-api {
|
||||
@server(
|
||||
handler: GreetHandler
|
||||
@@ -140,7 +140,7 @@ go get -u github.com/tal-tech/go-zero
|
||||
|
||||
the generated files look like:
|
||||
|
||||
```
|
||||
```Plain Text
|
||||
├── greet
|
||||
│ ├── etc
|
||||
│ │ └── greet-api.yaml // configuration file
|
||||
@@ -159,6 +159,7 @@ go get -u github.com/tal-tech/go-zero
|
||||
│ └── types.go // request/response defined here
|
||||
└── greet.api // api description file
|
||||
```
|
||||
|
||||
the generated code can be run directly:
|
||||
|
||||
```shell
|
||||
@@ -184,8 +185,9 @@ go get -u github.com/tal-tech/go-zero
|
||||
|
||||
4. Write the business logic code
|
||||
|
||||
* the dependencies can be passed into the logic within servicecontext.go, like mysql, reds etc.
|
||||
* add the logic code in logic package according to .api file
|
||||
* the dependencies can be passed into the logic within servicecontext.go, like mysql, reds etc.
|
||||
* add the logic code in logic package according to .api file
|
||||
|
||||
5. Generate code like Java, TypeScript, Dart, JavaScript etc. just from the api file
|
||||
|
||||
```shell
|
||||
|
||||
100
readme.md
@@ -1,7 +1,7 @@
|
||||
[English](readme-en.md) | 简体中文
|
||||
|
||||
# go-zero
|
||||
|
||||
[English](readme-en.md) | 简体中文
|
||||
|
||||
[](https://github.com/tal-tech/go-zero/actions)
|
||||
[](https://codecov.io/gh/tal-tech/go-zero)
|
||||
[](https://goreportcard.com/report/github.com/tal-tech/go-zero)
|
||||
@@ -107,91 +107,41 @@ go get -u github.com/tal-tech/go-zero
|
||||
|
||||
确保goctl可执行
|
||||
|
||||
2. 定义API文件,比如greet.api,可以在vs code里安装`goctl`插件,支持api语法
|
||||
|
||||
```go
|
||||
type Request struct {
|
||||
Name string `path:"name,options=you|me"` // 框架自动验证请求参数是否合法
|
||||
}
|
||||
|
||||
type Response struct {
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
service greet-api {
|
||||
@server(
|
||||
handler: GreetHandler
|
||||
)
|
||||
get /greet/from/:name(Request) returns (Response);
|
||||
}
|
||||
```
|
||||
|
||||
也可以通过goctl生成api模本文件,命令如下:
|
||||
2. 快速生成api服务
|
||||
|
||||
```shell
|
||||
goctl api -o greet.api
|
||||
goctl api new greet
|
||||
cd greet
|
||||
go run greet.go -f etc/greet-api.yaml
|
||||
```
|
||||
|
||||
3. 生成go服务端代码
|
||||
默认侦听在8888端口(可以在配置文件里修改),可以通过curl请求:
|
||||
|
||||
```shell
|
||||
goctl api go -api greet.api -dir greet
|
||||
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
|
||||
```
|
||||
├── greet
|
||||
│ ├── etc
|
||||
│ │ └── greet-api.yaml // 配置文件
|
||||
│ ├── greet.go // main文件
|
||||
│ └── internal
|
||||
│ ├── config
|
||||
│ │ └── config.go // 配置定义
|
||||
│ ├── handler
|
||||
│ │ ├── greethandler.go // get/put/post/delete等路由定义文件
|
||||
│ │ └── routes.go // 路由列表
|
||||
│ ├── logic
|
||||
│ │ └── greetlogic.go // 请求逻辑处理文件
|
||||
│ ├── svc
|
||||
│ │ └── servicecontext.go // 请求上下文,可以传入mysql, redis等依赖
|
||||
│ └── types
|
||||
│ └── types.go // 请求、返回等类型定义
|
||||
└── greet.api // api描述文件
|
||||
```
|
||||
生成的代码可以直接运行:
|
||||
|
||||
```shell
|
||||
cd greet
|
||||
go run greet.go -f etc/greet-api.yaml
|
||||
```
|
||||
编写业务代码:
|
||||
|
||||
默认侦听在8888端口(可以在配置文件里修改),可以通过curl请求:
|
||||
* api文件定义了服务对外暴露的路由,可参考[api规范](https://github.com/tal-tech/go-zero/blob/master/doc/goctl.md)
|
||||
* 可以在servicecontext.go里面传递依赖给logic,比如mysql, redis等
|
||||
* 在api定义的get/post/put/delete等请求对应的logic里增加业务处理逻辑
|
||||
|
||||
```shell
|
||||
curl -i http://localhost:8888/greet/from/you
|
||||
```
|
||||
3. 可以根据api文件生成前端需要的Java, TypeScript, Dart, JavaScript代码
|
||||
|
||||
返回如下:
|
||||
|
||||
```http
|
||||
HTTP/1.1 200 OK
|
||||
Date: Sun, 30 Aug 2020 15:32:35 GMT
|
||||
Content-Length: 0
|
||||
```
|
||||
|
||||
编写业务代码:
|
||||
|
||||
* 可以在servicecontext.go里面传递依赖给logic,比如mysql, redis等
|
||||
* 在api定义的get/post/put/delete等请求对应的logic里增加业务处理逻辑
|
||||
|
||||
4. 可以根据api文件生成前端需要的Java, TypeScript, Dart, JavaScript代码
|
||||
|
||||
```shell
|
||||
goctl api java -api greet.api -dir greet
|
||||
goctl api dart -api greet.api -dir greet
|
||||
...
|
||||
```
|
||||
```shell
|
||||
goctl api java -api greet.api -dir greet
|
||||
goctl api dart -api greet.api -dir greet
|
||||
...
|
||||
```
|
||||
|
||||
## 7. Benchmark
|
||||
|
||||
@@ -206,6 +156,10 @@ Content-Length: 0
|
||||
* [goctl使用帮助](doc/goctl.md)
|
||||
* [通过MapReduce降低服务响应时间](doc/mapreduce.md)
|
||||
* [关键字替换和敏感词过滤工具](doc/keywords.md)
|
||||
* [进程内缓存使用方法](doc/collection.md)
|
||||
* [防止缓存击穿之进程内共享调用](doc/sharedcalls.md)
|
||||
* [基于prometheus的微服务指标监控](doc/metric.md)
|
||||
* [文本序列化和反序列化](doc/mapping.md)
|
||||
|
||||
## 9. 微信交流群
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ type (
|
||||
|
||||
// why not name it as Conf, because we need to consider usage like:
|
||||
// type Config struct {
|
||||
// rpcx.RpcConf
|
||||
// zrpc.RpcConf
|
||||
// rest.RestConf
|
||||
// }
|
||||
// if with the name Conf, there will be two Conf inside Config.
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
version := $(shell /bin/date "+%Y-%m-%d %H:%M")
|
||||
|
||||
build:
|
||||
go build -ldflags="-s -w" -ldflags="-X 'main.BuildTime=$(version)'" goctl.go && upx goctl
|
||||
go build -ldflags="-s -w" -ldflags="-X 'main.BuildTime=$(version)'" goctl.go
|
||||
command -v upx &> /dev/null && upx goctl
|
||||
mac:
|
||||
GOOS=darwin go build -ldflags="-s -w" -ldflags="-X 'main.BuildTime=$(version)'" -o goctl-darwin goctl.go && upx goctl-darwin
|
||||
GOOS=darwin go build -ldflags="-s -w" -ldflags="-X 'main.BuildTime=$(version)'" -o goctl-darwin goctl.go
|
||||
command -v upx &> /dev/null && upx goctl-darwin
|
||||
win:
|
||||
GOOS=windows go build -ldflags="-s -w" -ldflags="-X 'main.BuildTime=$(version)'" -o goctl.exe goctl.go && upx goctl.exe
|
||||
GOOS=windows go build -ldflags="-s -w" -ldflags="-X 'main.BuildTime=$(version)'" -o goctl.exe goctl.go
|
||||
command -v upx &> /dev/null && upx goctl.exe
|
||||
linux:
|
||||
GOOS=linux go build -ldflags="-s -w" -ldflags="-X 'main.BuildTime=$(version)'" -o goctl-linux goctl.go && upx goctl-linux
|
||||
GOOS=linux go build -ldflags="-s -w" -ldflags="-X 'main.BuildTime=$(version)'" -o goctl-linux goctl.go
|
||||
command -v upx &> /dev/null && upx goctl-linux
|
||||
|
||||
@@ -14,12 +14,13 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/logrusorgru/aurora"
|
||||
"github.com/urfave/cli"
|
||||
|
||||
"github.com/tal-tech/go-zero/core/logx"
|
||||
apiformat "github.com/tal-tech/go-zero/tools/goctl/api/format"
|
||||
"github.com/tal-tech/go-zero/tools/goctl/api/parser"
|
||||
apiutil "github.com/tal-tech/go-zero/tools/goctl/api/util"
|
||||
"github.com/tal-tech/go-zero/tools/goctl/util"
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
|
||||
const tmpFile = "%s-%d"
|
||||
@@ -29,6 +30,7 @@ var tmpDir = path.Join(os.TempDir(), "goctl")
|
||||
func GoCommand(c *cli.Context) error {
|
||||
apiFile := c.String("api")
|
||||
dir := c.String("dir")
|
||||
force := c.Bool("force")
|
||||
if len(apiFile) == 0 {
|
||||
return errors.New("missing -api")
|
||||
}
|
||||
@@ -36,6 +38,10 @@ func GoCommand(c *cli.Context) error {
|
||||
return errors.New("missing -dir")
|
||||
}
|
||||
|
||||
return DoGenProject(apiFile, dir, force)
|
||||
}
|
||||
|
||||
func DoGenProject(apiFile, dir string, force bool) error {
|
||||
p, err := parser.NewParser(apiFile)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -50,9 +56,9 @@ func GoCommand(c *cli.Context) error {
|
||||
logx.Must(genConfig(dir))
|
||||
logx.Must(genMain(dir, api))
|
||||
logx.Must(genServiceContext(dir, api))
|
||||
logx.Must(genTypes(dir, api))
|
||||
logx.Must(genTypes(dir, api, force))
|
||||
logx.Must(genHandlers(dir, api))
|
||||
logx.Must(genRoutes(dir, api))
|
||||
logx.Must(genRoutes(dir, api, force))
|
||||
logx.Must(genLogic(dir, api))
|
||||
// it does not work
|
||||
format(dir)
|
||||
|
||||
@@ -164,7 +164,7 @@ func genHandlerImports(group spec.Group, route spec.Route, parentPkg string) str
|
||||
imports = append(imports, fmt.Sprintf("\"%s\"",
|
||||
util.JoinPackages(parentPkg, getLogicFolderPath(group, route))))
|
||||
imports = append(imports, fmt.Sprintf("\"%s\"", util.JoinPackages(parentPkg, contextDir)))
|
||||
if len(route.RequestType.Name) > 0 || len(route.ResponseType.Name) > 0 {
|
||||
if len(route.RequestType.Name) > 0 {
|
||||
imports = append(imports, fmt.Sprintf("\"%s\"\n", util.JoinPackages(parentPkg, typesDir)))
|
||||
}
|
||||
imports = append(imports, fmt.Sprintf("\"%s/rest/httpx\"", vars.ProjectOpenSourceUrl))
|
||||
|
||||
@@ -16,6 +16,7 @@ const mainTemplate = `package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
|
||||
{{.importPackages}}
|
||||
)
|
||||
@@ -33,6 +34,8 @@ func main() {
|
||||
defer server.Stop()
|
||||
|
||||
handler.RegisterHandlers(server, ctx)
|
||||
|
||||
fmt.Printf("Starting server at %s:%d...\n", c.Host, c.Port)
|
||||
server.Start()
|
||||
}
|
||||
`
|
||||
|
||||
@@ -60,7 +60,7 @@ type (
|
||||
}
|
||||
)
|
||||
|
||||
func genRoutes(dir string, api *spec.ApiSpec) error {
|
||||
func genRoutes(dir string, api *spec.ApiSpec, force bool) error {
|
||||
var builder strings.Builder
|
||||
groups, err := getRoutes(api)
|
||||
if err != nil {
|
||||
@@ -102,8 +102,10 @@ func genRoutes(dir string, api *spec.ApiSpec) error {
|
||||
}
|
||||
|
||||
filename := path.Join(dir, handlerDir, routesFilename)
|
||||
if err := util.RemoveOrQuit(filename); err != nil {
|
||||
return err
|
||||
if !force {
|
||||
if err := util.RemoveOrQuit(filename); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
fp, created, err := apiutil.MaybeCreateFile(dir, handlerDir, routesFilename)
|
||||
|
||||
@@ -42,15 +42,17 @@ func BuildTypes(types []spec.Type) (string, error) {
|
||||
return builder.String(), nil
|
||||
}
|
||||
|
||||
func genTypes(dir string, api *spec.ApiSpec) error {
|
||||
func genTypes(dir string, api *spec.ApiSpec, force bool) error {
|
||||
val, err := BuildTypes(api.Types)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
filename := path.Join(dir, typesDir, typesFile)
|
||||
if err := util.RemoveOrQuit(filename); err != nil {
|
||||
return err
|
||||
if !force {
|
||||
if err := util.RemoveOrQuit(filename); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
fp, created, err := apiutil.MaybeCreateFile(dir, typesDir, typesFile)
|
||||
|
||||
65
tools/goctl/api/new/newservice.go
Normal file
@@ -0,0 +1,65 @@
|
||||
package new
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"text/template"
|
||||
|
||||
"github.com/tal-tech/go-zero/tools/goctl/api/gogen"
|
||||
"github.com/tal-tech/go-zero/tools/goctl/util"
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
|
||||
const apiTemplate = `
|
||||
type Request struct {
|
||||
Name string ` + "`" + `path:"name,options=you|me"` + "`" + ` // 框架自动验证请求参数是否合法
|
||||
}
|
||||
|
||||
type Response struct {
|
||||
Message string ` + "`" + `json:"message"` + "`" + `
|
||||
}
|
||||
|
||||
service {{.name}}-api {
|
||||
@server(
|
||||
handler: GreetHandler
|
||||
)
|
||||
get /greet/from/:name(Request) returns (Response);
|
||||
}
|
||||
`
|
||||
|
||||
func NewService(c *cli.Context) error {
|
||||
args := c.Args()
|
||||
dirName := "greet"
|
||||
if len(args) > 0 {
|
||||
dirName = args.First()
|
||||
}
|
||||
|
||||
abs, err := filepath.Abs(dirName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = util.MkdirIfNotExist(abs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
dirName = filepath.Base(filepath.Clean(abs))
|
||||
filename := dirName + ".api"
|
||||
apiFilePath := filepath.Join(abs, filename)
|
||||
fp, err := os.Create(apiFilePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer fp.Close()
|
||||
t := template.Must(template.New("template").Parse(apiTemplate))
|
||||
if err := t.Execute(fp, map[string]string{
|
||||
"name": dirName,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = gogen.DoGenProject(apiFilePath, abs, true)
|
||||
return err
|
||||
}
|
||||
@@ -1,9 +1,13 @@
|
||||
package parser
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"github.com/tal-tech/go-zero/core/stringx"
|
||||
"github.com/tal-tech/go-zero/tools/goctl/api/spec"
|
||||
)
|
||||
|
||||
@@ -54,32 +58,55 @@ type serviceEntityParser struct {
|
||||
}
|
||||
|
||||
func (p *serviceEntityParser) parseLine(line string, api *spec.ApiSpec, annos []spec.Annotation) error {
|
||||
fields := strings.Fields(line)
|
||||
if len(fields) < 2 {
|
||||
return fmt.Errorf("wrong line %q", line)
|
||||
var defaultErr = fmt.Errorf("wrong line %q, %q", line, routeSyntax)
|
||||
|
||||
line = strings.TrimSpace(line)
|
||||
var buffer = new(bytes.Buffer)
|
||||
buffer.WriteString(line)
|
||||
reader := bufio.NewReader(buffer)
|
||||
var builder strings.Builder
|
||||
var fields = make([]string, 0)
|
||||
for {
|
||||
ch, _, err := reader.ReadRune()
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
switch {
|
||||
case isSpace(ch), ch == leftParenthesis, ch == rightParenthesis, ch == semicolon:
|
||||
if builder.Len() == 0 {
|
||||
continue
|
||||
}
|
||||
token := builder.String()
|
||||
builder.Reset()
|
||||
fields = append(fields, token)
|
||||
default:
|
||||
builder.WriteRune(ch)
|
||||
}
|
||||
}
|
||||
|
||||
if len(fields) < 3 {
|
||||
return defaultErr
|
||||
}
|
||||
|
||||
method := fields[0]
|
||||
pathAndRequest := fields[1]
|
||||
pos := strings.Index(pathAndRequest, "(")
|
||||
if pos < 0 {
|
||||
return fmt.Errorf("wrong line %q", line)
|
||||
}
|
||||
path := strings.TrimSpace(pathAndRequest[:pos])
|
||||
pathAndRequest = pathAndRequest[pos+1:]
|
||||
pos = strings.Index(pathAndRequest, ")")
|
||||
if pos < 0 {
|
||||
return fmt.Errorf("wrong line %q", line)
|
||||
}
|
||||
req := pathAndRequest[:pos]
|
||||
path := fields[1]
|
||||
req := fields[2]
|
||||
var returns string
|
||||
if len(fields) > 2 {
|
||||
returns = fields[2]
|
||||
|
||||
if stringx.Contains(fields, returnsTag) {
|
||||
if fields[len(fields)-1] != returnsTag {
|
||||
returns = fields[len(fields)-1]
|
||||
} else {
|
||||
return defaultErr
|
||||
}
|
||||
if fields[2] == returnsTag {
|
||||
req = ""
|
||||
}
|
||||
}
|
||||
returns = strings.ReplaceAll(returns, "returns", "")
|
||||
returns = strings.ReplaceAll(returns, "(", "")
|
||||
returns = strings.ReplaceAll(returns, ")", "")
|
||||
returns = strings.TrimSpace(returns)
|
||||
|
||||
p.acceptRoute(spec.Route{
|
||||
Annotations: annos,
|
||||
|
||||
@@ -170,9 +170,9 @@ func parseTag(basicLit *ast.BasicLit) string {
|
||||
}
|
||||
|
||||
// returns
|
||||
// resp1:type can convert to *spec.PointerType|*spec.BasicType|*spec.MapType|*spec.ArrayType|*spec.InterfaceType
|
||||
// resp2:type's string expression,like int、string、[]int64、map[string]User、*User
|
||||
// resp3:error
|
||||
// resp1: type can convert to *spec.PointerType|*spec.BasicType|*spec.MapType|*spec.ArrayType|*spec.InterfaceType
|
||||
// resp2: type's string expression,like int、string、[]int64、map[string]User、*User
|
||||
// resp3: error
|
||||
func parseType(expr ast.Expr) (interface{}, string, error) {
|
||||
if expr == nil {
|
||||
return nil, "", ErrUnSupportType
|
||||
@@ -214,14 +214,17 @@ func parseType(expr ast.Expr) (interface{}, string, error) {
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
value, valueStringExpr, err := parseType(v.Value)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
keyType, ok := key.(*spec.BasicType)
|
||||
if !ok {
|
||||
return nil, "", fmt.Errorf("[%+v] - unsupport type of map key", v.Key)
|
||||
return nil, "", fmt.Errorf("[%+v] - unsupported type of map key", v.Key)
|
||||
}
|
||||
|
||||
e := fmt.Sprintf("map[%s]%s", keyStringExpr, valueStringExpr)
|
||||
return &spec.MapType{
|
||||
Key: keyType.Name,
|
||||
@@ -233,16 +236,17 @@ func parseType(expr ast.Expr) (interface{}, string, error) {
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
e := fmt.Sprintf("[]%s", stringExpr)
|
||||
return &spec.ArrayType{ArrayType: arrayType, StringExpr: e}, e, nil
|
||||
case *ast.InterfaceType:
|
||||
return &spec.InterfaceType{StringExpr: interfaceExpr}, interfaceExpr, nil
|
||||
case *ast.ChanType:
|
||||
return nil, "", errors.New("[chan] - unsupport type")
|
||||
return nil, "", errors.New("[chan] - unsupported type")
|
||||
case *ast.FuncType:
|
||||
return nil, "", errors.New("[func] - unsupport type")
|
||||
return nil, "", errors.New("[func] - unsupported type")
|
||||
case *ast.StructType: // todo can optimize
|
||||
return nil, "", errors.New("[struct] - unsupport inline struct type")
|
||||
return nil, "", errors.New("[struct] - unsupported inline struct type")
|
||||
case *ast.SelectorExpr:
|
||||
x := v.X
|
||||
sel := v.Sel
|
||||
@@ -252,6 +256,7 @@ func parseType(expr ast.Expr) (interface{}, string, error) {
|
||||
if name != "time" && sel.Name != "Time" {
|
||||
return nil, "", fmt.Errorf("[outter package] - package:%s, unsupport type", name)
|
||||
}
|
||||
|
||||
tm := fmt.Sprintf("time.Time")
|
||||
return &spec.TimeType{
|
||||
StringExpr: tm,
|
||||
|
||||
@@ -5,6 +5,8 @@ const (
|
||||
serviceDirective = "service"
|
||||
typeDirective = "type"
|
||||
typeStruct = "struct"
|
||||
routeSyntax = "route syntax: [get/post/delete] /path(request) returns[(response)]"
|
||||
returnsTag = "returns"
|
||||
at = '@'
|
||||
colon = ':'
|
||||
leftParenthesis = '('
|
||||
@@ -13,4 +15,5 @@ const (
|
||||
rightBrace = '}'
|
||||
multilineBeginTag = '>'
|
||||
multilineEndTag = '<'
|
||||
semicolon = ';'
|
||||
)
|
||||
|
||||
@@ -73,6 +73,7 @@ type (
|
||||
StringExpr string
|
||||
Name string
|
||||
}
|
||||
|
||||
PointerType struct {
|
||||
StringExpr string
|
||||
// it can be asserted as BasicType: int、bool、
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"github.com/tal-tech/go-zero/tools/goctl/api/gogen"
|
||||
"github.com/tal-tech/go-zero/tools/goctl/api/javagen"
|
||||
"github.com/tal-tech/go-zero/tools/goctl/api/ktgen"
|
||||
"github.com/tal-tech/go-zero/tools/goctl/api/new"
|
||||
"github.com/tal-tech/go-zero/tools/goctl/api/tsgen"
|
||||
"github.com/tal-tech/go-zero/tools/goctl/api/validate"
|
||||
"github.com/tal-tech/go-zero/tools/goctl/configgen"
|
||||
@@ -36,6 +37,11 @@ var (
|
||||
},
|
||||
Action: apigen.ApiCommand,
|
||||
Subcommands: []cli.Command{
|
||||
{
|
||||
Name: "new",
|
||||
Usage: "fast create api service",
|
||||
Action: new.NewService,
|
||||
},
|
||||
{
|
||||
Name: "format",
|
||||
Usage: "format api files",
|
||||
@@ -90,6 +96,10 @@ var (
|
||||
Name: "api",
|
||||
Usage: "the api file",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "force",
|
||||
Usage: "force override the exist files",
|
||||
},
|
||||
},
|
||||
Action: gogen.GoCommand,
|
||||
},
|
||||
@@ -193,6 +203,17 @@ var (
|
||||
Name: "rpc",
|
||||
Usage: "generate rpc code",
|
||||
Subcommands: []cli.Command{
|
||||
{
|
||||
Name: "new",
|
||||
Usage: `generate rpc demo service`,
|
||||
Flags: []cli.Flag{
|
||||
cli.BoolFlag{
|
||||
Name: "idea",
|
||||
Usage: "whether the command execution environment is from idea plugin. [option]",
|
||||
},
|
||||
},
|
||||
Action: rpc.RpcNew,
|
||||
},
|
||||
{
|
||||
Name: "template",
|
||||
Usage: `generate proto template`,
|
||||
@@ -224,10 +245,6 @@ var (
|
||||
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]",
|
||||
|
||||
@@ -1,31 +1,27 @@
|
||||
# goctl使用说明
|
||||
# goctl使用
|
||||
|
||||
## goctl用途
|
||||
|
||||
* 定义api请求
|
||||
* 根据定义的api自动生成golang(后端), java(iOS & Android), typescript(web & 晓程序),dart(flutter)
|
||||
* 生成MySQL CURD 详情见[goctl model模块](model/sql)
|
||||
|
||||
## goctl使用说明
|
||||
#### goctl参数说明
|
||||
|
||||
### goctl 参数说明
|
||||
|
||||
`goctl api [go/java/ts] [-api user/user.api] [-dir ./src]`
|
||||
|
||||
> api 后面接生成的语言,现支持go/java/typescript
|
||||
|
||||
>
|
||||
> -api 自定义api所在路径
|
||||
|
||||
>
|
||||
> -dir 自定义生成目录
|
||||
|
||||
#### 保持goctl总是最新版
|
||||
|
||||
第一次运行会在~/.goctl里增加下面两行:
|
||||
|
||||
```
|
||||
url = http://47.97.184.41:7777/
|
||||
```
|
||||
|
||||
#### API 语法说明
|
||||
|
||||
```
|
||||
```golang
|
||||
|
||||
info(
|
||||
title: doc title
|
||||
desc: >
|
||||
@@ -122,14 +118,16 @@ service user-api {
|
||||
head /api/ping()
|
||||
}
|
||||
```
|
||||
|
||||
1. info部分:描述了api基本信息,比如Auth,api是哪个用途。
|
||||
2. type部分:type类型声明和golang语法兼容。
|
||||
3. service部分:service代表一组服务,一个服务可以由多组名称相同的service组成,可以针对每一组service配置jwt和auth认证,另外通过folder属性可以指定service生成所在子目录。
|
||||
3. service部分:service代表一组服务,一个服务可以由多组名称相同的service组成,可以针对每一组service配置folder属性来指定service生成所在子目录。
|
||||
service里面包含api路由,比如上面第一组service的第一个路由,doc用来描述此路由的用途,GetProfileHandler表示处理这个路由的handler,
|
||||
`get /api/profile/:name(getRequest) returns(getResponse)` 中get代表api的请求方式(get/post/put/delete), `/api/profile/:name` 描述了路由path,`:name`通过
|
||||
请求getRequest里面的属性赋值,getResponse为返回的结构体,这两个类型都定义在2描述的类型中。
|
||||
|
||||
#### api vscode插件
|
||||
|
||||
开发者可以在vscode中搜索goctl的api插件,它提供了api语法高亮,语法检测和格式化相关功能。
|
||||
|
||||
1. 支持语法高亮和类型导航。
|
||||
@@ -141,7 +139,7 @@ service user-api {
|
||||
命令如下:
|
||||
`goctl api go -api user/user.api -dir user`
|
||||
|
||||
```
|
||||
```Plain Text
|
||||
|
||||
.
|
||||
├── internal
|
||||
@@ -171,20 +169,28 @@ service user-api {
|
||||
└── user.go
|
||||
|
||||
```
|
||||
|
||||
生成的代码可以直接跑,有几个地方需要改:
|
||||
|
||||
* 在`servicecontext.go`里面增加需要传递给logic的一些资源,比如mysql, redis,rpc等
|
||||
* 在定义的get/post/put/delete等请求的handler和logic里增加处理业务逻辑的代码
|
||||
* 在`servicecontext.go`里面增加需要传递给logic的一些资源,比如mysql, redis,rpc等
|
||||
* 在定义的get/post/put/delete等请求的handler和logic里增加处理业务逻辑的代码
|
||||
|
||||
#### 根据定义好的api文件生成java代码
|
||||
`goctl api java -api user/user.api -dir ./src`
|
||||
|
||||
```Plain Text
|
||||
goctl api java -api user/user.api -dir ./src
|
||||
```
|
||||
|
||||
#### 根据定义好的api文件生成typescript代码
|
||||
`goctl api ts -api user/user.api -dir ./src -webapi ***`
|
||||
|
||||
ts需要指定webapi所在目录
|
||||
|
||||
```Plain Text
|
||||
goctl api ts -api user/user.api -dir ./src -webapi ***
|
||||
```
|
||||
|
||||
ts需要指定webapi所在目录
|
||||
|
||||
#### 根据定义好的api文件生成Dart代码
|
||||
`goctl api dart -api user/user.api -dir ./src`
|
||||
|
||||
* 如有不理解的地方,随时问Kim/Kevin
|
||||
|
||||
```Plain Text
|
||||
goctl api dart -api user/user.api -dir ./src
|
||||
```
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
# Change log
|
||||
|
||||
# 2020-08-20
|
||||
## 2020-08-20
|
||||
|
||||
* 新增支持通过连接数据库生成model
|
||||
* 支持数据库多表生成
|
||||
* 优化stringx
|
||||
|
||||
# 2020-08-19
|
||||
## 2020-08-19
|
||||
|
||||
* 重构model代码生成逻辑
|
||||
* 实现从ddl解析表信息生成代码
|
||||
* 实现从ddl解析表信息生成代码
|
||||
|
||||
@@ -2,189 +2,186 @@
|
||||
|
||||
goctl model 为go-zero下的工具模块中的组件之一,目前支持识别mysql ddl进行model层代码生成,通过命令行或者idea插件(即将支持)可以有选择地生成带redis cache或者不带redis cache的代码逻辑。
|
||||
|
||||
# 快速开始
|
||||
## 快速开始
|
||||
|
||||
* 通过ddl生成
|
||||
|
||||
```shell script
|
||||
$ goctl model mysql ddl -src="./sql/user.sql" -dir="./sql/model" -c=true
|
||||
goctl model mysql ddl -src="./sql/user.sql" -dir="./sql/model" -c=true
|
||||
```
|
||||
|
||||
执行上述命令后即可快速生成CURD代码。
|
||||
|
||||
```
|
||||
```Plain Text
|
||||
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"
|
||||
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)
|
||||
|
||||
|
||||
* 生成代码示例
|
||||
|
||||
``` go
|
||||
package model
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/tal-tech/go-zero/core/stores/cache"
|
||||
"github.com/tal-tech/go-zero/core/stores/sqlc"
|
||||
"github.com/tal-tech/go-zero/core/stores/sqlx"
|
||||
"github.com/tal-tech/go-zero/core/stringx"
|
||||
"github.com/tal-tech/go-zero/tools/goctl/model/sql/builderx"
|
||||
)
|
||||
|
||||
var (
|
||||
userFieldNames = builderx.FieldNames(&User{})
|
||||
userRows = strings.Join(userFieldNames, ",")
|
||||
userRowsExpectAutoSet = strings.Join(stringx.Remove(userFieldNames, "id", "create_time", "update_time"), ",")
|
||||
userRowsWithPlaceHolder = strings.Join(stringx.Remove(userFieldNames, "id", "create_time", "update_time"), "=?,") + "=?"
|
||||
|
||||
cacheUserMobilePrefix = "cache#User#mobile#"
|
||||
cacheUserIdPrefix = "cache#User#id#"
|
||||
cacheUserNamePrefix = "cache#User#name#"
|
||||
)
|
||||
|
||||
type (
|
||||
UserModel struct {
|
||||
sqlc.CachedConn
|
||||
table string
|
||||
}
|
||||
|
||||
User struct {
|
||||
Id int64 `db:"id"`
|
||||
Name string `db:"name"` // 用户名称
|
||||
Password string `db:"password"` // 用户密码
|
||||
Mobile string `db:"mobile"` // 手机号
|
||||
Gender string `db:"gender"` // 男|女|未公开
|
||||
Nickname string `db:"nickname"` // 用户昵称
|
||||
CreateTime time.Time `db:"create_time"`
|
||||
UpdateTime time.Time `db:"update_time"`
|
||||
}
|
||||
)
|
||||
|
||||
func NewUserModel(conn sqlx.SqlConn, c cache.CacheConf, table string) *UserModel {
|
||||
return &UserModel{
|
||||
CachedConn: sqlc.NewConn(conn, c),
|
||||
table: table,
|
||||
}
|
||||
}
|
||||
|
||||
func (m *UserModel) Insert(data User) (sql.Result, error) {
|
||||
query := `insert into ` + m.table + `(` + userRowsExpectAutoSet + `) value (?, ?, ?, ?, ?)`
|
||||
return m.ExecNoCache(query, data.Name, data.Password, data.Mobile, data.Gender, data.Nickname)
|
||||
}
|
||||
|
||||
func (m *UserModel) FindOne(id int64) (*User, error) {
|
||||
userIdKey := fmt.Sprintf("%s%v", cacheUserIdPrefix, id)
|
||||
var resp User
|
||||
err := m.QueryRow(&resp, userIdKey, func(conn sqlx.SqlConn, v interface{}) error {
|
||||
query := `select ` + userRows + ` from ` + m.table + ` where id = ? limit 1`
|
||||
return conn.QueryRow(v, query, id)
|
||||
})
|
||||
switch err {
|
||||
case nil:
|
||||
return &resp, nil
|
||||
case sqlc.ErrNotFound:
|
||||
return nil, ErrNotFound
|
||||
default:
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
func (m *UserModel) FindOneByName(name string) (*User, error) {
|
||||
userNameKey := fmt.Sprintf("%s%v", cacheUserNamePrefix, name)
|
||||
var resp User
|
||||
err := m.QueryRowIndex(&resp, userNameKey, func(primary interface{}) string {
|
||||
return fmt.Sprintf("%s%v", cacheUserIdPrefix, primary)
|
||||
}, func(conn sqlx.SqlConn, v interface{}) (i interface{}, e error) {
|
||||
query := `select ` + userRows + ` from ` + m.table + ` where name = ? limit 1`
|
||||
if err := conn.QueryRow(&resp, query, name); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return resp.Id, nil
|
||||
}, func(conn sqlx.SqlConn, v, primary interface{}) error {
|
||||
query := `select ` + userRows + ` from ` + m.table + ` where id = ? limit 1`
|
||||
return conn.QueryRow(v, query, primary)
|
||||
})
|
||||
switch err {
|
||||
case nil:
|
||||
return &resp, nil
|
||||
case sqlc.ErrNotFound:
|
||||
return nil, ErrNotFound
|
||||
default:
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
func (m *UserModel) FindOneByMobile(mobile string) (*User, error) {
|
||||
userMobileKey := fmt.Sprintf("%s%v", cacheUserMobilePrefix, mobile)
|
||||
var resp User
|
||||
err := m.QueryRowIndex(&resp, userMobileKey, func(primary interface{}) string {
|
||||
return fmt.Sprintf("%s%v", cacheUserIdPrefix, primary)
|
||||
}, func(conn sqlx.SqlConn, v interface{}) (i interface{}, e error) {
|
||||
query := `select ` + userRows + ` from ` + m.table + ` where mobile = ? limit 1`
|
||||
if err := conn.QueryRow(&resp, query, mobile); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return resp.Id, nil
|
||||
}, func(conn sqlx.SqlConn, v, primary interface{}) error {
|
||||
query := `select ` + userRows + ` from ` + m.table + ` where id = ? limit 1`
|
||||
return conn.QueryRow(v, query, primary)
|
||||
})
|
||||
switch err {
|
||||
case nil:
|
||||
return &resp, nil
|
||||
case sqlc.ErrNotFound:
|
||||
return nil, ErrNotFound
|
||||
default:
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
func (m *UserModel) Update(data User) error {
|
||||
userIdKey := fmt.Sprintf("%s%v", cacheUserIdPrefix, data.Id)
|
||||
_, err := m.Exec(func(conn sqlx.SqlConn) (result sql.Result, err error) {
|
||||
query := `update ` + m.table + ` set ` + userRowsWithPlaceHolder + ` where id = ?`
|
||||
return conn.Exec(query, data.Name, data.Password, data.Mobile, data.Gender, data.Nickname, data.Id)
|
||||
}, userIdKey)
|
||||
return err
|
||||
}
|
||||
|
||||
func (m *UserModel) Delete(id int64) error {
|
||||
data, err := m.FindOne(id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
userIdKey := fmt.Sprintf("%s%v", cacheUserIdPrefix, id)
|
||||
userNameKey := fmt.Sprintf("%s%v", cacheUserNamePrefix, data.Name)
|
||||
userMobileKey := fmt.Sprintf("%s%v", cacheUserMobilePrefix, data.Mobile)
|
||||
_, err = m.Exec(func(conn sqlx.SqlConn) (result sql.Result, err error) {
|
||||
query := `delete from ` + m.table + ` where id = ?`
|
||||
return conn.Exec(query, id)
|
||||
}, userIdKey, userNameKey, userMobileKey)
|
||||
return err
|
||||
}
|
||||
package model
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/tal-tech/go-zero/core/stores/cache"
|
||||
"github.com/tal-tech/go-zero/core/stores/sqlc"
|
||||
"github.com/tal-tech/go-zero/core/stores/sqlx"
|
||||
"github.com/tal-tech/go-zero/core/stringx"
|
||||
"github.com/tal-tech/go-zero/tools/goctl/model/sql/builderx"
|
||||
)
|
||||
|
||||
var (
|
||||
userFieldNames = builderx.FieldNames(&User{})
|
||||
userRows = strings.Join(userFieldNames, ",")
|
||||
userRowsExpectAutoSet = strings.Join(stringx.Remove(userFieldNames, "id", "create_time", "update_time"), ",")
|
||||
userRowsWithPlaceHolder = strings.Join(stringx.Remove(userFieldNames, "id", "create_time", "update_time"), "=?,") + "=?"
|
||||
|
||||
cacheUserMobilePrefix = "cache#User#mobile#"
|
||||
cacheUserIdPrefix = "cache#User#id#"
|
||||
cacheUserNamePrefix = "cache#User#name#"
|
||||
)
|
||||
|
||||
type (
|
||||
UserModel struct {
|
||||
sqlc.CachedConn
|
||||
table string
|
||||
}
|
||||
|
||||
User struct {
|
||||
Id int64 `db:"id"`
|
||||
Name string `db:"name"` // 用户名称
|
||||
Password string `db:"password"` // 用户密码
|
||||
Mobile string `db:"mobile"` // 手机号
|
||||
Gender string `db:"gender"` // 男|女|未公开
|
||||
Nickname string `db:"nickname"` // 用户昵称
|
||||
CreateTime time.Time `db:"create_time"`
|
||||
UpdateTime time.Time `db:"update_time"`
|
||||
}
|
||||
)
|
||||
|
||||
func NewUserModel(conn sqlx.SqlConn, c cache.CacheConf, table string) *UserModel {
|
||||
return &UserModel{
|
||||
CachedConn: sqlc.NewConn(conn, c),
|
||||
table: table,
|
||||
}
|
||||
}
|
||||
|
||||
func (m *UserModel) Insert(data User) (sql.Result, error) {
|
||||
query := `insert into ` + m.table + `(` + userRowsExpectAutoSet + `) value (?, ?, ?, ?, ?)`
|
||||
return m.ExecNoCache(query, data.Name, data.Password, data.Mobile, data.Gender, data.Nickname)
|
||||
}
|
||||
|
||||
func (m *UserModel) FindOne(id int64) (*User, error) {
|
||||
userIdKey := fmt.Sprintf("%s%v", cacheUserIdPrefix, id)
|
||||
var resp User
|
||||
err := m.QueryRow(&resp, userIdKey, func(conn sqlx.SqlConn, v interface{}) error {
|
||||
query := `select ` + userRows + ` from ` + m.table + ` where id = ? limit 1`
|
||||
return conn.QueryRow(v, query, id)
|
||||
})
|
||||
switch err {
|
||||
case nil:
|
||||
return &resp, nil
|
||||
case sqlc.ErrNotFound:
|
||||
return nil, ErrNotFound
|
||||
default:
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
func (m *UserModel) FindOneByName(name string) (*User, error) {
|
||||
userNameKey := fmt.Sprintf("%s%v", cacheUserNamePrefix, name)
|
||||
var resp User
|
||||
err := m.QueryRowIndex(&resp, userNameKey, func(primary interface{}) string {
|
||||
return fmt.Sprintf("%s%v", cacheUserIdPrefix, primary)
|
||||
}, func(conn sqlx.SqlConn, v interface{}) (i interface{}, e error) {
|
||||
query := `select ` + userRows + ` from ` + m.table + ` where name = ? limit 1`
|
||||
if err := conn.QueryRow(&resp, query, name); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return resp.Id, nil
|
||||
}, func(conn sqlx.SqlConn, v, primary interface{}) error {
|
||||
query := `select ` + userRows + ` from ` + m.table + ` where id = ? limit 1`
|
||||
return conn.QueryRow(v, query, primary)
|
||||
})
|
||||
switch err {
|
||||
case nil:
|
||||
return &resp, nil
|
||||
case sqlc.ErrNotFound:
|
||||
return nil, ErrNotFound
|
||||
default:
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
func (m *UserModel) FindOneByMobile(mobile string) (*User, error) {
|
||||
userMobileKey := fmt.Sprintf("%s%v", cacheUserMobilePrefix, mobile)
|
||||
var resp User
|
||||
err := m.QueryRowIndex(&resp, userMobileKey, func(primary interface{}) string {
|
||||
return fmt.Sprintf("%s%v", cacheUserIdPrefix, primary)
|
||||
}, func(conn sqlx.SqlConn, v interface{}) (i interface{}, e error) {
|
||||
query := `select ` + userRows + ` from ` + m.table + ` where mobile = ? limit 1`
|
||||
if err := conn.QueryRow(&resp, query, mobile); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return resp.Id, nil
|
||||
}, func(conn sqlx.SqlConn, v, primary interface{}) error {
|
||||
query := `select ` + userRows + ` from ` + m.table + ` where id = ? limit 1`
|
||||
return conn.QueryRow(v, query, primary)
|
||||
})
|
||||
switch err {
|
||||
case nil:
|
||||
return &resp, nil
|
||||
case sqlc.ErrNotFound:
|
||||
return nil, ErrNotFound
|
||||
default:
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
func (m *UserModel) Update(data User) error {
|
||||
userIdKey := fmt.Sprintf("%s%v", cacheUserIdPrefix, data.Id)
|
||||
_, err := m.Exec(func(conn sqlx.SqlConn) (result sql.Result, err error) {
|
||||
query := `update ` + m.table + ` set ` + userRowsWithPlaceHolder + ` where id = ?`
|
||||
return conn.Exec(query, data.Name, data.Password, data.Mobile, data.Gender, data.Nickname, data.Id)
|
||||
}, userIdKey)
|
||||
return err
|
||||
}
|
||||
|
||||
func (m *UserModel) Delete(id int64) error {
|
||||
data, err := m.FindOne(id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
userIdKey := fmt.Sprintf("%s%v", cacheUserIdPrefix, id)
|
||||
userNameKey := fmt.Sprintf("%s%v", cacheUserNamePrefix, data.Name)
|
||||
userMobileKey := fmt.Sprintf("%s%v", cacheUserMobilePrefix, data.Mobile)
|
||||
_, err = m.Exec(func(conn sqlx.SqlConn) (result sql.Result, err error) {
|
||||
query := `delete from ` + m.table + ` where id = ?`
|
||||
return conn.Exec(query, id)
|
||||
}, userIdKey, userNameKey, userMobileKey)
|
||||
return err
|
||||
}
|
||||
```
|
||||
|
||||
# 用法
|
||||
## 用法
|
||||
|
||||
```
|
||||
$ goctl model mysql -h
|
||||
```Plain Text
|
||||
goctl model mysql -h
|
||||
```
|
||||
|
||||
```
|
||||
```Plain Text
|
||||
NAME:
|
||||
goctl model mysql - generate mysql model"
|
||||
|
||||
@@ -199,52 +196,56 @@ OPTIONS:
|
||||
--help, -h show help
|
||||
```
|
||||
|
||||
# 生成规则
|
||||
## 生成规则
|
||||
|
||||
* 默认规则
|
||||
|
||||
我们默认用户在建表时会创建createTime、updateTime字段(忽略大小写、下划线命名风格)且默认值均为`CURRENT_TIMESTAMP`,而updateTime支持`ON UPDATE CURRENT_TIMESTAMP`,对于这两个字段生成`insert`、`update`时会被移除,不在赋值范畴内,当然,如果你不需要这两个字段那也无大碍。
|
||||
* 带缓存模式
|
||||
* 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
|
||||
```
|
||||
* 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`字段均属于单字段索引。
|
||||
|
||||
* 不带缓存模式
|
||||
|
||||
|
||||
* ddl
|
||||
|
||||
```shell script
|
||||
$ goctl model -src={filename} -dir={dir}
|
||||
goctl model -src={filename} -dir={dir}
|
||||
```
|
||||
|
||||
* datasource
|
||||
|
||||
```shell script
|
||||
$ goctl model mysql datasource -url={datasource} -table={tables} -dir={dir}
|
||||
goctl model mysql datasource -url={datasource} -table={tables} -dir={dir}
|
||||
```
|
||||
|
||||
or
|
||||
* ddl
|
||||
|
||||
|
||||
```shell script
|
||||
$ goctl model -src={filename} -dir={dir} -cache=false
|
||||
goctl model -src={filename} -dir={dir} -cache=false
|
||||
```
|
||||
|
||||
* datasource
|
||||
|
||||
```shell script
|
||||
$ goctl model mysql datasource -url={datasource} -table={tables} -dir={dir} -cache=false
|
||||
goctl model mysql datasource -url={datasource} -table={tables} -dir={dir} -cache=false
|
||||
```
|
||||
|
||||
生成代码仅基本的CURD结构。
|
||||
生成代码仅基本的CURD结构。
|
||||
|
||||
# 缓存
|
||||
## 缓存
|
||||
|
||||
对于缓存这一块我选择用一问一答的形式进行罗列。我想这样能够更清晰的描述model中缓存的功能。
|
||||
|
||||
@@ -264,14 +265,8 @@ OPTIONS:
|
||||
|
||||
目前,我认为除了基本的CURD外,其他的代码均属于<i>业务型</i>代码,这个我觉得开发人员根据业务需要进行编写更好。
|
||||
|
||||
# QA
|
||||
## QA
|
||||
|
||||
* goctl model除了命令行模式,支持插件模式吗?
|
||||
|
||||
很快支持idea插件。
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ func genDelete(table Table, withCache bool) (string, error) {
|
||||
}
|
||||
var containsIndexCache = false
|
||||
for _, item := range table.Fields {
|
||||
if item.IsKey && !item.IsPrimaryKey {
|
||||
if item.IsUniqueKey {
|
||||
containsIndexCache = true
|
||||
break
|
||||
}
|
||||
|
||||
@@ -9,12 +9,12 @@ import (
|
||||
"github.com/tal-tech/go-zero/tools/goctl/util/stringx"
|
||||
)
|
||||
|
||||
func genFineOneByField(table Table, withCache bool) (string, error) {
|
||||
func genFindOneByField(table Table, withCache bool) (string, error) {
|
||||
t := util.With("findOneByField").Parse(template.FindOneByField)
|
||||
var list []string
|
||||
camelTableName := table.Name.ToCamel()
|
||||
for _, field := range table.Fields {
|
||||
if field.IsPrimaryKey || !field.IsKey {
|
||||
if field.IsPrimaryKey || !field.IsUniqueKey {
|
||||
continue
|
||||
}
|
||||
camelFieldName := field.Name.ToCamel()
|
||||
@@ -162,7 +162,7 @@ func (g *defaultGenerator) genModel(in parser.Table, withCache bool) (string, er
|
||||
return "", err
|
||||
}
|
||||
|
||||
findOneByFieldCode, err := genFineOneByField(table, withCache)
|
||||
findOneByFieldCode, err := genFindOneByField(table, withCache)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
@@ -29,22 +29,21 @@ func genCacheKeys(table parser.Table) (map[string]Key, error) {
|
||||
camelTableName := table.Name.ToCamel()
|
||||
lowerStartCamelTableName := stringx.From(camelTableName).UnTitle()
|
||||
for _, field := range fields {
|
||||
if !field.IsKey {
|
||||
continue
|
||||
}
|
||||
camelFieldName := field.Name.ToCamel()
|
||||
lowerStartCamelFieldName := stringx.From(camelFieldName).UnTitle()
|
||||
left := fmt.Sprintf("cache%s%sPrefix", camelTableName, camelFieldName)
|
||||
right := fmt.Sprintf("cache#%s#%s#", camelTableName, lowerStartCamelFieldName)
|
||||
variable := fmt.Sprintf("%s%sKey", lowerStartCamelTableName, camelFieldName)
|
||||
m[field.Name.Source()] = Key{
|
||||
VarExpression: fmt.Sprintf(`%s = "%s"`, left, right),
|
||||
Left: left,
|
||||
Right: right,
|
||||
Variable: variable,
|
||||
KeyExpression: fmt.Sprintf(`%s := fmt.Sprintf("%s%s", %s,%s)`, variable, "%s", "%v", left, lowerStartCamelFieldName),
|
||||
DataKeyExpression: fmt.Sprintf(`%s := fmt.Sprintf("%s%s",%s, data.%s)`, variable, "%s", "%v", left, camelFieldName),
|
||||
RespKeyExpression: fmt.Sprintf(`%s := fmt.Sprintf("%s%s", %s,resp.%s)`, variable, "%s", "%v", left, camelFieldName),
|
||||
if field.IsUniqueKey || field.IsPrimaryKey {
|
||||
camelFieldName := field.Name.ToCamel()
|
||||
lowerStartCamelFieldName := stringx.From(camelFieldName).UnTitle()
|
||||
left := fmt.Sprintf("cache%s%sPrefix", camelTableName, camelFieldName)
|
||||
right := fmt.Sprintf("cache#%s#%s#", camelTableName, lowerStartCamelFieldName)
|
||||
variable := fmt.Sprintf("%s%sKey", lowerStartCamelTableName, camelFieldName)
|
||||
m[field.Name.Source()] = Key{
|
||||
VarExpression: fmt.Sprintf(`%s = "%s"`, left, right),
|
||||
Left: left,
|
||||
Right: right,
|
||||
Variable: variable,
|
||||
KeyExpression: fmt.Sprintf(`%s := fmt.Sprintf("%s%s", %s,%s)`, variable, "%s", "%v", left, lowerStartCamelFieldName),
|
||||
DataKeyExpression: fmt.Sprintf(`%s := fmt.Sprintf("%s%s",%s, data.%s)`, variable, "%s", "%v", left, camelFieldName),
|
||||
RespKeyExpression: fmt.Sprintf(`%s := fmt.Sprintf("%s%s", %s,resp.%s)`, variable, "%s", "%v", left, camelFieldName),
|
||||
}
|
||||
}
|
||||
}
|
||||
return m, nil
|
||||
|
||||
@@ -36,6 +36,7 @@ type (
|
||||
DataType string
|
||||
IsKey bool
|
||||
IsPrimaryKey bool
|
||||
IsUniqueKey bool
|
||||
Comment string
|
||||
}
|
||||
|
||||
@@ -124,6 +125,7 @@ func Parse(ddl string) (*Table, error) {
|
||||
if ok {
|
||||
field.IsKey = true
|
||||
field.IsPrimaryKey = key == primary
|
||||
field.IsUniqueKey = key == unique
|
||||
if field.IsPrimaryKey {
|
||||
primaryKey.Field = field
|
||||
if column.Type.Autoincrement {
|
||||
|
||||
@@ -1,9 +1,16 @@
|
||||
# Change log
|
||||
|
||||
# 2020-08-29
|
||||
## 2020-09-10
|
||||
|
||||
* rpc greet服务一键生成
|
||||
* 修复相对路径生成rpc服务package引入错误bug
|
||||
* 移除`--shared`参数
|
||||
|
||||
## 2020-08-29
|
||||
|
||||
* 新增支持windows生成
|
||||
|
||||
# 2020-08-27
|
||||
## 2020-08-27
|
||||
|
||||
* 新增支持rpc模板生成
|
||||
* 新增支持rpc服务生成
|
||||
|
||||
|
||||
@@ -1,83 +1,131 @@
|
||||
# Rpc Generation
|
||||
|
||||
Goctl Rpc是`goctl`脚手架下的一个rpc服务代码生成模块,支持proto模板生成和rpc服务代码生成,通过此工具生成代码你只需要关注业务逻辑编写而不用去编写一些重复性的代码。这使得我们把精力重心放在业务上,从而加快了开发效率且降低了代码出错率。
|
||||
|
||||
# 特性
|
||||
## 特性
|
||||
|
||||
* 简单易用
|
||||
* 快速提升开发效率
|
||||
* 出错率低
|
||||
|
||||
# 快速开始
|
||||
## 快速开始
|
||||
|
||||
### 生成proto模板
|
||||
### 方式一:快速生成greet服务
|
||||
|
||||
```shell script
|
||||
$ goctl rpc template -o=user.proto
|
||||
```
|
||||
通过命令 `goctl rpc new ${servieName}`生成
|
||||
|
||||
```golang
|
||||
syntax = "proto3";
|
||||
如生成greet rpc服务:
|
||||
|
||||
package remote;
|
||||
```shell script
|
||||
goctl rpc new greet
|
||||
```
|
||||
|
||||
message Request {
|
||||
// 用户名
|
||||
string username = 1;
|
||||
// 用户密码
|
||||
string password = 2;
|
||||
}
|
||||
执行后代码结构如下:
|
||||
|
||||
message Response {
|
||||
// 用户名称
|
||||
string name = 1;
|
||||
// 用户性别
|
||||
string gender = 2;
|
||||
}
|
||||
|
||||
service User {
|
||||
// 登录
|
||||
rpc Login(Request)returns(Response);
|
||||
}
|
||||
```
|
||||
### 生成rpc服务代码
|
||||
|
||||
生成user rpc服务
|
||||
```
|
||||
$ goctl rpc proto -src=user.proto
|
||||
```
|
||||
|
||||
代码tree
|
||||
|
||||
```
|
||||
user
|
||||
```golang
|
||||
└── greet
|
||||
├── etc
|
||||
│ └── user.json
|
||||
│ └── greet.yaml
|
||||
├── go.mod
|
||||
├── go.sum
|
||||
├── greet
|
||||
│ ├── greet.go
|
||||
│ ├── greet_mock.go
|
||||
│ └── types.go
|
||||
├── greet.go
|
||||
├── greet.proto
|
||||
├── internal
|
||||
│ ├── config
|
||||
│ │ └── config.go
|
||||
│ ├── handler
|
||||
│ │ ├── loginhandler.go
|
||||
│ ├── logic
|
||||
│ │ └── loginlogic.go
|
||||
│ │ └── pinglogic.go
|
||||
│ ├── server
|
||||
│ │ └── greetserver.go
|
||||
│ └── svc
|
||||
│ └── servicecontext.go
|
||||
├── pb
|
||||
│ └── user.pb.go
|
||||
├── shared
|
||||
│ ├── mockusermodel.go
|
||||
│ ├── types.go
|
||||
│ └── usermodel.go
|
||||
├── user.go
|
||||
└── user.proto
|
||||
└── pb
|
||||
└── greet.pb.go
|
||||
```
|
||||
|
||||
rpc一键生成常见问题解决见 <a href="#常见问题解决">常见问题解决</a>
|
||||
|
||||
### 方式二:通过指定proto生成rpc服务
|
||||
|
||||
* 生成proto模板
|
||||
|
||||
```shell script
|
||||
goctl rpc template -o=user.proto
|
||||
```
|
||||
|
||||
```golang
|
||||
syntax = "proto3";
|
||||
|
||||
package remote;
|
||||
|
||||
message Request {
|
||||
// 用户名
|
||||
string username = 1;
|
||||
// 用户密码
|
||||
string password = 2;
|
||||
}
|
||||
|
||||
message Response {
|
||||
// 用户名称
|
||||
string name = 1;
|
||||
// 用户性别
|
||||
string gender = 2;
|
||||
}
|
||||
|
||||
service User {
|
||||
// 登录
|
||||
rpc Login(Request)returns(Response);
|
||||
}
|
||||
```
|
||||
|
||||
* 生成rpc服务代码
|
||||
|
||||
```shell script
|
||||
goctl rpc proto -src=user.proto
|
||||
```
|
||||
|
||||
代码tree
|
||||
|
||||
```Plain Text
|
||||
user
|
||||
├── etc
|
||||
│ └── user.json
|
||||
├── internal
|
||||
│ ├── config
|
||||
│ │ └── config.go
|
||||
│ ├── handler
|
||||
│ │ ├── loginhandler.go
|
||||
│ ├── logic
|
||||
│ │ └── loginlogic.go
|
||||
│ └── svc
|
||||
│ └── servicecontext.go
|
||||
├── pb
|
||||
│ └── user.pb.go
|
||||
├── shared
|
||||
│ ├── mockusermodel.go
|
||||
│ ├── types.go
|
||||
│ └── usermodel.go
|
||||
├── user.go
|
||||
└── user.proto
|
||||
```
|
||||
|
||||
## 准备工作
|
||||
|
||||
```
|
||||
# 准备工作
|
||||
* 安装了go环境
|
||||
* 安装了protoc&protoc-gen-go,并且已经设置环境变量
|
||||
* mockgen(可选)
|
||||
* mockgen(可选,将移除)
|
||||
* 更多问题请见 <a href="#注意事项">注意事项</a>
|
||||
|
||||
## 用法
|
||||
|
||||
### rpc服务生成用法
|
||||
|
||||
# 用法
|
||||
```shell script
|
||||
$ goctl rpc proto -h
|
||||
goctl rpc proto -h
|
||||
```
|
||||
|
||||
```shell script
|
||||
@@ -91,50 +139,99 @@ OPTIONS:
|
||||
--src value, -s value the file path of the proto source file
|
||||
--dir value, -d value the target path of the code,default path is "${pwd}". [option]
|
||||
--service value, --srv value the name of rpc service. [option]
|
||||
--shared value the dir of the shared file,default path is "${pwd}/shared. [option]"
|
||||
--shared[已废弃] value the dir of the shared file,default path is "${pwd}/shared. [option]"
|
||||
--idea whether the command execution environment is from idea plugin. [option]
|
||||
|
||||
```
|
||||
|
||||
* 参数说明
|
||||
* --src 必填,proto数据源,目前暂时支持单个proto文件生成,这里不支持(不建议)外部依赖
|
||||
* --dir 非必填,默认为proto文件所在目录,生成代码的目标目录
|
||||
* --service 服务名称,非必填,默认为proto文件所在目录名称,但是,如果proto所在目录为一下结构:
|
||||
```shell script
|
||||
user
|
||||
├── cmd
|
||||
│ └── rpc
|
||||
│ └── user.proto
|
||||
```
|
||||
则服务名称亦为user,而非proto所在文件夹名称了,这里推荐使用这种结构,可以方便在同一个服务名下建立不同类型的服务(api、rpc、mq等),便于代码管理与维护。
|
||||
* --shared 非必填,默认为$dir(xxx.proto)/shared,rpc client逻辑代码存放目录。
|
||||
|
||||
> 注意:这里的shared文件夹名称将会是代码中的package名称。
|
||||
|
||||
* --idea 非必填,是否为idea插件中执行,保留字段,终端执行可以忽略
|
||||
|
||||
# 开发人员需要做什么
|
||||
### 参数说明
|
||||
|
||||
* --src 必填,proto数据源,目前暂时支持单个proto文件生成,这里不支持(不建议)外部依赖
|
||||
* --dir 非必填,默认为proto文件所在目录,生成代码的目标目录
|
||||
* --service 服务名称,非必填,默认为proto文件所在目录名称,但是,如果proto所在目录为一下结构:
|
||||
|
||||
```shell script
|
||||
user
|
||||
├── cmd
|
||||
│ └── rpc
|
||||
│ └── user.proto
|
||||
```
|
||||
|
||||
则服务名称亦为user,而非proto所在文件夹名称了,这里推荐使用这种结构,可以方便在同一个服务名下建立不同类型的服务(api、rpc、mq等),便于代码管理与维护。
|
||||
* --shared[⚠️已废弃] 非必填,默认为$dir(xxx.proto)/shared,rpc client逻辑代码存放目录。
|
||||
|
||||
> 注意:这里的shared文件夹名称将会是代码中的package名称。
|
||||
|
||||
* --idea 非必填,是否为idea插件中执行,保留字段,终端执行可以忽略
|
||||
|
||||
### 开发人员需要做什么
|
||||
|
||||
关注业务代码编写,将重复性、与业务无关的工作交给goctl,生成好rpc服务代码后,开饭人员仅需要修改
|
||||
|
||||
* 服务中的配置文件编写(etc/xx.json、internal/config/config.go)
|
||||
* 服务中业务逻辑编写(internal/logic/xxlogic.go)
|
||||
* 服务中资源上下文的编写(internal/svc/servicecontext.go)
|
||||
|
||||
# 扩展
|
||||
## 扩展
|
||||
|
||||
对于需要进行rpc mock的开发人员,在安装了`mockgen`工具的前提下可以在rpc的shared文件中生成好对应的mock文件。
|
||||
|
||||
# 注意事项
|
||||
### 注意事项
|
||||
|
||||
* `google.golang.org/grpc`需要降级到v1.26.0,且protoc-gen-go版本不能高于v1.3.2(see [https://github.com/grpc/grpc-go/issues/3347](https://github.com/grpc/grpc-go/issues/3347))即
|
||||
|
||||
```shell script
|
||||
replace google.golang.org/grpc => google.golang.org/grpc v1.26.0
|
||||
```
|
||||
|
||||
* proto不支持暂多文件同时生成
|
||||
* proto不支持外部依赖包引入,message不支持inline
|
||||
* 目前main文件、shared文件、handler文件会被强制覆盖,而和开发人员手动需要编写的则不会覆盖生成,这一类在代码头部均有
|
||||
```shell script
|
||||
|
||||
```shell script
|
||||
// Code generated by goctl. DO NOT EDIT!
|
||||
// Source: xxx.proto
|
||||
```
|
||||
|
||||
的标识,请注意不要将也写业务性代码写在里面。
|
||||
|
||||
## 常见问题解决(go mod工程)
|
||||
|
||||
* 错误一:
|
||||
|
||||
```golang
|
||||
pb/xx.pb.go:220:7: undefined: grpc.ClientConnInterface
|
||||
pb/xx.pb.go:224:11: undefined: grpc.SupportPackageIsVersion6
|
||||
pb/xx.pb.go:234:5: undefined: grpc.ClientConnInterface
|
||||
pb/xx.pb.go:237:24: undefined: grpc.ClientConnInterface
|
||||
```
|
||||
|
||||
解决方法:请将`protoc-gen-go`版本降至v1.3.2及一下
|
||||
|
||||
* 错误二:
|
||||
|
||||
```golang
|
||||
|
||||
# go.etcd.io/etcd/clientv3/balancer/picker
|
||||
../../../go/pkg/mod/go.etcd.io/etcd@v0.0.0-20200402134248-51bdeb39e698/clientv3/balancer/picker/err.go:25:9: cannot use &errPicker literal (type *errPicker) as type Picker in return argument:*errPicker does not implement Picker (wrong type for Pick method)
|
||||
have Pick(context.Context, balancer.PickInfo) (balancer.SubConn, func(balancer.DoneInfo), error)
|
||||
want Pick(balancer.PickInfo) (balancer.PickResult, error)
|
||||
../../../go/pkg/mod/go.etcd.io/etcd@v0.0.0-20200402134248-51bdeb39e698/clientv3/balancer/picker/roundrobin_balanced.go:33:9: cannot use &rrBalanced literal (type *rrBalanced) as type Picker in return argument:
|
||||
*rrBalanced does not implement Picker (wrong type for Pick method)
|
||||
have Pick(context.Context, balancer.PickInfo) (balancer.SubConn, func(balancer.DoneInfo), error)
|
||||
want Pick(balancer.PickInfo) (balancer.PickResult, error)
|
||||
#github.com/tal-tech/go-zero/zrpc/internal/balancer/p2c
|
||||
../../../go/pkg/mod/github.com/tal-tech/go-zero@v1.0.12/zrpc/internal/balancer/p2c/p2c.go:41:32: not enough arguments in call to base.NewBalancerBuilder
|
||||
have (string, *p2cPickerBuilder)
|
||||
want (string, base.PickerBuilder, base.Config)
|
||||
../../../go/pkg/mod/github.com/tal-tech/go-zero@v1.0.12/zrpc/internal/balancer/p2c/p2c.go:58:9: cannot use &p2cPicker literal (type *p2cPicker) as type balancer.Picker in return argument:
|
||||
*p2cPicker does not implement balancer.Picker (wrong type for Pick method)
|
||||
have Pick(context.Context, balancer.PickInfo) (balancer.SubConn, func(balancer.DoneInfo), error)
|
||||
want Pick(balancer.PickInfo) (balancer.PickResult, error)
|
||||
```
|
||||
|
||||
解决方法:
|
||||
|
||||
```golang
|
||||
replace google.golang.org/grpc => google.golang.org/grpc v1.26.0
|
||||
```
|
||||
的标识,请注意不要将也写业务性代码写在里面。
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/tal-tech/go-zero/tools/goctl/rpc/ctx"
|
||||
"github.com/tal-tech/go-zero/tools/goctl/rpc/gen"
|
||||
"github.com/tal-tech/go-zero/tools/goctl/util"
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
|
||||
@@ -17,6 +22,39 @@ func RpcTemplate(c *cli.Context) error {
|
||||
out := c.String("out")
|
||||
idea := c.Bool("idea")
|
||||
generator := gen.NewRpcTemplate(out, idea)
|
||||
generator.MustGenerate()
|
||||
generator.MustGenerate(true)
|
||||
return nil
|
||||
}
|
||||
|
||||
func RpcNew(c *cli.Context) error {
|
||||
idea := c.Bool("idea")
|
||||
arg := c.Args().First()
|
||||
if len(arg) == 0 {
|
||||
arg = "greet"
|
||||
}
|
||||
abs, err := filepath.Abs(arg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = os.Stat(abs)
|
||||
if err != nil {
|
||||
if !os.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
err = util.MkdirIfNotExist(abs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
dir := filepath.Base(filepath.Clean(abs))
|
||||
|
||||
protoSrc := filepath.Join(abs, fmt.Sprintf("%v.proto", dir))
|
||||
templateGenerator := gen.NewRpcTemplate(protoSrc, idea)
|
||||
templateGenerator.MustGenerate(false)
|
||||
|
||||
rpcCtx := ctx.MustCreateRpcContext(protoSrc, "", "", idea)
|
||||
generator := gen.NewDefaultRpcGenerator(rpcCtx)
|
||||
rpcCtx.Must(generator.Generate())
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -30,13 +30,12 @@ type RpcContext struct {
|
||||
ProtoFileSrc string
|
||||
ProtoSource string
|
||||
TargetDir string
|
||||
IsInGoEnv bool
|
||||
console.Console
|
||||
}
|
||||
|
||||
func MustCreateRpcContext(protoSrc, targetDir, serviceName string, idea bool) *RpcContext {
|
||||
log := console.NewConsole(idea)
|
||||
info, err := project.Prepare(targetDir, true)
|
||||
log.Must(err)
|
||||
|
||||
if stringx.From(protoSrc).IsEmptyOrSpace() {
|
||||
log.Fatalln("expected proto source, but nothing found")
|
||||
@@ -62,6 +61,9 @@ func MustCreateRpcContext(protoSrc, targetDir, serviceName string, idea bool) *R
|
||||
log.Fatalln("service name is not found")
|
||||
}
|
||||
|
||||
info, err := project.Prepare(targetDir, true)
|
||||
log.Must(err)
|
||||
|
||||
return &RpcContext{
|
||||
ProjectPath: info.Path,
|
||||
ProjectName: stringx.From(info.Name),
|
||||
@@ -71,6 +73,7 @@ func MustCreateRpcContext(protoSrc, targetDir, serviceName string, idea bool) *R
|
||||
ProtoFileSrc: srcFp,
|
||||
ProtoSource: filepath.Base(srcFp),
|
||||
TargetDir: targetDirFp,
|
||||
IsInGoEnv: info.IsInGoEnv,
|
||||
Console: log,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,6 +42,11 @@ func (g *defaultRpcGenerator) Generate() (err error) {
|
||||
return
|
||||
}
|
||||
|
||||
err = g.initGoMod()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = g.genEtc()
|
||||
if err != nil {
|
||||
return
|
||||
@@ -82,5 +87,5 @@ func (g *defaultRpcGenerator) Generate() (err error) {
|
||||
return
|
||||
}
|
||||
|
||||
return nil
|
||||
return
|
||||
}
|
||||
|
||||