This magicstring
package is designed to attach arbitrary data to a Go built-in string
type and read the data later. The string with attached data is called "magic string" here.
Call Attach
to attach data to a string and Read
to read the attached data in the magic string. Read
is thread-safe and extremely fast.
type T struct {
Name string
}
s1 := "Hello, world!"
data := &T{Name: "Kanon"}
s2 := Attach(s1, data)
attached := Read(s2).(*T)
fmt.Println(s1 == s2) // true
fmt.Println(attached == data) // true
Call Is
if we want to know whether a string is a magic string, .
s1 := "ordinary string"
s2 := Attach("magic string", 123)
s3 := s2
s4 := fmt.Sprint(s2)
fmt.Println(Is(s1)) // false
fmt.Println(Is(s2)) // true
fmt.Println(Is(s3)) // true
fmt.Println(Is(s4)) // false
Call Replace
if we want to replace the attached data to a new one in a magic string. As Replace
modifies the payload in a magic string, this call will affect all copies of this magic string and is not thread-safe.
s := Attach("magic string", 123)
fmt.Println(Read(s)) // 123
success := Replace(s, "replaced")
fmt.Println(success) // true
fmt.Println(Read(s)) // replaced
In general, we can use a magic string like an ordinary string. The attached data will be kept during all kinds of assignments. However, if we copy the content of a string to a buffer and create a new string from the buffer, we will lose the attached data.
s1 := Attach("magic string", 123)
buf := make([]byte, len(s1))
copy(buf, s1)
s2 := string(buf)
fmt.Println(Is(s1)) // true
fmt.Println(Is(s2)) // false
The simplest way to create an ordinary string from a magic string is to call Detach
. This function is optimized for ordinary strings. If a string is not a magic string, the Detach
simply returns the string to avoid an unnecessary memory allocation and memory copy.
A magic string cannot be sliced by built-in slice expression. If we want to keep the attachment in a magic string, we must call Slice
to slice it. If a string is not a magic string, Slice
just works the same as slice expression.
Memory allocation is highly optimized for small strings. The maximum size of a small string is 18,408 bytes right now. It's the maximum size of memory span classes, which is 18,432 bytes provided by runtime.ReadMemStats()
, minus the size of magic string payload struct, which is 24 bytes right now.
Here is the performance data running on my MacBook.
goos: darwin
goarch: amd64
pkg: github.com/huandu/go-magicstring
cpu: Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz
BenchmarkAttachSmallString-12 12312567 83.78 ns/op 32 B/op 1 allocs/op
BenchmarkAttachLarge1MBString-12 8511 165978 ns/op 1057046 B/op 3 allocs/op
BenchmarkReplaceSmallString-12 225952905 5.329 ns/op 0 B/op 0 allocs/op
BenchmarkReplaceLarge1MBString-12 228580016 5.268 ns/op 0 B/op 0 allocs/op
BenchmarkReadSmallString-12 312613485 3.915 ns/op 0 B/op 0 allocs/op
BenchmarkReadLarge1MBString-12 301951902 3.964 ns/op 0 B/op 0 allocs/op
This package is licensed under MIT license. See LICENSE for details.