Repository: zeromicro/go-zero (go)

Issue:
goctl template  --home 指定自定义模板目录不生效
我还是用中文描述吧！

**Describe the bug**

goctl release v1.2.4 使用 --home 指定自定义模板目录不生效
版本：
```
➜ goctl --version
goctl version 1.2.3-cli darwin/amd64
```

**To Reproduce**
Steps to reproduce the behavior, if applicable:

1. The code is

   ```go
   ➜ goctl api go -api="test.api" -dir="api" -style GoZero --home ./template/1.2.3-cli
   ```

2. The error is

我修改了默认模板中 api 的 handler 和 logic 模板  `api/handler.tpl` 和 `api/logic.tpl` ，通过上面的命令生成后发现还是使用的默认自带的模板。
更具体一点，我是参照 https://go-zero.dev/cn/template.html 这个例子，更改了 handler 中的返回格式 将 `httpx.OkJson(w, body)` 改成了 `reponse.Response(w, resp, err)`  这种。

开始我以为是目录的问题，然后我就放到了 `~/.goctl` 下面，测试发现还是不生效。

后来我又换回了之前使用的 goctl v1.2.1 , 发现这个是生效的。所以怀疑是 v1.2.4 有问题。

**Expected behavior**
A clear and concise description of what you expected to happen.


**Environments (please complete the following information):**
 - OS: [macbook 11.4]
 - go-zero version [1.2.4]
 - goctl version [1.2.4]

**More description**
Add any other context about the problem here.


Pull request:
fix #1330

Patch:
diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml
deleted file mode 100644
index 61f5ab50..00000000
--- a/.github/workflows/release.yaml
+++ /dev/null
@@ -1,28 +0,0 @@
-on:
-  release:
-    types: [created]
-
-jobs:
-  releases-matrix:
-    name: Release goctl binary
-    runs-on: ubuntu-latest
-    strategy:
-      matrix:
-        # build and publish in parallel: linux/386, linux/amd64, linux/arm64,
-        # windows/386, windows/amd64, windows/arm64, darwin/amd64, darwin/arm64
-        goos: [linux, windows, darwin]
-        goarch: ["386", amd64, arm64]
-        exclude:
-          - goarch: "386"
-            goos: darwin
-    steps:
-      - uses: actions/checkout@v2
-      - uses: wangyoucao577/go-release-action@v1.22
-        with:
-          github_token: ${{ secrets.GITHUB_TOKEN }}
-          goos: ${{ matrix.goos }}
-          goarch: ${{ matrix.goarch }}
-          goversion: "https://dl.google.com/go/go1.17.5.linux-amd64.tar.gz"
-          project_path: "tools/goctl"
-          binary_name: "goctl"
-          extra_files: tools/goctl/goctl.md
diff --git a/core/discov/accountregistry.go b/core/discov/accountregistry.go
index 5e8481ee..bbb1d4a9 100644
--- a/core/discov/accountregistry.go
+++ b/core/discov/accountregistry.go
@@ -2,6 +2,7 @@ package discov
 
 import "github.com/tal-tech/go-zero/core/discov/internal"
 
+// RegisterAccount registers the username/password to the given etcd cluster.
 func RegisterAccount(endpoints []string, user, pass string) {
 	internal.AddAccount(endpoints, user, pass)
 }
diff --git a/core/discov/internal/accountmanager.go b/core/discov/internal/accountmanager.go
index f0791292..4b84c9de 100644
--- a/core/discov/internal/accountmanager.go
+++ b/core/discov/internal/accountmanager.go
@@ -2,16 +2,18 @@ package internal
 
 import "sync"
 
-type Account struct {
-	User string
-	Pass string
-}
-
 var (
 	accounts = make(map[string]Account)
 	lock     sync.RWMutex
 )
 
+// Account holds the username/password for an etcd cluster.
+type Account struct {
+	User string
+	Pass string
+}
+
+// AddAccount adds the username/password for the given etcd cluster.
 func AddAccount(endpoints []string, user, pass string) {
 	lock.Lock()
 	defer lock.Unlock()
@@ -22,6 +24,7 @@ func AddAccount(endpoints []string, user, pass string) {
 	}
 }
 
+// GetAccount gets the username/password for the given etcd cluster.
 func GetAccount(endpoints []string) (Account, bool) {
 	lock.RLock()
 	defer lock.RUnlock()
diff --git a/core/discov/subscriber.go b/core/discov/subscriber.go
index b70e76b2..7b70bb45 100644
--- a/core/discov/subscriber.go
+++ b/core/discov/subscriber.go
@@ -58,6 +58,7 @@ func Exclusive() SubOption {
 	}
 }
 
+// WithSubEtcdAccount customizes the Subscriber with given etcd username/password.
 func WithSubEtcdAccount(user, pass string) SubOption {
 	return func(sub *Subscriber) {
 		internal.AddAccount(sub.endpoints, user, pass)
diff --git a/readme-cn.md b/readme-cn.md
index 2bce3716..d92fa2a3 100644
--- a/readme-cn.md
+++ b/readme-cn.md
@@ -141,7 +141,7 @@ GO111MODULE=on GOPROXY=https://goproxy.cn/,direct go get -u github.com/tal-tech/
 
     编写业务代码：
 
-      * api 文件定义了服务对外 HTTP 接口，可参考 [api 规范](https://github.com/zeromicro/zero-doc/blob/main/doc/goctl.md)
+      * api 文件定义了服务对外 HTTP 接口，可参考 [api 规范](https://github.com/zeromicro/zero-doc/blob/main/docs/zero/goctl-api.md)
       * 可以在 `servicecontext.go` 里面传递依赖给 logic，比如 mysql, redis 等
       * 在 api 定义的 `get/post/put/delete` 等请求对应的 logic 里增加业务处理逻辑
 
diff --git a/tools/goctl/api/parser/g4/gen/api/apiparser_parser.go b/tools/goctl/api/parser/g4/gen/api/apiparser_parser.go
old mode 100755
new mode 100644
index fbb26a9a..8599337c
--- a/tools/goctl/api/parser/g4/gen/api/apiparser_parser.go
+++ b/tools/goctl/api/parser/g4/gen/api/apiparser_parser.go
@@ -1,4 +1,5 @@
-package api // ApiParser
+package api
+
 import (
 	"fmt"
 	"reflect"
diff --git a/tools/goctl/api/spec/spec.go b/tools/goctl/api/spec/spec.go
index 199e2251..1410843e 100644
--- a/tools/goctl/api/spec/spec.go
+++ b/tools/goctl/api/spec/spec.go
@@ -1,5 +1,6 @@
 package spec
 
+// RoutePrefixKey is the prefix keyword for the routes.
 const RoutePrefixKey = "prefix"
 
 type (
diff --git a/tools/goctl/go.mod b/tools/goctl/go.mod
index d446bc01..c831cd7f 100644
--- a/tools/goctl/go.mod
+++ b/tools/goctl/go.mod
@@ -1,6 +1,6 @@
 module github.com/tal-tech/go-zero/tools/goctl
 
-go 1.14
+go 1.17
 
 require (
 	github.com/DATA-DOG/go-sqlmock v1.5.0
@@ -10,8 +10,28 @@ require (
 	github.com/iancoleman/strcase v0.2.0
 	github.com/logrusorgru/aurora v2.0.3+incompatible
 	github.com/stretchr/testify v1.7.0
-	github.com/tal-tech/go-zero v1.2.5-0.20211215114449-776e6e647d96
+	github.com/tal-tech/go-zero v1.2.5-0.20211230062155-e4ab51857606
 	github.com/urfave/cli v1.22.5
 	github.com/zeromicro/antlr v0.0.1
 	github.com/zeromicro/ddl-parser v0.0.0-20210712021150-63520aca7348
 )
+
+require (
+	github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a // indirect
+	github.com/alicebob/miniredis/v2 v2.16.0 // indirect
+	github.com/antlr/antlr4/runtime/Go/antlr v0.0.0-20210521184019-c5ad59b459ec // indirect
+	github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d // indirect
+	github.com/davecgh/go-spew v1.1.1 // indirect
+	github.com/go-redis/redis v6.15.9+incompatible // indirect
+	github.com/lib/pq v1.10.3 // indirect
+	github.com/pmezard/go-difflib v1.0.0 // indirect
+	github.com/russross/blackfriday/v2 v2.0.1 // indirect
+	github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect
+	github.com/spaolacci/murmur3 v1.1.0 // indirect
+	github.com/yuin/gopher-lua v0.0.0-20200816102855-ee81675732da // indirect
+	go.opentelemetry.io/otel v1.1.0 // indirect
+	go.opentelemetry.io/otel/trace v1.1.0 // indirect
+	go.uber.org/automaxprocs v1.4.0 // indirect
+	gopkg.in/yaml.v2 v2.4.0 // indirect
+	gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
+)
diff --git a/tools/goctl/go.sum b/tools/goctl/go.sum
index bfb24906..c257cdf3 100644
--- a/tools/goctl/go.sum
+++ b/tools/goctl/go.sum
@@ -169,7 +169,6 @@ github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QD
 github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
 github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
 github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
-github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
 github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
 github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
 github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
@@ -273,7 +272,6 @@ github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8m
 github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
 github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
 github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw=
-github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32 h1:W6apQkHrMkS0Muv8G/TipAy/FJl/rCYT0+EuS8+Z0z4=
 github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32/go.mod h1:9wM+0iRr9ahx58uYLpLIr5fm8diHn0JbqRycJi6w0Ms=
 github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
 github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
@@ -351,8 +349,8 @@ github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5
 github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
 github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
-github.com/tal-tech/go-zero v1.2.5-0.20211215114449-776e6e647d96 h1:t3mkcOE4mXGmZdNi2RyxQ8Ysm++oOPWttGfGq7hzxuE=
-github.com/tal-tech/go-zero v1.2.5-0.20211215114449-776e6e647d96/go.mod h1:OM2UangPHFMJ+5boRUaeQ/z6xpVvqprZda/JUWG1D7Y=
+github.com/tal-tech/go-zero v1.2.5-0.20211230062155-e4ab51857606 h1:u6Nm/S8pOqHDdjYs8aFBxDGC1BfjVmJoOus4g2RanZE=
+github.com/tal-tech/go-zero v1.2.5-0.20211230062155-e4ab51857606/go.mod h1:OM2UangPHFMJ+5boRUaeQ/z6xpVvqprZda/JUWG1D7Y=
 github.com/urfave/cli v1.22.5 h1:lNq9sAHXK2qfdI8W+GRItjCEkI+2oR4d+MEHy1CKXoU=
 github.com/urfave/cli v1.22.5/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
 github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI=
@@ -580,7 +578,6 @@ golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
-golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
 golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
 google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
@@ -645,7 +642,6 @@ google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpAD
 google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
 google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
 google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
-google.golang.org/protobuf v1.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+RurQ=
 google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
 gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
diff --git a/tools/goctl/internal/version/version.go b/tools/goctl/internal/version/version.go
index ba466ae2..f4d8cbf4 100644
--- a/tools/goctl/internal/version/version.go
+++ b/tools/goctl/internal/version/version.go
@@ -6,7 +6,7 @@ import (
 )
 
 // BuildVersion is the version of goctl.
-const BuildVersion = "1.2.4"
+const BuildVersion = "1.2.5"
 
 var tag = map[string]int{"pre-alpha": 0, "alpha": 1, "pre-bata": 2, "beta": 3, "released": 4, "": 5}
 
diff --git a/tools/goctl/model/sql/parser/parser.go b/tools/goctl/model/sql/parser/parser.go
index e220dd06..28f69a43 100644
--- a/tools/goctl/model/sql/parser/parser.go
+++ b/tools/goctl/model/sql/parser/parser.go
@@ -371,6 +371,7 @@ func getTableFields(table *model.Table) (map[string]*Field, error) {
 	return fieldM, nil
 }
 
+// GetSafeTables escapes the golang keywords from sql tables.
 func GetSafeTables(tables []*parser.Table) []*parser.Table {
 	var list []*parser.Table
 	for _, t := range tables {
@@ -381,6 +382,7 @@ func GetSafeTables(tables []*parser.Table) []*parser.Table {
 	return list
 }
 
+// GetSafeTable escapes the golang keywords from sql table.
 func GetSafeTable(table *parser.Table) *parser.Table {
 	table.Name = su.EscapeGolangKeyword(table.Name)
 	for _, c := range table.Columns {
diff --git a/tools/goctl/util/file.go b/tools/goctl/util/file.go
index 3f7bf9a8..4032f339 100644
--- a/tools/goctl/util/file.go
+++ b/tools/goctl/util/file.go
@@ -95,12 +95,30 @@ func GetGitHome() (string, error) {
 
 // GetTemplateDir returns the category path value in GoctlHome where could get it by GetGoctlHome
 func GetTemplateDir(category string) (string, error) {
-	goctlHome, err := GetGoctlHome()
+	home, err := GetGoctlHome()
 	if err != nil {
 		return "", err
 	}
+	if home == goctlHome {
+		// backward compatible, it will be removed in the feature
+		// backward compatible start
+		beforeTemplateDir := filepath.Join(home, version.GetGoctlVersion(), category)
+		fs, _ := ioutil.ReadDir(beforeTemplateDir)
+		var hasContent bool
+		for _, e := range fs {
+			if e.Size() > 0 {
+				hasContent = true
+			}
+		}
+		if hasContent {
+			return beforeTemplateDir, nil
+		}
+		// backward compatible end
+
+		return filepath.Join(home, category), nil
+	}
 
-	return filepath.Join(goctlHome, version.GetGoctlVersion(), category), nil
+	return filepath.Join(home, version.GetGoctlVersion(), category), nil
 }
 
 // InitTemplates creates template files GoctlHome where could get it by GetGoctlHome
diff --git a/tools/goctl/util/string.go b/tools/goctl/util/string.go
index 0aa93845..8422e4e4 100644
--- a/tools/goctl/util/string.go
+++ b/tools/goctl/util/string.go
@@ -96,6 +96,7 @@ func isNumber(r rune) bool {
 	return '0' <= r && r <= '9'
 }
 
+// EscapeGolangKeyword escapes the golang keywords.
 func EscapeGolangKeyword(s string) string {
 	if !isGolangKeyword(s) {
 		return s


Test patch:
diff --git a/core/search/tree_test.go b/core/search/tree_test.go
index b7166e5a..315bef9e 100644
--- a/core/search/tree_test.go
+++ b/core/search/tree_test.go
@@ -1,9 +1,12 @@
 package search
 
 import (
+	"math/rand"
+	"strings"
 	"testing"
 
 	"github.com/stretchr/testify/assert"
+	"github.com/tal-tech/go-zero/core/stringx"
 )
 
 type mockedRoute struct {
@@ -139,11 +142,9 @@ func TestStrictSearchSibling(t *testing.T) {
 		tree.Add(r.route, r.value)
 	}
 
-	for i := 0; i < 1000; i++ {
-		result, ok := tree.Search(query)
-		assert.True(t, ok)
-		assert.Equal(t, 3, result.Item.(int))
-	}
+	result, ok := tree.Search(query)
+	assert.True(t, ok)
+	assert.Equal(t, 3, result.Item.(int))
 }
 
 func TestAddDuplicate(t *testing.T) {
@@ -185,3 +186,41 @@ func TestSearchInvalidItem(t *testing.T) {
 	err := tree.Add("/", nil)
 	assert.Equal(t, errEmptyItem, err)
 }
+
+func BenchmarkSearchTree(b *testing.B) {
+	const (
+		avgLen  = 1000
+		entries = 10000
+	)
+
+	tree := NewTree()
+	generate := func() string {
+		var buf strings.Builder
+		size := rand.Intn(avgLen) + avgLen/2
+		val := stringx.Randn(size)
+		prev := 0
+		for j := rand.Intn(9) + 1; j < size; j += rand.Intn(9) + 1 {
+			buf.WriteRune('/')
+			buf.WriteString(val[prev:j])
+			prev = j
+		}
+		if prev < size {
+			buf.WriteRune('/')
+			buf.WriteString(val[prev:])
+		}
+		return buf.String()
+	}
+	index := rand.Intn(entries)
+	var query string
+	for i := 0; i < entries; i++ {
+		val := generate()
+		if i == index {
+			query = val
+		}
+		tree.Add(val, i)
+	}
+
+	for i := 0; i < b.N; i++ {
+		tree.Search(query)
+	}
+}
diff --git a/tools/goctl/util/file_test.go b/tools/goctl/util/file_test.go
index b0927f07..77aa5a59 100644
--- a/tools/goctl/util/file_test.go
+++ b/tools/goctl/util/file_test.go
@@ -1,13 +1,66 @@
 package util
 
 import (
+	"io/ioutil"
 	"os"
 	"path/filepath"
 	"testing"
 
 	"github.com/stretchr/testify/assert"
+	"github.com/tal-tech/go-zero/tools/goctl/internal/version"
 )
 
+func TestGetTemplateDir(t *testing.T) {
+	category := "foo"
+	t.Run("before_have_templates", func(t *testing.T) {
+		home := t.TempDir()
+		RegisterGoctlHome("")
+		RegisterGoctlHome(home)
+		v := version.GetGoctlVersion()
+		dir := filepath.Join(home, v, category)
+		err := MkdirIfNotExist(dir)
+		if err != nil {
+			return
+		}
+		tempFile := filepath.Join(dir, "bar.txt")
+		err = ioutil.WriteFile(tempFile, []byte("foo"), os.ModePerm)
+		if err != nil {
+			return
+		}
+		templateDir, err := GetTemplateDir(category)
+		if err != nil {
+			return
+		}
+		assert.Equal(t, dir, templateDir)
+		RegisterGoctlHome("")
+	})
+
+	t.Run("before_has_no_template", func(t *testing.T) {
+		home := t.TempDir()
+		RegisterGoctlHome("")
+		RegisterGoctlHome(home)
+		dir := filepath.Join(home, category)
+		err := MkdirIfNotExist(dir)
+		if err != nil {
+			return
+		}
+		templateDir, err := GetTemplateDir(category)
+		if err != nil {
+			return
+		}
+		assert.Equal(t, dir, templateDir)
+	})
+
+	t.Run("default", func(t *testing.T) {
+		RegisterGoctlHome("")
+		dir, err := GetTemplateDir(category)
+		if err != nil {
+			return
+		}
+		assert.Contains(t, dir, version.BuildVersion)
+	})
+}
+
 func TestGetGitHome(t *testing.T) {
 	homeDir, err := os.UserHomeDir()
 	if err != nil {