Repository: go-kratos/kratos
Pull request #2536 (2022-11-23 09:57:18)

feat: consul registry support get services from multiple datacenters
<!--
🎉 Thanks for sending a pull request to Kratos! Here are some tips for you:

1. If this is your first time contributing to Kratos, please read our contribution guide: https://go-kratos.dev/en/docs/community/contribution/
2. Ensure you have added or ran the appropriate tests and lint for your PR, please use `make lint` and `make test` before filing your PR, use `make clean` to tidy your go mod.
3. If the PR is unfinished, you may need to mark it as a WIP(Work In Progress) PR or draft PR
4. Please use a semantic commits format title, such as `<type>[optional scope]: <description>`, see: https://go-kratos.dev/docs/community/contribution#type
5. at the same time, please note that similar work should be submitted in one PR as far as possible to reduce the workload of reviewers. Do not split a work into multiple PR unless it should.
-->

<!--
🎉 感谢您向 Kratos 发送 PR 请求！以下是一些提示：
如果这是你第一次为 Kratos 贡献，请阅读我们的贡献指南：https://go-kratos.dev/en/docs/community/contribution/
2、确保您已经为您的 PR 添加或运行了适当的测试和lint，请在提交PR之前使用“make lint”和“make test”，使用“make clean”整理您的 go.mod。
3、如果请购单未完成，您可能需要将其标记为 WIP（在制品）PR 或 draft PR
4、请使用语义提交格式标题，如“<类型>[可选范围]：<说明>`，请参阅：https://go-kratos.dev/docs/community/contribution#type
5. 同时请注意，同类的工作请尽量在一个PR中提交，以减轻 review 者的工作负担，不要把一项工作拆分成很多个PR，除非它应该这样做。
-->

<!--
-->

#### Description (what this PR does / why we need it):
<!--
* The description should include the motivation for this PR or contrast this with previous behavior
-->


#### Which issue(s) this PR fixes (resolves / be part of):
<!--
* Automatically closes linked issue when PR is merged.
* If your PR is not fully resolved the issue, please use `part of #<issue number>` instead.

Usage: `fixes/resolves #<issue number>`, or `fixes/resolves (paste link of issue)`.
-->


#### Other special notes for the reviewers:
<!--
* Somethings that need extra attention for the reviewers
* Some additional notes, TODO list, etc.
-->


codecov-commenter: # [Codecov](https://codecov.io/gh/go-kratos/kratos/pull/2536?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=go-kratos) Report
> Merging [#2536](https://codecov.io/gh/go-kratos/kratos/pull/2536?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=go-kratos) (d7b81bf) into [main](https://codecov.io/gh/go-kratos/kratos/commit/878eca54ab95530a1737d202dee214ef13ed253e?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=go-kratos) (878eca5) will **decrease** coverage by `0.05%`.
> The diff coverage is `n/a`.

```diff
@@            Coverage Diff             @@
##             main    #2536      +/-   ##
==========================================
- Coverage   85.23%   85.18%   -0.06%     
==========================================
  Files          88       88              
  Lines        3826     3826              
==========================================
- Hits         3261     3259       -2     
- Misses        391      393       +2     
  Partials      174      174              
```


| [Impacted Files](https://codecov.io/gh/go-kratos/kratos/pull/2536?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=go-kratos) | Coverage Δ | |
|---|---|---|
| [internal/context/context.go](https://codecov.io/gh/go-kratos/kratos/pull/2536/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=go-kratos#diff-aW50ZXJuYWwvY29udGV4dC9jb250ZXh0Lmdv) | `97.01% <0.00%> (-2.99%)` | :arrow_down: |

Help us with your feedback. Take ten seconds to tell us [how you rate us](https://about.codecov.io/nps?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=go-kratos). Have a feature suggestion? [Share it here.](https://app.codecov.io/gh/feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=go-kratos)


Patch:
diff --git a/contrib/config/apollo/apollo.go b/contrib/config/apollo/apollo.go
index bd96ef11..d463df29 100644
--- a/contrib/config/apollo/apollo.go
+++ b/contrib/config/apollo/apollo.go
@@ -25,6 +25,8 @@ const (
 	properties = "properties"
 )
 
+var formats map[string]struct{}
+
 // Option is apollo option
 type Option func(*options)
 
@@ -129,10 +131,16 @@ func NewSource(opts ...Option) config.Source {
 
 func format(ns string) string {
 	arr := strings.Split(ns, ".")
-	if len(arr) <= 1 || arr[len(arr)-1] == properties {
+	suffix := arr[len(arr)-1]
+	if len(arr) <= 1 || suffix == properties {
+		return json
+	}
+	if _, ok := formats[suffix]; !ok {
+		// fallback
 		return json
 	}
-	return arr[len(arr)-1]
+
+	return suffix
 }
 
 func (e *apollo) load() []*config.KeyValue {
@@ -149,7 +157,7 @@ func (e *apollo) load() []*config.KeyValue {
 			kvs = append(kvs, kv)
 			continue
 		}
-		if strings.Contains(ns, ".") && !strings.Contains(ns, properties) &&
+		if strings.Contains(ns, ".") && !strings.HasSuffix(ns, "."+properties) &&
 			(format(ns) == yaml || format(ns) == yml || format(ns) == json) {
 			kv, err := e.getOriginConfig(ns)
 			if err != nil {
@@ -252,16 +260,28 @@ func resolve(key string, value interface{}, target map[string]interface{}) {
 // eg: namespace.ext with subKey got namespace.subKey
 func genKey(ns, sub string) string {
 	arr := strings.Split(ns, ".")
-	if len(arr) < 1 {
-		return sub
-	}
-
 	if len(arr) == 1 {
 		if ns == "" {
 			return sub
 		}
+
 		return ns + "." + sub
 	}
 
-	return strings.Join(arr[:len(arr)-1], ".") + "." + sub
+	suffix := arr[len(arr)-1]
+	_, ok := formats[suffix]
+	if ok {
+		return strings.Join(arr[:len(arr)-1], ".") + "." + sub
+	}
+
+	return ns + "." + sub
+}
+
+func init() {
+	formats = make(map[string]struct{})
+
+	formats[yaml] = struct{}{}
+	formats[yml] = struct{}{}
+	formats[json] = struct{}{}
+	formats[properties] = struct{}{}
 }
diff --git a/contrib/config/apollo/watcher.go b/contrib/config/apollo/watcher.go
index ea5ef108..d59bdd21 100644
--- a/contrib/config/apollo/watcher.go
+++ b/contrib/config/apollo/watcher.go
@@ -25,7 +25,7 @@ type customChangeListener struct {
 
 func (c *customChangeListener) onChange(namespace string, changes map[string]*storage.ConfigChange) []*config.KeyValue {
 	kv := make([]*config.KeyValue, 0, 2)
-	if strings.Contains(namespace, ".") && !strings.Contains(namespace, properties) &&
+	if strings.Contains(namespace, ".") && !strings.HasSuffix(namespace, "."+properties) &&
 		(format(namespace) == yaml || format(namespace) == yml || format(namespace) == json) {
 		value, err := c.apollo.client.GetConfigCache(namespace).Get("content")
 		if err != nil {
diff --git a/contrib/log/aliyun/go.mod b/contrib/log/aliyun/go.mod
index 30613448..4e9ab817 100644
--- a/contrib/log/aliyun/go.mod
+++ b/contrib/log/aliyun/go.mod
@@ -3,7 +3,7 @@ module github.com/go-kratos/kratos/contrib/log/aliyun/v2
 go 1.16
 
 require (
-	github.com/aliyun/aliyun-log-go-sdk v0.1.40
+	github.com/aliyun/aliyun-log-go-sdk v0.1.41
 	github.com/go-kratos/kratos/v2 v2.5.3
 	google.golang.org/protobuf v1.28.1
 )
diff --git a/contrib/log/aliyun/go.sum b/contrib/log/aliyun/go.sum
index 0b42927b..eef1bd63 100644
--- a/contrib/log/aliyun/go.sum
+++ b/contrib/log/aliyun/go.sum
@@ -12,8 +12,8 @@ github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuy
 github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
 github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
 github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
-github.com/aliyun/aliyun-log-go-sdk v0.1.40 h1:GNlRoTe9Fi7dfhIMHAnb+b5837/fIw8lqs1DRqaU1CM=
-github.com/aliyun/aliyun-log-go-sdk v0.1.40/go.mod h1:1QQ59pEJiVVXqKgbHcU6FWIgxT5RKBt+CT8AiQ2bEts=
+github.com/aliyun/aliyun-log-go-sdk v0.1.41 h1:aitUXlMIzMb8KZvwAqrJ77Nad9FOpAFi1Ju7rjFOYCM=
+github.com/aliyun/aliyun-log-go-sdk v0.1.41/go.mod h1:1QQ59pEJiVVXqKgbHcU6FWIgxT5RKBt+CT8AiQ2bEts=
 github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
 github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
 github.com/apache/thrift v0.13.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
diff --git a/contrib/registry/consul/client.go b/contrib/registry/consul/client.go
index c1364bb3..dec4a7f8 100644
--- a/contrib/registry/consul/client.go
+++ b/contrib/registry/consul/client.go
@@ -15,8 +15,16 @@ import (
 	"github.com/hashicorp/consul/api"
 )
 
+type Datacenter string
+
+const (
+	SingleDatacenter Datacenter = "SINGLE"
+	MultiDatacenter  Datacenter = "MULTI"
+)
+
 // Client is consul client config
 type Client struct {
+	dc     Datacenter
 	cli    *api.Client
 	ctx    context.Context
 	cancel context.CancelFunc
@@ -33,15 +41,21 @@ type Client struct {
 	serviceChecks api.AgentServiceChecks
 }
 
-// NewClient creates consul client
+// Deprecated use newClient instead.
 func NewClient(cli *api.Client) *Client {
+	return newClient(cli, SingleDatacenter)
+}
+
+func newClient(cli *api.Client, dc Datacenter) *Client {
 	c := &Client{
+		dc:                             dc,
 		cli:                            cli,
 		resolver:                       defaultResolver,
 		healthcheckInterval:            10,
 		heartbeat:                      true,
 		deregisterCriticalServiceAfter: 600,
 	}
+
 	c.ctx, c.cancel = context.WithCancel(context.Background())
 	return c
 }
@@ -83,16 +97,66 @@ type ServiceResolver func(ctx context.Context, entries []*api.ServiceEntry) []*r
 
 // Service get services from consul
 func (c *Client) Service(ctx context.Context, service string, index uint64, passingOnly bool) ([]*registry.ServiceInstance, uint64, error) {
+	if c.dc == MultiDatacenter {
+		return c.multiDCService(ctx, service, index, passingOnly)
+	}
+
+	opts := &api.QueryOptions{
+		WaitIndex:  index,
+		WaitTime:   time.Second * 55,
+		Datacenter: string(c.dc),
+	}
+	opts = opts.WithContext(ctx)
+
+	if c.dc == SingleDatacenter {
+		opts.Datacenter = ""
+	}
+
+	entries, meta, err := c.singleDCEntries(service, "", passingOnly, opts)
+	if err != nil {
+		return nil, 0, err
+	}
+	return c.resolver(ctx, entries), meta.LastIndex, nil
+}
+
+func (c *Client) multiDCService(ctx context.Context, service string, index uint64, passingOnly bool) ([]*registry.ServiceInstance, uint64, error) {
 	opts := &api.QueryOptions{
 		WaitIndex: index,
 		WaitTime:  time.Second * 55,
 	}
 	opts = opts.WithContext(ctx)
-	entries, meta, err := c.cli.Health().Service(service, "", passingOnly, opts)
+
+	var instances []*registry.ServiceInstance
+
+	dcs, err := c.cli.Catalog().Datacenters()
 	if err != nil {
 		return nil, 0, err
 	}
-	return c.resolver(ctx, entries), meta.LastIndex, nil
+
+	for _, dc := range dcs {
+		opts.Datacenter = dc
+		e, m, err := c.singleDCEntries(service, "", passingOnly, opts)
+		if err != nil {
+			return nil, 0, err
+		}
+
+		ins := c.resolver(ctx, e)
+		for _, in := range ins {
+			if in.Metadata == nil {
+				in.Metadata = make(map[string]string, 1)
+			}
+			in.Metadata["dc"] = dc
+		}
+
+		instances = append(instances, ins...)
+		opts.WaitIndex = m.LastIndex
+	}
+
+	return instances, opts.WaitIndex, nil
+}
+
+func (c *Client) singleDCEntries(service, tag string, passingOnly bool, opts *api.QueryOptions) ([]*api.ServiceEntry, *api.QueryMeta, error) {
+	return c.cli.Health().Service(service, tag, passingOnly, opts)
 }
 
 // Register register service instance to consul
diff --git a/contrib/registry/consul/registry.go b/contrib/registry/consul/registry.go
index b2f2a3fe..b85f929f 100644
--- a/contrib/registry/consul/registry.go
+++ b/contrib/registry/consul/registry.go
@@ -27,6 +27,13 @@ func WithHealthCheck(enable bool) Option {
 	}
 }
 
+// WithDatacenter with registry datacenter option
+func WithDatacenter(dc Datacenter) Option {
+	return func(o *Registry) {
+		o.dc = dc
+	}
+}
+
 // WithHeartbeat enable or disable heartbeat
 func WithHeartbeat(enable bool) Option {
 	return func(o *Registry) {
@@ -83,18 +90,20 @@ type Registry struct {
 	enableHealthCheck bool
 	registry          map[string]*serviceSet
 	lock              sync.RWMutex
+	dc                Datacenter
 }
 
 // New creates consul registry
 func New(apiClient *api.Client, opts ...Option) *Registry {
 	r := &Registry{
-		cli:               NewClient(apiClient),
+		dc:                SingleDatacenter,
 		registry:          make(map[string]*serviceSet),
 		enableHealthCheck: true,
 	}
 	for _, o := range opts {
 		o(r)
 	}
+	r.cli = newClient(apiClient, r.dc)
 	return r
 }
 
diff --git a/encoding/form/proto_encode.go b/encoding/form/proto_encode.go
index 94eac6ab..25d4ac19 100644
--- a/encoding/form/proto_encode.go
+++ b/encoding/form/proto_encode.go
@@ -46,10 +46,8 @@ func encodeByField(u url.Values, path string, m protoreflect.Message) (finalErr
 			newPath = path + "." + key
 		}
 		if of := fd.ContainingOneof(); of != nil {
-			if f := m.WhichOneof(of); f != nil {
-				if f != fd {
-					return true
-				}
+			if f := m.WhichOneof(of); f != nil && f != fd {
+				return true
 			}
 		}
 		switch {
@@ -60,7 +58,9 @@ func encodeByField(u url.Values, path string, m protoreflect.Message) (finalErr
 					finalErr = err
 					return false
 				}
-				u[newPath] = list
+				for _, item := range list {
+					u.Add(newPath, item)
+				}
 			}
 		case fd.IsMap():
 			if v.Map().Len() > 0 {
@@ -70,13 +70,13 @@ func encodeByField(u url.Values, path string, m protoreflect.Message) (finalErr
 					return false
 				}
 				for k, value := range m {
-					u[fmt.Sprintf("%s[%s]", newPath, k)] = []string{value}
+					u.Set(fmt.Sprintf("%s[%s]", newPath, k), value)
 				}
 			}
 		case (fd.Kind() == protoreflect.MessageKind) || (fd.Kind() == protoreflect.GroupKind):
 			value, err := encodeMessage(fd.Message(), v)
 			if err == nil {
-				u[newPath] = []string{value}
+				u.Set(newPath, value)
 				return true
 			}
 			if err = encodeByField(u, newPath, v.Message()); err != nil {
@@ -89,7 +89,7 @@ func encodeByField(u url.Values, path string, m protoreflect.Message) (finalErr
 				finalErr = err
 				return false
 			}
-			u[newPath] = []string{value}
+			u.Set(newPath, value)
 		}
 		return true
 	})
diff --git a/encoding/proto/proto.go b/encoding/proto/proto.go
index 96051a96..0da5e0dc 100644
--- a/encoding/proto/proto.go
+++ b/encoding/proto/proto.go
@@ -3,9 +3,9 @@
 package proto
 
 import (
-	"github.com/go-kratos/kratos/v2/encoding"
-
 	"google.golang.org/protobuf/proto"
+
+	"github.com/go-kratos/kratos/v2/encoding"
 )
 
 // Name is the name registered for the proto compressor.
diff --git a/middleware/circuitbreaker/circuitbreaker.go b/middleware/circuitbreaker/circuitbreaker.go
index 31433ee7..76f261fd 100644
--- a/middleware/circuitbreaker/circuitbreaker.go
+++ b/middleware/circuitbreaker/circuitbreaker.go
@@ -26,6 +26,15 @@ func WithGroup(g *group.Group) Option {
 	}
 }
 
+// WithCircuitBreaker with circuit breaker genFunc.
+func WithCircuitBreaker(genBreakerFunc func() circuitbreaker.CircuitBreaker) Option {
+	return func(o *options) {
+		o.group = group.NewGroup(func() interface{} {
+			return genBreakerFunc()
+		})
+	}
+}
+
 type options struct {
 	group *group.Group
 }
diff --git a/transport/grpc/server.go b/transport/grpc/server.go
index 9e5ff17f..e3a0cea9 100644
--- a/transport/grpc/server.go
+++ b/transport/grpc/server.go
@@ -67,6 +67,13 @@ func Middleware(m ...middleware.Middleware) ServerOption {
 	}
 }
 
+// CustomHealth Checks server.
+func CustomHealth() ServerOption {
+	return func(s *Server) {
+		s.customHealth = true
+	}
+}
+
 // TLSConfig with TLS config.
 func TLSConfig(c *tls.Config) ServerOption {
 	return func(s *Server) {
@@ -105,20 +112,21 @@ func Options(opts ...grpc.ServerOption) ServerOption {
 // Server is a gRPC server wrapper.
 type Server struct {
 	*grpc.Server
-	baseCtx    context.Context
-	tlsConf    *tls.Config
-	lis        net.Listener
-	err        error
-	network    string
-	address    string
-	endpoint   *url.URL
-	timeout    time.Duration
-	middleware matcher.Matcher
-	unaryInts  []grpc.UnaryServerInterceptor
-	streamInts []grpc.StreamServerInterceptor
-	grpcOpts   []grpc.ServerOption
-	health     *health.Server
-	metadata   *apimd.Server
+	baseCtx      context.Context
+	tlsConf      *tls.Config
+	lis          net.Listener
+	err          error
+	network      string
+	address      string
+	endpoint     *url.URL
+	timeout      time.Duration
+	middleware   matcher.Matcher
+	unaryInts    []grpc.UnaryServerInterceptor
+	streamInts   []grpc.StreamServerInterceptor
+	grpcOpts     []grpc.ServerOption
+	health       *health.Server
+	customHealth bool
+	metadata     *apimd.Server
 }
 
 // NewServer creates a gRPC server by options.
@@ -159,7 +167,9 @@ func NewServer(opts ...ServerOption) *Server {
 	srv.Server = grpc.NewServer(grpcOpts...)
 	srv.metadata = apimd.NewServer(srv.Server)
 	// internal register
-	grpc_health_v1.RegisterHealthServer(srv.Server, srv.health)
+	if !srv.customHealth {
+		grpc_health_v1.RegisterHealthServer(srv.Server, srv.health)
+	}
 	apimd.RegisterMetadataServer(srv.Server, srv.metadata)
 	reflection.Register(srv.Server)
 	return srv


Test patch:
diff --git a/cmd/kratos/internal/base/mod_test.go b/cmd/kratos/internal/base/mod_test.go
index 577896b0..1cf4be96 100644
--- a/cmd/kratos/internal/base/mod_test.go
+++ b/cmd/kratos/internal/base/mod_test.go
@@ -18,8 +18,6 @@ func TestModulePath(t *testing.T) {
 		t.Fatal(err)
 	}
 
-	defer os.RemoveAll("/tmp/test_mod")
-
 	f, err := os.Create("/tmp/test_mod/go.mod")
 	if err != nil {
 		t.Fatal(err)
@@ -38,6 +36,8 @@ go 1.16`
 		t.Fatal(err)
 	}
 	if p != "github.com/go-kratos/kratos/v2" {
-		t.Fatalf("want: %s, got: %s", "module github.com/go-kratos/kratos/v2", p)
+		t.Fatalf("want: %s, got: %s", "github.com/go-kratos/kratos/v2", p)
 	}
+
+	t.Cleanup(func() { os.RemoveAll("/tmp/test_mod") })
 }
diff --git a/contrib/config/apollo/apollo_test.go b/contrib/config/apollo/apollo_test.go
new file mode 100644
index 00000000..185ca4c6
--- /dev/null
+++ b/contrib/config/apollo/apollo_test.go
@@ -0,0 +1,90 @@
+package apollo
+
+import (
+	"testing"
+)
+
+func Test_genKey(t *testing.T) {
+	type args struct {
+		ns  string
+		sub string
+	}
+	tests := []struct {
+		name string
+		args args
+		want string
+	}{
+		{
+			name: "blank namespace",
+			args: args{
+				ns:  "",
+				sub: "x.y",
+			},
+			want: "x.y",
+		},
+		{
+			name: "properties namespace",
+			args: args{
+				ns:  "application",
+				sub: "x.y",
+			},
+			want: "application.x.y",
+		},
+		{
+			name: "namespace with format",
+			args: args{
+				ns:  "app.yaml",
+				sub: "x.y",
+			},
+			want: "app.x.y",
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			if got := genKey(tt.args.ns, tt.args.sub); got != tt.want {
+				t.Errorf("genKey() = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}
+
+func Test_format(t *testing.T) {
+	tests := []struct {
+		name      string
+		namespace string
+		want      string
+	}{
+		{
+			name:      "properties namespace",
+			namespace: "application",
+			want:      "json",
+		},
+		{
+			name:      "properties namespace #1",
+			namespace: "app.setting",
+			want:      "json",
+		},
+		{
+			name:      "namespace with format[yaml]",
+			namespace: "app.yaml",
+			want:      "yaml",
+		},
+		{
+			name:      "namespace with format[yml]",
+			namespace: "app.yml",
+			want:      "yml",
+		},
+		{
+			name:      "namespace with format[json]",
+			namespace: "app.json",
+			want:      "json",
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			if got := format(tt.namespace); got != tt.want {
+				t.Errorf("format() = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}
diff --git a/encoding/form/form_test.go b/encoding/form/form_test.go
index 647d1edc..013edbd1 100644
--- a/encoding/form/form_test.go
+++ b/encoding/form/form_test.go
@@ -21,11 +21,6 @@ type LoginRequest struct {
 	Password string `json:"password,omitempty"`
 }
 
-type TestModel struct {
-	ID   int32  `json:"id"`
-	Name string `json:"name"`
-}
-
 func TestFormCodecMarshal(t *testing.T) {
 	req := &LoginRequest{
 		Username: "kratos",
@@ -33,10 +28,10 @@ func TestFormCodecMarshal(t *testing.T) {
 	}
 	content, err := encoding.GetCodec(Name).Marshal(req)
 	if err != nil {
-		t.Errorf("marshal error: %v", err)
+		t.Fatal(err)
 	}
 	if !reflect.DeepEqual([]byte("password=kratos_pwd&username=kratos"), content) {
-		t.Errorf("expect %v, got %v", []byte("password=kratos_pwd&username=kratos"), content)
+		t.Errorf("expect %s, got %s", "password=kratos_pwd&username=kratos", content)
 	}
 
 	req = &LoginRequest{
@@ -45,23 +40,25 @@ func TestFormCodecMarshal(t *testing.T) {
 	}
 	content, err = encoding.GetCodec(Name).Marshal(req)
 	if err != nil {
-		t.Errorf("expect %v, got %v", nil, err)
+		t.Fatal(err)
 	}
 	if !reflect.DeepEqual([]byte("username=kratos"), content) {
-		t.Errorf("expect %v, got %v", []byte("username=kratos"), content)
+		t.Errorf("expect %s, got %s", "username=kratos", content)
 	}
 
-	m := &TestModel{
+	m := struct {
+		ID   int32  `json:"id"`
+		Name string `json:"name"`
+	}{
 		ID:   1,
 		Name: "kratos",
 	}
 	content, err = encoding.GetCodec(Name).Marshal(m)
-	t.Log(string(content))
 	if err != nil {
-		t.Errorf("expect %v, got %v", nil, err)
+		t.Fatal(err)
 	}
 	if !reflect.DeepEqual([]byte("id=1&name=kratos"), content) {
-		t.Errorf("expect %v, got %v", []byte("id=1&name=kratos"), content)
+		t.Errorf("expect %s, got %s", "id=1&name=kratos", content)
 	}
 }
 
@@ -72,13 +69,13 @@ func TestFormCodecUnmarshal(t *testing.T) {
 	}
 	content, err := encoding.GetCodec(Name).Marshal(req)
 	if err != nil {
-		t.Errorf("expect %v, got %v", nil, err)
+		t.Fatal(err)
 	}
 
 	bindReq := new(LoginRequest)
 	err = encoding.GetCodec(Name).Unmarshal(content, bindReq)
 	if err != nil {
-		t.Errorf("expect %v, got %v", nil, err)
+		t.Fatal(err)
 	}
 	if !reflect.DeepEqual("kratos", bindReq.Username) {
 		t.Errorf("expect %v, got %v", "kratos", bindReq.Username)
@@ -119,41 +116,41 @@ func TestProtoEncodeDecode(t *testing.T) {
 	}
 	content, err := encoding.GetCodec(Name).Marshal(in)
 	if err != nil {
-		t.Errorf("expect %v, got %v", nil, err)
+		t.Fatal(err)
 	}
-	if !reflect.DeepEqual("a=19&age=18&b=true&bool=false&byte=MTIz&bytes=MTIz&count=3&d=22.22&double=12.33&duration="+
+	if "a=19&age=18&b=true&bool=false&byte=MTIz&bytes=MTIz&count=3&d=22.22&double=12.33&duration="+
 		"2m0.000000022s&field=1%2C2&float=12.34&id=2233&int32=32&int64=64&map%5Bkratos%5D=https%3A%2F%2Fgo-kratos.dev%2F&"+
 		"numberOne=2233&price=11.23&sex=woman&simples=3344&simples=5566&string=go-kratos"+
-		"&timestamp=1970-01-01T00%3A00%3A20.000000002Z&uint32=32&uint64=64&very_simple.component=5566", string(content)) {
-		t.Errorf("rawpath is not equal to %v", string(content))
+		"&timestamp=1970-01-01T00%3A00%3A20.000000002Z&uint32=32&uint64=64&very_simple.component=5566" != string(content) {
+		t.Errorf("rawpath is not equal to %s", content)
 	}
 	in2 := &complex.Complex{}
 	err = encoding.GetCodec(Name).Unmarshal(content, in2)
 	if err != nil {
-		t.Errorf("expect %v, got %v", nil, err)
+		t.Fatal(err)
 	}
-	if !reflect.DeepEqual(int64(2233), in2.Id) {
+	if int64(2233) != in2.Id {
 		t.Errorf("expect %v, got %v", int64(2233), in2.Id)
 	}
-	if !reflect.DeepEqual("2233", in2.NoOne) {
+	if "2233" != in2.NoOne {
 		t.Errorf("expect %v, got %v", "2233", in2.NoOne)
 	}
-	if reflect.DeepEqual(in2.Simple, nil) {
+	if in2.Simple == nil {
 		t.Errorf("expect %v, got %v", nil, in2.Simple)
 	}
-	if !reflect.DeepEqual("5566", in2.Simple.Component) {
+	if "5566" != in2.Simple.Component {
 		t.Errorf("expect %v, got %v", "5566", in2.Simple.Component)
 	}
-	if reflect.DeepEqual(in2.Simples, nil) {
+	if in2.Simples == nil {
 		t.Errorf("expect %v, got %v", nil, in2.Simples)
 	}
-	if !reflect.DeepEqual(len(in2.Simples), 2) {
+	if len(in2.Simples) != 2 {
 		t.Errorf("expect %v, got %v", 2, len(in2.Simples))
 	}
-	if !reflect.DeepEqual("3344", in2.Simples[0]) {
+	if "3344" != in2.Simples[0] {
 		t.Errorf("expect %v, got %v", "3344", in2.Simples[0])
 	}
-	if !reflect.DeepEqual("5566", in2.Simples[1]) {
+	if "5566" != in2.Simples[1] {
 		t.Errorf("expect %v, got %v", "5566", in2.Simples[1])
 	}
 }
@@ -162,19 +159,18 @@ func TestDecodeStructPb(t *testing.T) {
 	req := new(ectest.StructPb)
 	query := `data={"name":"kratos"}&data_list={"name1": "kratos"}&data_list={"name2": "go-kratos"}`
 	if err := encoding.GetCodec(Name).Unmarshal([]byte(query), req); err != nil {
-		t.Errorf("expect %v, got %v", nil, err)
+		t.Fatal(err)
 	}
-	if !reflect.DeepEqual("kratos", req.Data.GetFields()["name"].GetStringValue()) {
+	if "kratos" != req.Data.GetFields()["name"].GetStringValue() {
 		t.Errorf("except %v, got %v", "kratos", req.Data.GetFields()["name"].GetStringValue())
 	}
 	if len(req.DataList) != 2 {
-		t.Errorf("execpt %v, got %v", 2, len(req.DataList))
-		return
+		t.Fatalf("execpt %v, got %v", 2, len(req.DataList))
 	}
-	if !reflect.DeepEqual("kratos", req.DataList[0].GetFields()["name1"].GetStringValue()) {
+	if "kratos" != req.DataList[0].GetFields()["name1"].GetStringValue() {
 		t.Errorf("except %v, got %v", "kratos", req.Data.GetFields()["name1"].GetStringValue())
 	}
-	if !reflect.DeepEqual("go-kratos", req.DataList[1].GetFields()["name2"].GetStringValue()) {
+	if "go-kratos" != req.DataList[1].GetFields()["name2"].GetStringValue() {
 		t.Errorf("except %v, got %v", "go-kratos", req.Data.GetFields()["name2"].GetStringValue())
 	}
 }
@@ -185,10 +181,10 @@ func TestDecodeBytesValuePb(t *testing.T) {
 	content := "bytes=" + val
 	in2 := &complex.Complex{}
 	if err := encoding.GetCodec(Name).Unmarshal([]byte(content), in2); err != nil {
-		t.Errorf("expect %v, got %v", nil, err)
+		t.Error(err)
 	}
-	if !reflect.DeepEqual(url, string(in2.Bytes.Value)) {
-		t.Errorf("except %v, got %v", val, string(in2.Bytes.Value))
+	if url != string(in2.Bytes.Value) {
+		t.Errorf("except %s, got %s", val, in2.Bytes.Value)
 	}
 }
 
@@ -198,8 +194,6 @@ func TestEncodeFieldMask(t *testing.T) {
 	}
 	if v := EncodeFieldMask(req.ProtoReflect()); v != "updateMask=foo,bar" {
 		t.Errorf("got %s", v)
-	} else {
-		t.Log(v)
 	}
 }
 
@@ -210,9 +204,8 @@ func TestOptional(t *testing.T) {
 		Sub:      &bdtest.Sub{Name: "bar"},
 		OptInt32: &v,
 	}
-	if v, _ := EncodeValues(req); v.Encode() != "name=foo&optInt32=100&sub.naming=bar" {
-		t.Errorf("got %s", v.Encode())
-	} else {
-		t.Log(v)
+	query, _ := EncodeValues(req)
+	if query.Encode() != "name=foo&optInt32=100&sub.naming=bar" {
+		t.Fatalf("got %s", query.Encode())
 	}
 }
diff --git a/encoding/form/proto_encode_test.go b/encoding/form/proto_encode_test.go
new file mode 100644
index 00000000..7d0cfa9e
--- /dev/null
+++ b/encoding/form/proto_encode_test.go
@@ -0,0 +1,110 @@
+package form
+
+import (
+	"testing"
+
+	"google.golang.org/protobuf/types/known/durationpb"
+	"google.golang.org/protobuf/types/known/fieldmaskpb"
+	"google.golang.org/protobuf/types/known/timestamppb"
+	"google.golang.org/protobuf/types/known/wrapperspb"
+
+	"github.com/go-kratos/kratos/v2/internal/testdata/complex"
+)
+
+func TestEncodeValues(t *testing.T) {
+	in := &complex.Complex{
+		Id:      2233,
+		NoOne:   "2233",
+		Simple:  &complex.Simple{Component: "5566"},
+		Simples: []string{"3344", "5566"},
+		B:       true,
+		Sex:     complex.Sex_woman,
+		Age:     18,
+		A:       19,
+		Count:   3,
+		Price:   11.23,
+		D:       22.22,
+		Byte:    []byte("123"),
+		Map:     map[string]string{"kratos": "https://go-kratos.dev/", "kratos_start": "https://go-kratos.dev/en/docs/getting-started/start/"},
+
+		Timestamp: &timestamppb.Timestamp{Seconds: 20, Nanos: 2},
+		Duration:  &durationpb.Duration{Seconds: 120, Nanos: 22},
+		Field:     &fieldmaskpb.FieldMask{Paths: []string{"1", "2"}},
+		Double:    &wrapperspb.DoubleValue{Value: 12.33},
+		Float:     &wrapperspb.FloatValue{Value: 12.34},
+		Int64:     &wrapperspb.Int64Value{Value: 64},
+		Int32:     &wrapperspb.Int32Value{Value: 32},
+		Uint64:    &wrapperspb.UInt64Value{Value: 64},
+		Uint32:    &wrapperspb.UInt32Value{Value: 32},
+		Bool:      &wrapperspb.BoolValue{Value: false},
+		String_:   &wrapperspb.StringValue{Value: "go-kratos"},
+		Bytes:     &wrapperspb.BytesValue{Value: []byte("123")},
+	}
+	query, err := EncodeValues(in)
+	if err != nil {
+		t.Fatal(err)
+	}
+	want := "a=19&age=18&b=true&bool=false&byte=MTIz&bytes=MTIz&count=3&d=22.22&double=12.33&duration=2m0.000000022s&field=1%2C2&float=12.34&id=2233&int32=32&int64=64&map%5Bkratos%5D=https%3A%2F%2Fgo-kratos.dev%2F&map%5Bkratos_start%5D=https%3A%2F%2Fgo-kratos.dev%2Fen%2Fdocs%2Fgetting-started%2Fstart%2F&numberOne=2233&price=11.23&sex=woman&simples=3344&simples=5566&string=go-kratos&timestamp=1970-01-01T00%3A00%3A20.000000002Z&uint32=32&uint64=64&very_simple.component=5566" // nolint:lll
+	if got := query.Encode(); want != got {
+		t.Errorf("want: %s, got: %s", want, got)
+	}
+}
+
+func TestJsonCamelCase(t *testing.T) {
+	tests := []struct {
+		camelCase string
+		snakeCase string
+	}{
+		{
+			"userId", "user_id",
+		},
+		{
+			"user", "user",
+		},
+		{
+			"userIdAndUsername", "user_id_and_username",
+		},
+		{
+			"", "",
+		},
+	}
+	for _, test := range tests {
+		t.Run(test.snakeCase, func(t *testing.T) {
+			camel := jsonCamelCase(test.snakeCase)
+			if camel != test.camelCase {
+				t.Errorf("want: %s, got: %s", test.camelCase, camel)
+			}
+		})
+	}
+}
+
+func TestIsASCIILower(t *testing.T) {
+	tests := []struct {
+		b     byte
+		lower bool
+	}{
+		{
+			'A', false,
+		},
+		{
+			'a', true,
+		},
+		{
+			',', false,
+		},
+		{
+			'1', false,
+		},
+		{
+			' ', false,
+		},
+	}
+	for _, test := range tests {
+		t.Run(string(test.b), func(t *testing.T) {
+			lower := isASCIILower(test.b)
+			if test.lower != lower {
+				t.Errorf("'%s' is not ascii lower", string(test.b))
+			}
+		})
+	}
+}
diff --git a/encoding/form/well_known_types_test.go b/encoding/form/well_known_types_test.go
index 5598211b..5018cf7a 100644
--- a/encoding/form/well_known_types_test.go
+++ b/encoding/form/well_known_types_test.go
@@ -26,12 +26,12 @@ func TestMarshalTimeStamp(t *testing.T) {
 		},
 	}
 	for _, v := range tests {
-		content, err := marshalTimestamp(v.input.ProtoReflect())
+		got, err := marshalTimestamp(v.input.ProtoReflect())
 		if err != nil {
-			t.Errorf("expect %v,got %v", nil, err)
+			t.Fatal(err)
 		}
-		if got, want := content, v.expect; got != want {
-			t.Errorf("expect %v,got %v", want, got)
+		if want := v.expect; got != want {
+			t.Errorf("expect %v, got %v", want, got)
 		}
 	}
 }
@@ -59,12 +59,12 @@ func TestMarshalDuration(t *testing.T) {
 		},
 	}
 	for _, v := range tests {
-		content, err := marshalDuration(v.input.ProtoReflect())
+		got, err := marshalDuration(v.input.ProtoReflect())
 		if err != nil {
-			t.Errorf("expect %v,got %v", nil, err)
+			t.Fatal(err)
 		}
-		if got, want := content, v.expect; got != want {
-			t.Errorf("expect %v,got %v", want, got)
+		if want := v.expect; got != want {
+			t.Errorf("expect %s, got %s", want, got)
 		}
 	}
 }
@@ -84,12 +84,12 @@ func TestMarshalBytes(t *testing.T) {
 		},
 	}
 	for _, v := range tests {
-		content, err := marshalBytes(v.input)
+		got, err := marshalBytes(v.input)
 		if err != nil {
-			t.Errorf("expect %v,got %v", nil, err)
+			t.Fatal(err)
 		}
-		if got, want := content, v.expect; got != want {
-			t.Errorf("expect %v,got %v", want, got)
+		if want := v.expect; got != want {
+			t.Errorf("expect %v, got %v", want, got)
 		}
 	}
 }