From 12998d33639006d8c961c98b1b3220b01d1e6f34 Mon Sep 17 00:00:00 2001 From: Justin Hwang Date: Wed, 16 Jul 2025 13:21:07 -0400 Subject: [PATCH] chore: use go 1.20 idiomatic string<->byte conversion As of Go 1.20, this is the idiomatic way to convert from Bytes to String and vice-versa. This updates `go.mod` to 1.20, builds have already been running on 1.23.x and 1.24.x since #3274. --- go.mod | 2 +- internal/util/unsafe.go | 9 +- internal/util/unsafe_test.go | 158 +++++++++++++++++++++++++++++++++++ 3 files changed, 161 insertions(+), 8 deletions(-) create mode 100644 internal/util/unsafe_test.go diff --git a/go.mod b/go.mod index 83e8fd3d6d..0fc6b4f8b1 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 cbcd2cc090..f4c3c3f33c 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 0000000000..f49ae79ff9 --- /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) + } + }) +}