Go String 笔记

什么是 string ?

标准库builtin的解释:

type 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.

简单的来说字符串是一系列 8 位字节的集合,通常但不一定代表 UTF-8 编码的文本。字符串可以为空,但不能为 nil。而且字符串的值是不能改变的。
不同的语言字符串有不同的实现,在 go 的源码中 src/runtime/string.go,string 在底层的定义如下:

type stringStruct struct {
    str unsafe.Pointer
    len int
}

可以看到 str 其实是个指针,指向某个数组的首地址,这个数组就是一个字节数组,里面存着 string 的真正内容。其实字节数组指针更像是 c 语言的字符串形式,而在 go 里,对其进行封装。不同的是, c 语言的 string 是以 null 或 /0 结尾,计算长度的时候对其遍历;而 go 的 string 结尾没有特殊符号,只不过用空间换时间,把长度存在了 len 字段里。

那么问题来了,我们平时用的 string 又是什么呢?它的定义如下:

type string string

。。。好像和刚刚说的不太一样哈(-_-!)。这个 string 就是一个名叫 string 的类型,其实什么也不代表。只不过为了直观,使用的时候,把 stringStruct 转换成 string 类型。

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

为了验证,我们可以试一下:

package main

import (
   "fmt"
   "unsafe"
)

func main()  {
   var a = "nnnn"
   fmt.Println(a)
   var b = (*int)(unsafe.Pointer(uintptr(unsafe.Pointer(&a)) + 8));
    // 按照 stringStruct 结构,把 a 地址偏移 int 的长度位,得到 len 字段地址
    // 这里我的电脑是 64 位,而系统寻址以一个在节为单位,所以 +8
   fmt.Println(*b) // 这里输出的是 a 的长度 4
}

string 操作

拼接

我们可以用 + 来完成字符串的拼接,就像这样:s := x+y+z+… 底层如何实现的呢?

type tmpBuf [tmpStringBufSize]byte // 这是一个很重要的类型,tmpStringBufSize 为常量 32,但这个值并没有什么科学依据(-_-!)

func concatstrings(buf *tmpBuf, a []string) string {// 把所有要拼接的字符串放到 a 里面
    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 "" 
        // 需要注意的是,虽然空字符串看起来不占空间,可是底层还是 stringStruct,仍要占两个 int 空间
    }

    if count == 1 && (buf != nil || !stringDataOnStack(a[idx])) {
        return a[idx] // count 为 1 表明不需要拼接,直接返回源 string,并且没有内存拷贝
    }
    s, b := rawstringtmp(buf, l) // 这里分配了一个长度为 l 字节的内存,这个内存并没有初始化
    for _, x := range a {
        copy(b, x) // 把每个字符串的内容复制到新的字节数组里面
        b = b[len(x):]
    }
    return s
}

可是这里有个问题,b 是一个字节切片,而 x 是字符串,为什么能直接复制呢?

与切片的转换

内置函数copy会有一种特殊情况copy(dst []byte, src string) int,但是两者并不能直接 copy,需要把 string 转换成 []byte。

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
}

// 申请新的内存,返回切片
func rawbyteslice(size int) (b []byte) {
    cap := roundupsize(uintptr(size)) // 使申请内存的大小为 8 的倍数
    p := mallocgc(cap, nil, false) // 第三个参数为 FALSE 表示不用给分配的内存清零
    if cap != uintptr(size) {
        memclrNoHeapPointers(add(p, uintptr(size)), cap-uintptr(size)) // 超出需要的部分内存清零
    }

    *(*slice)(unsafe.Pointer(&b)) = slice{p, size, int(cap)}
    return
}

string 与内存

string 字面量

前面提到过,字符串的值是不能改变的,可是为什么呢?

这里说的字符串通常指的是 字符串字面量,因为它的存储位置不在堆和栈上,通常 string 常量是编译器分配到只读段的(.rodata),对应的数据地址不可修改。

不过等等,好像有什么不对?下面的代码为啥改了呢?

var str = "aaaa"
str = "bbbb"

这是因为前面提到过的 stringStruct,我们拿到的 str 实际上是 stringStruct 转换成 string 的。常量aaaa被保存在了只读段,下面函数参数 str 就是这个常量的地址:

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

所以我们拿到的 str 本来是 stringStruct.str ,给 str 赋值相当于给 stringStruct.str 赋值,使其指向 bbbb所在地只读段地址,而 aaaa本身是没有改变的。在改变 stringStruct.str 的同时,解释器也会更新 stringStruct.len 的值。

动态 string

所谓动态是指字符串 stringStruct.str 指向的地址不在只读段,而是指向由 malloc 动态分配的堆地址。尽管如此,直接修改 string 的内容还是非法的。要修改内容,可以先把 string 转成 []byte,不过这里会有一次内存拷贝,这点在转换的代码中可以看到。不过也可以做到 ‘零拷贝转换’:

func stringtoslicebyte(s string) []byte {
    sh := (*reflect.StringHeader)(unsafe.Pointer(&s))
    bh := reflect.SliceHeader{
        Data: sh.Data,
        Len:  sh.Len,
        Cap:  sh.Len,
    }
    return *(*[]byte)(unsafe.Pointer(&bh))
}

不过这种方法不建议使用,因为一旦 string 指向的内存位于只读段,转换成 []byte 后对其进行写操作会引发系统的段错误。

临时 string

有时候我们会把 []byte 转换成 string,通常也会发生一次内存拷贝,但有的时候我们只需要 ‘临时的’ 字符串,比如:

  • 使用 m[string(k)] 来查找map
  • 用作字符拼接: "<"+string(b)+">"
  • 用于比较: string(b)=="foo"

这些情况下我们都只是临时的使用一下一个 []byte 的字符串形式的值,如果分配内存有点不划算,所以编译器会做出一些优化,使用如下函数来转换:

func slicebytetostringtmp(b []byte) string {
    if raceenabled && len(b) > 0 {
        racereadrangepc(unsafe.Pointer(&b[0]),
            uintptr(len(b)),
            getcallerpc(),
            funcPC(slicebytetostringtmp))
    }
    if msanenabled && len(b) > 0 {
        msanread(unsafe.Pointer(&b[0]), uintptr(len(b)))
    }
    // 注意,以上两个 if 都为假,所以不会执行。不知道有什么用(-_-!)
    return *(*string)(unsafe.Pointer(&b))
}

以上是读了 src/runtime/string.go 代码的一些个人想法,连蒙带猜,所以有些地方可能不太对,欢迎指出啦(_)!

你可能感兴趣的:(Go String 笔记)