Repository: zeromicro/go-zero
Pull request #1603 (2022-03-02 10:58:41)

feat(goctl): api dart support flutter v2
0. support null-safety code gen
1. supports -legacy flag for legacy code gen
2. supports -hostname flag for server hostname
3. use dart official format
4. fix some some bugs

Resolves: #1602


Patch:
diff --git a/core/stores/mongoc/cachedcollection.go b/core/stores/mongoc/cachedcollection.go
index 49fdd203..3b4742fb 100644
--- a/core/stores/mongoc/cachedcollection.go
+++ b/core/stores/mongoc/cachedcollection.go
@@ -12,8 +12,8 @@ var (
 	ErrNotFound = mgo.ErrNotFound
 
 	// can't use one SingleFlight per conn, because multiple conns may share the same cache key.
-	sharedCalls = syncx.NewSingleFlight()
-	stats       = cache.NewStat("mongoc")
+	singleFlight = syncx.NewSingleFlight()
+	stats        = cache.NewStat("mongoc")
 )
 
 type (
diff --git a/core/stores/mongoc/cachedmodel.go b/core/stores/mongoc/cachedmodel.go
index c366cc3c..919afcea 100644
--- a/core/stores/mongoc/cachedmodel.go
+++ b/core/stores/mongoc/cachedmodel.go
@@ -38,7 +38,7 @@ func MustNewModel(url, collection string, c cache.CacheConf, opts ...cache.Optio
 
 // NewModel returns a Model with a cache cluster.
 func NewModel(url, collection string, conf cache.CacheConf, opts ...cache.Option) (*Model, error) {
-	c := cache.New(conf, sharedCalls, stats, mgo.ErrNotFound, opts...)
+	c := cache.New(conf, singleFlight, stats, mgo.ErrNotFound, opts...)
 	return NewModelWithCache(url, collection, c)
 }
 
@@ -51,7 +51,7 @@ func NewModelWithCache(url, collection string, c cache.Cache) (*Model, error) {
 
 // NewNodeModel returns a Model with a cache node.
 func NewNodeModel(url, collection string, rds *redis.Redis, opts ...cache.Option) (*Model, error) {
-	c := cache.NewNode(rds, sharedCalls, stats, mgo.ErrNotFound, opts...)
+	c := cache.NewNode(rds, singleFlight, stats, mgo.ErrNotFound, opts...)
 	return NewModelWithCache(url, collection, c)
 }
 
diff --git a/readme-cn.md b/readme-cn.md
index da868a2a..e8bae5ca 100644
--- a/readme-cn.md
+++ b/readme-cn.md
@@ -19,9 +19,9 @@
 >
 > 从 v1.3.0 之前版本升级请执行以下命令：
 > 
-> GOPROXY=https://goproxy.cn/,direct go install github.com/zeromicro/go-zero/tools/goctl@latest
+> `GOPROXY=https://goproxy.cn/,direct go install github.com/zeromicro/go-zero/tools/goctl@latest`
 > 
-> goctl migrate —verbose —version v1.3.0
+> `goctl migrate —verbose —version v1.3.1`
 
 ## 0. go-zero 介绍
 
@@ -252,6 +252,8 @@ go-zero 已被许多公司用于生产部署，接入场景如在线教育、电
 >56. 时代脉搏网络科技（云浮市）有限公司
 >57. 店有帮
 >58. 七牛云
+>59. 费芮网络
+>60. 51CTO
 
 如果贵公司也已使用 go-zero，欢迎在 [登记地址](https://github.com/zeromicro/go-zero/issues/602) 登记，仅仅为了推广，不做其它用途。
 
diff --git a/readme.md b/readme.md
index 3aff815d..0b9a8bff 100644
--- a/readme.md
+++ b/readme.md
@@ -16,11 +16,11 @@ English | [简体中文](readme-cn.md)
 
 > ***Important!***
 > 
-> To upgrade from previous versions, run the following commands.
+> To upgrade from versions eariler than v1.3.0, run the following commands.
 > 
 > `go install github.com/zeromicro/go-zero/tools/goctl@latest`
 > 
-> `goctl migrate —verbose —version v1.3.0`
+> `goctl migrate —verbose —version v1.3.1`
 
 ## 0. what is go-zero
 
diff --git a/tools/goctl/.gitignore b/tools/goctl/.gitignore
new file mode 100644
index 00000000..722d5e71
--- /dev/null
+++ b/tools/goctl/.gitignore
@@ -0,0 +1,1 @@
+.vscode
diff --git a/tools/goctl/api/dartgen/format.go b/tools/goctl/api/dartgen/format.go
new file mode 100644
index 00000000..029e3bec
--- /dev/null
+++ b/tools/goctl/api/dartgen/format.go
@@ -0,0 +1,40 @@
+package dartgen
+
+import (
+	"fmt"
+	"os"
+	"os/exec"
+)
+
+const dartExec = "dart"
+
+func formatDir(dir string) error {
+	ok, err := dirctoryExists(dir)
+	if err != nil {
+		return err
+	}
+	if !ok {
+		return fmt.Errorf("format failed, directory %q does not exist", dir)
+	}
+
+	_, err = exec.LookPath(dartExec)
+	if err != nil {
+		return err
+	}
+	cmd := exec.Command(dartExec, "format", dir)
+	cmd.Env = os.Environ()
+	cmd.Stderr = os.Stderr
+
+	return cmd.Run()
+}
+
+func dirctoryExists(dir string) (bool, error) {
+	_, err := os.Stat(dir)
+	if err == nil {
+		return true, nil
+	}
+	if os.IsNotExist(err) {
+		return false, nil
+	}
+	return false, err
+}
diff --git a/tools/goctl/api/dartgen/gen.go b/tools/goctl/api/dartgen/gen.go
index 3bb08a99..c7078bf2 100644
--- a/tools/goctl/api/dartgen/gen.go
+++ b/tools/goctl/api/dartgen/gen.go
@@ -2,6 +2,7 @@ package dartgen
 
 import (
 	"errors"
+	"fmt"
 	"strings"
 
 	"github.com/urfave/cli"
@@ -13,12 +14,18 @@ import (
 func DartCommand(c *cli.Context) error {
 	apiFile := c.String("api")
 	dir := c.String("dir")
+	isLegacy := c.Bool("legacy")
+	hostname := c.String("hostname")
 	if len(apiFile) == 0 {
 		return errors.New("missing -api")
 	}
 	if len(dir) == 0 {
 		return errors.New("missing -dir")
 	}
+	if len(hostname) == 0 {
+		fmt.Println("you could use '-hostname' flag to specify your server hostname")
+		hostname = "go-zero.dev"
+	}
 
 	api, err := parser.Parse(apiFile)
 	if err != nil {
@@ -30,8 +37,11 @@ func DartCommand(c *cli.Context) error {
 		dir = dir + "/"
 	}
 	api.Info.Title = strings.Replace(apiFile, ".api", "", -1)
-	logx.Must(genData(dir+"data/", api))
-	logx.Must(genApi(dir+"api/", api))
-	logx.Must(genVars(dir + "vars/"))
+	logx.Must(genData(dir+"data/", api, isLegacy))
+	logx.Must(genApi(dir+"api/", api, isLegacy))
+	logx.Must(genVars(dir+"vars/", isLegacy, hostname))
+	if err := formatDir(dir); err != nil {
+		logx.Errorf("failed to format, %v", err)
+	}
 	return nil
 }
diff --git a/tools/goctl/api/dartgen/genapi.go b/tools/goctl/api/dartgen/genapi.go
index 380b858a..7f735a81 100644
--- a/tools/goctl/api/dartgen/genapi.go
+++ b/tools/goctl/api/dartgen/genapi.go
@@ -2,13 +2,14 @@ package dartgen
 
 import (
 	"os"
+	"strings"
 	"text/template"
 
 	"github.com/zeromicro/go-zero/tools/goctl/api/spec"
 )
 
 const apiTemplate = `import 'api.dart';
-import '../data/{{with .Info}}{{.Title}}{{end}}.dart';
+import '../data/{{with .Info}}{{getBaseName .Title}}{{end}}.dart';
 {{with .Service}}
 /// {{.Name}}
 {{range .Routes}}
@@ -22,24 +23,45 @@ Future {{pathToFuncName .Path}}( {{if ne .Method "get"}}{{with .RequestType}}{{.
     Function eventually}) async {
   await api{{if eq .Method "get"}}Get{{else}}Post{{end}}('{{.Path}}',{{if ne .Method "get"}}request,{{end}}
   	 ok: (data) {
-    if (ok != null) ok({{with .ResponseType}}{{.Name}}{{end}}.fromJson(data));
+    if (ok != null) ok({{with .ResponseType}}{{.Name}}.fromJson(data){{end}});
   }, fail: fail, eventually: eventually);
 }
 {{end}}
 {{end}}`
 
-func genApi(dir string, api *spec.ApiSpec) error {
+const apiTemplateV2 = `import 'api.dart';
+import '../data/{{with .Info}}{{getBaseName .Title}}{{end}}.dart';
+{{with .Service}}
+/// {{.Name}}
+{{range .Routes}}
+/// --{{.Path}}--
+///
+/// request: {{with .RequestType}}{{.Name}}{{end}}
+/// response: {{with .ResponseType}}{{.Name}}{{end}}
+Future {{pathToFuncName .Path}}( {{if ne .Method "get"}}{{with .RequestType}}{{.Name}} request,{{end}}{{end}}
+    {Function({{with .ResponseType}}{{.Name}}{{end}})? ok,
+    Function(String)? fail,
+    Function? eventually}) async {
+  await api{{if eq .Method "get"}}Get{{else}}Post{{end}}('{{.Path}}',{{if ne .Method "get"}}request,{{end}}
+  	 ok: (data) {
+    if (ok != null) ok({{with .ResponseType}}{{.Name}}.fromJson(data){{end}});
+  }, fail: fail, eventually: eventually);
+}
+{{end}}
+{{end}}`
+
+func genApi(dir string, api *spec.ApiSpec, isLegacy bool) error {
 	err := os.MkdirAll(dir, 0o755)
 	if err != nil {
 		return err
 	}
 
-	err = genApiFile(dir)
+	err = genApiFile(dir, isLegacy)
 	if err != nil {
 		return err
 	}
 
-	file, err := os.OpenFile(dir+api.Service.Name+".dart", os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o644)
+	file, err := os.OpenFile(dir+strings.ToLower(api.Service.Name+".dart"), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o644)
 	if err != nil {
 		return err
 	}
@@ -47,7 +69,11 @@ func genApi(dir string, api *spec.ApiSpec) error {
 	defer file.Close()
 	t := template.New("apiTemplate")
 	t = t.Funcs(funcMap)
-	t, err = t.Parse(apiTemplate)
+	tpl := apiTemplateV2
+	if isLegacy {
+		tpl = apiTemplate
+	}
+	t, err = t.Parse(tpl)
 	if err != nil {
 		return err
 	}
@@ -55,7 +81,7 @@ func genApi(dir string, api *spec.ApiSpec) error {
 	return t.Execute(file, api)
 }
 
-func genApiFile(dir string) error {
+func genApiFile(dir string, isLegacy bool) error {
 	path := dir + "api.dart"
 	if fileExists(path) {
 		return nil
@@ -66,6 +92,10 @@ func genApiFile(dir string) error {
 	}
 
 	defer apiFile.Close()
-	_, err = apiFile.WriteString(apiFileContent)
+	tpl := apiFileContentV2
+	if isLegacy {
+		tpl = apiFileContent
+	}
+	_, err = apiFile.WriteString(tpl)
 	return err
 }
diff --git a/tools/goctl/api/dartgen/gendata.go b/tools/goctl/api/dartgen/gendata.go
index f8e4d538..49c5f0a6 100644
--- a/tools/goctl/api/dartgen/gendata.go
+++ b/tools/goctl/api/dartgen/gendata.go
@@ -2,6 +2,7 @@ package dartgen
 
 import (
 	"os"
+	"strings"
 	"text/template"
 
 	"github.com/zeromicro/go-zero/tools/goctl/api/spec"
@@ -31,18 +32,40 @@ class {{.Name}}{
 {{end}}
 `
 
-func genData(dir string, api *spec.ApiSpec) error {
+const dataTemplateV2 = `// --{{with .Info}}{{.Title}}{{end}}--
+{{ range .Types}}
+class {{.Name}} {
+	{{range .Members}}
+	{{if .Comment}}{{.Comment}}{{end}}
+	final {{.Type.Name}} {{lowCamelCase .Name}};
+  {{end}}{{.Name}}({{if .Members}}{
+	{{range .Members}}  required this.{{lowCamelCase .Name}},
+	{{end}}}{{end}});
+	factory {{.Name}}.fromJson(Map<String,dynamic> m) {
+		return {{.Name}}({{range .Members}}
+			{{lowCamelCase .Name}}: {{if isDirectType .Type.Name}}m['{{getPropertyFromMember .}}']{{else if isClassListType .Type.Name}}(m['{{getPropertyFromMember .}}'] as List<dynamic>).map((i) => {{getCoreType .Type.Name}}.fromJson(i)){{else}}{{.Type.Name}}.fromJson(m['{{getPropertyFromMember .}}']){{end}},{{end}}
+		);
+	}
+	Map<String,dynamic> toJson() {
+		return { {{range .Members}}
+			'{{getPropertyFromMember .}}': {{if isDirectType .Type.Name}}{{lowCamelCase .Name}}{{else if isClassListType .Type.Name}}{{lowCamelCase .Name}}.map((i) => i.toJson()){{else}}{{lowCamelCase .Name}}.toJson(){{end}},{{end}}
+		};
+	}
+}
+{{end}}`
+
+func genData(dir string, api *spec.ApiSpec, isLegacy bool) error {
 	err := os.MkdirAll(dir, 0o755)
 	if err != nil {
 		return err
 	}
 
-	err = genTokens(dir)
+	err = genTokens(dir, isLegacy)
 	if err != nil {
 		return err
 	}
 
-	file, err := os.OpenFile(dir+api.Service.Name+".dart", os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o644)
+	file, err := os.OpenFile(dir+strings.ToLower(api.Service.Name+".dart"), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o644)
 	if err != nil {
 		return err
 	}
@@ -50,7 +73,11 @@ func genData(dir string, api *spec.ApiSpec) error {
 
 	t := template.New("dataTemplate")
 	t = t.Funcs(funcMap)
-	t, err = t.Parse(dataTemplate)
+	tpl := dataTemplateV2
+	if isLegacy {
+		tpl = dataTemplate
+	}
+	t, err = t.Parse(tpl)
 	if err != nil {
 		return err
 	}
@@ -63,7 +90,7 @@ func genData(dir string, api *spec.ApiSpec) error {
 	return t.Execute(file, api)
 }
 
-func genTokens(dir string) error {
+func genTokens(dir string, isLeagcy bool) error {
 	path := dir + "tokens.dart"
 	if fileExists(path) {
 		return nil
@@ -75,7 +102,11 @@ func genTokens(dir string) error {
 	}
 
 	defer tokensFile.Close()
-	_, err = tokensFile.WriteString(tokensFileContent)
+	tpl := tokensFileContentV2
+	if isLeagcy {
+		tpl = tokensFileContent
+	}
+	_, err = tokensFile.WriteString(tpl)
 	return err
 }
 
diff --git a/tools/goctl/api/dartgen/genvars.go b/tools/goctl/api/dartgen/genvars.go
index c6b90a56..51a9fa13 100644
--- a/tools/goctl/api/dartgen/genvars.go
+++ b/tools/goctl/api/dartgen/genvars.go
@@ -1,11 +1,13 @@
 package dartgen
 
 import (
+	"fmt"
 	"io/ioutil"
 	"os"
 )
 
-const varTemplate = `import 'dart:convert';
+const (
+	varTemplate = `import 'dart:convert';
 import 'package:shared_preferences/shared_preferences.dart';
 import '../data/tokens.dart';
 
@@ -40,21 +42,59 @@ Future<Tokens> getTokens() async {
 }
 `
 
-func genVars(dir string) error {
+	varTemplateV2 = `import 'dart:convert';
+import 'package:shared_preferences/shared_preferences.dart';
+import '../data/tokens.dart';
+
+const String _tokenKey = 'tokens';
+
+/// Saves tokens
+Future<bool> setTokens(Tokens tokens) async {
+  var sp = await SharedPreferences.getInstance();
+  return await sp.setString(_tokenKey, jsonEncode(tokens.toJson()));
+}
+
+/// remove tokens
+Future<bool> removeTokens() async {
+  var sp = await SharedPreferences.getInstance();
+  return sp.remove(_tokenKey);
+}
+
+/// Reads tokens
+Future<Tokens?> getTokens() async {
+  try {
+    var sp = await SharedPreferences.getInstance();
+    var str = sp.getString('tokens');
+    if (str.isEmpty) {
+      return null;
+    }
+    return Tokens.fromJson(jsonDecode(str));
+  } catch (e) {
+    print(e);
+    return null;
+  }
+}`
+)
+
+func genVars(dir string, isLegacy bool, hostname string) error {
 	err := os.MkdirAll(dir, 0o755)
 	if err != nil {
 		return err
 	}
 
 	if !fileExists(dir + "vars.dart") {
-		err = ioutil.WriteFile(dir+"vars.dart", []byte(`const serverHost='demo-crm.xiaoheiban.cn';`), 0o644)
+		err = ioutil.WriteFile(dir+"vars.dart", []byte(fmt.Sprintf(`const serverHost='%s';`, hostname)), 0o644)
 		if err != nil {
 			return err
 		}
 	}
 
 	if !fileExists(dir + "kv.dart") {
-		err = ioutil.WriteFile(dir+"kv.dart", []byte(varTemplate), 0o644)
+		tpl := varTemplateV2
+		if isLegacy {
+			tpl = varTemplate
+		}
+		err = ioutil.WriteFile(dir+"kv.dart", []byte(tpl), 0o644)
 		if err != nil {
 			return err
 		}
diff --git a/tools/goctl/api/dartgen/util.go b/tools/goctl/api/dartgen/util.go
index 92cf2a82..0cccf36d 100644
--- a/tools/goctl/api/dartgen/util.go
+++ b/tools/goctl/api/dartgen/util.go
@@ -4,6 +4,7 @@ import (
 	"errors"
 	"fmt"
 	"os"
+	"path"
 	"strings"
 
 	"github.com/zeromicro/go-zero/tools/goctl/api/spec"
@@ -34,6 +35,10 @@ func pathToFuncName(path string) string {
 	return util.ToLower(camel[:1]) + camel[1:]
 }
 
+func getBaseName(str string) string {
+	return path.Base(str)
+}
+
 func getPropertyFromMember(member spec.Member) string {
 	name, err := member.GetPropertyName()
 	if err != nil {
diff --git a/tools/goctl/api/dartgen/vars.go b/tools/goctl/api/dartgen/vars.go
index 67aafde2..621a7c3a 100644
--- a/tools/goctl/api/dartgen/vars.go
+++ b/tools/goctl/api/dartgen/vars.go
@@ -3,6 +3,7 @@ package dartgen
 import "text/template"
 
 var funcMap = template.FuncMap{
+	"getBaseName":           getBaseName,
 	"getPropertyFromMember": getPropertyFromMember,
 	"isDirectType":          isDirectType,
 	"isClassListType":       isClassListType,
@@ -99,6 +100,96 @@ Future _apiRequest(String method, String path, dynamic data,
 }
 `
 
+	apiFileContentV2 = `import 'dart:io';
+	import 'dart:convert';
+	import '../vars/kv.dart';
+	import '../vars/vars.dart';
+
+	/// send request with post method
+	///
+	/// data: any request class that will be converted to json automatically
+	/// ok: is called when request succeeds
+	/// fail: is called when request fails
+	/// eventually: is always called until the nearby functions returns
+	Future apiPost(String path, dynamic data,
+			{Map<String, String>? header,
+			Function(Map<String, dynamic>)? ok,
+			Function(String)? fail,
+			Function? eventually}) async {
+		await _apiRequest('POST', path, data,
+				header: header, ok: ok, fail: fail, eventually: eventually);
+	}
+
+	/// send request with get method
+	///
+	/// ok: is called when request succeeds
+	/// fail: is called when request fails
+	/// eventually: is always called until the nearby functions returns
+	Future apiGet(String path,
+			{Map<String, String>? header,
+			Function(Map<String, dynamic>)? ok,
+			Function(String)? fail,
+			Function? eventually}) async {
+		await _apiRequest('GET', path, null,
+				header: header, ok: ok, fail: fail, eventually: eventually);
+	}
+
+	Future _apiRequest(String method, String path, dynamic data,
+			{Map<String, String>? header,
+			Function(Map<String, dynamic>)? ok,
+			Function(String)? fail,
+			Function? eventually}) async {
+		var tokens = await getTokens();
+		try {
+			var client = HttpClient();
+			HttpClientRequest r;
+			if (method == 'POST') {
+				r = await client.postUrl(Uri.parse('https://' + serverHost + path));
+			} else {
+				r = await client.getUrl(Uri.parse('https://' + serverHost + path));
+			}
+
+			r.headers.set('Content-Type', 'application/json');
+			if (tokens != null) {
+				r.headers.set('Authorization', tokens.accessToken);
+			}
+			if (header != null) {
+				header.forEach((k, v) {
+					r.headers.set(k, v);
+				});
+			}
+			var strData = '';
+			if (data != null) {
+				strData = jsonEncode(data);
+			}
+			r.write(strData);
+			var rp = await r.close();
+			var body = await rp.transform(utf8.decoder).join();
+			print('${rp.statusCode} - $path');
+			print('-- request --');
+			print(strData);
+			print('-- response --');
+			print('$body \n');
+			if (rp.statusCode == 404) {
+				if (fail != null) fail('404 not found');
+			} else {
+				Map<String, dynamic> base = jsonDecode(body);
+				if (rp.statusCode == 200) {
+					if (base['code'] != 0) {
+						if (fail != null) fail(base['desc']);
+					} else {
+						if (ok != null) ok(base['data']);
+					}
+				} else if (base['code'] != 0) {
+					if (fail != null) fail(base['desc']);
+				}
+			}
+		} catch (e) {
+			if (fail != null) fail(e.toString());
+		}
+		if (eventually != null) eventually();
+	}`
+
 	tokensFileContent = `class Tokens {
   /// 用于访问的token, 每次请求都必须带在Header里面
   final String accessToken;
@@ -132,5 +223,41 @@ Future _apiRequest(String method, String path, dynamic data,
     };
   }
 }
+`
+
+	tokensFileContentV2 = `class Tokens {
+  /// 用于访问的token, 每次请求都必须带在Header里面
+  final String accessToken;
+  final int accessExpire;
+
+  /// 用于刷新token
+  final String refreshToken;
+  final int refreshExpire;
+  final int refreshAfter;
+  Tokens({
+		required this.accessToken,
+		required this.accessExpire,
+		required this.refreshToken,
+		required this.refreshExpire,
+		required this.refreshAfter
+	});
+  factory Tokens.fromJson(Map<String, dynamic> m) {
+    return Tokens(
+        accessToken: m['access_token'],
+        accessExpire: m['access_expire'],
+        refreshToken: m['refresh_token'],
+        refreshExpire: m['refresh_expire'],
+        refreshAfter: m['refresh_after']);
+  }
+  Map<String, dynamic> toJson() {
+    return {
+      'access_token': accessToken,
+      'access_expire': accessExpire,
+      'refresh_token': refreshToken,
+      'refresh_expire': refreshExpire,
+      'refresh_after': refreshAfter,
+    };
+  }
+}
 `
 )
diff --git a/tools/goctl/goctl.go b/tools/goctl/goctl.go
index 77c23a28..f9af586a 100644
--- a/tools/goctl/goctl.go
+++ b/tools/goctl/goctl.go
@@ -266,6 +266,14 @@ var commands = []cli.Command{
 						Name:  "api",
 						Usage: "the api file",
 					},
+					cli.BoolFlag{
+						Name:  "legacy",
+						Usage: "legacy generator for flutter v1",
+					},
+					cli.StringFlag{
+						Name:  "hostname",
+						Usage: "hostname of the server",
+					},
 				},
 				Action: dartgen.DartCommand,
 			},
diff --git a/zrpc/proxy.go b/zrpc/proxy.go
index d02f7ff4..7bb997f8 100644
--- a/zrpc/proxy.go
+++ b/zrpc/proxy.go
@@ -12,20 +12,20 @@ import (
 
 // A RpcProxy is a rpc proxy.
 type RpcProxy struct {
-	backend     string
-	clients     map[string]Client
-	options     []internal.ClientOption
-	sharedCalls syncx.SingleFlight
-	lock        sync.Mutex
+	backend      string
+	clients      map[string]Client
+	options      []internal.ClientOption
+	singleFlight syncx.SingleFlight
+	lock         sync.Mutex
 }
 
 // NewProxy returns a RpcProxy.
 func NewProxy(backend string, opts ...internal.ClientOption) *RpcProxy {
 	return &RpcProxy{
-		backend:     backend,
-		clients:     make(map[string]Client),
-		options:     opts,
-		sharedCalls: syncx.NewSingleFlight(),
+		backend:      backend,
+		clients:      make(map[string]Client),
+		options:      opts,
+		singleFlight: syncx.NewSingleFlight(),
 	}
 }
 
@@ -33,7 +33,7 @@ func NewProxy(backend string, opts ...internal.ClientOption) *RpcProxy {
 func (p *RpcProxy) TakeConn(ctx context.Context) (*grpc.ClientConn, error) {
 	cred := auth.ParseCredential(ctx)
 	key := cred.App + "/" + cred.Token
-	val, err := p.sharedCalls.Do(key, func() (interface{}, error) {
+	val, err := p.singleFlight.Do(key, func() (interface{}, error) {
 		p.lock.Lock()
 		client, ok := p.clients[key]
 		p.lock.Unlock()


Test patch:
diff --git a/core/stores/mongoc/cachedcollection_test.go b/core/stores/mongoc/cachedcollection_test.go
index 6f839b13..f7eaa254 100644
--- a/core/stores/mongoc/cachedcollection_test.go
+++ b/core/stores/mongoc/cachedcollection_test.go
@@ -34,7 +34,7 @@ func TestCollection_Count(t *testing.T) {
 	assert.Nil(t, err)
 	defer clean()
 
-	cach := cache.NewNode(r, sharedCalls, stats, mgo.ErrNotFound)
+	cach := cache.NewNode(r, singleFlight, stats, mgo.ErrNotFound)
 	c := newCollection(dummyConn{}, cach)
 	val, err := c.Count("any")
 	assert.Nil(t, err)
@@ -98,7 +98,7 @@ func TestStat(t *testing.T) {
 	assert.Nil(t, err)
 	defer clean()
 
-	cach := cache.NewNode(r, sharedCalls, stats, mgo.ErrNotFound)
+	cach := cache.NewNode(r, singleFlight, stats, mgo.ErrNotFound)
 	c := newCollection(dummyConn{}, cach).(*cachedCollection)
 
 	for i := 0; i < 10; i++ {
@@ -121,7 +121,7 @@ func TestStatCacheFails(t *testing.T) {
 	defer log.SetOutput(os.Stdout)
 
 	r := redis.New("localhost:59999")
-	cach := cache.NewNode(r, sharedCalls, stats, mgo.ErrNotFound)
+	cach := cache.NewNode(r, singleFlight, stats, mgo.ErrNotFound)
 	c := newCollection(dummyConn{}, cach)
 
 	for i := 0; i < 20; i++ {
@@ -142,7 +142,7 @@ func TestStatDbFails(t *testing.T) {
 	assert.Nil(t, err)
 	defer clean()
 
-	cach := cache.NewNode(r, sharedCalls, stats, mgo.ErrNotFound)
+	cach := cache.NewNode(r, singleFlight, stats, mgo.ErrNotFound)
 	c := newCollection(dummyConn{}, cach).(*cachedCollection)
 
 	for i := 0; i < 20; i++ {
@@ -164,7 +164,7 @@ func TestStatFromMemory(t *testing.T) {
 	assert.Nil(t, err)
 	defer clean()
 
-	cach := cache.NewNode(r, sharedCalls, stats, mgo.ErrNotFound)
+	cach := cache.NewNode(r, singleFlight, stats, mgo.ErrNotFound)
 	c := newCollection(dummyConn{}, cach).(*cachedCollection)
 
 	var all sync.WaitGroup
diff --git a/zrpc/proxy_test.go b/zrpc/proxy_test.go
index 97a24fe4..935914a4 100644
--- a/zrpc/proxy_test.go
+++ b/zrpc/proxy_test.go
@@ -64,3 +64,9 @@ func TestProxy(t *testing.T) {
 		})
 	}
 }
+
+func TestRpcProxy_TakeConnNewClientFailed(t *testing.T) {
+	proxy := NewProxy("foo", WithDialOption(grpc.WithInsecure()), WithDialOption(grpc.WithBlock()))
+	_, err := proxy.TakeConn(context.Background())
+	assert.NotNil(t, err)
+}
diff --git a/zrpc/server_test.go b/zrpc/server_test.go
index 5dad1b99..14360ccc 100644
--- a/zrpc/server_test.go
+++ b/zrpc/server_test.go
@@ -101,6 +101,21 @@ func TestServer_HasEtcd(t *testing.T) {
 	srv.Stop()
 }
 
+func TestServer_StartFailed(t *testing.T) {
+	srv := MustNewServer(RpcServerConf{
+		ServiceConf: service.ServiceConf{
+			Log: logx.LogConf{
+				ServiceName: "foo",
+				Mode:        "console",
+			},
+		},
+		ListenOn: "localhost:aaa",
+	}, func(server *grpc.Server) {
+	})
+
+	assert.Panics(t, srv.Start)
+}
+
 type mockedServer struct {
 	unaryInterceptors  []grpc.UnaryServerInterceptor
 	streamInterceptors []grpc.StreamServerInterceptor