Repository: zeromicro/go-zero
Pull request #2719 (2022-12-21 06:52:29)

:bug: debug grpc export (#2379)
#2379 Fixed the issue that the GRPC exporter did not establish an RPC link 原文使用的 otlptracegrpc.NewUnstarted创建的是一个未建立rpc连接的导出器，无法正常使用；改为otlptracegrpc.New才妥


Patch:
diff --git a/core/conf/config.go b/core/conf/config.go
index bdaf7061..08e7f90f 100644
--- a/core/conf/config.go
+++ b/core/conf/config.go
@@ -13,8 +13,6 @@ import (
 	"github.com/zeromicro/go-zero/internal/encoding"
 )
 
-const distanceBetweenUpperAndLower = 32
-
 var loaders = map[string]func([]byte, interface{}) error{
 	".json": LoadFromJsonBytes,
 	".toml": LoadFromTomlBytes,
@@ -66,9 +64,9 @@ func LoadFromJsonBytes(content []byte, v interface{}) error {
 	}
 
 	finfo := buildFieldsInfo(reflect.TypeOf(v))
-	camelCaseKeyMap := toCamelCaseKeyMap(m, finfo)
+	lowerCaseKeyMap := toLowerCaseKeyMap(m, finfo)
 
-	return mapping.UnmarshalJsonMap(camelCaseKeyMap, v, mapping.WithCanonicalKeyFunc(toCamelCase))
+	return mapping.UnmarshalJsonMap(lowerCaseKeyMap, v, mapping.WithCanonicalKeyFunc(toLowerCase))
 }
 
 // LoadConfigFromJsonBytes loads config into v from content json bytes.
@@ -129,7 +127,7 @@ func buildStructFieldsInfo(tp reflect.Type) map[string]fieldInfo {
 	for i := 0; i < tp.NumField(); i++ {
 		field := tp.Field(i)
 		name := field.Name
-		ccName := toCamelCase(name)
+		lowerCaseName := toLowerCase(name)
 		ft := mapping.Deref(field.Type)
 
 		// flatten anonymous fields
@@ -140,7 +138,7 @@ func buildStructFieldsInfo(tp reflect.Type) map[string]fieldInfo {
 					info[k] = v
 				}
 			} else {
-				info[ccName] = fieldInfo{
+				info[lowerCaseName] = fieldInfo{
 					name: name,
 					kind: ft.Kind(),
 				}
@@ -158,7 +156,7 @@ func buildStructFieldsInfo(tp reflect.Type) map[string]fieldInfo {
 			fields = buildFieldsInfo(ft.Elem())
 		}
 
-		info[ccName] = fieldInfo{
+		info[lowerCaseName] = fieldInfo{
 			name:     name,
 			kind:     ft.Kind(),
 			children: fields,
@@ -168,58 +166,18 @@ func buildStructFieldsInfo(tp reflect.Type) map[string]fieldInfo {
 	return info
 }
 
-func toCamelCase(s string) string {
-	var buf strings.Builder
-	buf.Grow(len(s))
-	var capNext bool
-	boundary := true
-	for _, v := range s {
-		isCap := v >= 'A' && v <= 'Z'
-		isLow := v >= 'a' && v <= 'z'
-		if boundary && (isCap || isLow) {
-			if capNext {
-				if isLow {
-					v -= distanceBetweenUpperAndLower
-				}
-			} else {
-				if isCap {
-					v += distanceBetweenUpperAndLower
-				}
-			}
-			boundary = false
-		}
-		if isCap || isLow {
-			buf.WriteRune(v)
-			capNext = false
-			continue
-		}
-
-		switch v {
-		// '.' is used for chained keys, e.g. "grand.parent.child"
-		case ' ', '.', '\t':
-			buf.WriteRune(v)
-			capNext = false
-			boundary = true
-		case '_':
-			capNext = true
-			boundary = true
-		default:
-			buf.WriteRune(v)
-			capNext = true
-		}
-	}
-
-	return buf.String()
+func toLowerCase(s string) string {
+	return strings.ToLower(s)
 }
 
-func toCamelCaseInterface(v interface{}, info map[string]fieldInfo) interface{} {
+func toLowerCaseInterface(v interface{}, info map[string]fieldInfo) interface{} {
 	switch vv := v.(type) {
 	case map[string]interface{}:
-		return toCamelCaseKeyMap(vv, info)
+		return toLowerCaseKeyMap(vv, info)
 	case []interface{}:
 		var arr []interface{}
 		for _, vvv := range vv {
-			arr = append(arr, toCamelCaseInterface(vvv, info))
+			arr = append(arr, toLowerCaseInterface(vvv, info))
 		}
 		return arr
 	default:
@@ -227,19 +185,19 @@ func toCamelCaseInterface(v interface{}, info map[string]fieldInfo) interface{}
 	}
 }
 
-func toCamelCaseKeyMap(m map[string]interface{}, info map[string]fieldInfo) map[string]interface{} {
+func toLowerCaseKeyMap(m map[string]interface{}, info map[string]fieldInfo) map[string]interface{} {
 	res := make(map[string]interface{})
 
 	for k, v := range m {
 		ti, ok := info[k]
 		if ok {
-			res[k] = toCamelCaseInterface(v, ti.children)
+			res[k] = toLowerCaseInterface(v, ti.children)
 			continue
 		}
 
-		cck := toCamelCase(k)
-		if ti, ok = info[cck]; ok {
-			res[toCamelCase(k)] = toCamelCaseInterface(v, ti.children)
+		lk := toLowerCase(k)
+		if ti, ok = info[lk]; ok {
+			res[lk] = toLowerCaseInterface(v, ti.children)
 		} else {
 			res[k] = v
 		}
diff --git a/core/logx/config.go b/core/logx/config.go
index dd96b6eb..7d1a4c1e 100644
--- a/core/logx/config.go
+++ b/core/logx/config.go
@@ -9,6 +9,7 @@ type LogConf struct {
 	Path                string `json:",default=logs"`
 	Level               string `json:",default=info,options=[debug,info,error,severe]"`
 	Compress            bool   `json:",optional"`
+	Stat                bool   `json:",default=true"`
 	KeepDays            int    `json:",optional"`
 	StackCooldownMillis int    `json:",default=100"`
 	// MaxBackups represents how many backup log files will be kept. 0 means all files will be kept forever.
diff --git a/core/logx/logs.go b/core/logx/logs.go
index 621b1ee3..65047d49 100644
--- a/core/logx/logs.go
+++ b/core/logx/logs.go
@@ -230,6 +230,10 @@ func SetUp(c LogConf) (err error) {
 	setupOnce.Do(func() {
 		setupLogLevel(c)
 
+		if !c.Stat {
+			DisableStat()
+		}
+
 		if len(c.TimeFormat) > 0 {
 			timeFormat = c.TimeFormat
 		}
diff --git a/core/mapping/jsonunmarshaler.go b/core/mapping/jsonunmarshaler.go
index 540581a3..f832224d 100644
--- a/core/mapping/jsonunmarshaler.go
+++ b/core/mapping/jsonunmarshaler.go
@@ -34,7 +34,7 @@ func getJsonUnmarshaler(opts ...UnmarshalOption) *Unmarshaler {
 }
 
 func unmarshalJsonBytes(content []byte, v interface{}, unmarshaler *Unmarshaler) error {
-	var m map[string]interface{}
+	var m interface{}
 	if err := jsonx.Unmarshal(content, &m); err != nil {
 		return err
 	}
@@ -43,7 +43,7 @@ func unmarshalJsonBytes(content []byte, v interface{}, unmarshaler *Unmarshaler)
 }
 
 func unmarshalJsonReader(reader io.Reader, v interface{}, unmarshaler *Unmarshaler) error {
-	var m map[string]interface{}
+	var m interface{}
 	if err := jsonx.UnmarshalFromReader(reader, &m); err != nil {
 		return err
 	}
diff --git a/core/mapping/unmarshaler.go b/core/mapping/unmarshaler.go
index 767b2903..e6c7425e 100644
--- a/core/mapping/unmarshaler.go
+++ b/core/mapping/unmarshaler.go
@@ -71,8 +71,29 @@ func UnmarshalKey(m map[string]interface{}, v interface{}) error {
 }
 
 // Unmarshal unmarshals m into v.
-func (u *Unmarshaler) Unmarshal(m map[string]interface{}, v interface{}) error {
-	return u.UnmarshalValuer(mapValuer(m), v)
+func (u *Unmarshaler) Unmarshal(i interface{}, v interface{}) error {
+	valueType := reflect.TypeOf(v)
+	if valueType.Kind() != reflect.Ptr {
+		return errValueNotSettable
+	}
+
+	elemType := valueType.Elem()
+	switch iv := i.(type) {
+	case map[string]interface{}:
+		if elemType.Kind() != reflect.Struct {
+			return errTypeMismatch
+		}
+
+		return u.UnmarshalValuer(mapValuer(iv), v)
+	case []interface{}:
+		if elemType.Kind() != reflect.Slice {
+			return errTypeMismatch
+		}
+
+		return u.fillSlice(elemType, reflect.ValueOf(v).Elem(), iv)
+	default:
+		return errUnsupportedType
+	}
 }
 
 // UnmarshalValuer unmarshals m into v.
diff --git a/core/trace/agent.go b/core/trace/agent.go
index 6ab15e87..f98e28cd 100644
--- a/core/trace/agent.go
+++ b/core/trace/agent.go
@@ -60,11 +60,12 @@ func createExporter(c Config) (sdktrace.SpanExporter, error) {
 	case kindZipkin:
 		return zipkin.New(c.Endpoint)
 	case kindGrpc:
-		return otlptracegrpc.NewUnstarted(
+		return otlptracegrpc.New(
+			context.Background(),
 			otlptracegrpc.WithInsecure(),
 			otlptracegrpc.WithEndpoint(c.Endpoint),
 			otlptracegrpc.WithDialOption(grpc.WithBlock()),
-		), nil
+		)
 	default:
 		return nil, fmt.Errorf("unknown exporter: %s", c.Batcher)
 	}
diff --git a/rest/engine.go b/rest/engine.go
index 5209693c..18a2f618 100644
--- a/rest/engine.go
+++ b/rest/engine.go
@@ -287,10 +287,9 @@ func (ng *engine) withTimeout() internal.StartOption {
 			// without this timeout setting, the server will time out and respond 503 Service Unavailable,
 			// which triggers the circuit breaker.
 			svr.ReadTimeout = 4 * time.Duration(timeout) * time.Millisecond / 5
-			// factor 0.9, to avoid clients not reading the response
-			// without this timeout setting, the server will time out and respond 503 Service Unavailable,
-			// which triggers the circuit breaker.
-			svr.WriteTimeout = 9 * time.Duration(timeout) * time.Millisecond / 10
+			// factor 1.1, to avoid servers don't have enough time to write responses.
+			// setting the factor less than 1.0 may lead clients not receiving the responses.
+			svr.WriteTimeout = 11 * time.Duration(timeout) * time.Millisecond / 10
 		}
 	}
 }
diff --git a/rest/server.go b/rest/server.go
index 4c7a30ff..f54435fd 100644
--- a/rest/server.go
+++ b/rest/server.go
@@ -90,6 +90,25 @@ func (s *Server) Routes() []Route {
 	return routes
 }
 
+// ServeHTTP is for test purpose, allow developer to do a unit test with
+// all defined router without starting an HTTP Server.
+//
+// For example:
+//
+//	server := MustNewServer(...)
+//	server.addRoute(...) // router a
+//	server.addRoute(...) // router b
+//	server.addRoute(...) // router c
+//
+//	r, _ := http.NewRequest(...)
+//	w := httptest.NewRecorder(...)
+//	server.ServeHTTP(w, r)
+//	// verify the response
+func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	s.ngin.bindRoutes(s.router)
+	s.router.ServeHTTP(w, r)
+}
+
 // Start starts the Server.
 // Graceful shutdown is enabled by default.
 // Use proc.SetTimeToForceQuit to customize the graceful shutdown period.
diff --git a/tools/goctl/model/sql/gen/vars.go b/tools/goctl/model/sql/gen/vars.go
index 9df8c32a..f5408bcd 100644
--- a/tools/goctl/model/sql/gen/vars.go
+++ b/tools/goctl/model/sql/gen/vars.go
@@ -2,6 +2,7 @@ package gen
 
 import (
 	"fmt"
+	"sort"
 	"strings"
 
 	"github.com/zeromicro/go-zero/core/collection"
@@ -43,7 +44,9 @@ func genVars(table Table, withCache, postgreSql bool) (string, error) {
 					set.AddStr(fmt.Sprintf("\"`%s`\"", c))
 				}
 			}
-			return strings.Join(set.KeysStr(), ", ")
+			list := set.KeysStr()
+			sort.Strings(list)
+			return strings.Join(list, ", ")
 		}(),
 	})
 	if err != nil {


Test patch:
diff --git a/core/conf/config_test.go b/core/conf/config_test.go
index 547b4745..82dc5545 100644
--- a/core/conf/config_test.go
+++ b/core/conf/config_test.go
@@ -237,23 +237,23 @@ func TestToCamelCase(t *testing.T) {
 		},
 		{
 			input:  "hello_world",
-			expect: "helloWorld",
+			expect: "hello_world",
 		},
 		{
 			input:  "Hello_world",
-			expect: "helloWorld",
+			expect: "hello_world",
 		},
 		{
 			input:  "hello_World",
-			expect: "helloWorld",
+			expect: "hello_world",
 		},
 		{
 			input:  "helloWorld",
-			expect: "helloWorld",
+			expect: "helloworld",
 		},
 		{
 			input:  "HelloWorld",
-			expect: "helloWorld",
+			expect: "helloworld",
 		},
 		{
 			input:  "hello World",
@@ -269,34 +269,34 @@ func TestToCamelCase(t *testing.T) {
 		},
 		{
 			input:  "Hello World foo_bar",
-			expect: "hello world fooBar",
+			expect: "hello world foo_bar",
 		},
 		{
 			input:  "Hello World foo_Bar",
-			expect: "hello world fooBar",
+			expect: "hello world foo_bar",
 		},
 		{
 			input:  "Hello World Foo_bar",
-			expect: "hello world fooBar",
+			expect: "hello world foo_bar",
 		},
 		{
 			input:  "Hello World Foo_Bar",
-			expect: "hello world fooBar",
+			expect: "hello world foo_bar",
 		},
 		{
 			input:  "Hello.World Foo_Bar",
-			expect: "hello.world fooBar",
+			expect: "hello.world foo_bar",
 		},
 		{
 			input:  "你好 World Foo_Bar",
-			expect: "你好 world fooBar",
+			expect: "你好 world foo_bar",
 		},
 	}
 
 	for _, test := range tests {
 		test := test
 		t.Run(test.input, func(t *testing.T) {
-			assert.Equal(t, test.expect, toCamelCase(test.input))
+			assert.Equal(t, test.expect, toLowerCase(test.input))
 		})
 	}
 }
@@ -332,6 +332,22 @@ func TestLoadFromYamlBytes(t *testing.T) {
 	assert.Equal(t, "foo", val.Layer1.Layer2.Layer3)
 }
 
+func TestLoadFromYamlBytesTerm(t *testing.T) {
+	input := []byte(`layer1:
+  layer2:
+    tls_conf: foo`)
+	var val struct {
+		Layer1 struct {
+			Layer2 struct {
+				Layer3 string `json:"tls_conf"`
+			}
+		}
+	}
+
+	assert.NoError(t, LoadFromYamlBytes(input, &val))
+	assert.Equal(t, "foo", val.Layer1.Layer2.Layer3)
+}
+
 func TestLoadFromYamlBytesLayers(t *testing.T) {
 	input := []byte(`layer1:
   layer2:
diff --git a/core/logx/logs_test.go b/core/logx/logs_test.go
index 415f9d42..e8068a78 100644
--- a/core/logx/logs_test.go
+++ b/core/logx/logs_test.go
@@ -794,11 +794,7 @@ func testSetLevelTwiceWithMode(t *testing.T, mode string, w *mockWriter) {
 		Mode:  mode,
 		Level: "error",
 		Path:  "/dev/null",
-	})
-	SetUp(LogConf{
-		Mode:  mode,
-		Level: "info",
-		Path:  "/dev/null",
+		Stat:  false,
 	})
 	const message = "hello there"
 	Info(message)
diff --git a/core/mapping/jsonunmarshaler_test.go b/core/mapping/jsonunmarshaler_test.go
index 28615d31..05af99c9 100644
--- a/core/mapping/jsonunmarshaler_test.go
+++ b/core/mapping/jsonunmarshaler_test.go
@@ -856,8 +856,7 @@ func TestUnmarshalBytesError(t *testing.T) {
 	}
 
 	err := UnmarshalJsonBytes([]byte(payload), &v)
-	assert.NotNil(t, err)
-	assert.True(t, strings.Contains(err.Error(), payload))
+	assert.Equal(t, errTypeMismatch, err)
 }
 
 func TestUnmarshalReaderError(t *testing.T) {
@@ -867,9 +866,7 @@ func TestUnmarshalReaderError(t *testing.T) {
 		Any string
 	}
 
-	err := UnmarshalJsonReader(reader, &v)
-	assert.NotNil(t, err)
-	assert.True(t, strings.Contains(err.Error(), payload))
+	assert.Equal(t, errTypeMismatch, UnmarshalJsonReader(reader, &v))
 }
 
 func TestUnmarshalMap(t *testing.T) {
@@ -920,3 +917,16 @@ func TestUnmarshalMap(t *testing.T) {
 		assert.Equal(t, "foo", v.Any)
 	})
 }
+
+func TestUnmarshalJsonArray(t *testing.T) {
+	var v []struct {
+		Name string `json:"name"`
+		Age  int    `json:"age"`
+	}
+
+	body := `[{"name":"kevin", "age": 18}]`
+	assert.NoError(t, UnmarshalJsonBytes([]byte(body), &v))
+	assert.Equal(t, 1, len(v))
+	assert.Equal(t, "kevin", v[0].Name)
+	assert.Equal(t, 18, v[0].Age)
+}
diff --git a/core/mapping/unmarshaler_test.go b/core/mapping/unmarshaler_test.go
index c45b0600..8916e183 100644
--- a/core/mapping/unmarshaler_test.go
+++ b/core/mapping/unmarshaler_test.go
@@ -23,7 +23,14 @@ func TestUnmarshalWithFullNameNotStruct(t *testing.T) {
 	var s map[string]interface{}
 	content := []byte(`{"name":"xiaoming"}`)
 	err := UnmarshalJsonBytes(content, &s)
-	assert.Equal(t, errValueNotStruct, err)
+	assert.Equal(t, errTypeMismatch, err)
+}
+
+func TestUnmarshalValueNotSettable(t *testing.T) {
+	var s map[string]interface{}
+	content := []byte(`{"name":"xiaoming"}`)
+	err := UnmarshalJsonBytes(content, s)
+	assert.Equal(t, errValueNotSettable, err)
 }
 
 func TestUnmarshalWithoutTagName(t *testing.T) {
diff --git a/core/trace/agent_test.go b/core/trace/agent_test.go
index bd3810b4..6935715d 100644
--- a/core/trace/agent_test.go
+++ b/core/trace/agent_test.go
@@ -13,7 +13,6 @@ func TestStartAgent(t *testing.T) {
 	const (
 		endpoint1 = "localhost:1234"
 		endpoint2 = "remotehost:1234"
-		endpoint3 = "localhost:1235"
 	)
 	c1 := Config{
 		Name: "foo",
@@ -30,12 +29,12 @@ func TestStartAgent(t *testing.T) {
 	}
 	c4 := Config{
 		Name:     "bla",
-		Endpoint: endpoint3,
+		Endpoint: endpoint1,
 		Batcher:  "otlp",
 	}
 	c5 := Config{
 		Name:     "grpc",
-		Endpoint: endpoint3,
+		Endpoint: endpoint1,
 		Batcher:  "grpc",
 	}
 
@@ -50,7 +49,7 @@ func TestStartAgent(t *testing.T) {
 	defer lock.Unlock()
 
 	// because remotehost cannot be resolved
-	assert.Equal(t, 3, len(agents))
+	assert.Equal(t, 2, len(agents))
 	_, ok := agents[""]
 	assert.True(t, ok)
 	_, ok = agents[endpoint1]
diff --git a/rest/engine_test.go b/rest/engine_test.go
index ef7ab474..f80c434d 100644
--- a/rest/engine_test.go
+++ b/rest/engine_test.go
@@ -323,7 +323,7 @@ func TestEngine_withTimeout(t *testing.T) {
 
 			assert.Equal(t, time.Duration(test.timeout)*time.Millisecond*4/5, svr.ReadTimeout)
 			assert.Equal(t, time.Duration(0), svr.ReadHeaderTimeout)
-			assert.Equal(t, time.Duration(test.timeout)*time.Millisecond*9/10, svr.WriteTimeout)
+			assert.Equal(t, time.Duration(test.timeout)*time.Millisecond*11/10, svr.WriteTimeout)
 			assert.Equal(t, time.Duration(0), svr.IdleTimeout)
 		})
 	}
diff --git a/rest/httpx/requests_test.go b/rest/httpx/requests_test.go
index 70c35c1d..d6601f33 100644
--- a/rest/httpx/requests_test.go
+++ b/rest/httpx/requests_test.go
@@ -223,6 +223,22 @@ func TestParseJsonBody(t *testing.T) {
 		assert.Equal(t, "", v.Name)
 		assert.Equal(t, 0, v.Age)
 	})
+
+	t.Run("array body", func(t *testing.T) {
+		var v []struct {
+			Name string `json:"name"`
+			Age  int    `json:"age"`
+		}
+
+		body := `[{"name":"kevin", "age": 18}]`
+		r := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(body))
+		r.Header.Set(ContentType, header.JsonContentType)
+
+		assert.NoError(t, ParseJsonBody(r, &v))
+		assert.Equal(t, 1, len(v))
+		assert.Equal(t, "kevin", v[0].Name)
+		assert.Equal(t, 18, v[0].Age)
+	})
 }
 
 func TestParseRequired(t *testing.T) {
diff --git a/rest/server_test.go b/rest/server_test.go
index ad205552..bd215fae 100644
--- a/rest/server_test.go
+++ b/rest/server_test.go
@@ -535,3 +535,91 @@ func TestServer_WithCors(t *testing.T) {
 	cr.ServeHTTP(httptest.NewRecorder(), req)
 	assert.Equal(t, int32(0), atomic.LoadInt32(&called))
 }
+
+func TestServer_ServeHTTP(t *testing.T) {
+	const configYaml = `
+Name: foo
+Port: 54321
+`
+
+	var cnf RestConf
+	assert.Nil(t, conf.LoadFromYamlBytes([]byte(configYaml), &cnf))
+
+	svr, err := NewServer(cnf)
+	assert.Nil(t, err)
+
+	svr.AddRoutes([]Route{
+		{
+			Method: http.MethodGet,
+			Path:   "/foo",
+			Handler: func(writer http.ResponseWriter, request *http.Request) {
+				_, _ = writer.Write([]byte("succeed"))
+				writer.WriteHeader(http.StatusOK)
+			},
+		},
+		{
+			Method: http.MethodGet,
+			Path:   "/bar",
+			Handler: func(writer http.ResponseWriter, request *http.Request) {
+				_, _ = writer.Write([]byte("succeed"))
+				writer.WriteHeader(http.StatusOK)
+			},
+		},
+		{
+			Method: http.MethodGet,
+			Path:   "/user/:name",
+			Handler: func(writer http.ResponseWriter, request *http.Request) {
+
+				var userInfo struct {
+					Name string `path:"name"`
+				}
+
+				err := httpx.Parse(request, &userInfo)
+				if err != nil {
+					_, _ = writer.Write([]byte("failed"))
+					writer.WriteHeader(http.StatusBadRequest)
+					return
+				}
+
+				_, _ = writer.Write([]byte("succeed"))
+				writer.WriteHeader(http.StatusOK)
+			},
+		},
+	})
+
+	testCase := []struct {
+		name string
+		path string
+		code int
+	}{
+		{
+			name: "URI : /foo",
+			path: "/foo",
+			code: http.StatusOK,
+		},
+		{
+			name: "URI : /bar",
+			path: "/bar",
+			code: http.StatusOK,
+		},
+		{
+			name: "URI : undefined path",
+			path: "/test",
+			code: http.StatusNotFound,
+		},
+		{
+			name: "URI : /user/:name",
+			path: "/user/abc",
+			code: http.StatusOK,
+		},
+	}
+
+	for _, test := range testCase {
+		t.Run(test.name, func(t *testing.T) {
+			w := httptest.NewRecorder()
+			req, _ := http.NewRequest("GET", test.path, nil)
+			svr.ServeHTTP(w, req)
+			assert.Equal(t, test.code, w.Code)
+		})
+	}
+}
diff --git a/tools/goctl/util/stringx/string_test.go b/tools/goctl/util/stringx/string_test.go
index be51442a..1ed1ee7c 100644
--- a/tools/goctl/util/stringx/string_test.go
+++ b/tools/goctl/util/stringx/string_test.go
@@ -92,19 +92,19 @@ func TestString_Camel2Snake(t *testing.T) {
 	}{
 		{
 			input: "goZero",
-			want: "go_zero",
+			want:  "go_zero",
 		},
 		{
 			input: "Gozero",
-			want: "gozero",
+			want:  "gozero",
 		},
 		{
 			input: "GoZero",
-			want: "go_zero",
+			want:  "go_zero",
 		},
 		{
 			input: "Go_Zero",
-			want: "go__zero",
+			want:  "go__zero",
 		},
 	}
 	for _, c := range cases {