diff --git a/core/jsonx/json.go b/core/jsonx/json.go index 61b4a4193e52..4f50e265c006 100644 --- a/core/jsonx/json.go +++ b/core/jsonx/json.go @@ -6,8 +6,28 @@ import ( "fmt" "io" "strings" + "sync" ) +const maxBufferSize = 64 * 1024 // 64KB + +var bufferPool = sync.Pool{ + New: func() any { + return bytes.NewBuffer(make([]byte, 0, maxBufferSize)) + }, +} + +func getBuffer() *bytes.Buffer { + return bufferPool.Get().(*bytes.Buffer) +} + +func putBuffer(buf *bytes.Buffer) { + buf.Reset() + if buf.Cap() <= maxBufferSize { + bufferPool.Put(buf) + } +} + // Marshal marshals v into json bytes, without escaping HTML and removes the trailing newline. func Marshal(v any) ([]byte, error) { // why not use json.Marshal? https://github.com/golang/go/issues/28453 @@ -29,6 +49,25 @@ func Marshal(v any) ([]byte, error) { return bs, nil } +func MarshalWithBuffer(v any) ([]byte, error) { + buf := getBuffer() + defer putBuffer(buf) + + enc := json.NewEncoder(buf) + enc.SetEscapeHTML(false) + if err := enc.Encode(v); err != nil { + return nil, err + } + + bs := buf.Bytes() + // Remove trailing newline added by json.Encoder.Encode + if len(bs) > 0 && bs[len(bs)-1] == '\n' { + bs = bs[:len(bs)-1] + } + + return bs, nil +} + // MarshalToString marshals v into a string. func MarshalToString(v any) (string, error) { data, err := Marshal(v) diff --git a/core/jsonx/json_test.go b/core/jsonx/json_test.go index 27e22b3ab387..0af4d7d73aab 100644 --- a/core/jsonx/json_test.go +++ b/core/jsonx/json_test.go @@ -9,7 +9,7 @@ import ( ) func TestMarshal(t *testing.T) { - var v = struct { + v := struct { Name string `json:"name"` Age int `json:"age"` }{ @@ -22,7 +22,7 @@ func TestMarshal(t *testing.T) { } func TestMarshalToString(t *testing.T) { - var v = struct { + v := struct { Name string `json:"name"` Age int `json:"age"` }{ @@ -204,3 +204,290 @@ func Test_doMarshalJson(t *testing.T) { }) } } + +func TestMarshalWithBuffer(t *testing.T) { + v := struct { + Name string `json:"name"` + Age int `json:"age"` + }{ + Name: "John", + Age: 30, + } + + bs, err := MarshalWithBuffer(v) + assert.Nil(t, err) + assert.Equal(t, `{"name":"John","age":30}`, string(bs)) + + // Test consistency with Marshal + bs_marshal, err := Marshal(v) + assert.Nil(t, err) + assert.Equal(t, string(bs_marshal), string(bs)) +} + +func TestMarshalWithBufferError(t *testing.T) { + _, err := MarshalWithBuffer(make(chan int)) + assert.NotNil(t, err) +} + +func TestMarshalWithBufferReuse(t *testing.T) { + // First marshal + v1 := map[string]string{"key": "value1"} + bs1, err := MarshalWithBuffer(v1) + assert.Nil(t, err) + assert.Equal(t, `{"key":"value1"}`, string(bs1)) + + // Reset buffer and reuse + v2 := map[string]string{"key": "value2"} + bs2, err := MarshalWithBuffer(v2) + assert.Nil(t, err) + assert.Equal(t, `{"key":"value2"}`, string(bs2)) +} + +func TestMarshalWithBufferMultipleTypes(t *testing.T) { + tests := []struct { + name string + args any + want string + }{ + { + name: "nil", + args: nil, + want: "null", + }, + { + name: "string", + args: "hello", + want: `"hello"`, + }, + { + name: "int", + args: 42, + want: "42", + }, + { + name: "bool", + args: true, + want: "true", + }, + { + name: "struct", + args: struct { + Name string `json:"name"` + }{Name: "test"}, + want: `{"name":"test"}`, + }, + { + name: "slice", + args: []int{1, 2, 3}, + want: "[1,2,3]", + }, + { + name: "map", + args: map[string]int{"a": 1, "b": 2}, + want: `{"a":1,"b":2}`, + }, + { + name: "url with special characters", + args: "https://example.com/api?name=test&age=25", + want: `"https://example.com/api?name=test&age=25"`, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := MarshalWithBuffer(tt.args) + assert.Nil(t, err, "MarshalWithBuffer should not return error for %v", tt.args) + assert.Equal(t, tt.want, string(got), "MarshalWithBuffer(%v)", tt.args) + }) + } +} + +// 基准测试数据结构 +type Person struct { + ID int `json:"id"` + Name string `json:"name"` + Email string `json:"email"` + Age int `json:"age"` + Address Address `json:"address"` + PhoneNumber string `json:"phone_number"` + IsActive bool `json:"is_active"` + Tags []string `json:"tags"` + Metadata map[string]string `json:"metadata"` +} + +type Address struct { + Street string `json:"street"` + City string `json:"city"` + State string `json:"state"` + ZipCode string `json:"zip_code"` + Country string `json:"country"` +} + +func generateTestData(size int) []Person { + people := make([]Person, size) + for i := 0; i < size; i++ { + people[i] = Person{ + ID: i + 1, + Name: fmt.Sprintf("Person %d", i+1), + Email: fmt.Sprintf("person%d@example.com", i+1), + Age: 20 + (i % 50), + Address: Address{ + Street: fmt.Sprintf("%d Main St", (i+1)*100), + City: "New York", + State: "NY", + ZipCode: fmt.Sprintf("100%02d", i%100), + Country: "USA", + }, + PhoneNumber: fmt.Sprintf("+1-555-%04d", i+1), + IsActive: i%2 == 0, + Tags: []string{"tag1", "tag2", "tag3"}, + Metadata: map[string]string{ + "department": "Engineering", + "level": "Senior", + "location": "Remote", + }, + } + } + return people +} + +// 基准测试:对比 Marshal 和 MarshalWithBuffer 的性能 +func BenchmarkMarshal_vs_MarshalWithBuffer(b *testing.B) { + person := generateTestData(1)[0] + + b.Run("Marshal", func(b *testing.B) { + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, err := Marshal(person) + if err != nil { + b.Fatal(err) + } + } + }) + + b.Run("MarshalWithBuffer", func(b *testing.B) { + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, err := MarshalWithBuffer(person) + if err != nil { + b.Fatal(err) + } + } + }) +} + +// 测试不同大小数据的性能 +func BenchmarkMarshal_DifferentSizes(b *testing.B) { + sizes := []int{1, 10, 100, 1000} + + for _, size := range sizes { + data := generateTestData(size) + + b.Run(fmt.Sprintf("Marshal_%d_items", size), func(b *testing.B) { + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, err := Marshal(data) + if err != nil { + b.Fatal(err) + } + } + }) + + b.Run(fmt.Sprintf("MarshalWithBuffer_%d_items", size), func(b *testing.B) { + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, err := MarshalWithBuffer(data) + if err != nil { + b.Fatal(err) + } + } + }) + } +} + +// 内存分配对比测试 +func BenchmarkMarshal_MemoryAllocs(b *testing.B) { + person := generateTestData(1)[0] + + b.Run("Marshal_Allocs", func(b *testing.B) { + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, err := Marshal(person) + if err != nil { + b.Fatal(err) + } + } + }) + + b.Run("MarshalWithBuffer_Allocs", func(b *testing.B) { + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, err := MarshalWithBuffer(person) + if err != nil { + b.Fatal(err) + } + } + }) +} + +// 并发性能测试 +func BenchmarkMarshal_Concurrent(b *testing.B) { + person := generateTestData(1)[0] + + b.Run("Marshal_Concurrent", func(b *testing.B) { + b.ResetTimer() + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + _, err := Marshal(person) + if err != nil { + b.Fatal(err) + } + } + }) + }) + + b.Run("MarshalWithBuffer_Concurrent", func(b *testing.B) { + b.ResetTimer() + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + _, err := MarshalWithBuffer(person) + if err != nil { + b.Fatal(err) + } + } + }) + }) +} + +// 测试简单数据类型的性能 +func BenchmarkMarshal_SimpleTypes(b *testing.B) { + simpleData := map[string]any{ + "string": "hello world", + "int": 42, + "bool": true, + "slice": []int{1, 2, 3, 4, 5}, + "map": map[string]int{"a": 1, "b": 2, "c": 3}, + } + + b.Run("Marshal_SimpleTypes", func(b *testing.B) { + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, err := Marshal(simpleData) + if err != nil { + b.Fatal(err) + } + } + }) + + b.Run("MarshalWithBuffer_SimpleTypes", func(b *testing.B) { + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, err := MarshalWithBuffer(simpleData) + if err != nil { + b.Fatal(err) + } + } + }) +} diff --git a/rest/httpx/responses.go b/rest/httpx/responses.go index dc1a7ecedcdb..a280cfd40b0c 100644 --- a/rest/httpx/responses.go +++ b/rest/httpx/responses.go @@ -29,7 +29,8 @@ func Error(w http.ResponseWriter, err error, fns ...func(w http.ResponseWriter, // ErrorCtx writes err into w. func ErrorCtx(ctx context.Context, w http.ResponseWriter, err error, - fns ...func(w http.ResponseWriter, err error)) { + fns ...func(w http.ResponseWriter, err error), +) { writeJson := func(w http.ResponseWriter, code int, v any) { WriteJsonCtx(ctx, w, code, v) } @@ -141,7 +142,8 @@ func buildErrorHandler(ctx context.Context) func(error) (int, any) { func doHandleError(w http.ResponseWriter, err error, handler func(error) (int, any), writeJson func(w http.ResponseWriter, code int, v any), - fns ...func(w http.ResponseWriter, err error)) { + fns ...func(w http.ResponseWriter, err error), +) { if handler == nil { if len(fns) > 0 { for _, fn := range fns { @@ -173,7 +175,7 @@ func doHandleError(w http.ResponseWriter, err error, handler func(error) (int, a } func doWriteJson(w http.ResponseWriter, code int, v any) error { - bs, err := jsonx.Marshal(v) + bs, err := jsonx.MarshalWithBuffer(v) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return fmt.Errorf("marshal json failed, error: %w", err)