Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions core/jsonx/json.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
Expand Down
291 changes: 289 additions & 2 deletions core/jsonx/json_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import (
)

func TestMarshal(t *testing.T) {
var v = struct {
v := struct {
Name string `json:"name"`
Age int `json:"age"`
}{
Expand All @@ -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"`
}{
Expand Down Expand Up @@ -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%[email protected]", 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)
}
}
})
}
8 changes: 5 additions & 3 deletions rest/httpx/responses.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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)
Expand Down
Loading