string 是Go语言中的基础数据类型。
声明string变量非常简单,常见的方式有以下两种:
声明一个空字符串后再赋值
var s string
s = "hello world"
需要注意的是空字符只是长度为0,但不是nil。不存在值为nil的string
使用简短变量声明:
s := "hello world" //直接初始化字符串
字符串不仅可以使用双引号赋值,也可以使用反单引号赋值,它们的区别是在于对特殊字符的处理。
假如我们希望string变量表示下面的字符串,它包括换行符和双引号:
Hi,
this is "Steven".
使用双引号表示时,需要对特殊字符转义,如下所示:
s:= "Hi, \nthis is \"Steven\"."
如果使用反单引号时,不需要对特殊符号转义,如下所示:
s := `Hi,
this is "Steven".`
使用反单引号表示字符串比较直观,可以清晰的看出字符串内容。如果有数据库sql以及描述性说明,可以优先使用反单引号的方式表达。
字符串可以使用加号进行拼接:
s = s + "a" + "b"
需要注意的是,字符串拼接会触发内存分配以及内存拷贝,单行语句拼接多个字符串只分配一次内存。比如上面的语句中,在拼接时,会先计算最终字符串的长度后再分配内存。
项目中,数据经常需要在string和字节[]byte之间转换
[]byte 转 string
func ByteToString(){
b:=[]byte{'h','e','l','l','o'}
s:=string(b)
fmt.Println(s) //hello
}
string 转 []byte
func StringToByte(){
s := "hello"
b := []byte(s)
fmt.Println(b)
}
需要注意的是,无论是字符串转成[]byte,还是[]byte转成string,都将发生一次内存拷贝,会有一定的性能开销。
正因为string和[]byte之间的转换非常方便,在某些高频场景中往往会成为性能的瓶颈,比如数据库访问、http请求处理等。
string使用8比特字节的集合来存储字符,而且存储的字符是UTF-8编码。
在使用for-range 遍历字符串时,每次迭代将返回UTF-8编码的首个字节下标以及字节值,这意味着下标可能不连续。
比如下面的函数:
func StringIteration(){
s :="中国"
for i,v :=range s {
fmt.Printf("i : %d , v : %c \n ",i , v)
}
}
函数输出:
i : 0 , v : 中
i : 3 , v : 国
此外,字符串的长度是指字节数,而非字符数。 比如汉字"中"和"国"的UTF-8编码各占3个字节,字符串"中国"的长度是6而不是2。
字符串可以为空,但值不会是nil。另外字符串不可以修改(和Java语言中的String一样)。字符串变量可以接受新的字符串赋值,但是不能通过下标的方式进行修改字符串的值。
如下所示:
s := "Hello"
&s[0]=byte(104) //非法
s = "hello" //合法
字符串不支持取地址操作,也就无法修改字符串的值,上面的语句中会出现编译错误:
cannot take the address of s[0]
标准库strings包提供了大量的字符串操作函数。可以参考 [Go语言中文网] ,如下所示:
Go标准库builtin中定义了string类型:
type string string
8位byte序列构成的字符串,约定但不必须是utf-8编码的文本。字符串可以为空但不能是nil,其值不可变。
源码包中 src/runtime/string.go:stringStruct 定义了string的数据结构:
type stringStruct struct {
str unsafe.Pointer
len int
}
string的数据结构很简单,只包含两个成员。
string的数据结构跟切片类似,只不过切片slice还有一个表示容量的变量,事实上string和切片slice([]byte)经常转换。
在runtime包中使用gostringnocopy()函数来生成字符串。如下代码所示,声明一个string变量并赋值:
var str string
str = "Hello World"
字符串生成时,会先构建stringStruct对象,再转成string。转换的源码如下:
//go:nosplit
func gostringnocopy(str *byte) string {
//先构造stringStruct
ss := stringStruct{str: unsafe.Pointer(str), len: findnull(str)}
//再将stringStruct转成string
s := *(*string)(unsafe.Pointer(&ss))
return s
}
string在runtime包中是stringStruct类型,对外呈现则为string类型。
字符串使用Unicode编码存储字节,对于英文字符来说,每个字符的Unicode编码只用一个字节即可表示,如下图所示:
此时字符串的长度等于字符数。而对于非ASCII字符来说,其Unicode编码可能需要更多的字节来表示,如下图所示:
此时字符串的长度会大于实际字符数,字符串的长度实际上表现的是字节数。
字符串可以很方便的拼接,像下面所示:
str :="str1" + "str2" + "str3"
即便有非常多的字符串需要拼接,性能上也有比较好的保证,因为新的字符串内存空间是一次性分配完成的,所以性能主要是消耗在内存拷贝上。
在runtime包中,使用concatstrings()函数来拼接字符串。在一个拼接语句中,所有待拼接字符串都被编译器组织到一个切片中并传入concatstrings()函数中,拼接过程需要遍历两次切片,第一次遍历获取总的字符串长度,据此来申请内存空间,第二次遍历会将字符串逐个拷贝进去。
// concatstrings implements a Go string concatenation x+y+z+...
// The operands are passed in the slice a.
// If buf != nil, the compiler has determined that the result does not
// escape the calling function, so the string data can be stored in buf
// if small enough.
func concatstrings(buf *tmpBuf, a []string) string {
idx := 0
l := 0
count := 0
for i, x := range a {
n := len(x)
if n == 0 {
continue
}
if l+n < l {
throw("string concatenation too long")
}
l += n
count++
idx = i
}
if count == 0 {
return ""
}
// If there is just one string and either it is not on the stack
// or our result does not escape the calling frame (buf != nil),
// then we can return that string directly.
if count == 1 && (buf != nil || !stringDataOnStack(a[idx])) {
return a[idx]
}
//分配内存,返回一个string和切片,二者共享内存空间
s, b := rawstringtmp(buf, l)
//string无法修改,只能通过[]byte来修改
for _, x := range a {
copy(b, x)
b = b[len(x):]
}
return s
}
因为string无法直接修改,所以这里使用rawstringtmp()函数初始化一个指定的大小的string,同时返回一个切片,二者共享同一块内存空间,后者向切片中拷贝数据,也就间接的修改了string。
rawstringtmp()函数
func rawstringtmp(buf *tmpBuf, l int) (s string, b []byte) {
if buf != nil && l <= len(buf) {
b = buf[:l]
s = slicebytetostringtmp(&b[0], len(b))
} else {
s, b = rawstring(l)
}
return
}
rawstring()函数
//生成一个新的string,返回的string和切片共享相同的空间
// rawstring allocates storage for a new string. The returned
// string and byte slice both refer to the same storage.
// The storage is not zeroed. Callers should use
// b to set the string contents and then drop b.
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
}
byte切片可以很方便地转成string:
func GetStringBySlice(s []byte)string {
return string(s)
}
需要注意的是:这种转换需要一次内存拷贝。
转换过程如下:
转换示意图如下所示:
在runtime包中使用slicebytetostring()函数将[]byte转成string。
slicebytetostring() 函数如下:
// slicebytetostring converts a byte slice to a string.
// It is inserted by the compiler into generated code.
// ptr is a pointer to the first element of the slice;
// n is the length of the slice.
// Buf is a fixed-size buffer for the result,
// it is not nil if the result does not escape.
func slicebytetostring(buf *tmpBuf, ptr *byte, n int) (str string) {
if n == 0 {
// Turns out to be a relatively common case.
// Consider that you want to parse out data between parens in "foo()bar",
// you find the indices and convert the subslice to string.
return ""
}
if raceenabled {
racereadrangepc(unsafe.Pointer(ptr),
uintptr(n),
getcallerpc(),
funcPC(slicebytetostring))
}
if msanenabled {
msanread(unsafe.Pointer(ptr), uintptr(n))
}
if n == 1 {
p := unsafe.Pointer(&staticuint64s[*ptr])
if sys.BigEndian {
p = add(p, 7)
}
stringStructOf(&str).str = p
stringStructOf(&str).len = 1
return
}
var p unsafe.Pointer
if buf != nil && n <= len(buf) {
//如果预留buf够用,则用预留的buf
p = unsafe.Pointer(buf)
} else {
//否则重新申请内存
p = mallocgc(uintptr(n), nil, false)
}
//构建字符串
stringStructOf(&str).str = p
stringStructOf(&str).len = n
//将切片底层数组中数据拷贝到字符串中
memmove(p, unsafe.Pointer(ptr), uintptr(n))
return
}
slicebytetostring()函数会优先使用一个固定大小的buf,当buf长度不够时才会申请新的内存,这样子避免了内存空间浪费。
string也可以很方便的转成byte切片
func GetSliceByString(str string) []byte {
return []byte(str)
}
string转成byte切片同样也需要一次内存拷贝的动作,其过程如下:
转换示意图如下所示:
在runtime包中,使用stringtoslicebyte()函数将string转成[]byte
stringtoslicebyte()函数如下:
func stringtoslicebyte(buf *tmpBuf, s string) []byte {
var b []byte
if buf != nil && len(s) <= len(buf) {
*buf = tmpBuf{}
//从预留buf中切出新的切片
b = buf[:len(s)]
} else {
//生成新的切片
b = rawbyteslice(len(s))
}
copy(b, s)
return b
}
// rawbyteslice allocates a new byte slice. The byte slice is not zeroed.
func rawbyteslice(size int) (b []byte) {
cap := roundupsize(uintptr(size))
p := mallocgc(cap, nil, false)
if cap != uintptr(size) {
memclrNoHeapPointers(add(p, uintptr(size)), cap-uintptr(size))
}
*(*slice)(unsafe.Pointer(&b)) = slice{p, size, int(cap)}
return
}
stringtoslicebyte()函数中也使用了预留buf,并只在该buf长度不够时才会申请内存,其中rawbyteslice()函数用于申请新的未初始化的切片。由于字符串内容将完整覆盖切片的存储空间,所以可以不初始化切片从而提升分配效率。
byte切片转换成string的场景很多,出于性能上的考虑,有时候只是应用于在临时需要字符串的场景下,byte切片转换成string时并不会拷贝内存,而是直接返回一个string,这个string的指针(string.str)指向切片的内存地址。
比如,编译器会识别如下临时场景:
由于只是临时把byte切片转换成string,也就避免了因byte切片内容修改而导致string数据变化的问题,所以此时可以不必拷贝内存。
像C++语言中的string,其本身拥有内存空间,修改string是支持的。但在Go语言的实现中,string不包含内存空间,只有一个内存的指针和长度,这样做的好处是string变得非常轻量级,可以很方便地进行传递而不用担心内存拷贝。
因为string通常指向字符串字面量,而字符串字面量存储的位置是只读段,而不是堆或者栈上,所以才有了string不可修改的约定。
string和[]byte都可以表示字符串,但是因为数据结构不同,其衍生出来的方法也不一样,要根据具体的场景选择不同的结构来使用。
string擅长的场景:
[]byte擅长的场景:
虽然看起来string使用的场景不多,但是因为string直观,在实际应用中还是大量存在的,相对而言,在偏底层的实现中[]byte使用的更多一些。