特性
从标准库文件 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 可以为空(长度为0),但不会是 nil,并且 string 对象不可修改。
字符串可以使用双引号赋值,也可以使用反单引号赋值。使用双引号声明的字符串和其他语言中的字符串没有太多的区别,它只能用于单行字符串的初始化,如果字符串内部出现换行符或双引号等特殊符号,需要使用 \ 符号转义;而反引号声明的字符串可以摆脱单行的限制,并且可以在字符串内部直接使用特殊符号,在遇到需要手写 JSON 或者其他复杂数据格式的场景下非常方便。
实现原理
数据结构
源码包 src/runtime/string.go:stringStruct
定义了 string 的数据结构:
type stringStruct struct {
str unsafe.Pointer
len int
}
结构很简单,两个字段分别表示字符串的首地址和长度。
生成字符串时,会先构建 stringStruct 对象,再转换成 string,代码如下:
func gostringnocopy(str *byte) string {
ss := stringStruct{str: unsafe.Pointer(str), len: findnull(str)}
s := *(*string)(unsafe.Pointer(&ss))
return s
}
相关操作
字符串拼接
在 runtime 包中,使用 concatstrings 函数来拼接字符串,所有待拼接字符串被组织到一个切片中传入,核心源码如下:
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 ""
}
// 如果非空字符串的数量为 1 且当前字符串不在栈上,直接返回该字符串
if count == 1 && (buf != nil || !stringDataOnStack(a[idx])) {
return a[idx]
}
// 分配内存,构造一个字符串和切片,二者共享内存
s, b := rawstringtmp(buf, l)
// 向切片中拷贝待拼接字符串
for _, x := range a {
copy(b, x)
b = b[len(x):]
}
// 返回拼接后字符串
return s
}
需要注意的是,在正常情况下,运行时会调用 copy 将输入的多个字符串拷贝到目标字符串所在的内存空间。一旦需要拼接的字符串非常大,拷贝带来的性能损失是无法忽略的。
类型转换
当我们使用 Go 语言解析和序列化 JSON 等数据格式时,经常需要将数据在 string 和 []byte 之间来回转换。
从字节数组到字符串的转换需要使用 slicebytetostring 函数,核心源码如下:
func slicebytetostring(buf *tmpBuf, ptr *byte, n int) (str string) {
// 字节数组长度为 0 或 1 时特殊处理
if n == 0 {
return ""
}
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) {
p = unsafe.Pointer(buf)
} else {
p = mallocgc(uintptr(n), nil, false)
}
stringStructOf(&str).str = p
stringStructOf(&str).len = n
// 将原 []byte 中的字节全部复制到新的内存空间中
memmove(p, unsafe.Pointer(ptr), uintptr(n))
return
}
当我们想要将字符串转换成 []byte 类型时,需要使用 stringtoslicebyte 函数,该函数的实现非常容易理解:
func stringtoslicebyte(buf *tmpBuf, s string) []byte {
var b []byte
// 当传入缓冲区并且空间足够时,从该缓冲区切取字符串长度大小切片,否则构造一个切片
if buf != nil && len(s) <= len(buf) {
*buf = tmpBuf{}
b = buf[:len(s)]
} else {
b = rawbyteslice(len(s))
}
// 将字符串复制到切片中
copy(b, s)
return b
}
[]byte 转换成 string 的场景有很多,出于性能上的考虑,有时候只是临时需要字符串的情景下,此时不会发生拷贝,而是直接返回一个 string,其中的指针指向 []byte 的地址。而且,我们需谨记:类型转换的开销并没有想象中那么小,经常会成为程序的性能热点。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。