string的基本组成
在go语言中,string的组成其实和slice非常相似,只不过string是不可变的。
type StringHeader struct {
Data uintptr
Len int
}
所以它的内部结构相比slice少了个cap。我们可以观察下面的小例子:
func print() {
str1 := "abc"
str2 := str1
str2 = "def"
fmt.Println(str1, str2) // 输出"abe def"
fmt.Println(&str1)
fmt.Println(&str2)
}
运行代码会发现,str1和str2的结果不一样,输出的地址也会不一样。换句话说,这两个是完全不一样的string。当我们对str2修改的时候,实际上是重新分配内存并构造了一个新的string,需要注意的是:由于它内部有两个字段,这种复制并不是原子的,并发读写实际会有问题
string什么时候会共享底层的数据,什么时候不会?
有了上面的例子,我们不禁发问。string内部变化的规律是咋样的呢。我们看一个更复杂的例子:
func main() {
s1 := "hello world"
s2 := s1
s3 := s1[:5]
x1 := (*reflect.StringHeader)(unsafe.Pointer(&s1))
x2 := (*reflect.StringHeader)(unsafe.Pointer(&s2))
x3 := (*reflect.StringHeader)(unsafe.Pointer(&s3))
fmt.Printf("x1: Data: %p, Len: %d\n", x1.Data, x1.Len)
fmt.Printf("x2: Data: %p, Len: %d\n", x2.Data, x2.Len)
fmt.Printf("x3: Data: %p, Len: %d\n", x3.Data, x3.Len)
s2 = "test"
x1 = (*reflect.StringHeader)(unsafe.Pointer(&s1))
x2 = (*reflect.StringHeader)(unsafe.Pointer(&s2))
x3 = (*reflect.StringHeader)(unsafe.Pointer(&s3))
fmt.Printf("x1: Data: %p, Len: %d\n", x1.Data, x1.Len)
fmt.Printf("x2: Data: %p, Len: %d\n", x2.Data, x2.Len)
fmt.Printf("x3: Data: %p, Len: %d\n", x3.Data, x3.Len)
}
观察代码的输出可以发现:
- 对于单纯的赋值给一个新的字符串对象,会共享底层的数据
- 修改共享变量会重新分配内存
- 对string的切片也是共享底层数据结构
同时,编译器对于string是有一定优化的。字符串内部化(string intern)是指一种让相同的字符串在内存中只保存一份的技术。对于要存储大量字符串的应用来说,它可以显著降低内存占用。换句话说:对于在编译期可以确定的字符串会共享同一份数据,而运行期才能确定的string则单独分配内存。
s1 := "123"
s2 := "1" + "23"
s3 := strconv.Itoa(123)
x1 := (*reflect.StringHeader)(unsafe.Pointer(&s1))
x2 := (*reflect.StringHeader)(unsafe.Pointer(&s2))
x3 := (*reflect.StringHeader)(unsafe.Pointer(&s3))
fmt.Printf("x1: Data: %p, Len: %d\n", x1.Data, x1.Len)
fmt.Printf("x2: Data: %p, Len: %d\n", x2.Data, x2.Len)
fmt.Printf("x3: Data: %p, Len: %d\n", x3.Data, x3.Len)
}
观察代码的输出可以发现:对于运行期转型过来的字符串,字符串内部化并没有生效。所以为了节省内存,我们可以维护一些字符串常量,运行的时候再到池里取用。若是修改则重新赋值生成,否则可以共用底层的数据结构。
string的并发读写
string的并发读写并不是原子的,像下面的代码就会触发panic。所以在复杂的项目中,对于string的修改也需要注意。
package main
import (
"fmt"
"time"
)
func main() {
flag := "init"
go func() {
for i := 1; i < 10000; i++ {
request(flag)
}
}()
for {
flag = ""
time.Sleep(10 * time.Nanosecond)
flag = "test"
time.Sleep(10 * time.Nanosecond)
}
}
func request(f string) {
k := f
println(fmt.Sprintf("fullPath: %s", k))
}
string的循环读取
string的默认存储是utf-8,它是一种字符的可变编码。在go语言中,也就是[]rune类型,它是实际上就是4个字节的长度。当我们想要取string中的某个字符的时候,不同的迭代方式会有不同的结果。
package main
import (
"fmt"
)
func main() {
str := "hello 世界"
//英文字符正常输出,中文乱码
for i := 0; i < len(str); i++ {
ch := str[i]
fmt.Println(i, ch, string(ch))
}
// 全都正常输出
for i, ch := range str {
fmt.Println(i, ch, string(ch))
}
// 以下都会正常输出
r := []rune(str)
for i := 0; i < len(r); i++ {
fmt.Println(i, string(r[i]))
}
for i, ch := range r {
fmt.Println(i, string(ch))
}
}
之所以能前面正常输出结果,是因为这里都是英文字符。后半部分是中文,使用下标每次取出一字节就会乱码。
string和[]byte互相转换
切片比string多了一个字段,转换的时候需要注意。这种转换避免了内存的拷贝,速度很快。但前提是保证slice是不可变的,否则转过去的string也会发生变化。
func stringTobyteSlice(s string) []byte {
tmp1 := (*[2]uintptr)(unsafe.Pointer(&s))
// 这里的slice,len和cap相等
tmp2 := [3]uintptr{tmp1[0], tmp1[1], tmp1[1]}
return *(*[]byte)(unsafe.Pointer(&tmp2))
}
func byteSliceToString(bytes []byte) string {
// 直接忽略cap字段
return *(*string)(unsafe.Pointer(&bytes))
}
//或者采用以下方法,更具可读性
func ZeroCopyStringToBytes(s string) (b []byte) {
bh := (*reflect.SliceHeader)(unsafe.Pointer(&b))
sh := (*reflect.StringHeader)(unsafe.Pointer(&s))
bh.Data = sh.Data
bh.Len = sh.Len
bh.Cap = sh.Len
return b
}
func ZeroCopyBytesToString(b []byte) string {
return *(*string)(unsafe.Pointer(&b))
}