string

2021/07/13 go探索发现 共 2824 字,约 9 分钟

用法

双引号和反单引号

使用双引号表示时,需要对特殊字符转义,而反单引号不需要对特殊字符转义。

字符串拼接

字符串拼接会触发内存分配及内存拷贝,单行语句拼接多个字符串值分配一次内存。 s=s+"a"+"b"会计算最终字符串长度后再分配内存。 多个字符串被编译器组织到一个切片中,拼接过程遍历两次切片,第一次获取字符串总长度,据此申请内存,第二次便利拷贝数据到变量。

类型转换

string 和 []byte 可以相互转换,但会发生一次内存拷贝,有一定的开销,在高频场景会成为性能瓶颈,数据库访问,http请求处理等。

[]byte 转 string

  • 根据切片长度申请内存空间,假设内存地址为p,长度len。
  • 构建string(string.str=p; string.len=len;)
  • 拷贝切片数据到新申请的内存地址p.

编译优化

byte切片转换成string的场景很多,出于性能考虑,有时候只是应用在临时需要字符串场景下,转换并不会拷贝内存,而是直接返回一个string,string(string.str)的指针指向切片的内存。

比如 编译器会识别如下临时场景:

  • 字符串比较 string(b)==”foo”
  • 字符串拼接:“a”+”string(b)”+”c”
  • m[string(b)]查找map

由于只是临时把byte切片转换成string,避免了因byte切片内容修改而导致string数据变化的问题,所以不必拷贝内存。

特点

UTF8 编码

string使用8比特字节集合来存储字符,而且存储字符是UTF-8编码。像一个汉子会占用2-3个字节。 如下所示:

s="世界"
for index,value:=range s{
   fmt.Printf("index:%d, value: %c\n",index,value)
}

索引的下标和字符是对不上的,字符串的长度是指字节数,而非字符数

值不可修改

字符串不可以修改,字符串变量可以接受新的字符串值,但不能通过下标方式修改字符串中的值。

数据结构

type stringStruct struct{
   str unsafe.Pointer //字符串首地址长度
   len int
}

string 结构和slice 只差cap字段,所以也叫做只读切片。
string结构体的str指针指向的是一个字符常量的地址, 这个地址里面的内容是不可以被改变的,因为它是只读的,但是这个指针可以指向不同的地址。 因为字符串作为只读的类型,我们并不会直接向字符串直接追加元素改变其本身的内存空间,所有在字符串上的写入操作都是通过拷贝实现的。

字符串生成时,先构建stringStruct对象,再转换成string. string在runtime包是stringStruct类型,对外呈现string类型。

函数传参 切片和string

string是以值传递的方式传入,由于string底层是结构体值传递方式拷贝了string结构体,结构体地址会发生变化,但是结构体中str指针地址在传参过程中没有发生变化。 str指向的是只读内存,对string字符串操作时产生了内存拷贝,str指针也指向了新的内存地址。

s := "a"
fmt.Printf("%p\n", &s)
b := func(s string) {
   fmt.Printf("%p\n", &s)
   s = s + "b"

}
b(s)
fmt.Println(s)
-----
0xc000010250
0xc000010260
a

切片作为参数传入时,结构体内部指向切片第一个元素的指针没有发生变化,对切片元素修改时,内存地址不会发生变化。但是进行append操作时,有可能会超出容量,产生内存拷贝导致内存地址发生变化,这种情况下传参前后切片不在指向同一个内存地址。

s := []int{1, 2}
fmt.Printf("%p\n", &s)
b := func(s []int) {
  s[0] = 3
  fmt.Printf("%p\n", &s)
}
b(s)
fmt.Println(s)
----
0xc00009a018
0xc00009a030
[3 2]

小结

为什么不允许修改字符串

C++ string 本身拥有内存空间,修改string是支持的,但在Go实现中,string包不包含内存空间,只有一个内存的指针。这样做的好处是string变得非常轻量,可以很方便地进行传递而不用担心内存拷贝。

因为string通常指字符串的字面量,字面量存储位置是只读段,而不是堆栈,所以才有了不可修改的约定。

string和[]byte应用场景:

string 场景:

  • 需要字符串比较多场景
  • 不需要nil字符串

[]byte擅长的场景:

  • 修改字符串
  • 函数返回值,需要用nil表示含义的场景。
  • 需要切片操作

作业

package main

import (
	"fmt"
	"reflect"
	"unsafe"
)

// slice 和 string 的底层结构
type slice struct {
	array unsafe.Pointer
	len   int
	cap   int
}

type stringStruct struct {
	str unsafe.Pointer
	len int
}

func set(s string) {
	fmt.Println("param s addr:", &s)
	s = "456"
}

func setPtr(s *string) {
	fmt.Println("param s addr:", s)
	*s = "456"
}
func main() {
	var a string = "123"
	fmt.Println(a)
	//stringHeader 也可以由自定义的stringStruct替代
	hdr1 := (*reflect.StringHeader)(unsafe.Pointer(&a))
	fmt.Println(&a)
	fmt.Printf("str:%s, data addr:%d, len:%d\n", a, hdr1.Data, hdr1.Len)
	set(a)
	fmt.Printf("str:%s, data addr:%d, len:%d\n", a, hdr1.Data, hdr1.Len)
	fmt.Println(&a)

	setPtr(&a)
	fmt.Printf("str:%s, data addr:%d, len:%d\n", a, hdr1.Data, hdr1.Len)
	fmt.Println(&a)
}

set和setptr操作输出的结果会是什么?

结果

string a addr: 0xc00010c210
str:123, data addr:4807580, len:3
param s addr: 0xc00010c230
str:123, data addr:4807580, len:3
string a addr: 0xc00010c210
param s addr: 0xc00010c210
str:456, data addr:4807586, len:3
string a addr: 0xc00010c210

文档信息

Search

    Table of Contents