diff --git a/go.mod b/go.mod index 83e8fd3d6..0fc6b4f8b 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/redis/go-redis/v9 -go 1.18 +go 1.20 require ( github.com/bsm/ginkgo/v2 v2.12.0 diff --git a/internal/util/unsafe.go b/internal/util/unsafe.go index cbcd2cc09..f4c3c3f33 100644 --- a/internal/util/unsafe.go +++ b/internal/util/unsafe.go @@ -8,15 +8,10 @@ import ( // BytesToString converts byte slice to string. func BytesToString(b []byte) string { - return *(*string)(unsafe.Pointer(&b)) + return unsafe.String(unsafe.SliceData(b), len(b)) } // StringToBytes converts string to byte slice. func StringToBytes(s string) []byte { - return *(*[]byte)(unsafe.Pointer( - &struct { - string - Cap int - }{s, len(s)}, - )) + return unsafe.Slice(unsafe.StringData(s), len(s)) } diff --git a/internal/util/unsafe_test.go b/internal/util/unsafe_test.go new file mode 100644 index 000000000..f49ae79ff --- /dev/null +++ b/internal/util/unsafe_test.go @@ -0,0 +1,158 @@ +package util + +import ( + "reflect" + "runtime" + "sync" + "testing" +) + +var ( + _tmpBytes []byte + _tmpString string +) + +func TestBytesToString(t *testing.T) { + tests := []struct { + input string + expect string + }{ + { + input: "string", + expect: "string", + }, + { + input: "", + expect: "", + }, + } + + for _, tt := range tests { + t.Run(tt.input, func(t *testing.T) { + input := []byte(tt.input) + if result := BytesToString(input); !reflect.DeepEqual(tt.expect, result) { + t.Errorf("BytesToString: Expected = %v, Got = %v", tt.expect, result) + } + + if len(tt.input) == 0 { + return + } + + input[0] = 'x' + if result := BytesToString(input); reflect.DeepEqual(tt.expect, result) { + t.Errorf("BytesToString: expected not equal: %v", tt.expect) + } + }) + } +} + +func TestStringToBytes(t *testing.T) { + tests := []struct { + input string + expect []byte + }{ + { + input: "string", + expect: []byte("string"), + }, + { + input: "", + expect: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.input, func(t *testing.T) { + if result := StringToBytes(tt.input); !reflect.DeepEqual(tt.expect, result) { + t.Errorf("StringToBytes: Expected = %v, Got = %v", tt.expect, result) + } + }) + } +} + +func TestBytesToStringGC(t *testing.T) { + var ( + expect = t.Name() + x string + wg sync.WaitGroup + ) + + wg.Add(1) + go func() { + defer wg.Done() + tmp := append([]byte(nil), t.Name()...) + x = BytesToString(tmp) + }() + wg.Wait() + + for i := 0; i < 100; i++ { + runtime.GC() + } + + if !reflect.DeepEqual(expect, x) { + t.Errorf("Expected = %v, Got = %v", expect, x) + } +} + +func TestStringToBytesGC(t *testing.T) { + var ( + expect = []byte(t.Name()) + x []byte + wg sync.WaitGroup + ) + + wg.Add(1) + go func() { + defer wg.Done() + tmp := append([]byte(nil), t.Name()...) + x = StringToBytes(string(tmp)) + }() + wg.Wait() + + for i := 0; i < 100; i++ { + runtime.GC() + } + if !reflect.DeepEqual(expect, x) { + t.Errorf("Expected = %v, Got = %v", expect, x) + } +} + +func BenchmarkStringToBytes(b *testing.B) { + input := b.Name() + + b.Run("copy", func(b *testing.B) { + b.ReportAllocs() + + for i := 0; i < b.N; i++ { + _tmpBytes = []byte(input) + } + }) + + b.Run("unsafe", func(b *testing.B) { + b.ReportAllocs() + + for i := 0; i < b.N; i++ { + _tmpBytes = StringToBytes(input) + } + }) +} + +func BenchmarkBytesToString(b *testing.B) { + input := []byte(b.Name()) + + b.Run("copy", func(b *testing.B) { + b.ReportAllocs() + + for i := 0; i < b.N; i++ { + _tmpString = string(input) + } + }) + + b.Run("unsafe", func(b *testing.B) { + b.ReportAllocs() + + for i := 0; i < b.N; i++ { + _tmpString = BytesToString(input) + } + }) +}