mirror of
https://gitee.com/johng/gf
synced 2026-06-06 16:21:40 +08:00
## Summary
- Fix false positive cycle detection in `gutil.Dump`
- Change from global pointer tracking to path-based cycle detection
- Shared references (multiple fields pointing to same object) no longer
incorrectly marked as cycles
## Problem
When using `gutil.Dump` with structs containing fields that share the
same `reflect.Type` (e.g., multiple `int` fields), the second field's
type was incorrectly displayed as `<cycle dump 0x...>`.
Example from issue:
```go
type User struct {
Id int `params:"id"`
Name int `params:"name"`
}
fields, _ := gstructs.TagFields(&user, []string{"p", "params"})
gutil.Dump(fields) // Second field's Type shows "<cycle dump>" instead of "int"
```
## Solution
Change cycle detection from global to path-based:
- Add `defer delete()` to remove pointer from tracking set when function
returns
- Only detect true cycles (A→B→A), not shared references (A,B both point
to C)
## Benchmark Comparison
Run benchmark with:
```bash
cd util/gutil && go test -bench=Benchmark_Dump -benchmem -run=^$
```
**Before fix (master branch):**
| Benchmark | ns/op | B/op | allocs/op |
|-----------|-------|------|-----------|
| Shallow | 4071 | 5989 | 85 |
| Nested20 | 105700 | 173993 | 1952 |
| Deep50 | 422515 | 692298 | 4869 |
**After fix (this PR):**
| Benchmark | ns/op | B/op | allocs/op |
|-----------|-------|------|-----------|
| Shallow | 4049 | 5989 | 85 |
| Nested20 | 103065 | 173990 | 1952 |
| Deep50 | 469502 | 692291 | 4869 |
**Performance impact**:
- Memory allocation (B/op and allocs/op) is **identical**
- Execution time is within normal variance (±5-10%)
- The `defer delete()` operation is O(1), negligible compared to
reflection overhead
## Test plan
- [x] All existing `gutil` tests pass (68 tests)
- [x] Added `Test_Dump_Issue2902_SharedPointer` - shared pointer not
marked as cycle
- [x] Added `Test_Dump_Issue2902_SameTypeFields` - original issue
scenario
- [x] Added benchmark tests for performance tracking
- [x] Verified real cycles still detected correctly
Fixes #2902
391 lines
8.9 KiB
Go
Executable File
391 lines
8.9 KiB
Go
Executable File
// Copyright GoFrame Author(https://goframe.org). All Rights Reserved.
|
|
//
|
|
// This Source Code Form is subject to the terms of the MIT License.
|
|
// If a copy of the MIT was not distributed with this file,
|
|
// You can obtain one at https://github.com/gogf/gf.
|
|
|
|
package gutil_test
|
|
|
|
import (
|
|
"bytes"
|
|
"testing"
|
|
|
|
"github.com/gogf/gf/v2/container/gtype"
|
|
"github.com/gogf/gf/v2/frame/g"
|
|
"github.com/gogf/gf/v2/net/ghttp"
|
|
"github.com/gogf/gf/v2/os/gstructs"
|
|
"github.com/gogf/gf/v2/os/gtime"
|
|
"github.com/gogf/gf/v2/test/gtest"
|
|
"github.com/gogf/gf/v2/text/gstr"
|
|
"github.com/gogf/gf/v2/util/gmeta"
|
|
"github.com/gogf/gf/v2/util/gutil"
|
|
)
|
|
|
|
func Test_Dump(t *testing.T) {
|
|
type CommonReq struct {
|
|
AppId int64 `json:"appId" v:"required" in:"path" des:"应用Id" sum:"应用Id Summary"`
|
|
ResourceId string `json:"resourceId" in:"query" des:"资源Id" sum:"资源Id Summary"`
|
|
}
|
|
type SetSpecInfo struct {
|
|
StorageType string `v:"required|in:CLOUD_PREMIUM,CLOUD_SSD,CLOUD_HSSD" des:"StorageType"`
|
|
Shards int32 `des:"shards 分片数" sum:"Shards Summary"`
|
|
Params []string `des:"默认参数(json 串-ClickHouseParams)" sum:"Params Summary"`
|
|
}
|
|
type CreateResourceReq struct {
|
|
CommonReq
|
|
gmeta.Meta `path:"/CreateResourceReq" method:"POST" tags:"default" sum:"CreateResourceReq sum"`
|
|
Name string
|
|
CreatedAt *gtime.Time
|
|
SetMap map[string]*SetSpecInfo
|
|
SetSlice []SetSpecInfo
|
|
Handler ghttp.HandlerFunc
|
|
internal string
|
|
}
|
|
req := &CreateResourceReq{
|
|
CommonReq: CommonReq{
|
|
AppId: 12345678,
|
|
ResourceId: "tdchqy-xxx",
|
|
},
|
|
Name: "john",
|
|
CreatedAt: gtime.Now(),
|
|
SetMap: map[string]*SetSpecInfo{
|
|
"test1": {
|
|
StorageType: "ssd",
|
|
Shards: 2,
|
|
Params: []string{"a", "b", "c"},
|
|
},
|
|
"test2": {
|
|
StorageType: "hssd",
|
|
Shards: 10,
|
|
Params: []string{},
|
|
},
|
|
},
|
|
SetSlice: []SetSpecInfo{
|
|
{
|
|
StorageType: "hssd",
|
|
Shards: 10,
|
|
Params: []string{"h"},
|
|
},
|
|
},
|
|
}
|
|
gtest.C(t, func(t *gtest.T) {
|
|
gutil.Dump(map[int]int{
|
|
100: 100,
|
|
})
|
|
gutil.Dump(req)
|
|
gutil.Dump(true, false)
|
|
gutil.Dump(make(chan int))
|
|
gutil.Dump(func() {})
|
|
gutil.Dump(nil)
|
|
gutil.Dump(gtype.NewInt(1))
|
|
})
|
|
}
|
|
|
|
func Test_Dump_Map(t *testing.T) {
|
|
gtest.C(t, func(t *gtest.T) {
|
|
buffer := bytes.NewBuffer(nil)
|
|
m := g.Map{
|
|
"k1": g.Map{
|
|
"k2": "v2",
|
|
},
|
|
}
|
|
gutil.DumpTo(buffer, m, gutil.DumpOption{})
|
|
t.Assert(buffer.String(), `{
|
|
"k1": {
|
|
"k2": "v2",
|
|
},
|
|
}`)
|
|
})
|
|
}
|
|
|
|
func TestDumpWithType(t *testing.T) {
|
|
type CommonReq struct {
|
|
AppId int64 `json:"appId" v:"required" in:"path" des:"应用Id" sum:"应用Id Summary"`
|
|
ResourceId string `json:"resourceId" in:"query" des:"资源Id" sum:"资源Id Summary"`
|
|
}
|
|
type SetSpecInfo struct {
|
|
StorageType string `v:"required|in:CLOUD_PREMIUM,CLOUD_SSD,CLOUD_HSSD" des:"StorageType"`
|
|
Shards int32 `des:"shards 分片数" sum:"Shards Summary"`
|
|
Params []string `des:"默认参数(json 串-ClickHouseParams)" sum:"Params Summary"`
|
|
}
|
|
type CreateResourceReq struct {
|
|
CommonReq
|
|
gmeta.Meta `path:"/CreateResourceReq" method:"POST" tags:"default" sum:"CreateResourceReq sum"`
|
|
Name string
|
|
CreatedAt *gtime.Time
|
|
SetMap map[string]*SetSpecInfo `v:"required" des:"配置Map"`
|
|
SetSlice []SetSpecInfo `v:"required" des:"配置Slice"`
|
|
Handler ghttp.HandlerFunc
|
|
internal string
|
|
}
|
|
req := &CreateResourceReq{
|
|
CommonReq: CommonReq{
|
|
AppId: 12345678,
|
|
ResourceId: "tdchqy-xxx",
|
|
},
|
|
Name: "john",
|
|
CreatedAt: gtime.Now(),
|
|
SetMap: map[string]*SetSpecInfo{
|
|
"test1": {
|
|
StorageType: "ssd",
|
|
Shards: 2,
|
|
Params: []string{"a", "b", "c"},
|
|
},
|
|
"test2": {
|
|
StorageType: "hssd",
|
|
Shards: 10,
|
|
Params: []string{},
|
|
},
|
|
},
|
|
SetSlice: []SetSpecInfo{
|
|
{
|
|
StorageType: "hssd",
|
|
Shards: 10,
|
|
Params: []string{"h"},
|
|
},
|
|
},
|
|
}
|
|
gtest.C(t, func(t *gtest.T) {
|
|
gutil.DumpWithType(map[int]int{
|
|
100: 100,
|
|
})
|
|
gutil.DumpWithType(req)
|
|
gutil.DumpWithType([][]byte{[]byte("hello")})
|
|
})
|
|
}
|
|
|
|
func Test_Dump_Slashes(t *testing.T) {
|
|
type Req struct {
|
|
Content string
|
|
}
|
|
req := &Req{
|
|
Content: `{"name":"john", "age":18}`,
|
|
}
|
|
gtest.C(t, func(t *gtest.T) {
|
|
gutil.Dump(req)
|
|
gutil.Dump(req.Content)
|
|
|
|
gutil.DumpWithType(req)
|
|
gutil.DumpWithType(req.Content)
|
|
})
|
|
}
|
|
|
|
// https://github.com/gogf/gf/issues/1661
|
|
func Test_Dump_Issue1661(t *testing.T) {
|
|
type B struct {
|
|
ba int
|
|
bb string
|
|
}
|
|
type A struct {
|
|
aa int
|
|
ab string
|
|
cc []B
|
|
}
|
|
gtest.C(t, func(t *gtest.T) {
|
|
var q1 []A
|
|
var q2 []A
|
|
q2 = make([]A, 0)
|
|
q1 = []A{{aa: 1, ab: "1", cc: []B{{ba: 1}, {ba: 2}, {ba: 3}}}, {aa: 2, ab: "2", cc: []B{{ba: 1}, {ba: 2}, {ba: 3}}}}
|
|
for _, q1v := range q1 {
|
|
x := []string{"11", "22"}
|
|
for _, iv2 := range x {
|
|
ls := q1v
|
|
for i := range ls.cc {
|
|
sj := iv2
|
|
ls.cc[i].bb = sj
|
|
}
|
|
q2 = append(q2, ls)
|
|
}
|
|
}
|
|
buffer := bytes.NewBuffer(nil)
|
|
gutil.DumpTo(buffer, q2, gutil.DumpOption{})
|
|
t.Assert(buffer.String(), `[
|
|
{
|
|
aa: 1,
|
|
ab: "1",
|
|
cc: [
|
|
{
|
|
ba: 1,
|
|
bb: "22",
|
|
},
|
|
{
|
|
ba: 2,
|
|
bb: "22",
|
|
},
|
|
{
|
|
ba: 3,
|
|
bb: "22",
|
|
},
|
|
],
|
|
},
|
|
{
|
|
aa: 1,
|
|
ab: "1",
|
|
cc: [
|
|
{
|
|
ba: 1,
|
|
bb: "22",
|
|
},
|
|
{
|
|
ba: 2,
|
|
bb: "22",
|
|
},
|
|
{
|
|
ba: 3,
|
|
bb: "22",
|
|
},
|
|
],
|
|
},
|
|
{
|
|
aa: 2,
|
|
ab: "2",
|
|
cc: [
|
|
{
|
|
ba: 1,
|
|
bb: "22",
|
|
},
|
|
{
|
|
ba: 2,
|
|
bb: "22",
|
|
},
|
|
{
|
|
ba: 3,
|
|
bb: "22",
|
|
},
|
|
],
|
|
},
|
|
{
|
|
aa: 2,
|
|
ab: "2",
|
|
cc: [
|
|
{
|
|
ba: 1,
|
|
bb: "22",
|
|
},
|
|
{
|
|
ba: 2,
|
|
bb: "22",
|
|
},
|
|
{
|
|
ba: 3,
|
|
bb: "22",
|
|
},
|
|
],
|
|
},
|
|
]`)
|
|
})
|
|
}
|
|
|
|
func Test_Dump_Cycle_Attribute(t *testing.T) {
|
|
type Abc struct {
|
|
ab int
|
|
cd *Abc
|
|
}
|
|
abc := Abc{ab: 3}
|
|
abc.cd = &abc
|
|
gtest.C(t, func(t *gtest.T) {
|
|
buffer := bytes.NewBuffer(nil)
|
|
g.DumpTo(buffer, abc, gutil.DumpOption{})
|
|
t.Assert(gstr.Contains(buffer.String(), "cycle"), true)
|
|
})
|
|
}
|
|
|
|
func Test_DumpJson(t *testing.T) {
|
|
gtest.C(t, func(t *gtest.T) {
|
|
var jsonContent = `{"a":1,"b":2}`
|
|
gutil.DumpJson(jsonContent)
|
|
})
|
|
}
|
|
|
|
// https://github.com/gogf/gf/issues/2902
|
|
func Test_Dump_Issue2902_SharedPointer(t *testing.T) {
|
|
type Inner struct {
|
|
Value int
|
|
}
|
|
type Outer struct {
|
|
A *Inner
|
|
B *Inner
|
|
}
|
|
gtest.C(t, func(t *gtest.T) {
|
|
// Shared pointer (not a cycle) should not be marked as cycle dump.
|
|
shared := &Inner{Value: 100}
|
|
data := Outer{A: shared, B: shared}
|
|
buffer := bytes.NewBuffer(nil)
|
|
g.DumpTo(buffer, data, gutil.DumpOption{})
|
|
output := buffer.String()
|
|
// The second field should show the actual value, not "cycle dump".
|
|
// Both fields point to the same object, but it's not a cycle.
|
|
t.Assert(gstr.Contains(output, "cycle"), false)
|
|
t.Assert(gstr.Count(output, "Value"), 2)
|
|
t.Assert(gstr.Count(output, "100"), 2)
|
|
})
|
|
}
|
|
|
|
// https://github.com/gogf/gf/issues/2902
|
|
func Test_Dump_Issue2902_SameTypeFields(t *testing.T) {
|
|
type User struct {
|
|
Id int `params:"id"`
|
|
Name int `params:"name"`
|
|
}
|
|
gtest.C(t, func(t *gtest.T) {
|
|
// Fields with same type (e.g., both are int) share the same reflect.Type,
|
|
// which should not be marked as cycle dump.
|
|
var user User
|
|
fields, _ := gstructs.TagFields(&user, []string{"p", "params"})
|
|
buffer := bytes.NewBuffer(nil)
|
|
g.DumpTo(buffer, fields, gutil.DumpOption{})
|
|
output := buffer.String()
|
|
// Both fields' Type should show "int", not "cycle dump".
|
|
t.Assert(gstr.Contains(output, "cycle"), false)
|
|
t.Assert(gstr.Count(output, `Type:`), 2)
|
|
})
|
|
}
|
|
|
|
type benchStruct struct {
|
|
A int
|
|
B string
|
|
C *benchStruct
|
|
D []int
|
|
E map[string]int
|
|
}
|
|
|
|
func createBenchNested(depth int) *benchStruct {
|
|
if depth <= 0 {
|
|
return nil
|
|
}
|
|
return &benchStruct{
|
|
A: depth,
|
|
B: "test",
|
|
C: createBenchNested(depth - 1),
|
|
D: []int{1, 2, 3, 4, 5},
|
|
E: map[string]int{"x": 1, "y": 2},
|
|
}
|
|
}
|
|
|
|
var (
|
|
benchShallow = &benchStruct{A: 1, B: "test", D: []int{1, 2, 3}, E: map[string]int{"a": 1}}
|
|
benchNested20 = createBenchNested(20)
|
|
benchDeep50 = createBenchNested(50)
|
|
)
|
|
|
|
func Benchmark_Dump_Shallow(b *testing.B) {
|
|
for i := 0; i < b.N; i++ {
|
|
var buf bytes.Buffer
|
|
gutil.DumpTo(&buf, benchShallow, gutil.DumpOption{})
|
|
}
|
|
}
|
|
|
|
func Benchmark_Dump_Nested20(b *testing.B) {
|
|
for i := 0; i < b.N; i++ {
|
|
var buf bytes.Buffer
|
|
gutil.DumpTo(&buf, benchNested20, gutil.DumpOption{})
|
|
}
|
|
}
|
|
|
|
func Benchmark_Dump_Deep50(b *testing.B) {
|
|
for i := 0; i < b.N; i++ {
|
|
var buf bytes.Buffer
|
|
gutil.DumpTo(&buf, benchDeep50, gutil.DumpOption{})
|
|
}
|
|
}
|