string和[]byte详解
string标准概念
Go标准库builtin
给出了所有内置类型的定义。 源代码位于src/builtin/builtin.go
,其中关于string的描述如下:
// string is the set of all strings of 8-bit bytes, conventionally but not
// necessarily representing UTF-8-encoded text. A string may be empty, but
// not nil. Values of string type are immutable.
type string string
可以看出,string是对单位字节的集合,通常但并不一定是UTF-8编码的文本。
另外,还提到了两点,非常重要:
- string可以为空(长度为0),但不会是nil;
- string对象不可以修改。
string 数据结构
源码包src/runtime/string.go:stringStruct
定义了string的数据结构:
type stringStruct struct {
str unsafe.Pointer //字符串的首地址;
len int //字符串的长度;
}
string数据结构跟切片有些类似,只不过切片还有一个表示容量的成员,事实上string和切片,准确的说是byte切片经常发生转换
string操作
声明
如下代码所示,可以声明一个string变量变赋予初值:
var str string
str = "Hello World"
//或者
str2:="123"
字符串构建过程是先跟据字符串构建stringStruct,再转换成string。转换的源码如下:
func gostringnocopy(str *byte) string { // 跟据字符串地址构建string
ss := stringStruct{str: unsafe.Pointer(str), len: findnull(str)} // 先构造stringStruct
s := *(*string)(unsafe.Pointer(&ss)) // 再将stringStruct转换成string
return s
}
string在runtime包中就是stringStruct,对外呈现叫做string
[]byte转string
func GetStringBySlice(s []byte) string {
return string(s)
}
需要注意的是这种转换需要一次内存拷贝。
转换过程如下:
- 跟据切片的长度申请内存空间,假设内存地址为p,切片长度为len(b);
- 构建string(string.str = p;string.len = len;)
- 拷贝数据(切片中数据拷贝到新申请的内存空间)
举例
ex := []byte{'3', '2', '1'}
strEx := string(ex)
ex[1] = '3'
fmt.Println(string(ex))
fmt.Println(strEx)
//打印结果331 和321
转换示意图:
string转[]byte
string也可以方便的转成byte切片,如下所示:
func GetSliceByString(str string) []byte {
return []byte(str)
}
string转换成byte切片,也需要一次内存拷贝,其过程如下:
- 申请切片内存空间
- 将string拷贝到切片
转换示意图:
字符串拼接
字符串可以很方便的拼接,像下面这样:
str := "Str1" + "Str2" + "Str3"
即便有非常多的字符串需要拼接,性能上也有比较好的保证,因为新字符串的内存空间是一次分配完成的,所以性能消耗主要在拷贝数据上。
一个拼接语句的字符串编译时都会被存放到一个切片中,拼接过程需要遍历两次切片,第一次遍历获取总的字符串长度,据此申请内存,第二次遍历会把字符串逐个拷贝过去。
func concatstrings(a []string) string { // 字符串拼接
length := 0 // 拼接后总的字符串长度
for _, str := range a {
length += length(str)
}
s, b := rawstring(length) // 生成指定大小的字符串,返回一个string和切片,二者共享内存空间
for _, str := range a {
copy(b, str) // string无法修改,只能通过切片修改
b = b[len(str):]
}
return s
}
rawstring()源代码如下:
func rawstring(size int) (s string, b []byte) { // 生成一个新的string,返回的string和切片共享相同的空间
p := mallocgc(uintptr(size), nil, false)
stringStructOf(&s).str = p
stringStructOf(&s).len = size
*(*slice)(unsafe.Pointer(&b)) = slice{p, size, size}
return
}
为什么字符串不允许修改?
可以看到修改字符串时会报错。
s := "foobar阿斯蒂芬"
fmt.Println(s)
fmt.Println(&s)
s = "qweqweqweqweqwe"
fmt.Println(s)
fmt.Println(&s)
你改变一个字符串变量的值,只是改变指针字段的指向,指向新的值转化而来的字节数组,原来的字节数组并没有被改变,而只是被弃用了而已(若引用数为零则被回收)
不可变的意思是说 不能像c语言那样去修改字符串的值
string通常指向字符串字面量,而字符串字面量存储位置是只读段,而不是堆或栈上,所以才有了string不可修改的约定。
[]byte转换成string一定会拷贝内存吗?
byte切片转换成string的场景很多,为了性能上的考虑,有时候只是临时需要字符串的场景下,byte切片转换成string时并不会拷贝内存,而是直接返回一个string,这个string的指针(string.str)指向切片的内存。
比如,编译器会识别如下临时场景:
- 使用m[string(b)]来查找map(map是string为key,临时把切片b转成string);
- 字符串拼接,如”<” + “string(b)” + “>”;
- 字符串比较:string(b) == “foo”
因为是临时把byte切片转换成string,也就避免了因byte切片同容改成而导致string引用失败的情况,所以此时可以不必拷贝内存新建一个string。
string和[]byte如何取舍
string和[]byte都可以表示字符串,但因数据结构不同,其衍生出来的方法也不同,要跟据实际应用场景来选择。
string 擅长的场景:
- 需要字符串比较的场景;
- 不需要nil字符串的场景;
[]byte擅长的场景:
- 修改字符串的场景,尤其是修改粒度为1个字节;
- 函数返回值,需要用nil表示含义的场景;
- 需要切片操作的场景;
虽然看起来string适用的场景不如[]byte多,但因为string直观,在实际应用中还是大量存在,在偏底层的实现中[]byte使用更多