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是8比特字节的集合,通常但不一定是UTF-8编码的文本。

  • string可以是空,但不会是nil。
  • string对象不可修改

数据结构

src/runtime/string.go:stringStruct定义了string的数据结构。

type stringStruct struct {
    str unsafe.Pointer
    len int
}

可以看到string的数据结构很简单

  • str:指向字符串首地址的指针
  • len:字符串的长度

string跟切片的结构非常相似,只不过切片还有一个表示容量的成员,事实上,string和byte切片经常发生转换。

创建string

string的创建只需要简单的声明即可

var str string
str = "hello world"

实际的构建过程时先根据字符串构建stringStruct,再转换成string,源码如下:

func gostringncopy(str *byte) string {
	ss := stringStruct{str: unsafe.Pointer(str), len: findnull(str)}
	s := *(*string)(unsafe.Pointer(&ss))
	return s
}

string在runtime包中就是stringStruct,对外呈现的叫做string。

类型转换

byte切片转string

func GetStringBySlice(s []byte) string {
	return string(s)
}

需要注意这种转换需要一次内存拷贝

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

string转byte切片

func GetSliceByString(str strng) []byte {
	return []byte(str)
}
  • 申请切片内存空间
  • 将string拷贝到切片 所以这种转换过程中也发生了一次内存拷贝。

byte切片转换为string不拷贝内存的场景

前面的两种转换都需要进行一次内存拷贝,为了性能上的考虑,在临时需要字符串的场景下不会拷贝内存,而是直接返回一个string。

比如:

  • 使用m[string(b)]来查找map
  • 字符串拼接,如"<" + string(b) + ">"
  • 字符串比较:string(b) == "foo" 这种临时使用的场景,只是暂时读取内存来使用,没有必要拷贝内存新建一个string。

字符串的拼接

使用加号可以很方便的进行拼接

str := "str1" + "str2" + "str3"

字符串是不可变的,所以拼接字符串其实就是创建一个新的字符串的过程。

不过GO语言对字符串的拼接进行了优化,纵使+拼接的字符串串很多,也具有较好的性能:将新字符串的内存空间一次分配完成。 在编译的时候拼接语句的字符串都会放到一个切片中,拼接过程需要遍历两次切片,第一次获得字符串的总长度然后据此申请内存,第二次将字符串逐个拷贝过去。

这个过程用伪代码表示就是:

func concatstrings(a []string) string {
	lenght := 0
	for _, str := range a {
		length += lenght(str)
	}
	// 生成指定大小的字符串,返回一个string和切片,两者共享内存
	// 所以修改切片也就修改了字符串的内容
	s, b := rawstring(length)
	for _, str := range a {
		copy(b, str)
		b = b[len(str):]
	}
	return s
}
 
func rawstring(size int) (s string ,b []byte) {
	p := mallocgc(uintptr(size), nil, false)
	stringStructOf(&s).str = p
	stringStructOf(&s).len = size
	*(*slice)(unsafe.Pointer(&b)) = slice(p, size, size)
	return
}

字符串为什么不允许修改

像C++这种语言的字符串,自身拥有内存空间的,是支持修改string的。

但是Go的实现中,string不包含内存空间,只是有一个内存的指针,这样使得string变得很轻量,进行传递而不用担心内存拷贝。

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

string和byte切片的选择

string使用的场景:

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

byte切片使用的场景:

  • 修改字符串的场景,尤其是修改粒度为1个字节。
  • 函数返回值,需要用nil表示含义的场景
  • 需要切片操作的场景。 因为string的不可变性,所以实际使用的场景中还是byte切片使用的多,并且在偏底层的实现上要尤为明显。