Why Go 1.22’s []byte(str) Conversion Beats Unsafe: Benchmarks and Deep Dive
The article investigates Go 1.22’s claim that simple type casting []byte(str) can replace unsafe‑based string‑to‑byte conversions, presents four implementation variants, runs detailed benchmarks on macOS M2 and Linux amd64, analyses compiler inlining and escape behavior, and explains the hidden pitfalls of capacity and mutability in the k8s shortcut.
Background
The claim that Go 1.22 allows []byte(str) without the unsafe package originates from Kubernetes issue #124656 ( https://github.com/kubernetes/kubernetes/issues/124656 ). The article verifies this claim and compares it with three other conversion techniques.
Conversion Techniques
Direct cast (plain type conversion):
func toRawBytes(s string) []byte {
if len(s) == 0 {
return nil
}
return []byte(s)
}
func toRawString(b []byte) string {
if len(b) == 0 {
return ""
}
return string(b)
}Traditional unsafe (reflect‑based) :
type SliceHeader struct { Data uintptr; Len int; Cap int }
type StringHeader struct { Data uintptr; Len int }
func toReflectBytes(s string) []byte {
if len(s) == 0 { return nil }
x := (*[2]uintptr)(unsafe.Pointer(&s))
h := [3]uintptr{x[0], x[1], x[1]}
return *(*[]byte)(unsafe.Pointer(&h))
}
func toReflectString(b []byte) string {
if len(b) == 0 { return "" }
return *(*string)(unsafe.Pointer(&b))
}New unsafe (Go 1.22 helpers) :
func toBytes(s string) []byte {
if len(s) == 0 { return nil }
return unsafe.Slice(unsafe.StringData(s), len(s))
}
func toString(b []byte) string {
if len(b) == 0 { return "" }
return unsafe.String(unsafe.SliceData(b), len(b))
}Kubernetes shortcut (minimal unsafe reinterpretation):
func toK8sBytes(s string) []byte { return *(*[]byte)(unsafe.Pointer(&s)) }
func toK8sString(b []byte) string { return *(*string)(unsafe.Pointer(&b)) }Benchmark Setup
Two benchmark functions iterate over a map of the four implementations, converting a constant string "hello, world" and its byte slice counterpart. The benchmarks record ns/op, memory allocation and allocation count.
var s = "hello, world"
var bts = []byte("hello, world")
func BenchmarkStringToBytes(b *testing.B) { /* iterate over map and assign */ }
func BenchmarkBytesToString(b *testing.B) { /* iterate over map and assign */ }Runs were performed on:
macOS M2 (go1.22.6, arm64)
Linux amd64 (go1.22.0, Intel Xeon Platinum)
Benchmark Results
macOS M2
BenchmarkStringToBytes/DirectCast-8 78,813,638 14.73 ns/op 16 B/op 1 allocs/op
BenchmarkStringToBytes/Traditional-8 599,346,962 2.010 ns/op 0 B/op 0 allocs/op
BenchmarkStringToBytes/NewUnsafe-8 624,976,126 1.929 ns/op 0 B/op 0 allocs/op
BenchmarkStringToBytes/K8s-8 887,370,499 1.211 ns/op 0 B/op 0 allocs/op
BenchmarkBytesToString/DirectCast-8 92,011,309 12.68 ns/op 16 B/op 1 allocs/op
BenchmarkBytesToString/Traditional-8 815,922,964 1.471 ns/op 0 B/op 0 allocs/op
BenchmarkBytesToString/NewUnsafe-8 624,965,414 1.922 ns/op 0 B/op 0 allocs/op
BenchmarkBytesToString/K8s-8 1,000,000,000 1.194 ns/op 0 B/op 0 allocs/opLinux amd64
BenchmarkStringToBytes/DirectCast-2 30,606,319 42.02 ns/op 16 B/op 1 allocs/op
BenchmarkStringToBytes/Traditional-2 315,913,948 3.779 ns/op 0 B/op 0 allocs/op
BenchmarkStringToBytes/NewUnsafe-2 411,972,518 2.753 ns/op 0 B/op 0 allocs/op
BenchmarkStringToBytes/K8s-2 449,640,819 2.770 ns/op 0 B/op 0 allocs/op
BenchmarkBytesToString/DirectCast-2 38,716,465 29.18 ns/op 16 B/op 1 allocs/op
BenchmarkBytesToString/Traditional-2 458,832,459 2.593 ns/op 0 B/op 0 allocs/op
BenchmarkBytesToString/NewUnsafe-2 439,537,762 2.762 ns/op 0 B/op 0 allocs/op
BenchmarkBytesToString/K8s-2 478,885,546 2.375 ns/op 0 B/op 0 allocs/opOn both platforms the Kubernetes shortcut is fastest for both directions, while the three unsafe‑based methods are close to each other. The direct cast is consistently the slowest when the result escapes.
Why Direct Cast Appears Slow
A second benchmark isolates the conversion functions without storing the result, forcing the compiler to inline and apply escape analysis:
func BenchmarkStringToBytesRaw(b *testing.B) {
for i := 0; i < b.N; i++ { _ = toRawBytes(s) }
}
func BenchmarkBytesToStringRaw(b *testing.B) {
for i := 0; i < b.N; i++ { _ = toRawString(bts) }
}Running with -gcflags="-m=2" shows that ([]byte)(s) does not escape, yielding 0.2921 ns/op for the string‑to‑byte direction. In the earlier benchmark the assignment bts = fn(s) forces heap allocation, causing the observed slowdown.
Capacity Mystery in the Kubernetes Shortcut
Reinterpreting a string header as a slice leaves the cap field undefined. A test prints the three underlying fields of the slice header:
4375580047, 12, 4375914624
4375580047, 12, 4375914624The third value is random memory, not the true capacity. Consequences:
Appending to the slice triggers a SIGBUS panic because the capacity is not a real allocation.
Modifying the slice does not change the original string (the string remains immutable), but the operation still panics due to the invalid capacity.
Converting a []byte back to a string via the same unsafe trick makes the string appear mutable: after changing the first byte of the slice, the printed string changes from hello, world to Hello, world.
Overall Conclusions
Go 1.22’s compiler can turn a plain cast []byte(str) into a zero‑copy conversion when the result does not escape, delivering performance comparable to the unsafe helpers. However, the optimization is not universal; in many real‑world scenarios the compiler cannot eliminate the allocation, and the direct cast remains slower.
The Kubernetes shortcut is the fastest in the measured benchmarks but suffers from an undefined slice capacity and safety issues when the resulting slice is mutated or appended.
Recommended practice:
Prefer the direct cast only when you are certain the result stays on the stack (e.g., short‑lived functions).
Use the new unsafe helpers ( unsafe.Slice, unsafe.String, unsafe.StringData, unsafe.SliceData) for predictable zero‑copy behavior.
Avoid the Kubernetes‑style reinterpretation unless you explicitly manage capacity and mutability concerns.
Future Go releases (e.g., the upcoming 1.23 change referenced in cmd/compile: restore zero-copy string->[]byte optimization – https://github.com/golang/go/commit/925d2fb36c8e4c9c0e6e240a1621db36c34e5d31 ) may further improve zero‑copy support.
References
[1] Kubernetes issue: https://github.com/kubernetes/kubernetes/issues/124656
[2] Colobu article on Go 1.20 conversion: https://colobu.com/2022/09/06/string-byte-convertion/
[3] Go commit restoring zero‑copy optimization: https://github.com/golang/go/commit/925d2fb36c8e4c9c0e6e240a1621db36c34e5d31
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
BirdNest Tech Talk
Author of the rpcx microservice framework, original book author, and chair of Baidu's Go CMC committee.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
