0

【Go进阶—数据结构】string

 2 years ago
source link: https://segmentfault.com/a/1190000040796850
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

特性

从标准库文件 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 的地址。而且,我们需谨记:类型转换的开销并没有想象中那么小,经常会成为程序的性能热点。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK