字符

Unicode

Golang字符编码基础

Golang的标识符可以包含“任何Unicode编码可以表示的字母字符”。

虽然可以直接把一个整数值转换为一个string类型的值,但是被转换的整数值应该可以代表一个有效的Unicode代码点,否则转换的结果将会是,即,一个仅由高亮的问好组成的字符串值。

当一个string类型的值被转换为[]rune类型值的时候,其中的字符串会被拆分为一个个Unicode字符。

Golang的代码正式由Unicode字符组成的,所以的源代码都必须按照Unicode编码规范中的UTF-8编码格式进行编码。也就是说,Golang的源码文件必须使用UTF-8编码格式进行存储,如果源码文件中出现了非UTF-8编码的字符,那么在构建、安装和运行的时候,go命令会报错“illegal UTF-8 encoding”。

ASCII编码

ASCII由ANSI制定的单字节字符编码方案,可以用于基于文本的数据交换。ASCII编码方案使用单个字节(byte)的二进制数来编码一个字符。

  • 标准的ASCII编码用一个字节的最高比特(bit)位作为奇偶校验位,

  • 而扩展的ASCII编码则将刺猬用于表示字符。

ASCII编码支持的可打印字符和控制字符的集合叫做ASCII编码集

Unicode编码规范是另一种更通用、针对数码字符和文本的字符编码标准,它为世界上现存的所有自然语言中的每个字符,都设定了一个唯一的二进制编码。它定义了不同的自然语言的文本数据在国际间交换的统一方式,并为全球化软件创建了一个重要的基础。

Unicode编码规范以ASCII编码集为出发点,并突破了ASCII只能对拉丁字母进行编码的限制。它不但提供了可以对世界上超过百万的字符进行编码的能力,还支持所有已知的转义序列和控制代码。

在计算机内部,抽象的字符会被编码为整数,这些整数的范围被成为代码空间,在代码空间之内,每一个特定的整数都被成为一个代码点。一个受支持的抽象字符会被映射并分配给某个特定的代码点,反过来,一个代码点总是可以被看成一个被编码的字符。

Unicode编码

Unicode编码规范通常使用十六进制标表示法来表示Unicode代码点的整数值,并使用“U+”作为前缀。

如,英文字母字符“a”的Unicode代码点是“U+0061”。

在Unicode编码规范中,一个字符能且只能有与它对应的那个代码点表示。Unicode编码规范提供了三种不同的编码格式:

  • UTF-8

  • UTF-16

  • UTF-32

在这三种编码格式的名称中,“-”右边的整数的含义是,以多少个比特位作为一个编码单元

例如,UTF-8就是以8个比特位,也就是一个字节作为一个编码单元,并且,它与标砖的ASCII编码是完全兼容的,也就是说在[0x00,0x7F]的范围内,这两种编码表示的字符都是相同的,这是UTF-8的巨大优势

UTF是UCS Transformation Format是缩写,UCS是Universal Charater Set或者Unicode Character Set的缩写,所以UTF可以翻译为UNicode转换格式,它代表的是字符与字节序列之间的转换方式

UTF-8是一种可变宽的编码方案,它会用一个或者多个字节的二进制数字来表示某个字符,最多使用四个字节。比如:

  • 对于一个英文字符,它仅需要一个字节的二进制数就可以表示

  • 对于一个中文字符,它需要要三个字节的二进制数才能够表示

不论怎么,一个受支持的字符总是可以由UTF-8编码为一个字节序列,简称UTF-8编码值。

string类型的值在底层的表达

在底层,string类型的值是由一系列相对应的Unicode代码点的UTF-8编码值来表达的。在Golang中一个string类型的值:

  • 既可以被拆分为一个包含多个字符的序列:以rune为元素的切片可以表示

  • 也可以被拆分为一个包含多个字节的序列:以byte为元素的切片可以表示

rune是Golang特有的一个基本数据类型,它的一个值表示一个字符,即:一个Unicode字符。比如'G'、'o'、'爱'、'好'、'者'代表的就是一个Unicode字符。

因为UTF-8编码格式会把一个Unicode字符编码为一个长度为1~4个字节序列,所以一个rune类型的值可以由一个或多个字节来表示。

type rune = int32
// rune类型实际上是int32类型的一个别名类型
// 一个rune类型的值会由四个字节宽度的空间来存储
// 它的存储空间总是能够存下一个UTF-8编码值

一个rune类型的值,在底层就是一个UTF-8编码值。前者是便于人类理解的外部展现,后者是便于计算机系统理解的内在表达。

str := "Go爱好者 "
fmt.Printf("The string: %q\n", str)

// 字符串值“Go 爱好者”被转换为[]rune类型的值,
// 其中每一个字符(不论中英文)都会独立成为一个rune类型的元素值
fmt.Printf("  => runes(char): %q\n", []rune(str))   //   => runes(char): ['G' 'o' '爱' '好' '者']

// 每个rune类型的值在底层都是由一个UTF-8编码值来表达的,
// 所以可以使用下面这种方式展示字符序列
fmt.Printf("  => runes(hex): %x\n", []rune(str))    //   => runes(hex): [47 6f 7231 597d 8005]

// 将每个字符的UTF-8编码值都拆成响应的字节序列
// 每三个字节对应一个中文字符
fmt.Printf("  => bytes(hex): [% x]\n", []byte(str))     //   => bytes(hex): [47 6f e7 88 b1 e5 a5 bd e8 80 85]

对于一个多字节的UTF-8编码值来说,可以把它当成一个整体转换为单一的整数,也可以先把它拆成字节序列,在把每个字节分别转换为一个整数,从而得到多个整数。这两种表示法展现出来的内容往往会不一样,比如:

  • 对于中文字符‘爱’它的UTF-8编码值

  • 可以展现为单一的整数:7231

  • 也可以展现为三个整数:e7 88 b1

images

一个string类型的值会由若干个Unicode字符组成,每个Unicode字符都可以由一个rune类型的值来承载。这些字符在底层都会被转换为UTF-8编码值,而这些UTF-8编码值有会以字节序列的形式表达和存储。因此一个string类型的值在底层就是一个能够表达若干个UTF-8编码值的字节序列。

range子句遍历字符串

带有range子句的for语句会先把遍历的字符串值拆成一个字节序列,然后再试图找出这个字节序列中包含的一个UTF-8编码值,或者说每一个Unicode字符。

这样的for语句可以为两个迭代变量赋值:

  • 第一个变量的值:就是当前字节序列中的某个UTF-8编码值的第一个字节所对应的那个索引值

  • 第二个变量的值:这个UTF-8编码值代表的那个Unicode字符,其类型是rune

str := "Go 爱好者 "
for i, c := range str {
 fmt.Printf("%d: %q [% x]\n", i, c, []byte(string(c)))
}

// output
// 输出for-range语句的两个迭代变量和第二个值的字节序列形式
0: 'G' [47]
1: 'o' [6f]
2: '爱' [e7 88 b1]
5: '好' [e5 a5 bd]
8: '者' [e8 80 85]

// for-range语句可以逐一地迭代出字符串值里的每个Unicode字符,
// 但是相邻的Unicode字符的索引值并不一定是连续的,
// 这取决与前一个Unicode字符是否为单字节字符。

strings

Golang不但拥有可以独立代表Unicode字符的类型rune,而且还可以对字符串值进行Unicode字符拆分的for语句。标准库Unicode包及其子包还提供了很多函数和数据类型,可以解析各种内容中的Unicode字符。

标准库中的strings代码包,用到了unicode包和unicode/utf8包中的程序,如:

  • strings.Builder类型的WriteRune方法

  • strings.Reader类型的ReadRune方法

与string值相比strings.Builder类型的优势

  1. 已存在的内容不可变,但可以拼接更多的内容

  2. 减少内存分配和内容拷贝的次数

  3. 可将内容充值,可重用值

string类型

在Golang中,string类型的值是不可变的,如果想要获得一个不一样的字符串,只能基于原来的字符串进行裁剪、拼接等操作,从而生成一个新的字符串。

  • 裁剪:可以使用切片表达式

  • 拼接:可以使用操作符“+”实现

在底层,一个string值的内容会被存储到一块连续的内存空间中,同时,这块内存容纳的字节数量会被记录下来,并用于表示该string值的长度。可以把这块内存的内容看成一个字节数组,而相应的string值则包含了指向字节数组头部的指针值。这样在string值上应用切片表达式,就相当于在对其底层的字节数组做切片。

在进行字符串拼接的时候,Golang会把所有被拼接的字符串依次拷贝到一个崭新且足够大的连续内存空间中,并把持有相应指针的string值作为结果返回。当程序中存在过多的字符串拼接操作的时候,会对内存分配产生非常大的压力

虽然string值在内部持有一个指针,但其类型仍然属于值类型,由于string值的不可变,其中的指针值也为内存空间的节省做出了贡献。

一个string值会在底层与它所有的副本共用同一个字节数组,由于这里的字节数组永远不会被改变,所有这样是绝对安全的

strings.Builder类型

与string相比strings.Builder值的优势体现在字符串拼接方面。Builder值中有一个用于承载内容的容器(简称内容容器),它是一个以byte为元素类型的切片(字节切片),由于这样的字节切片的底层数组就是一个字节数组,它与string值存储内容的方式是一样的。

实际上它们都是通过unsafe.Pointer类型的字段来持有那个指向了底层字节数组的指针值。因为这样的构造Builder值拥有高校利用内存的前提条件。

对于字节切片本身来说,它包含的任何元素值都可以被修改,但是Builder值并不允许这样做,其中的内容只能够被拼接或者被重置。这就意味着Builder值中的内容是不可变的。因此,利用Builder值提供的方法(Write、WriteByte、WriteRune、WriteString)拼接更多的内容,而丝毫不用担心这些方法会影响到已存在的内容。

通过调用上述方法把新的内容拼接到已存在的内容的尾部,如果有必要,Builder会自动地对自身的内容容器进行扩容,这里的自动扩容策略与切片的扩容策略一致。内容容器的容量足够时不会进行扩容,没有扩容,那么已存在的内容就不会被拷贝。

通过Builder的Grow方法可以进行手动扩容,它接收一个int类型的参数n,该参数用于代表将要扩充的字节数量。Grow方法会把其所属的内容容器的容量增加n个字节,它会生成一个字节切片作为新的内容容器,该切片的容量会是原容器容量的两被在加上n,之后会把原容器中的所有字节全部拷贝到新容器中。

var builder1 strings.Builder
// 省略若干代码。
fmt.Println("Grow the builder ...")
builder1.Grow(10)
fmt.Printf("The length of contents in the builder is %d.\n", builder1.Len())
// 如果当前内容容器的未用容量已经够用,即未用容量 >=n ,那么Grow方法什么也不做

fmt.Println("Reset the builder ...")
// 调用Reset方法,可以让Builder值重新回到零值状态,就像它从未被使用过那样
// 一旦被重置,Builder值中原有的内容容器会被直接丢弃
// 与其他的所有内容,将会被Go语言的垃圾回器标记并回收掉
builder1.Reset()
fmt.Printf("The third output(%d):\n%q\n", builder1.Len(), builder1.String())

strings.Builder类型的使用约束

  • 在已被真正使用后就不可在被复制

    调用Builder值的拼接方法和扩容方法就意味着开始真正使用它了,因为这些方法都会改变其所属值中的内容容器的状态,一旦调用了它们,就不能再以任何的方式对其所属值进行复制了,否则只要在任何副本上调用上述方法都会引发panic。 这种panic会告诉我们,这样的使用方式并不合法,因为这里的Builder值是副本而不是原值,这的复制方法包括但不限于函数间传递值、通过通道传递值、把值赋予变量等

      var builder1 strings.Builder
      builder1.Grow(1)
      builder3 := builder1
      //builder3.Grow(1) // 这里会引发 panic。
      _ = builder3

    由于Builder值不能在被复制,所以肯定不会出现多个Builder值中的内容容器公用一个底层字节数组的情况,这样避免了多个同源的Builder值在拼接内容时可能产生的冲突问题。

    虽然已经使用的Builder值不能再被复制,但是它的指针值却可以,无论什么时候,都可以通过任何方式复制这样的指针值。这样的指针值指向的都会是同一个Builder值。

      f2 := func(bp *strings.Builder) {
      (*bp).Grow(1) // 这里虽然不会引发 panic,但不是并发安全的。
      builder4 := *bp
      //builder4.Grow(1) // 这里会引发 panic。
      _ = builder4
      }
      f2(&builder1)

    这就产生了一个问题,如果Builder值被多方同时操作,那么其中的内容就可能产生混乱,这就是所说的操作冲突和并发安全问题。

  • 由于其内容不是完全不可变,所以需要使用方自行解决操作冲突和并发安全问题

    1. Builder值自己是无法解决这些问题的,所以在通过传递其指针值共享Builder值的时候,一定要确保各方对它的使用是正确的、有序的,并且是并发安全的,彻底的解决方案是,绝对不要共享Builder值以及它的指针值

    2. 可以在各处分别声明一个Builder值来使用,也可以先声明一个Builder值,然后在真正使用它之前,便将它的副本传到各处。

    3. 先使用在传递,只要在传递之前调用它的Reset方法即可。

      builder1.Reset()
      builder5 := builder1
      builder5.Grow(1) // 这里不会引发 panic。

关于复制Builder值的约束是有意义的,也是很有必要的。虽然仍然在通过某些方式共享Builder值,但最好还是不要以身犯险,各自为政是最好的解决方案。对于处在零值状态的Builder值,复制不会有任何问题

strings.Reader类型的值如何高效读取字符串

strings.Builder类型恰恰相反,strings.Reader类型是为了高效读取字符串而存在的。它的高效体现在它对字符串的读取机制上,它封装了很多用于在string值上读取内容的最佳实践

strings.Reader类型的值(简称Reader值),可以让我们很方便地读取一个字符串中的内容,在读取的过程中,Reader值会保存已读取的字节的计数(简称已读计数)。

  • 已读计数,代表这下一个读取的起始索引为止,Reader值就是依靠这样的一个计数,以及针对字符串的切片表达式,从而实现快读速读取的。

  • 已读计数,也是读取回退和位置设定时的重要依据。虽然它属于Reader的内部结构,但我们还是可以通过该值的Len方法和Size方法把它计算出来的。如下代码所示:

var reader1 strings.Reader
// 省略若干代码。
readingIndex := reader1.Size() - int64(reader1.Len()) // 计算出的已读计数。

Reader值拥有的大部分用于读取的方法都会及时更新已读计数,比如:

  • ReadByte方法会在读取成功后将这个计数的值加1

  • ReadRune方法读取成功之后,会把被读取的字符所占用的字节数作为计数的增量

  • Seek方法也会更新该值的已读计数器,它的主要擢用正是设定下一次读取的起始索引位置。如果把io.SeekCurrent的值作为第二个参数传给该方法,那么它会依据当前的已读计数,以及第一个参数offset的值来计算新的计数值。Seek方法会返回新的计数值,所以我们可以很容易地验证这一点,如下所示。

      offset2 := int64(17)
      expectedIndex := reader1.Size() - int64(reader1.Len()) + offset2
      fmt.Printf("Seek with offset %d and whence %d ...\n", offset2, io.SeekCurrent)
      readingIndex, _ := reader1.Seek(offset2, io.SeekCurrent)
      fmt.Printf("The reading index in reader: %d (returned by Seek)\n", readingIndex)
      fmt.Printf("The reading index in reader: %d (computed by me)\n", expectedIndex)

ReadAt方法是一个例外,它既不会依据已读计数进行读取,也不会在读取之后更新它。因此这个方法可以自由地读取其所属的Reader值中的任何内容。

综上所属,Reader值实现高效读取的关键在于它内部的已读计数,计数的值就代表这下一次读取的起始索引位置,它可以很容地被计算出来,Reader值的Seek方法可以直接设定该值中的已读计数值。

bytes

strings包和bytes包在API方面非常相似,单从它们提供的函数的数量和功能上说,差别微乎其微。

  • strings包主要面向的是Unicode字符和经过UTF-8编码的字符串

  • bytes包主要面向的是字节和字节切片

bytes.Buffer

// Buffer是一个具有读写方法的可变大小的字节缓冲区。
// 零值的Buffer是一个准备使用的空缓冲区。
type Buffer struct {
    buf     []byte // contents are the bytes buf[off : len(buf)]
    off      int    // read at &buf[off], write at &buf[len(buf)]
    lastRead readOp // last read operation, so that Unread* can work correctly.
}

主要用途,作为字节序列的缓冲区。与strings.Builder一样,bytes.Buffer也是开箱即用。

  • strings.Builder:只能拼接和导出字符串

  • bytes.Buffer:不但可以拼接、截断其中的字节序列,以各种形式导出其中的内容,还可以顺序读取其中的子序列

在内部,bytes.Buffer类型同样使用字节切片作为内容容器,并且与strings.Reader类型类似,bytes.Buffer有一个int类型的字段,用于表示已读字节的计数。已读字节的计数无法通过bytes.Buffer提供的方法计算出来

var buffer1 bytes.Buffer
contents := "Simple byte buffer for marshaling data."
fmt.Printf("Writing contents %q ...\n", contents)
buffer1.WriteString(contents)
fmt.Printf("The length of buffer: %d\n", buffer1.Len()) // 返回其中未被读取部分的长度,而不是已存内容的总长度
fmt.Printf("The capacity of buffer: %d\n", buffer1.Cap())

// Output
Writing contents "Simple byte buffer for marshaling data."
The length of buffer: 39    // 空格,标点,字符加起来,未调用任何读取方法,因此已读计数为零
The capacity of buffer: 64  // 根据切片自动扩容策略,64看起来也很合理
p1 := make([]byte, 7)
n, _ := buffer1.Read(p1)    // 从buffer中读取内容,并用它们填满长度为7的字节切片
fmt.Printf("%d bytes were read. (call Read)\n", n)
fmt.Printf("The length of buffer: %d\n", buffer1.Len())
fmt.Printf("The capacity of buffer: %d\n", buffer1.Cap())

// Output
7 bytes were read. (call Read)
The length of buffer: 32
The capacity of buffer: 64
  • Buffer 值的长度:Buffer值的长度是未读内容的长度,而不是已存在内容的总长度。它与在当前值之上的读操作和写操作都有关系,随着这两种操作的进行而改变。

  • Buffer值的容量:Buffer值的容量是指它的内容容器(字节切片)的容量,它只与在当前值之上的写操作有关,并会随着内容的写入而不断增长。

  • 已读字节计数:在strings.Reader中有Size方法可以得出内容长度的值,所以用内容长度减去未读部分长度就是已读计数,但是bytes.Buffer类型没有这样的方法,它的Cap方法提供的是内容容器的容量,而不是内容的长度,大部分情况下这两者是不同的,因此很难估计Buffer值的已读计数。

已读字节计数的作用

bytes.Buffer中的已读计数的大致功能如下:

  1. 读取内容时,相应方法会根据已读计数找到未读部分,并在读取后更新计数

  2. 写入内容时,如需扩容,相应方法会根据已读计数实现扩容策略

  3. 截断内容时,相应方法截掉的是已读计数代表索引之后的未读部分

  4. 读回退时,相应方法需要用已读计数记录回退点

  5. 重置内容时,相应方法会把已读计数重置为0

  6. 导出内容时,相应方法只会导出已读计数代表的索引之后的未读部分

  7. 获取长度时,相应方法会依据已读计数和内容容器的长度,计算未读部分的长度并返回

已读计数在bytes.Buffer类型中的重要作用,大部分方法都用到它

读取内容

  1. 相应方法先根据已读计数,判断一下内容容器中是否还有未读的内容

  2. 如果有,那么它就会从已读计数代表的索引处开始读取

  3. 读取完成后,及时更新已读计数,也就是说会记录一下又有多少字节被读取了

读取内容的相应方法:包括所有名称以Read开头的方法,已经Next方法和WriteTo方法。

写入内容

  1. 绝大多数的相应方法都会先检查当前的内容容器,是否有足够的容量容纳新的内容

  2. 如果没有,就对内容容器进行扩容

  3. 扩容的时候,方法会在必要时,依据已读计数找到未读部分,把其中的内容拷贝到扩容后的内容容器的头部位置

  4. 然后方法把已读计数的值重置为0,以表示下一次读取需要从内容容器的第一个字节开始

写入内容的相应方法:包括所有名称以Write开头的方法,以及ReadFrom方法。

截取内容

截取内容的方法Truncate,它接收一个int类型的参数,这个参数的值代表了:在截取时需要保留头部的多少个字节。

这里的头部,指的并不是内容容器的头部,而是其中未读部分的头部。头部的起始索引正是由已读计数的值表示的。在这种情况下,已读计数的值再加上参数值后得到的和,就是内容容器新的总长度。

读回退

bytes.Buffer中,用于读回退的方法有:

  • UnreadByte:

    • 回退一个字节

    • 实现方法是把已读计数减一

  • UnreadRune:

    • 回退一个Unicode字符

    • 实现方法是在已读计数中减去上一个被读取的Unicode字符所占用的字节数

    • 这个字节数有bytes.Buffer的另一个字段负责存储,它在这里的有效取值范围是[1,4],只有ReadRune方法才会把这个字段的值设定在此范围之内

      只有ReadRune方法之后,UnreadRune方法的调用才会成功,该方法比UnreadByte方法的适用面更窄。

调用它们一般都是为了退回上一次被读取内容末尾的那个分隔符,或者在重新读取前一个字节或字符做准备。

退回的前提是,在调用它们之前的那个操作必须是“读取”,并且是成功的读取,否则这些方法只能忽略后续操作并返回一个非nil的错误值。

bytes.Buffer的Len方法返回的是内容容器中未读部分的长度,而不是其中已存内容的总长度,该类型的Bytes方法和String方法的行为与Len方法的行为保存一直,只会访问未读部分中的内容,并返回相应的结果值。

在已读计数代表的索引之前的那些内容,永远都是已经被读过的,它们几乎没有机会再次被读取。这些已读内容所在的内存空间可能会被存入新的内容,这一般都是由于重置或者扩充内容容器导致的。这时,已读计数一定会被置为0,从而再次指向内容容器中的第一个字节。这有时候也是为了避免内存分配和重用内存空间。

扩容策略

Buffer值既可以手动扩容,也可以自动扩容,这两种扩容方式的策略基本一直,除非完全确定后续内容所需的字节数,否则让Buffer值自动扩容就好了。

扩容时,扩容代码会先判断内容容器的剩余容量,是否可以满足调用方的要求,或者是否足够容纳新的内容。如果可以,扩容代码会在当前的内容容器之上,进行长度扩充。

  1. 如果内容容器的容量与其长度的差,大于或等于另需的字节数,那么扩容代码就会通过切片操作对原有的内容容器的长度进行扩充,如下所示:

     b.buf = b.buf[:length+need]
  2. 如果内容容器剩余的容量不够了,那么扩容代码可能就会用新的内容容器去代替原有的内容容器,从而实现扩容。此处进行一步优化。 1. 如果当前内容容器的容量的一半,仍然大于或等于其现有长度再加上另需的字节数的和,即cap(b.buf)/2 >= len(b.buf)+need,那么扩容代码就会服用现有的内容容器,并把容器中的未读内容拷贝到它的头部位置。这样已读的内容会被全部未读的内容和之后的新内容覆盖掉。这样的复用至少节省一次扩容带来的内存分配以及若干字节的拷贝。 2. 若不满足优化条件,即当前内容容器的容量小于新长度的两倍。那么扩容代码就只能创建一个新的内容容器,并把原有容器中的未读内容拷贝进去,最后再用新的内容容器替换原来的。

    新容器的容量=2×原来容量+所需字节数

通过上述步骤,对内容容器的扩容基本完成,不过为了内部数据的一致性,以及避免原有的已读内容造成的数据混乱,扩容代码会把已读计数重置为0,并再对内容容器做一次切片操作,以掩盖掉原有的已读内容。

对于处于零值状态的Buffer值来说,如果第一次扩容时的另需字节数不大于64,那么该值会基于一个预先定义好的,长度为64的字节数组来创建内容容器,这样内容容器的容量就是64,这样做的目的是为了让Buffer值在刚被真正使用的时候就可以快速地做好准备

哪些方法可能造成内容泄露

内容泄露是指使用Buffer值的一方通过某种非标准方式等到了本不该得到的内容。

比如,通过调用Buffer值的某个用于读取内容的方法,得到了一部分未读内容,我们应该也只应该通过这个方法的结果值,拿到在那一时刻值中的未读内容,但是,在这个Buffer值又有了一些新的内容之后,却可以通过当时得到的结果值,直接获得新的内容,而不需要再次调用相应的方法。

这就是典型的非标准读取方式。这种读取方式不应存在,如果存在,不应该使用。

bytes.Buffer中,Bytes方法和Next方法都可能会造成内容的泄露,原因在于,它们都把基于内容容器的切片直接返回给了方法的调用方。

基于切片可以直接访问和操作它的底层数组,不论这个切片是基于某个数组得来的,还是通过另一个切片操作获得的

在这里Bytes方法和Next方法返回的字节切片,都是通过对内容容器做切片操作得到的,它们与内容容器共用同一个底层数组,起码在一段时间内是这样的。

例子

Bytes方法会返回在调用那一刻它所属值中的所以未读内容:

contents := "ab"
buffer1 := bytes.NewBufferString(contents)
fmt.Printf("The capacity of new buffer with contents %q: %d\n", contents, buffer1.Cap())
// 内容容器的容量为:8。
unreadBytes := buffer1.Bytes()
fmt.Printf("The unread bytes of the buffer: %v\n", unreadBytes) // 未读内容为:[97 98]。


buffer1.WriteString("cdefg")// 在向buffer1中写入字符串值“cdefg”
fmt.Printf("The capacity of buffer: %d\n", buffer1.Cap()) // 内容容器的容量仍为:8。
// 通过简单的在切片操作,就可以利用这个结果值拿到buffer1在此时的所有未读内容
unreadBytes = unreadBytes[:cap(unreadBytes)]
fmt.Printf("The unread bytes of the buffer: %v\n", unreadBytes) // 基于前面获取到的结果值可得,未读内容为:[97 98 99 100 101 102 103 0]。

// 如果将unreadBytes的值传到外界,那么就可以通过该值操纵buffer1的内容
unreadBytes[len(unreadBytes)-2] = byte('X') // 'X'的 ASCII 编码为 88。
fmt.Printf("The unread bytes of the buffer: %v\n", buffer1.Bytes()) // 未读内容变为了:[97 98 99 100 101 102 88]。

Next方法也有这样的问题。

如果经过扩容,Buffer值的内容容器或者它的底层数组被重新设定了,那么之前的内容泄露问题就无法再进一步发展了。在传出切片这类值之前要做好隔离,比如,先对它们进行深度拷贝,然后再把副本传出来。

最后更新于

这有帮助吗?