Browse Source

rpc service generation (#26)

* add execute files

* add protoc-osx

* add rpc generation

* add rpc generation

* add: rpc template generation

* update usage

* fixed env prepare for project in go path

* optimize gomod cache

* add README.md

* format error

* reactor templatex.go

* remove waste code
Keson 4 years ago
parent
commit
db16115037

+ 2 - 2
go.mod

@@ -3,12 +3,12 @@ module github.com/tal-tech/go-zero
 go 1.14
 
 require (
-	9fans.net/go v0.0.2 // indirect
 	github.com/DATA-DOG/go-sqlmock v1.4.1
 	github.com/alicebob/gopher-json v0.0.0-20180125190556-5a6b3ba71ee6 // indirect
 	github.com/alicebob/miniredis v2.5.0+incompatible
 	github.com/dchest/siphash v1.2.1
 	github.com/dgrijalva/jwt-go v3.2.0+incompatible
+	github.com/dsymonds/gotoc v0.0.0-20160928043926-5aebcfc91819
 	github.com/fatih/color v1.9.0 // indirect
 	github.com/frankban/quicktest v1.7.2 // indirect
 	github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8
@@ -56,7 +56,7 @@ require (
 	golang.org/x/tools v0.0.0-20200410132612-ae9902aceb98 // indirect
 	google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f // indirect
 	google.golang.org/grpc v1.29.1
-	google.golang.org/protobuf v1.25.0 // indirect
+	google.golang.org/protobuf v1.25.0
 	gopkg.in/cheggaaa/pb.v1 v1.0.28
 	gopkg.in/yaml.v2 v2.2.8
 	honnef.co/go/tools v0.0.1-2020.1.4 // indirect

+ 2 - 2
go.sum

@@ -1,5 +1,3 @@
-9fans.net/go v0.0.2 h1:RYM6lWITV8oADrwLfdzxmt8ucfW6UtP9v1jg4qAbqts=
-9fans.net/go v0.0.2/go.mod h1:lfPdxjq9v8pVQXUMBCx5EO5oLXWQFlKRQgs1kEkjoIM=
 cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
 github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
 github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
@@ -50,6 +48,8 @@ github.com/dchest/siphash v1.2.1 h1:4cLinnzVJDKxTCl9B01807Yiy+W7ZzVHj/KIroQRvT4=
 github.com/dchest/siphash v1.2.1/go.mod h1:q+IRvb2gOSrUnYoPqHiyHXS0FOBBOdl6tONBlVnOnt4=
 github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
 github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
+github.com/dsymonds/gotoc v0.0.0-20160928043926-5aebcfc91819 h1:9778zj477h/VauD8kHbOtbytW2KGQefJ/wUGE5w+mzw=
+github.com/dsymonds/gotoc v0.0.0-20160928043926-5aebcfc91819/go.mod h1:MvzMVHq8BH2Ji/o8TGDocVA70byvLrAgFTxkEnmjO4Y=
 github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4 h1:qk/FSDDxo05wdJH28W+p5yivv7LuLYLRXPPD8KQCtZs=
 github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
 github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=

+ 54 - 4
tools/goctl/goctl.go

@@ -4,6 +4,8 @@ import (
 	"fmt"
 	"os"
 
+	"github.com/urfave/cli"
+
 	"github.com/tal-tech/go-zero/core/logx"
 	"github.com/tal-tech/go-zero/tools/goctl/api/apigen"
 	"github.com/tal-tech/go-zero/tools/goctl/api/dartgen"
@@ -17,8 +19,8 @@ import (
 	"github.com/tal-tech/go-zero/tools/goctl/configgen"
 	"github.com/tal-tech/go-zero/tools/goctl/docker"
 	"github.com/tal-tech/go-zero/tools/goctl/feature"
-	"github.com/tal-tech/go-zero/tools/goctl/model/sql/command"
-	"github.com/urfave/cli"
+	model "github.com/tal-tech/go-zero/tools/goctl/model/sql/command"
+	rpc "github.com/tal-tech/go-zero/tools/goctl/rpc/command"
 )
 
 var (
@@ -188,6 +190,54 @@ var (
 			},
 			Action: docker.DockerCommand,
 		},
+		{
+			Name:  "rpc",
+			Usage: "generate rpc code",
+			Subcommands: []cli.Command{
+				{
+					Name:  "template",
+					Usage: `generate proto template"`,
+					Flags: []cli.Flag{
+						cli.StringFlag{
+							Name:  "out, o",
+							Usage: "the target path of proto",
+						},
+						cli.BoolFlag{
+							Name:  "idea",
+							Usage: "whether the command execution environment is from idea plugin. [option]",
+						},
+					},
+					Action: rpc.RpcTemplate,
+				},
+				{
+					Name:  "proto",
+					Usage: `generate rpc from proto"`,
+					Flags: []cli.Flag{
+						cli.StringFlag{
+							Name:  "src, s",
+							Usage: "the file path of the proto source file",
+						},
+						cli.StringFlag{
+							Name:  "dir, d",
+							Usage: `the target path of the code,default path is "${pwd}". [option]`,
+						},
+						cli.StringFlag{
+							Name:  "service, srv",
+							Usage: `the name of rpc service. [option]`,
+						},
+						cli.StringFlag{
+							Name:  "shared",
+							Usage: `the dir of the shared file,default path is "${pwd}/shared. [option]"`,
+						},
+						cli.BoolFlag{
+							Name:  "idea",
+							Usage: "whether the command execution environment is from idea plugin. [option]",
+						},
+					},
+					Action: rpc.Rpc,
+				},
+			},
+		},
 		{
 			Name:  "model",
 			Usage: "generate model code",
@@ -217,7 +267,7 @@ var (
 									Usage: "for idea plugin [optional]",
 								},
 							},
-							Action: command.MysqlDDL,
+							Action: model.MysqlDDL,
 						},
 						{
 							Name:  "datasource",
@@ -244,7 +294,7 @@ var (
 									Usage: "for idea plugin [optional]",
 								},
 							},
-							Action: command.MyDataSource,
+							Action: model.MyDataSource,
 						},
 					},
 				},

+ 2 - 2
tools/goctl/model/sql/gen/delete.go

@@ -5,8 +5,8 @@ import (
 
 	"github.com/tal-tech/go-zero/core/collection"
 	"github.com/tal-tech/go-zero/tools/goctl/model/sql/template"
+	"github.com/tal-tech/go-zero/tools/goctl/util"
 	"github.com/tal-tech/go-zero/tools/goctl/util/stringx"
-	"github.com/tal-tech/go-zero/tools/goctl/util/templatex"
 )
 
 func genDelete(table Table, withCache bool) (string, error) {
@@ -28,7 +28,7 @@ func genDelete(table Table, withCache bool) (string, error) {
 		}
 	}
 	camel := table.Name.ToCamel()
-	output, err := templatex.With("delete").
+	output, err := util.With("delete").
 		Parse(template.Delete).
 		Execute(map[string]interface{}{
 			"upperStartCamelObject":     camel,

+ 2 - 2
tools/goctl/model/sql/gen/field.go

@@ -5,7 +5,7 @@ import (
 
 	"github.com/tal-tech/go-zero/tools/goctl/model/sql/parser"
 	"github.com/tal-tech/go-zero/tools/goctl/model/sql/template"
-	"github.com/tal-tech/go-zero/tools/goctl/util/templatex"
+	"github.com/tal-tech/go-zero/tools/goctl/util"
 )
 
 func genFields(fields []parser.Field) (string, error) {
@@ -25,7 +25,7 @@ func genField(field parser.Field) (string, error) {
 	if err != nil {
 		return "", err
 	}
-	output, err := templatex.With("types").
+	output, err := util.With("types").
 		Parse(template.Field).
 		Execute(map[string]interface{}{
 			"name":       field.Name.ToCamel(),

+ 2 - 2
tools/goctl/model/sql/gen/findone.go

@@ -2,13 +2,13 @@ package gen
 
 import (
 	"github.com/tal-tech/go-zero/tools/goctl/model/sql/template"
+	"github.com/tal-tech/go-zero/tools/goctl/util"
 	"github.com/tal-tech/go-zero/tools/goctl/util/stringx"
-	"github.com/tal-tech/go-zero/tools/goctl/util/templatex"
 )
 
 func genFindOne(table Table, withCache bool) (string, error) {
 	camel := table.Name.ToCamel()
-	output, err := templatex.With("findOne").
+	output, err := util.With("findOne").
 		Parse(template.FindOne).
 		Execute(map[string]interface{}{
 			"withCache":                 withCache,

+ 2 - 2
tools/goctl/model/sql/gen/fineonebyfield.go

@@ -5,12 +5,12 @@ import (
 	"strings"
 
 	"github.com/tal-tech/go-zero/tools/goctl/model/sql/template"
+	"github.com/tal-tech/go-zero/tools/goctl/util"
 	"github.com/tal-tech/go-zero/tools/goctl/util/stringx"
-	"github.com/tal-tech/go-zero/tools/goctl/util/templatex"
 )
 
 func genFineOneByField(table Table, withCache bool) (string, error) {
-	t := templatex.With("findOneByField").Parse(template.FindOneByField)
+	t := util.With("findOneByField").Parse(template.FindOneByField)
 	var list []string
 	camelTableName := table.Name.ToCamel()
 	for _, field := range table.Fields {

+ 1 - 2
tools/goctl/model/sql/gen/gen.go

@@ -12,7 +12,6 @@ import (
 	"github.com/tal-tech/go-zero/tools/goctl/util"
 	"github.com/tal-tech/go-zero/tools/goctl/util/console"
 	"github.com/tal-tech/go-zero/tools/goctl/util/stringx"
-	"github.com/tal-tech/go-zero/tools/goctl/util/templatex"
 )
 
 const (
@@ -119,7 +118,7 @@ type (
 )
 
 func (g *defaultGenerator) genModel(in parser.Table, withCache bool) (string, error) {
-	t := templatex.With("model").
+	t := util.With("model").
 		Parse(template.Model).
 		GoFmt(true)
 

+ 2 - 2
tools/goctl/model/sql/gen/insert.go

@@ -4,8 +4,8 @@ import (
 	"strings"
 
 	"github.com/tal-tech/go-zero/tools/goctl/model/sql/template"
+	"github.com/tal-tech/go-zero/tools/goctl/util"
 	"github.com/tal-tech/go-zero/tools/goctl/util/stringx"
-	"github.com/tal-tech/go-zero/tools/goctl/util/templatex"
 )
 
 func genInsert(table Table, withCache bool) (string, error) {
@@ -23,7 +23,7 @@ func genInsert(table Table, withCache bool) (string, error) {
 		expressionValues = append(expressionValues, "data."+camel)
 	}
 	camel := table.Name.ToCamel()
-	output, err := templatex.With("insert").
+	output, err := util.With("insert").
 		Parse(template.Insert).
 		Execute(map[string]interface{}{
 			"withCache":             withCache,

+ 2 - 2
tools/goctl/model/sql/gen/new.go

@@ -2,11 +2,11 @@ package gen
 
 import (
 	"github.com/tal-tech/go-zero/tools/goctl/model/sql/template"
-	"github.com/tal-tech/go-zero/tools/goctl/util/templatex"
+	"github.com/tal-tech/go-zero/tools/goctl/util"
 )
 
 func genNew(table Table, withCache bool) (string, error) {
-	output, err := templatex.With("new").
+	output, err := util.With("new").
 		Parse(template.New).
 		Execute(map[string]interface{}{
 			"withCache":             withCache,

+ 2 - 2
tools/goctl/model/sql/gen/tag.go

@@ -2,14 +2,14 @@ package gen
 
 import (
 	"github.com/tal-tech/go-zero/tools/goctl/model/sql/template"
-	"github.com/tal-tech/go-zero/tools/goctl/util/templatex"
+	"github.com/tal-tech/go-zero/tools/goctl/util"
 )
 
 func genTag(in string) (string, error) {
 	if in == "" {
 		return in, nil
 	}
-	output, err := templatex.With("tag").
+	output, err := util.With("tag").
 		Parse(template.Tag).
 		Execute(map[string]interface{}{
 			"field": in,

+ 2 - 2
tools/goctl/model/sql/gen/types.go

@@ -2,7 +2,7 @@ package gen
 
 import (
 	"github.com/tal-tech/go-zero/tools/goctl/model/sql/template"
-	"github.com/tal-tech/go-zero/tools/goctl/util/templatex"
+	"github.com/tal-tech/go-zero/tools/goctl/util"
 )
 
 func genTypes(table Table, withCache bool) (string, error) {
@@ -11,7 +11,7 @@ func genTypes(table Table, withCache bool) (string, error) {
 	if err != nil {
 		return "", err
 	}
-	output, err := templatex.With("types").
+	output, err := util.With("types").
 		Parse(template.Types).
 		Execute(map[string]interface{}{
 			"withCache":             withCache,

+ 2 - 2
tools/goctl/model/sql/gen/update.go

@@ -4,8 +4,8 @@ import (
 	"strings"
 
 	"github.com/tal-tech/go-zero/tools/goctl/model/sql/template"
+	"github.com/tal-tech/go-zero/tools/goctl/util"
 	"github.com/tal-tech/go-zero/tools/goctl/util/stringx"
-	"github.com/tal-tech/go-zero/tools/goctl/util/templatex"
 )
 
 func genUpdate(table Table, withCache bool) (string, error) {
@@ -22,7 +22,7 @@ func genUpdate(table Table, withCache bool) (string, error) {
 	}
 	expressionValues = append(expressionValues, "data."+table.PrimaryKey.Name.ToCamel())
 	camelTableName := table.Name.ToCamel()
-	output, err := templatex.With("update").
+	output, err := util.With("update").
 		Parse(template.Update).
 		Execute(map[string]interface{}{
 			"withCache":             withCache,

+ 2 - 2
tools/goctl/model/sql/gen/vars.go

@@ -4,8 +4,8 @@ import (
 	"strings"
 
 	"github.com/tal-tech/go-zero/tools/goctl/model/sql/template"
+	"github.com/tal-tech/go-zero/tools/goctl/util"
 	"github.com/tal-tech/go-zero/tools/goctl/util/stringx"
-	"github.com/tal-tech/go-zero/tools/goctl/util/templatex"
 )
 
 func genVars(table Table, withCache bool) (string, error) {
@@ -14,7 +14,7 @@ func genVars(table Table, withCache bool) (string, error) {
 		keys = append(keys, v.VarExpression)
 	}
 	camel := table.Name.ToCamel()
-	output, err := templatex.With("var").
+	output, err := util.With("var").
 		Parse(template.Vars).
 		GoFmt(true).
 		Execute(map[string]interface{}{

+ 6 - 0
tools/goctl/rpc/CHANGELOG.md

@@ -0,0 +1,6 @@
+# Change log
+
+# 2020-08-27
+* 新增支持rpc模板生成
+* 新增支持rpc服务生成
+

+ 161 - 0
tools/goctl/rpc/README.md

@@ -0,0 +1,161 @@
+# Rpc Generation
+Goctl Rpc是`goctl`脚手架下的一个rpc服务代码生成模块,支持proto模板生成和rpc服务代码生成,通过此工具生成代码你只需要关注业务逻辑编写而不用去编写一些重复性的代码。这使得我们把精力重心放在业务上,从而加快了开发效率且降低了代码出错率。
+
+# 特性
+* 简单易用
+* 快速提升开发效率
+* 出错率低
+
+# 快速开始
+
+### 生成proto模板
+
+```shell script
+$ goctl rpc template -o=user.proto
+```
+
+```golang
+syntax = "proto3";
+
+package remoteuser;
+
+message Request {
+  // 用户名
+  string username = 1;
+  // 用户密码
+  string password = 2;
+}
+
+message Response {
+  // 用户名称
+  string name = 1;
+  // 用户性别
+  string gender = 2;
+}
+
+service User{
+  // 登录
+  rpc Login(Request)returns(Response);
+}
+```
+### 生成rpc服务代码
+
+生成user rpc服务
+```
+$ goctl rpc proto -src=user.proto
+```
+
+代码tree
+
+```
+user
+    ├── etc
+    │   └── user.json
+    ├── internal
+    │   ├── config
+    │   │   └── config.go
+    │   ├── handler
+    │   │   ├── loginhandler.go
+    │   │   └── userhandler.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
+
+在使用goctl生成rpc服务代码时,我们默认会根据开发人员正在开发的工程依赖的`github.com/golang/protobuf`自动将插件重新`go install`到`${GOPATH}/bin`中,
+寻找方法:
+
+### go mod 工程  
+  对于`$ go version`不低于1.5版本的的工程,会优先寻找`$ go env GOMODCACHE`目录,如果没有则去`${GOPATH}`中查找(见下文),而低于1.5版本的则会优先从`${GOPATH}/pkg/mod`目录下查找,否则也从`% GOPATH`中查找(见下文)
+  
+### go path工程
+  对于没有使用go mod的工程,则默认当作在`${GOPATH}`中处理(暂不支持用户自定义的GOPATH),而这种情况下则会默认从`${GOPATH}/src`中查找
+  
+> 注意: 
+ * 对于以上两种工程如果没有在对应目录查找到`protoc-gen-go`则会提示相应错误,尽管`protoc-gen-go`可能在其他已经设置环境变量的目录中,这个将在后面版本进行优化。
+ * 对于go mod工程,如果工程没有依赖`github.com/golang/protobuf`则需要提前引入。
+ 
+### 好处
+* 保证grpc代码生成规范的一致性
+ 
+# 用法
+```shell script
+$ goctl rpc proto -h
+```
+
+```shell script
+NAME:
+   goctl rpc proto - generate rpc from proto"
+
+USAGE:
+   goctl rpc proto [command options] [arguments...]
+
+OPTIONS:
+   --src value, -s value         the file path of the proto source file
+   --dir value, -d value         the target path of the code,default path is "${pwd}". [option]
+   --service value, --srv value  the name of rpc service. [option]
+   --shared value                the dir of the shared file,default path is "${pwd}/shared. [option]"
+   --idea                        whether the command execution environment is from idea plugin. [option]
+
+```
+
+* 参数说明
+    * --src 必填,proto数据源,目前暂时支持单个proto文件生成,这里不支持(不建议)外部依赖
+    * --dir 非必填,默认为proto文件所在目录,生成代码的目标目录
+    * --service 服务名称,非必填,默认为proto文件所在目录名称,但是,如果proto所在目录为一下结构:
+        ```shell script
+        user
+            ├── cmd
+            │   └── rpc
+            │       └── user.proto
+        ```
+        则服务名称亦为user,而非proto所在文件夹名称了,这里推荐使用这种结构,可以方便在同一个服务名下建立不同类型的服务(api、rpc、mq等),便于代码管理与维护。
+    * --shared 非必填,默认为$dir(xxx.proto)/shared,rpc client逻辑代码存放目录。
+        
+      > 注意:这里的shared文件夹名称将会是代码中的package名称。
+    
+    * --idea 非必填,是否为idea插件中执行,保留字段,终端执行可以忽略
+     
+# 开发人员需要做什么
+
+关注业务代码编写,将重复性、与业务无关的工作交给goctl,生成好rpc服务代码后,开饭人员仅需要修改
+* 服务中的配置文件编写(etc/xx.json、internal/config/config.go)
+* 服务中业务逻辑编写(internal/logic/xxlogic.go)
+* 服务中资源上下文的编写(internal/svc/servicecontext.go)
+
+# 扩展
+对于需要进行rpc mock的开发人员,在安装了`mockgen`工具的前提下可以在rpc的shared文件中生成好对应的mock文件。
+
+# 注意事项
+* proto不支持暂多文件同时生成
+* proto不支持外部依赖包引入,message不支持inline
+* 目前main文件、shared文件、handler文件会被强制覆盖,而和开发人员手动需要编写的则不会覆盖生成,这一类在代码头部均有
+    ```shell script
+    // Code generated by goctl. DO NOT EDIT.
+    // Source: xxx.proto
+    ```
+  的标识,请注意不要将也写业务性代码写在里面。
+
+
+# 下一步规划
+* 尽快支持windows端rpc生成
+
+
+
+
+

+ 23 - 0
tools/goctl/rpc/command/command.go

@@ -0,0 +1,23 @@
+package command
+
+import (
+	"github.com/urfave/cli"
+
+	"github.com/tal-tech/go-zero/tools/goctl/rpc/ctx"
+	"github.com/tal-tech/go-zero/tools/goctl/rpc/goen"
+)
+
+func Rpc(c *cli.Context) error {
+	rpcCtx := ctx.MustCreateRpcContextFromCli(c)
+	generator := gogen.NewDefaultRpcGenerator(rpcCtx)
+	rpcCtx.Must(generator.Generate())
+	return nil
+}
+
+func RpcTemplate(c *cli.Context) error {
+	out := c.String("out")
+	idea := c.Bool("idea")
+	generator := gogen.NewRpcTemplate(out, idea)
+	generator.MustGenerate()
+	return nil
+}

+ 111 - 0
tools/goctl/rpc/ctx/ctx.go

@@ -0,0 +1,111 @@
+package ctx
+
+import (
+	"fmt"
+	"path/filepath"
+	"runtime"
+	"strings"
+
+	"github.com/urfave/cli"
+
+	"github.com/tal-tech/go-zero/core/logx"
+	"github.com/tal-tech/go-zero/tools/goctl/util"
+	"github.com/tal-tech/go-zero/tools/goctl/util/console"
+	"github.com/tal-tech/go-zero/tools/goctl/util/stringx"
+)
+
+const (
+	flagSrc     = "src"
+	flagDir     = "dir"
+	flagShared  = "shared"
+	flagService = "service"
+	flagIdea    = "idea"
+)
+
+type (
+	RpcContext struct {
+		ProjectPath  string
+		ProjectName  stringx.String
+		ServiceName  stringx.String
+		CurrentPath  string
+		Module       string
+		ProtoFileSrc string
+		ProtoSource  string
+		TargetDir    string
+		SharedDir    string
+		GoPath       string
+		console.Console
+	}
+)
+
+func MustCreateRpcContext(protoSrc, targetDir, sharedDir, serviceName string, idea bool) *RpcContext {
+	log := console.NewConsole(idea)
+	info, err := prepare(log)
+	log.Must(err)
+
+	if stringx.From(protoSrc).IsEmptyOrSpace() {
+		log.Fatalln("expected proto source, but nothing found")
+	}
+	srcFp, err := filepath.Abs(protoSrc)
+	log.Must(err)
+
+	if !util.FileExists(srcFp) {
+		log.Fatalln("%s is not exists", srcFp)
+	}
+	current := filepath.Dir(srcFp)
+	if stringx.From(targetDir).IsEmptyOrSpace() {
+		targetDir = current
+	}
+	if stringx.From(sharedDir).IsEmptyOrSpace() {
+		sharedDir = filepath.Join(current, "shared")
+	}
+	targetDirFp, err := filepath.Abs(targetDir)
+	log.Must(err)
+
+	sharedFp, err := filepath.Abs(sharedDir)
+	log.Must(err)
+
+	if stringx.From(serviceName).IsEmptyOrSpace() {
+		serviceName = getServiceFromRpcStructure(targetDirFp)
+	}
+	serviceNameString := stringx.From(serviceName)
+	if serviceNameString.IsEmptyOrSpace() {
+		log.Fatalln("service name is not found")
+	}
+
+	return &RpcContext{
+		ProjectPath:  info.Path,
+		ProjectName:  stringx.From(info.Name),
+		ServiceName:  serviceNameString,
+		CurrentPath:  current,
+		Module:       info.GoMod.Module,
+		ProtoFileSrc: srcFp,
+		ProtoSource:  filepath.Base(srcFp),
+		TargetDir:    targetDirFp,
+		SharedDir:    sharedFp,
+		GoPath:       info.GoPath,
+		Console:      log,
+	}
+}
+func MustCreateRpcContextFromCli(ctx *cli.Context) *RpcContext {
+	os := runtime.GOOS
+	switch os {
+	case "darwin":
+	case "windows":
+		logx.Must(fmt.Errorf("windows will support soon"))
+	default:
+		logx.Must(fmt.Errorf("unexpected os: %s", os))
+	}
+	protoSrc := ctx.String(flagSrc)
+	targetDir := ctx.String(flagDir)
+	sharedDir := ctx.String(flagShared)
+	serviceName := ctx.String(flagService)
+	idea := ctx.Bool(flagIdea)
+	return MustCreateRpcContext(protoSrc, targetDir, sharedDir, serviceName, idea)
+}
+
+func getServiceFromRpcStructure(targetDir string) string {
+	targetDir = filepath.Clean(targetDir)
+	suffix := filepath.Join("cmd", "rpc")
+	return filepath.Base(strings.TrimSuffix(targetDir, suffix))
+}

+ 208 - 0
tools/goctl/rpc/ctx/project.go

@@ -0,0 +1,208 @@
+package ctx
+
+import (
+	"errors"
+	"fmt"
+	"io/ioutil"
+	"os"
+	"os/exec"
+	"path/filepath"
+	"regexp"
+	"runtime"
+	"strings"
+
+	"github.com/tal-tech/go-zero/tools/goctl/rpc/execx"
+	"github.com/tal-tech/go-zero/tools/goctl/util"
+	"github.com/tal-tech/go-zero/tools/goctl/util/console"
+)
+
+var (
+	errProtobufNotFound = errors.New("github.com/golang/protobuf is not found,please ensure you has already [go get github.com/golang/protobuf]")
+)
+
+const (
+	constGo          = "go"
+	constProtoC      = "protoc"
+	constGoModOn     = "go env GO111MODULE"
+	constGoMod       = "go env GOMOD"
+	constGoModCache  = "go env GOMODCACHE"
+	constGoPath      = "go env GOPATH"
+	constProtoCGenGo = "protoc-gen-go"
+)
+
+type (
+	Project struct {
+		Path     string
+		Name     string
+		GoPath   string
+		Protobuf Protobuf
+		GoMod    GoMod
+	}
+
+	GoMod struct {
+		ModOn      bool
+		GoModCache string
+		GoMod      string
+		Module     string
+	}
+	Protobuf struct {
+		Path string
+	}
+)
+
+func prepare(log console.Console) (*Project, error) {
+	log.Info("check go env ...")
+	_, err := exec.LookPath(constGo)
+	if err != nil {
+		return nil, err
+	}
+
+	_, err = exec.LookPath(constProtoC)
+	if err != nil {
+		return nil, err
+	}
+
+	var (
+		goModOn                   bool
+		goMod, goModCache, module string
+		goPath                    string
+		name, path                string
+		protobufModule            string
+	)
+	ret, err := execx.Run(constGoModOn)
+	if err != nil {
+		return nil, err
+	}
+
+	goModOn = strings.TrimSpace(ret) == "on"
+	ret, err = execx.Run(constGoMod)
+	if err != nil {
+		return nil, err
+	}
+
+	goMod = strings.TrimSpace(ret)
+	ret, err = execx.Run(constGoModCache)
+	if err != nil {
+		return nil, err
+	}
+
+	goModCache = strings.TrimSpace(ret)
+	ret, err = execx.Run(constGoPath)
+	if err != nil {
+		return nil, err
+	}
+
+	goPath = strings.TrimSpace(ret)
+	src := filepath.Join(goPath, "src")
+	if len(goMod) > 0 {
+		if goModCache == "" {
+			goModCache = filepath.Join(goPath, "pkg", "mod")
+		}
+		path = filepath.Dir(goMod)
+		name = filepath.Base(path)
+		data, err := ioutil.ReadFile(goMod)
+		if err != nil {
+			return nil, err
+		}
+
+		module, err = matchModule(data)
+		if err != nil {
+			return nil, err
+		}
+
+		protobufModule, err = matchProtoBuf(data)
+		if err != nil {
+			return nil, err
+		}
+	} else {
+		if goModCache == "" {
+			goModCache = src
+		}
+		pwd, err := os.Getwd()
+		if err != nil {
+			return nil, err
+		}
+
+		if !strings.HasPrefix(pwd, src) {
+			return nil, fmt.Errorf("%s: project is not in go mod and go path", pwd)
+		}
+		r := strings.TrimPrefix(pwd, src+string(filepath.Separator))
+		name = filepath.Dir(r)
+		if name == "." {
+			name = r
+		}
+		path = filepath.Join(src, name)
+		module = name
+	}
+
+	protobuf := filepath.Join(goModCache, protobufModule)
+	if !util.FileExists(protobuf) {
+		return nil, fmt.Errorf("expected protobuf module in path: %s,please ensure you has already [go get github.com/golang/protobuf]", protobuf)
+	}
+
+	var protoCGenGoFilename string
+	os := runtime.GOOS
+	switch os {
+	case "darwin":
+		protoCGenGoFilename = filepath.Join(goPath, "bin", "protoc-gen-go")
+	case "windows":
+		protoCGenGoFilename = filepath.Join(goPath, "bin", "protoc-gen-go.exe")
+	default:
+		return nil, fmt.Errorf("unexpeted os: %s", os)
+	}
+
+	if !util.FileExists(protoCGenGoFilename) {
+		sh := "go install " + filepath.Join(protobuf, constProtoCGenGo)
+		log.Warning(sh)
+		stdout, err := execx.Run(sh)
+		if err != nil {
+			return nil, err
+		}
+
+		log.Info(stdout)
+	}
+	if !util.FileExists(protoCGenGoFilename) {
+		return nil, fmt.Errorf("protoc-gen-go is not found")
+	}
+	return &Project{
+		Name:   name,
+		Path:   path,
+		GoPath: goPath,
+		Protobuf: Protobuf{
+			Path: protobuf,
+		},
+		GoMod: GoMod{
+			ModOn:      goModOn,
+			GoModCache: goModCache,
+			GoMod:      goMod,
+			Module:     module,
+		},
+	}, nil
+}
+
+// github.com/golang/protobuf@{version}
+func matchProtoBuf(data []byte) (string, error) {
+	text := string(data)
+	re := regexp.MustCompile(`(?m)(github.com/golang/protobuf)\s+(v[0-9.]+)`)
+	matches := re.FindAllStringSubmatch(text, -1)
+	if len(matches) == 0 {
+		return "", errProtobufNotFound
+	}
+	groups := matches[0]
+	if len(groups) < 3 {
+		return "", errProtobufNotFound
+	}
+	return fmt.Sprintf("%s@%s", groups[1], groups[2]), nil
+}
+
+func matchModule(data []byte) (string, error) {
+	text := string(data)
+	re := regexp.MustCompile(`(?m)^\s*module\s+[a-z0-9/\-.]+$`)
+	matches := re.FindAllString(text, -1)
+	if len(matches) == 1 {
+		target := matches[0]
+		index := strings.Index(target, "module")
+		return strings.TrimSpace(target[index+6:]), nil
+	}
+	return "", nil
+}

+ 34 - 0
tools/goctl/rpc/execx/execx.go

@@ -0,0 +1,34 @@
+package execx
+
+import (
+	"bytes"
+	"errors"
+	"fmt"
+	"os/exec"
+	"runtime"
+)
+
+func Run(arg string) (string, error) {
+	goos := runtime.GOOS
+	var cmd *exec.Cmd
+	switch goos {
+	case "darwin":
+		cmd = exec.Command("sh", "-c", arg)
+	case "windows":
+		cmd = exec.Command("cmd.exe", "/c", arg)
+	default:
+		return "", fmt.Errorf("unexpected os: %v", goos)
+	}
+	dtsout := new(bytes.Buffer)
+	stderr := new(bytes.Buffer)
+	cmd.Stdout = dtsout
+	cmd.Stderr = stderr
+	err := cmd.Run()
+	if err != nil {
+		if stderr.Len() > 0 {
+			return "", errors.New(stderr.String())
+		}
+		return "", err
+	}
+	return dtsout.String(), nil
+}

+ 89 - 0
tools/goctl/rpc/goen/gen.go

@@ -0,0 +1,89 @@
+package gogen
+
+import (
+	"github.com/tal-tech/go-zero/tools/goctl/rpc/ctx"
+	"github.com/tal-tech/go-zero/tools/goctl/rpc/parser"
+)
+
+const (
+	dirTarget          = "dirTarget"
+	dirConfig          = "config"
+	dirEtc             = "etc"
+	dirSvc             = "svc"
+	dirShared          = "shared"
+	dirHandler         = "handler"
+	dirLogic           = "logic"
+	dirPb              = "pb"
+	dirInternal        = "internal"
+	fileConfig         = "config.go"
+	fileServiceContext = "servicecontext.go"
+)
+
+type (
+	defaultRpcGenerator struct {
+		dirM map[string]string
+		Ctx  *ctx.RpcContext
+		ast  *parser.PbAst
+	}
+)
+
+func NewDefaultRpcGenerator(ctx *ctx.RpcContext) *defaultRpcGenerator {
+	return &defaultRpcGenerator{
+		Ctx: ctx,
+	}
+}
+
+func (g *defaultRpcGenerator) Generate() (err error) {
+	g.Ctx.Info("code generating...")
+	defer func() {
+		if err == nil {
+			g.Ctx.Success("Done.")
+		}
+	}()
+	err = g.createDir()
+	if err != nil {
+		return
+	}
+
+	err = g.genEtc()
+	if err != nil {
+		return
+	}
+
+	err = g.genPb()
+	if err != nil {
+		return
+	}
+
+	err = g.genConfig()
+	if err != nil {
+		return
+	}
+
+	err = g.genSvc()
+	if err != nil {
+		return
+	}
+
+	err = g.genLogic()
+	if err != nil {
+		return
+	}
+
+	err = g.genRemoteHandler()
+	if err != nil {
+		return
+	}
+
+	err = g.genMain()
+	if err != nil {
+		return
+	}
+
+	err = g.genShared()
+	if err != nil {
+		return
+	}
+
+	return nil
+}

+ 29 - 0
tools/goctl/rpc/goen/genconfig.go

@@ -0,0 +1,29 @@
+package gogen
+
+import (
+	"io/ioutil"
+	"os"
+	"path/filepath"
+
+	"github.com/tal-tech/go-zero/tools/goctl/util"
+)
+
+var configTemplate = `package config
+
+import "github.com/tal-tech/go-zero/rpcx"
+
+type (
+	Config struct {
+		rpcx.RpcServerConf
+	}
+)
+`
+
+func (g *defaultRpcGenerator) genConfig() error {
+	configPath := g.dirM[dirConfig]
+	fileName := filepath.Join(configPath, fileConfig)
+	if util.FileExists(fileName) {
+		return nil
+	}
+	return ioutil.WriteFile(fileName, []byte(configTemplate), os.ModePerm)
+}

+ 45 - 0
tools/goctl/rpc/goen/gendir.go

@@ -0,0 +1,45 @@
+package gogen
+
+import (
+	"path/filepath"
+	"strings"
+
+	"github.com/tal-tech/go-zero/tools/goctl/util"
+)
+
+//  target
+//	├── etc
+//	├── internal
+//	│   ├── config
+//	│   ├── handler
+//	│   ├── logic
+//	│   ├── pb
+//	│   └── svc
+func (g *defaultRpcGenerator) createDir() error {
+	ctx := g.Ctx
+	m := make(map[string]string)
+	m[dirTarget] = ctx.TargetDir
+	m[dirEtc] = filepath.Join(ctx.TargetDir, dirEtc)
+	m[dirInternal] = filepath.Join(ctx.TargetDir, dirInternal)
+	m[dirConfig] = filepath.Join(ctx.TargetDir, dirInternal, dirConfig)
+	m[dirHandler] = filepath.Join(ctx.TargetDir, dirInternal, dirHandler)
+	m[dirLogic] = filepath.Join(ctx.TargetDir, dirInternal, dirLogic)
+	m[dirPb] = filepath.Join(ctx.TargetDir, dirPb)
+	m[dirSvc] = filepath.Join(ctx.TargetDir, dirInternal, dirSvc)
+	m[dirShared] = g.Ctx.SharedDir
+	for _, d := range m {
+		err := util.MkdirIfNotExist(d)
+		if err != nil {
+			return err
+		}
+	}
+	g.dirM = m
+	return nil
+}
+
+func (g *defaultRpcGenerator) mustGetPackage(dir string) string {
+	target := g.dirM[dir]
+	projectPath := g.Ctx.ProjectPath
+	relativePath := strings.TrimPrefix(target, projectPath)
+	return g.Ctx.Module + relativePath
+}

+ 34 - 0
tools/goctl/rpc/goen/genetc.go

@@ -0,0 +1,34 @@
+package gogen
+
+import (
+	"fmt"
+	"path/filepath"
+
+	"github.com/tal-tech/go-zero/tools/goctl/util"
+)
+
+var etcTemplate = `{
+  "Name": "{{.serviceName}}.rpc",
+  "Log": {
+    "Mode": "console"
+  },
+  "ListenOn": "127.0.0.1:8080",
+  "Etcd": {
+    "Hosts": ["127.0.0.1:6379"],
+    "Key": "{{.serviceName}}.rpc"
+  }
+}
+`
+
+func (g *defaultRpcGenerator) genEtc() error {
+	etdDir := g.dirM[dirEtc]
+	fileName := filepath.Join(etdDir, fmt.Sprintf("%v.json", g.Ctx.ServiceName.Lower()))
+	if util.FileExists(fileName) {
+		return nil
+	}
+	return util.With("etc").
+		Parse(etcTemplate).
+		SaveTo(map[string]interface{}{
+			"serviceName": g.Ctx.ServiceName.Lower(),
+		}, fileName, false)
+}

+ 109 - 0
tools/goctl/rpc/goen/genhandler.go

@@ -0,0 +1,109 @@
+package gogen
+
+import (
+	"fmt"
+	"path/filepath"
+	"strings"
+
+	"github.com/tal-tech/go-zero/tools/goctl/util"
+)
+
+var (
+	remoteTemplate = `{{.head}}
+
+package handler
+
+import (
+	{{.imports}}
+)
+
+type (
+	{{.types}}
+)
+
+{{.newFuncs}}
+`
+	functionTemplate = `{{.head}}
+
+package handler
+
+import (
+	"context"
+
+	{{.imports}}
+)
+
+{{if .hasComment}}{{.comment}}{{end}}
+func (s *{{.server}}Server) {{.method}} (ctx context.Context, in *{{.package}}.{{.request}}) (*{{.package}}.{{.response}}, error) {
+	l:=logic.New{{.logicName}}(ctx,s.svcCtx)
+	return l.{{.method}}(in)
+}
+`
+	typeFmt = `%sServer struct {
+		svcCtx *svc.ServiceContext
+	}`
+	newFuncFmt = `func New%sServer(svcCtx *svc.ServiceContext) *%sServer {
+	return &%sServer{
+		svcCtx: svcCtx,
+	}
+}`
+)
+
+func (g *defaultRpcGenerator) genRemoteHandler() error {
+	handlerPath := g.dirM[dirHandler]
+	serverGo := fmt.Sprintf("%vhandler.go", g.Ctx.ServiceName.Lower())
+	fileName := filepath.Join(handlerPath, serverGo)
+	file := g.ast
+	svcImport := fmt.Sprintf(`"%v"`, g.mustGetPackage(dirSvc))
+	types := make([]string, 0)
+	newFuncs := make([]string, 0)
+	head := util.GetHead(g.Ctx.ProtoSource)
+	for _, service := range file.Service {
+		types = append(types, fmt.Sprintf(typeFmt, service.Name.Title()))
+		newFuncs = append(newFuncs, fmt.Sprintf(newFuncFmt, service.Name.Title(), service.Name.Title(), service.Name.Title()))
+	}
+	err := util.With("server").GoFmt(true).Parse(remoteTemplate).SaveTo(map[string]interface{}{
+		"head":     head,
+		"types":    strings.Join(types, "\n"),
+		"newFuncs": strings.Join(newFuncs, "\n"),
+		"imports":  svcImport,
+	}, fileName, true)
+	if err != nil {
+		return err
+	}
+	return g.genFunctions()
+}
+
+func (g *defaultRpcGenerator) genFunctions() error {
+	handlerPath := g.dirM[dirHandler]
+	file := g.ast
+	pkg := file.Package
+
+	head := util.GetHead(g.Ctx.ProtoSource)
+	handlerImports := make([]string, 0)
+	pbImport := fmt.Sprintf(`%v "%v"`, pkg, g.mustGetPackage(dirPb))
+	handlerImports = append(handlerImports, pbImport, fmt.Sprintf(`"%v"`, g.mustGetPackage(dirLogic)))
+	for _, service := range file.Service {
+		for _, method := range service.Funcs {
+			handlerName := fmt.Sprintf("%shandler.go", method.Name.Lower())
+			filename := filepath.Join(handlerPath, handlerName)
+			// override
+			err := util.With("func").GoFmt(true).Parse(functionTemplate).SaveTo(map[string]interface{}{
+				"head":       head,
+				"server":     service.Name.Title(),
+				"imports":    strings.Join(handlerImports, "\r\n"),
+				"logicName":  fmt.Sprintf("%sLogic", method.Name.Title()),
+				"method":     method.Name.Title(),
+				"package":    pkg,
+				"request":    method.InType,
+				"response":   method.OutType,
+				"hasComment": len(method.Document),
+				"comment":    strings.Join(method.Document, "\r\n"),
+			}, filename, true)
+			if err != nil {
+				return err
+			}
+		}
+	}
+	return nil
+}

+ 95 - 0
tools/goctl/rpc/goen/genlogic.go

@@ -0,0 +1,95 @@
+package gogen
+
+import (
+	"fmt"
+	"path/filepath"
+	"strings"
+
+	"github.com/tal-tech/go-zero/core/collection"
+	"github.com/tal-tech/go-zero/tools/goctl/rpc/parser"
+	"github.com/tal-tech/go-zero/tools/goctl/util"
+)
+
+var (
+	logicTemplate = `package logic
+
+import (
+	"context"
+
+	{{.imports}}
+	"github.com/tal-tech/go-zero/core/logx"
+)
+
+type (
+	{{.logicName}} struct {
+		ctx context.Context
+		logx.Logger
+		// todo: add your logic here and delete this line
+	}
+)
+
+func New{{.logicName}}(ctx context.Context,svcCtx *svc.ServiceContext) *{{.logicName}} {
+	return &{{.logicName}}{
+		ctx:    ctx,
+		Logger: logx.WithContext(ctx),
+		// todo: add your logic here and delete this line
+	}
+}
+{{.functions}}
+`
+	logicFunctionTemplate = `{{if .hasComment}}{{.comment}}{{end}}
+func (l *{{.logicName}}) {{.method}} (in *{{.package}}.{{.request}}) (*{{.package}}.{{.response}}, error) {
+	var resp {{.package}}.{{.response}}
+	// todo: add your logic here and delete this line
+	
+	return &resp,nil
+}
+`
+)
+
+func (g *defaultRpcGenerator) genLogic() error {
+	logicPath := g.dirM[dirLogic]
+	protoPkg := g.ast.Package
+	service := g.ast.Service
+	for _, item := range service {
+		for _, method := range item.Funcs {
+			logicName := fmt.Sprintf("%slogic.go", method.Name.Lower())
+			filename := filepath.Join(logicPath, logicName)
+			functions, err := genLogicFunction(protoPkg, method)
+			if err != nil {
+				return err
+			}
+			imports := collection.NewSet()
+			pbImport := fmt.Sprintf(`%v "%v"`, protoPkg, g.mustGetPackage(dirPb))
+			svcImport := fmt.Sprintf(`"%v"`, g.mustGetPackage(dirSvc))
+			imports.AddStr(pbImport, svcImport)
+			err = util.With("logic").GoFmt(true).Parse(logicTemplate).SaveTo(map[string]interface{}{
+				"logicName": fmt.Sprintf("%sLogic", method.Name.Title()),
+				"functions": functions,
+				"imports":   strings.Join(imports.KeysStr(), "\r\n"),
+			}, filename, false)
+			if err != nil {
+				return err
+			}
+		}
+	}
+	return nil
+}
+
+func genLogicFunction(packageName string, method *parser.Func) (string, error) {
+	var functions = make([]string, 0)
+	buffer, err := util.With("fun").Parse(logicFunctionTemplate).Execute(map[string]interface{}{
+		"logicName":  fmt.Sprintf("%sLogic", method.Name.Title()),
+		"method":     method.Name.Title(),
+		"package":    packageName,
+		"request":    method.InType,
+		"response":   method.OutType,
+		"hasComment": len(method.Document) > 0,
+		"comment":    strings.Join(method.Document, "\r\n"),
+	})
+	if err != nil {
+		return "", err
+	}
+	functions = append(functions, buffer.String())
+	return strings.Join(functions, "\n"), nil
+}

+ 84 - 0
tools/goctl/rpc/goen/genmain.go

@@ -0,0 +1,84 @@
+package gogen
+
+import (
+	"fmt"
+	"path/filepath"
+	"strings"
+
+	"github.com/tal-tech/go-zero/tools/goctl/rpc/parser"
+	"github.com/tal-tech/go-zero/tools/goctl/util"
+)
+
+var mainTemplate = `{{.head}}
+
+package main
+
+import (
+	"flag"
+	"fmt"
+	"log"
+
+	"google.golang.org/grpc"
+
+	"github.com/tal-tech/go-zero/core/conf"
+	"github.com/tal-tech/go-zero/rpcx"
+
+	{{.imports}}
+)
+
+var configFile = flag.String("f", "etc/{{.serviceName}}.json", "the config file")
+
+func main() {
+	flag.Parse()
+
+	var c config.Config
+	conf.MustLoad(*configFile, &c)
+	ctx := svc.NewServiceContext(c)
+	{{.srv}}
+
+	s, err := rpcx.NewServer(c.RpcServerConf, func(grpcServer *grpc.Server) {
+		{{.registers}}
+	})
+	if err != nil {
+		log.Fatal(err)
+	}
+	fmt.Printf("Starting rpc server at %s...\n", c.ListenOn)
+	s.Start()
+}
+
+`
+
+func (g *defaultRpcGenerator) genMain() error {
+	mainPath := g.dirM[dirTarget]
+	file := g.ast
+	pkg := file.Package
+
+	fileName := filepath.Join(mainPath, fmt.Sprintf("%v.go", g.Ctx.ServiceName.Lower()))
+	imports := make([]string, 0)
+	pbImport := fmt.Sprintf(`%v "%v"`, pkg, g.mustGetPackage(dirPb))
+	svcImport := fmt.Sprintf(`"%v"`, g.mustGetPackage(dirSvc))
+	remoteImport := fmt.Sprintf(`"%v"`, g.mustGetPackage(dirHandler))
+	configImport := fmt.Sprintf(`"%v"`, g.mustGetPackage(dirConfig))
+	imports = append(imports, configImport, pbImport, remoteImport, svcImport)
+	srv, registers := g.genServer(pkg, file.Service)
+	head := util.GetHead(g.Ctx.ProtoSource)
+	return util.With("main").GoFmt(true).Parse(mainTemplate).SaveTo(map[string]interface{}{
+		"head":        head,
+		"package":     pkg,
+		"serviceName": g.Ctx.ServiceName.Lower(),
+		"srv":         srv,
+		"registers":   registers,
+		"imports":     strings.Join(imports, "\r\n"),
+	}, fileName, true)
+}
+
+func (g *defaultRpcGenerator) genServer(pkg string, list []*parser.RpcService) (string, string) {
+	list1 := make([]string, 0)
+	list2 := make([]string, 0)
+	for _, item := range list {
+		name := item.Name.UnTitle()
+		list1 = append(list1, fmt.Sprintf("%sSrv := handler.New%sServer(ctx)", name, item.Name.Title()))
+		list2 = append(list2, fmt.Sprintf("%s.Register%sServer(grpcServer, %sSrv)", pkg, item.Name.Title(), name))
+	}
+	return strings.Join(list1, "\n"), strings.Join(list2, "\n")
+}

+ 85 - 0
tools/goctl/rpc/goen/genpb.go

@@ -0,0 +1,85 @@
+package gogen
+
+import (
+	"errors"
+	"fmt"
+	"io/ioutil"
+	"path/filepath"
+	"strings"
+
+	"github.com/dsymonds/gotoc/parser"
+
+	"github.com/tal-tech/go-zero/core/lang"
+	"github.com/tal-tech/go-zero/tools/goctl/rpc/execx"
+	astParser "github.com/tal-tech/go-zero/tools/goctl/rpc/parser"
+	"github.com/tal-tech/go-zero/tools/goctl/util/stringx"
+)
+
+func (g *defaultRpcGenerator) genPb() error {
+	importPath, filename := filepath.Split(g.Ctx.ProtoFileSrc)
+	tree, err := parser.ParseFiles([]string{filename}, []string{importPath})
+	if err != nil {
+		return err
+	}
+
+	if len(tree.Files) == 0 {
+		return errors.New("proto ast parse failed")
+	}
+
+	file := tree.Files[0]
+	if len(file.Package) == 0 {
+		return errors.New("expected package, but nothing found")
+	}
+
+	targetStruct := make(map[string]lang.PlaceholderType)
+	for _, item := range file.Messages {
+		if len(item.Messages) > 0 {
+			return fmt.Errorf(`line %v: unexpected inner message near: "%v""`, item.Messages[0].Position.Line, item.Messages[0].Name)
+		}
+
+		name := stringx.From(item.Name)
+		if _, ok := targetStruct[name.Lower()]; ok {
+			return fmt.Errorf("line %v: duplicate %v", item.Position.Line, name)
+		}
+		targetStruct[name.Lower()] = lang.Placeholder
+	}
+
+	pbPath := g.dirM[dirPb]
+	protoFileName := filepath.Base(g.Ctx.ProtoFileSrc)
+	err = g.protocGenGo(pbPath)
+	if err != nil {
+		return err
+	}
+
+	pbGo := strings.TrimSuffix(protoFileName, ".proto") + ".pb.go"
+	pbFile := filepath.Join(pbPath, pbGo)
+	bts, err := ioutil.ReadFile(pbFile)
+	if err != nil {
+		return err
+	}
+
+	aspParser := astParser.NewAstParser(bts, targetStruct, g.Ctx.Console)
+	ast, err := aspParser.Parse()
+	if err != nil {
+		return err
+	}
+
+	if len(ast.Service) == 0 {
+		return fmt.Errorf("service not found")
+	}
+	g.ast = ast
+	return nil
+}
+
+func (g *defaultRpcGenerator) protocGenGo(target string) error {
+	src := filepath.Dir(g.Ctx.ProtoFileSrc)
+	sh := fmt.Sprintf(`export PATH=%s:$PATH
+protoc -I=%s --go_out=plugins=grpc:%s %s`, filepath.Join(g.Ctx.GoPath, "bin"), src, target, g.Ctx.ProtoFileSrc)
+	stdout, err := execx.Run(sh)
+	if err != nil {
+		return err
+	}
+
+	g.Ctx.Info(stdout)
+	return nil
+}

+ 216 - 0
tools/goctl/rpc/goen/genshared.go

@@ -0,0 +1,216 @@
+package gogen
+
+import (
+	"fmt"
+	"os"
+	"os/exec"
+	"path/filepath"
+	"strings"
+
+	"github.com/tal-tech/go-zero/tools/goctl/rpc/execx"
+	"github.com/tal-tech/go-zero/tools/goctl/rpc/parser"
+	"github.com/tal-tech/go-zero/tools/goctl/util"
+)
+
+var (
+	sharedTemplateText = `{{.head}}
+
+//go:generate mockgen -destination ./mock{{.name}}model.go -package {{.filePackage}} -source $GOFILE
+
+package {{.filePackage}}
+
+import (
+	"context"
+
+	{{.package}}
+	"github.com/tal-tech/go-zero/core/jsonx"
+	"github.com/tal-tech/go-zero/rpcx"
+)
+
+type (
+	{{.serviceName}}Model interface {
+		{{.interface}}
+	}
+	default{{.serviceName}}Model struct {
+		cli rpcx.Client
+	}
+)
+
+
+func NewDefault{{.serviceName}}Model(cli rpcx.Client) {{.serviceName}}Model {
+	return &default{{.serviceName}}Model{
+		cli: cli,
+	}
+}
+
+{{.functions}}
+`
+	sharedTemplateTypes = `{{.head}}
+
+package {{.filePackage}}
+
+import (
+	"errors"
+)
+
+var (
+	errJsonConvert = errors.New("json convert error")
+)
+
+{{.types}}
+
+`
+	sharedInterfaceFunctionTemplate = `{{if .hasComment}}{{.comment}}
+{{end}}{{.method}}(ctx context.Context,in *{{.pbRequest}}) {{if .hasResponse}}(*{{.pbResponse}},{{end}} error{{if .hasResponse}}){{end}}`
+	sharedFunctionTemplate = `
+{{if .hasComment}}{{.comment}}{{end}}
+func (m *default{{.rpcServiceName}}Model) {{.method}}(ctx context.Context,in *{{.pbRequest}}) {{if .hasResponse}}(*{{.pbResponse}},{{end}} error{{if .hasResponse}}){{end}} {
+	conn:= m.cli.Conn()
+	client := {{.package}}.New{{.rpcServiceName}}Client(conn)
+	var request {{.package}}.{{.pbRequest}}
+	bts, err := jsonx.Marshal(in)
+	if err != nil {
+		return {{if .hasResponse}}nil,{{end}}errJsonConvert
+	}
+	err = jsonx.Unmarshal(bts, &request)
+	if err != nil {
+		return {{if .hasResponse}}nil,{{end}}errJsonConvert
+	}
+	{{if .hasResponse}}resp,err:={{else}}_,err={{end}}client.{{.method}}(ctx, &request)
+	{{if .hasResponse}}if err!=nil{
+		return nil,err
+	}
+	var ret {{.pbResponse}}
+	bts,err=jsonx.Marshal(resp)
+	if err!=nil{
+		return nil,errJsonConvert
+	}
+	err=jsonx.Unmarshal(bts,&ret)
+	if err!=nil{
+		return nil,errJsonConvert
+	}
+	return &ret, nil{{else}}if err!=nil {
+		return err
+	}
+	return nil{{end}}
+}`
+)
+
+func (g *defaultRpcGenerator) genShared() error {
+	sharePackage := filepath.Base(g.Ctx.SharedDir)
+	file := g.ast
+	typeCode, err := file.GenTypesCode()
+	if err != nil {
+		return err
+	}
+
+	pbPkg := file.Package
+	remotePackage := fmt.Sprintf(`%v "%v"`, pbPkg, g.mustGetPackage(dirPb))
+	filename := filepath.Join(g.Ctx.SharedDir, "types.go")
+	head := util.GetHead(g.Ctx.ProtoSource)
+	err = util.With("types").GoFmt(true).Parse(sharedTemplateTypes).SaveTo(map[string]interface{}{
+		"head":                  head,
+		"filePackage":           sharePackage,
+		"pbPkg":                 pbPkg,
+		"serviceName":           g.Ctx.ServiceName.Title(),
+		"lowerStartServiceName": g.Ctx.ServiceName.UnTitle(),
+		"types":                 typeCode,
+	}, filename, true)
+
+	for _, service := range file.Service {
+		filename := filepath.Join(g.Ctx.SharedDir, fmt.Sprintf("%smodel.go", service.Name.Lower()))
+		functions, err := g.getFuncs(service)
+		if err != nil {
+			return err
+		}
+		iFunctions, err := g.getInterfaceFuncs(service)
+		if err != nil {
+			return err
+		}
+		mockFile := filepath.Join(g.Ctx.SharedDir, fmt.Sprintf("mock%smodel.go", service.Name.Lower()))
+		os.Remove(mockFile)
+		err = util.With("shared").GoFmt(true).Parse(sharedTemplateText).SaveTo(map[string]interface{}{
+			"name":        service.Name.Lower(),
+			"head":        head,
+			"filePackage": sharePackage,
+			"pbPkg":       pbPkg,
+			"package":     remotePackage,
+			"serviceName": service.Name.Title(),
+			"functions":   strings.Join(functions, "\n"),
+			"interface":   strings.Join(iFunctions, "\n"),
+		}, filename, true)
+		if err != nil {
+			return err
+		}
+	}
+
+	// if mockgen is already installed, it will generate code of gomock for shared files
+	_, err = exec.LookPath("mockgen")
+	if err != nil {
+		g.Ctx.Warning("warning:mockgen is not found")
+	} else {
+		execx.Run(fmt.Sprintf("cd %s \ngo generate", g.Ctx.SharedDir))
+	}
+	return nil
+}
+
+func (g *defaultRpcGenerator) getFuncs(service *parser.RpcService) ([]string, error) {
+	file := g.ast
+	pkgName := file.Package
+	functions := make([]string, 0)
+	for _, method := range service.Funcs {
+		data, found := file.Strcuts[strings.ToLower(method.OutType)]
+		if found {
+			found = len(data.Field) > 0
+		}
+		var comment string
+		if len(method.Document) > 0 {
+			comment = method.Document[0]
+		}
+		buffer, err := util.With("sharedFn").Parse(sharedFunctionTemplate).Execute(map[string]interface{}{
+			"rpcServiceName": service.Name.Title(),
+			"method":         method.Name.Title(),
+			"package":        pkgName,
+			"pbRequest":      method.InType,
+			"pbResponse":     method.OutType,
+			"hasResponse":    found,
+			"hasComment":     len(method.Document) > 0,
+			"comment":        comment,
+		})
+		if err != nil {
+			return nil, err
+		}
+
+		functions = append(functions, buffer.String())
+	}
+	return functions, nil
+}
+
+func (g *defaultRpcGenerator) getInterfaceFuncs(service *parser.RpcService) ([]string, error) {
+	file := g.ast
+	functions := make([]string, 0)
+	for _, method := range service.Funcs {
+		data, found := file.Strcuts[strings.ToLower(method.OutType)]
+		if found {
+			found = len(data.Field) > 0
+		}
+		var comment string
+		if len(method.Document) > 0 {
+			comment = method.Document[0]
+		}
+		buffer, err := util.With("interfaceFn").Parse(sharedInterfaceFunctionTemplate).Execute(map[string]interface{}{
+			"hasComment":  len(method.Document) > 0,
+			"comment":     comment,
+			"method":      method.Name.Title(),
+			"pbRequest":   method.InType,
+			"pbResponse":  method.OutType,
+			"hasResponse": found,
+		})
+		if err != nil {
+			return nil, err
+		}
+
+		functions = append(functions, buffer.String())
+	}
+	return functions, nil
+}

+ 34 - 0
tools/goctl/rpc/goen/gensvc.go

@@ -0,0 +1,34 @@
+package gogen
+
+import (
+	"fmt"
+	"path/filepath"
+
+	"github.com/tal-tech/go-zero/tools/goctl/util"
+)
+
+var svcTemplate = `package svc
+
+import {{.imports}}
+
+type (
+	ServiceContext struct {
+		c config.Config
+		// todo: add your logic here and delete this line
+	}
+)
+
+func NewServiceContext(c config.Config) *ServiceContext {
+	return &ServiceContext{
+		c:c,
+	}
+}
+`
+
+func (g *defaultRpcGenerator) genSvc() error {
+	svcPath := g.dirM[dirSvc]
+	fileName := filepath.Join(svcPath, fileServiceContext)
+	return util.With("svc").GoFmt(true).Parse(svcTemplate).SaveTo(map[string]interface{}{
+		"imports": fmt.Sprintf(`"%v"`, g.mustGetPackage(dirConfig)),
+	}, fileName, false)
+}

+ 44 - 0
tools/goctl/rpc/goen/template.go

@@ -0,0 +1,44 @@
+package gogen
+
+import (
+	"github.com/tal-tech/go-zero/tools/goctl/util"
+	"github.com/tal-tech/go-zero/tools/goctl/util/console"
+)
+
+var rpcTemplateText = `syntax = "proto3";
+
+package remoteuser;
+
+message Request {
+  string username = 1;
+  string password = 2;
+}
+
+message Response {
+  string name = 1;
+  string gender = 2;
+}
+
+service User{
+  rpc Login(Request)returns(Response);
+}`
+
+type (
+	rpcTemplate struct {
+		out string
+		console.Console
+	}
+)
+
+func NewRpcTemplate(out string, idea bool) *rpcTemplate {
+	return &rpcTemplate{
+		out:     out,
+		Console: console.NewConsole(idea),
+	}
+}
+
+func (r *rpcTemplate) MustGenerate() {
+	err := util.With("t").Parse(rpcTemplateText).SaveTo(nil, r.out, false)
+	r.Must(err)
+	r.Success("Done.")
+}

+ 483 - 0
tools/goctl/rpc/parser/pbast.go

@@ -0,0 +1,483 @@
+package parser
+
+import (
+	"errors"
+	"fmt"
+	"go/ast"
+	"go/parser"
+	"go/token"
+	"sort"
+	"strings"
+
+	"github.com/tal-tech/go-zero/core/lang"
+	sx "github.com/tal-tech/go-zero/core/stringx"
+	"github.com/tal-tech/go-zero/tools/goctl/util"
+	"github.com/tal-tech/go-zero/tools/goctl/util/console"
+	"github.com/tal-tech/go-zero/tools/goctl/util/stringx"
+)
+
+const (
+	flagStar                = "*"
+	suffixServer            = "Server"
+	referenceContext        = "context."
+	unknownPrefix           = "XXX_"
+	ignoreJsonTagExpression = `json:"-"`
+)
+
+var (
+	errorParseError = errors.New("pb parse error")
+	typeTemplate    = `type (
+	{{.types}}
+)`
+	structTemplate = `{{if .type}}type {{end}}{{.name}} struct {
+	{{.fields}}
+}`
+	fieldTemplate = `{{if .hasDoc}}{{.doc}}
+{{end}}{{.name}} {{.type}} {{.tag}}{{if .hasComment}}{{.comment}}{{end}}`
+	objectM = make(map[string]*Struct)
+)
+
+type (
+	astParser struct {
+		golang       []byte
+		filterStruct map[string]lang.PlaceholderType
+		console.Console
+		fileSet *token.FileSet
+	}
+	Field struct {
+		Name     stringx.String
+		TypeName string
+		JsonTag  string
+		Document []string
+		Comment  []string
+	}
+	Struct struct {
+		Name     stringx.String
+		Document []string
+		Comment  []string
+		Field    []*Field
+	}
+	Func struct {
+		Name        stringx.String
+		InType      string
+		InTypeName  string // remove *Context,such as LoginRequest、UserRequest
+		OutTypeName string // remove *Context
+		OutType     string
+		Document    []string
+	}
+	RpcService struct {
+		Name  stringx.String
+		Funcs []*Func
+	}
+	// parsing for rpc
+	PbAst struct {
+		Package string
+		// external reference
+		Imports map[string]string
+		Strcuts map[string]*Struct
+		// rpc server's functions,not all functions
+		Service []*RpcService
+	}
+)
+
+func NewAstParser(golang []byte, filterStruct map[string]lang.PlaceholderType, log console.Console) *astParser {
+	return &astParser{
+		golang:       golang,
+		filterStruct: filterStruct,
+		Console:      log,
+		fileSet:      token.NewFileSet(),
+	}
+}
+func (a *astParser) Parse() (*PbAst, error) {
+	fSet := a.fileSet
+	f, err := parser.ParseFile(fSet, "", a.golang, parser.ParseComments)
+	if err != nil {
+		return nil, err
+	}
+
+	commentMap := ast.NewCommentMap(fSet, f, f.Comments)
+	f.Comments = commentMap.Filter(f).Comments()
+	var pbAst PbAst
+	pbAst.Package = a.mustGetIndentName(f.Name)
+	imports := make(map[string]string)
+	for _, item := range f.Imports {
+		if item == nil {
+			continue
+		}
+		if item.Path == nil {
+			continue
+		}
+		key := a.mustGetIndentName(item.Name)
+		value := item.Path.Value
+		imports[key] = value
+	}
+	structs, funcs := a.mustScope(f.Scope)
+	pbAst.Imports = imports
+	pbAst.Strcuts = structs
+	pbAst.Service = funcs
+	return &pbAst, nil
+}
+
+func (a *astParser) mustScope(scope *ast.Scope) (map[string]*Struct, []*RpcService) {
+	if scope == nil {
+		return nil, nil
+	}
+
+	objects := scope.Objects
+	structs := make(map[string]*Struct)
+	serviceList := make([]*RpcService, 0)
+	for name, obj := range objects {
+		decl := obj.Decl
+		if decl == nil {
+			continue
+		}
+		typeSpec, ok := decl.(*ast.TypeSpec)
+		if !ok {
+			continue
+		}
+		tp := typeSpec.Type
+
+		switch v := tp.(type) {
+
+		case *ast.StructType:
+			st, err := a.parseObject(name, v)
+			a.Must(err)
+			structs[st.Name.Lower()] = st
+
+		case *ast.InterfaceType:
+			if !strings.HasSuffix(name, suffixServer) {
+				continue
+			}
+			list := a.mustServerFunctions(v)
+			serviceList = append(serviceList, &RpcService{
+				Name:  stringx.From(strings.TrimSuffix(name, suffixServer)),
+				Funcs: list,
+			})
+		}
+	}
+	targetStruct := make(map[string]*Struct)
+	for st := range a.filterStruct {
+		lower := strings.ToLower(st)
+		targetStruct[lower] = structs[lower]
+	}
+	return targetStruct, serviceList
+}
+
+func (a *astParser) mustServerFunctions(v *ast.InterfaceType) []*Func {
+	funcs := make([]*Func, 0)
+	methodObject := v.Methods
+	if methodObject == nil {
+		return nil
+	}
+
+	for _, method := range methodObject.List {
+		var item Func
+		name := a.mustGetIndentName(method.Names[0])
+		doc := a.parseCommentOrDoc(method.Doc)
+		item.Name = stringx.From(name)
+		item.Document = doc
+		types := method.Type
+		if types == nil {
+			funcs = append(funcs, &item)
+			continue
+		}
+		v, ok := types.(*ast.FuncType)
+		if !ok {
+			continue
+		}
+		params := v.Params
+		if params != nil {
+			inList, err := a.parseFields(params.List, true)
+			a.Must(err)
+
+			for _, data := range inList {
+				if strings.HasPrefix(data.TypeName, referenceContext) {
+					continue
+				}
+				// currently,does not support external references
+				item.InTypeName = data.TypeName
+				item.InType = strings.TrimPrefix(data.TypeName, flagStar)
+				break
+			}
+		}
+		results := v.Results
+		if results != nil {
+			outList, err := a.parseFields(results.List, true)
+			a.Must(err)
+
+			for _, data := range outList {
+				if strings.HasPrefix(data.TypeName, referenceContext) {
+					continue
+				}
+				// currently,does not support external references
+				item.OutTypeName = data.TypeName
+				item.OutType = strings.TrimPrefix(data.TypeName, flagStar)
+				break
+			}
+		}
+		funcs = append(funcs, &item)
+	}
+	return funcs
+}
+
+func (a *astParser) parseObject(structName string, tp *ast.StructType) (*Struct, error) {
+	if data, ok := objectM[structName]; ok {
+		return data, nil
+	}
+	var st Struct
+	st.Name = stringx.From(structName)
+	if tp == nil {
+		return &st, nil
+	}
+
+	fields := tp.Fields
+	if fields == nil {
+		objectM[structName] = &st
+		return &st, nil
+	}
+
+	fieldList := fields.List
+	members, err := a.parseFields(fieldList, false)
+	if err != nil {
+		return nil, err
+	}
+
+	for _, m := range members {
+		var field Field
+		field.Name = m.Name
+		field.TypeName = m.TypeName
+		field.JsonTag = m.JsonTag
+		field.Document = m.Document
+		field.Comment = m.Comment
+		st.Field = append(st.Field, &field)
+	}
+	objectM[structName] = &st
+	return &st, nil
+}
+
+func (a *astParser) parseFields(fields []*ast.Field, onlyType bool) ([]*Field, error) {
+	ret := make([]*Field, 0)
+	for _, field := range fields {
+		var item Field
+		tag := a.parseTag(field.Tag)
+		if tag == "" && !onlyType {
+			continue
+		}
+		if tag == ignoreJsonTagExpression {
+			continue
+		}
+
+		item.JsonTag = tag
+		name := a.parseName(field.Names)
+		if strings.HasPrefix(name, unknownPrefix) {
+			continue
+		}
+		item.Name = stringx.From(name)
+		typeName, err := a.parseType(field.Type)
+		if err != nil {
+			return nil, err
+		}
+
+		item.TypeName = typeName
+		if onlyType {
+			ret = append(ret, &item)
+			continue
+		}
+		docs := a.parseCommentOrDoc(field.Doc)
+		comments := a.parseCommentOrDoc(field.Comment)
+
+		item.Document = docs
+		item.Comment = comments
+
+		isInline := name == ""
+		if isInline {
+			return nil, a.wrapError(field.Pos(), "unexpected inline type:%s", name)
+		}
+
+		ret = append(ret, &item)
+
+	}
+	return ret, nil
+}
+
+func (a *astParser) parseTag(basicLit *ast.BasicLit) string {
+	if basicLit == nil {
+		return ""
+	}
+	value := basicLit.Value
+	splits := strings.Split(value, " ")
+	if len(splits) == 1 {
+		return fmt.Sprintf("`%s`", strings.ReplaceAll(splits[0], "`", ""))
+	} else {
+		return fmt.Sprintf("`%s`", strings.ReplaceAll(splits[1], "`", ""))
+	}
+}
+
+// returns
+// resp1:type's string expression,like int、string、[]int64、map[string]User、*User
+// resp2:error
+func (a *astParser) parseType(expr ast.Expr) (string, error) {
+	if expr == nil {
+		return "", errorParseError
+	}
+
+	switch v := expr.(type) {
+	case *ast.StarExpr:
+		stringExpr, err := a.parseType(v.X)
+		if err != nil {
+			return "", err
+		}
+
+		e := fmt.Sprintf("*%s", stringExpr)
+		return e, nil
+
+	case *ast.Ident:
+		return a.mustGetIndentName(v), nil
+	case *ast.MapType:
+		keyStringExpr, err := a.parseType(v.Key)
+		if err != nil {
+			return "", err
+		}
+
+		valueStringExpr, err := a.parseType(v.Value)
+		if err != nil {
+			return "", err
+		}
+
+		e := fmt.Sprintf("map[%s]%s", keyStringExpr, valueStringExpr)
+		return e, nil
+	case *ast.ArrayType:
+		stringExpr, err := a.parseType(v.Elt)
+		if err != nil {
+			return "", err
+		}
+
+		e := fmt.Sprintf("[]%s", stringExpr)
+		return e, nil
+	case *ast.InterfaceType:
+		return "interface{}", nil
+	case *ast.SelectorExpr:
+		join := make([]string, 0)
+		xIdent, ok := v.X.(*ast.Ident)
+		xIndentName := a.mustGetIndentName(xIdent)
+		if ok {
+			join = append(join, xIndentName)
+		}
+		sel := v.Sel
+		join = append(join, a.mustGetIndentName(sel))
+		return strings.Join(join, "."), nil
+	case *ast.ChanType:
+		return "", a.wrapError(v.Pos(), "unexpected type 'chan'")
+	case *ast.FuncType:
+		return "", a.wrapError(v.Pos(), "unexpected type 'func'")
+	case *ast.StructType:
+		return "", a.wrapError(v.Pos(), "unexpected inline struct type")
+	default:
+		return "", a.wrapError(v.Pos(), "unexpected type '%v'", v)
+	}
+}
+func (a *astParser) parseName(names []*ast.Ident) string {
+	if len(names) == 0 {
+		return ""
+	}
+	name := names[0]
+	return a.mustGetIndentName(name)
+}
+
+func (a *astParser) parseCommentOrDoc(cg *ast.CommentGroup) []string {
+	if cg == nil {
+		return nil
+	}
+	comments := make([]string, 0)
+	for _, comment := range cg.List {
+		if comment == nil {
+			continue
+		}
+		text := strings.TrimSpace(comment.Text)
+		if text == "" {
+			continue
+		}
+		comments = append(comments, text)
+	}
+	return comments
+}
+
+func (a *astParser) mustGetIndentName(ident *ast.Ident) string {
+	if ident == nil {
+		return ""
+	}
+	return ident.Name
+}
+
+func (a *astParser) wrapError(pos token.Pos, format string, arg ...interface{}) error {
+	file := a.fileSet.Position(pos)
+	return fmt.Errorf("line %v: %s", file.Line, fmt.Sprintf(format, arg...))
+}
+
+func (a *PbAst) GenTypesCode() (string, error) {
+	types := make([]string, 0)
+	sts := make([]*Struct, 0)
+	for _, item := range a.Strcuts {
+		sts = append(sts, item)
+	}
+	sort.Slice(sts, func(i, j int) bool {
+		return sts[i].Name.Source() < sts[j].Name.Source()
+	})
+	for _, s := range sts {
+		structCode, err := s.genCode(false)
+		if err != nil {
+			return "", err
+		}
+
+		if structCode == "" {
+			continue
+		}
+		types = append(types, structCode)
+	}
+	buffer, err := util.With("type").Parse(typeTemplate).Execute(map[string]interface{}{
+		"types": strings.Join(types, "\n"),
+	})
+	if err != nil {
+		return "", err
+	}
+
+	return buffer.String(), nil
+}
+
+func (s *Struct) genCode(containsTypeStatement bool) (string, error) {
+	if len(s.Field) == 0 {
+		return "", nil
+	}
+	fields := make([]string, 0)
+	for _, f := range s.Field {
+		var comment, doc string
+		if len(f.Comment) > 0 {
+			comment = f.Comment[0]
+		}
+		doc = strings.Join(f.Document, "\n")
+		buffer, err := util.With(sx.Rand()).Parse(fieldTemplate).Execute(map[string]interface{}{
+			"name":       f.Name.Title(),
+			"type":       f.TypeName,
+			"tag":        f.JsonTag,
+			"hasDoc":     len(f.Document) > 0,
+			"doc":        doc,
+			"hasComment": len(f.Comment) > 0,
+			"comment":    comment,
+		})
+		if err != nil {
+			return "", err
+		}
+
+		fields = append(fields, buffer.String())
+	}
+	buffer, err := util.With("struct").Parse(structTemplate).Execute(map[string]interface{}{
+		"type":   containsTypeStatement,
+		"name":   s.Name.Title(),
+		"fields": strings.Join(fields, "\n"),
+	})
+	if err != nil {
+		return "", err
+	}
+
+	return buffer.String(), nil
+}

+ 36 - 0
tools/goctl/util/console/console.go

@@ -2,6 +2,7 @@ package console
 
 import (
 	"fmt"
+	"os"
 
 	"github.com/logrusorgru/aurora"
 )
@@ -9,8 +10,11 @@ import (
 type (
 	Console interface {
 		Success(format string, a ...interface{})
+		Info(format string, a ...interface{})
 		Warning(format string, a ...interface{})
 		Error(format string, a ...interface{})
+		Fatalln(format string, a ...interface{})
+		Must(err error)
 	}
 	colorConsole struct {
 	}
@@ -30,6 +34,11 @@ func NewColorConsole() *colorConsole {
 	return &colorConsole{}
 }
 
+func (c *colorConsole) Info(format string, a ...interface{}) {
+	msg := fmt.Sprintf(format, a...)
+	fmt.Println(msg)
+}
+
 func (c *colorConsole) Success(format string, a ...interface{}) {
 	msg := fmt.Sprintf(format, a...)
 	fmt.Println(aurora.Green(msg))
@@ -45,10 +54,26 @@ func (c *colorConsole) Error(format string, a ...interface{}) {
 	fmt.Println(aurora.Red(msg))
 }
 
+func (c *colorConsole) Fatalln(format string, a ...interface{}) {
+	c.Error(format, a...)
+	os.Exit(1)
+}
+
+func (c *colorConsole) Must(err error) {
+	if err != nil {
+		c.Fatalln("%+v", err)
+	}
+}
+
 func NewIdeaConsole() *ideaConsole {
 	return &ideaConsole{}
 }
 
+func (i *ideaConsole) Info(format string, a ...interface{}) {
+	msg := fmt.Sprintf(format, a...)
+	fmt.Println(msg)
+}
+
 func (i *ideaConsole) Success(format string, a ...interface{}) {
 	msg := fmt.Sprintf(format, a...)
 	fmt.Println("[SUCCESS]: ", msg)
@@ -63,3 +88,14 @@ func (i *ideaConsole) Error(format string, a ...interface{}) {
 	msg := fmt.Sprintf(format, a...)
 	fmt.Println("[ERROR]: ", msg)
 }
+
+func (i *ideaConsole) Fatalln(format string, a ...interface{}) {
+	i.Error(format, a...)
+	os.Exit(1)
+}
+
+func (i *ideaConsole) Must(err error) {
+	if err != nil {
+		i.Fatalln("%+v", err)
+	}
+}

+ 0 - 47
tools/goctl/util/file.go

@@ -2,18 +2,12 @@ package util
 
 import (
 	"bufio"
-	"bytes"
 	"fmt"
-	"go/format"
-	"io/ioutil"
 	"os"
 	"path/filepath"
 	"strings"
-	"text/template"
-	"time"
 
 	"github.com/logrusorgru/aurora"
-	"github.com/tal-tech/go-zero/core/logx"
 )
 
 func CreateIfNotExist(file string) (*os.File, error) {
@@ -53,44 +47,3 @@ func FileExists(file string) bool {
 func FileNameWithoutExt(file string) string {
 	return strings.TrimSuffix(file, filepath.Ext(file))
 }
-
-func CreateTemplateAndExecute(filename, text string, arg map[string]interface{}, forceUpdate bool, disableFormatCodeArgs ...bool) error {
-	if FileExists(filename) && !forceUpdate {
-		return nil
-	}
-	var buffer = new(bytes.Buffer)
-	templateName := fmt.Sprintf("%d", time.Now().UnixNano())
-	t, err := template.New(templateName).Parse(text)
-	if err != nil {
-		return err
-	}
-	err = t.Execute(buffer, arg)
-	if err != nil {
-		return err
-	}
-	var disableFormatCode bool
-	for _, f := range disableFormatCodeArgs {
-		disableFormatCode = f
-	}
-	var bts = buffer.Bytes()
-	s := buffer.String()
-	logx.Info(s)
-	if !disableFormatCode {
-		bts, err = format.Source(buffer.Bytes())
-		if err != nil {
-			return err
-		}
-	}
-	return ioutil.WriteFile(filename, bts, os.ModePerm)
-}
-
-func FormatCodeAndWrite(filename string, code []byte) error {
-	if FileExists(filename) {
-		return nil
-	}
-	bts, err := format.Source(code)
-	if err != nil {
-		return err
-	}
-	return ioutil.WriteFile(filename, bts, os.ModePerm)
-}

+ 11 - 0
tools/goctl/util/head.go

@@ -0,0 +1,11 @@
+package util
+
+var headTemplate = `// Code generated by goctl. DO NOT EDIT.
+// Source: {{.source}}`
+
+func GetHead(source string) string {
+	buffer, _ := With("head").Parse(headTemplate).Execute(map[string]interface{}{
+		"source": source,
+	})
+	return buffer.String()
+}

+ 5 - 2
tools/goctl/util/templatex/templatex.go → tools/goctl/util/templatex.go

@@ -1,4 +1,4 @@
-package templatex
+package util
 
 import (
 	"bytes"
@@ -32,7 +32,10 @@ func (t *defaultTemplate) GoFmt(format bool) *defaultTemplate {
 	return t
 }
 
-func (t *defaultTemplate) SaveTo(data interface{}, path string) error {
+func (t *defaultTemplate) SaveTo(data interface{}, path string, forceUpdate bool) error {
+	if FileExists(path) && !forceUpdate {
+		return nil
+	}
 	output, err := t.execute(data)
 	if err != nil {
 		return err