Go36-38,39-bytes包

基本操作

bytes包和strings包非常相似,单从它们提供的函数的数量和功能上看,差别微乎其微。
strings包主要是面向Unicode字符和经过UTF-8编码的字符串,而bytes包主要是面对字节和字节切片。

bytes.Buffer类型

Buffer类型的用途主要是作为字节序列的缓冲区。
bytes.Buffer是开箱即用的。可以进行拼接、截断其中的字节序列,以各种形式导出其中的内容,还可以顺序的读取其中的子序列。所以是集读、写功能与一身的数据类型,这些也是作为缓冲区应该拥有的功能。
在内部,bytes.Buffer类型使用字节切片(bootstrap字段)作为内容容器。还有一个int类型(off字段)作为已读字节的计数器,简称为已读计数。不过这里的已读计数是不无获取也无法计算得到的。bytes.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)]
    bootstrap [64]byte // memory to hold first slice; helps small buffers avoid allocation.
    lastRead  readOp   // last read operation, so that Unread* can work correctly.
}

长度和容量

先看下示例:

package main

import (
    "fmt"
    "bytes"
)

func main() {
    var b1 bytes.Buffer
    contents := "Make the plan."
    b1.WriteString(contents)
    fmt.Println(b1.Len(), b1.Cap())

    p1 := make([]byte, 5)
    n, _ := b1.Read(p1)  // 忽略错误
    fmt.Println(n, string(p1))
    fmt.Println(b1.Len(), b1.Cap())
}

/* 执行结果
PS G:\Steed\Documents\Go\src\Go36\article38\example01> go run main.go
Lan: 14 Cap: 64
5 Make
Lan: 9  Cap: 64
PS G:\Steed\Documents\Go\src\Go36\article38\example01>
*/

先声明了一个byte.Buffer类型的变量,并写入一个字符串。然后打印了这个Buffer值的长度和容量。之后进行了一次读取,读取之后,再输出一个长度和容量。这里容量没有变,因为没有再写入任何内容。而长度变小了,这里的长度是未读内容的长度,一开始和存放的字节序列的长度一样,在读取操作之后,会随之变小,同样的,在写入操作之后,也会增大。

已读计数

没有办法可以直接得到Buffer值的已读计数,并且也很难估算它。但是为了用好bytes.Buffer,依然需要去源码里了解一下已读计数的作用。
bytes.Buffer中的已读计数的大致的功用如下:

  1. 读取内容时,相应方法会依据已读计数找到未读部分,并在读取后更新计数
  2. 写入内容时,如需扩容,相应方法会根据已读计数实现扩容策略
  3. 截断内容时,相应方法截掉的是已读计数代表的索引之后的未读部分
  4. 读回退时,相应方法需要用已读计数记录回退点
  5. 重置内容时,相应方法会把已读计数置为0
  6. 导出内容时,相应方法会导出已读计数代表的索引之后的未读部分
  7. 获取长度时,相应方法会依据已读计数和内容容器的长度,计算未读部分的长度并返回

通过以上功能的介绍,就能够体会到已读计数的重要性了。在bytes.Buffer的大多数的方法都用到了已读计数,而且都是非用不可的。

读取内容

在读取内容的时候,相应方法会先根据已读计数,判断一下内容容器中是否还有未读内容。如果有,那就会以已读计数为索引开始读取。读完之后,还会及时的更新已读计数。
读取内容的方法:

  • 所有名称以Read开头的方法
  • Next方法
  • WriteTo方法

写入内容

在写入内容的时候,绝大多数的响应方法都会先检查当前的内容容器,看看是否有足够的容量容纳新内容。如果没有,就会进行扩容。在扩容的时候,方法会在必要时,依据已读计数找到未读部分,并把其中的内容拷贝到扩容后的内容容器的头部位置。然后,方法将会把已读计数的值置为0,这样下一次读取的时候就会从新的内容容器的第一个字节开始了。
由于扩容后,已读的内容不会拷贝,所以就真正的丢弃了。不过Buffer本身也不支持对已读内容的再次操作,只是出于效率和值不可变的考虑,不会进行删除,而是等到扩容的时候忽略该部分内容不做拷贝,最后等着被回收掉。
写入内容的方法:

  • 所有名称以Write开头的方法
  • ReadFrom方法

示例:

func main() {
    var contents string
    b1 := bytes.NewBufferString(contents)
    fmt.Printf("Lan: %d, Cap: %d.\n", b1.Len(), b1.Cap())

    contents = "一二三四五"
    b1.WriteString(contents)
    fmt.Printf("Lan: %d, Cap: %d.\n", b1.Len(), b1.Cap())

    contents = "67"
    b1.WriteString(contents)
    fmt.Printf("Lan: %d, Cap: %d.\n", b1.Len(), b1.Cap())
}

截断内容

截断内容的方法:Truncate
该方法会接受一个int类型的参数,表示在截断时需要保留头部的多个个字节。注意这里所说的头部指的是未读部分的头部。这个头部的起始索引正是已读计数的值。
还是因为已读部分逻辑上就是不存在的,所以这里截断操作是从未读部分开始的

读回退

读回退有2个方法:

  • UnreadByte : 回退一个一节
  • UnreadRune : 回退一个Unicode字符

调用它们一般是为了退回到上一次被读取内容末尾的那个分隔符,或者为了重新读取前一个字节或字符做准备。回退是有前提的,在调用之前的哪一个操作必须是读取内容,并且是成功读取的。否则这写方法就会忽略后续操作并返回一个非nil的错误值。
UnreadByte方法比较简单,直接已读计数减1即可。
而UnreadRune方法需要从已读计数中减去的,是上一次被读取的Unicode字符所占用的字节数。这个字节数存在bytes.Buffer的lastRead字段里。只有在执行ReadRune方法中才会把这个字段设置为1至4的值,其他一些读写的方法中会在这个字段设置为0或-1。所以只有紧接在ReadRune方法之后,才能成功调用UnreadRune方法。这个方法明显比UnreadByte方法的适用面更小。

重置内容

重置内容的方法:Reset
不多解释了,直接看源码:

func (b *Buffer) Reset() {
    b.buf = b.buf[:0]
    b.off = 0
    b.lastRead = opInvalid
}

没有重置内容容器,这样避免了一次内存分配。

导出内容

导出内容的方法:

  • Bytes方法
  • String方法

访问未读部分的中的内容,并返回相应的结果。已读的部分可以认为是逻辑丢弃了,如果有过扩容,在垃圾清理后就是真正的物理丢弃了,所以也不应该获取到。

获取长度

获取长度的方法:Lan方法
返回内容容器中未读部分的长度。而不是其中已存内容的总长度,即:内容长度。

小结

已读计数器索引之前的那些内容,永远都是已经被读过的,几乎没有机会再次被读取到。
不过,这些已读内容所在的内存空间可能会被存入新的内容。这一般都是由于重置或者扩容内容容器导致的。重置或扩容后,已读计数一定会被置0,从而再次指向内容容器中的第一个字节。这有时候也是为了避免内存分配和重用内存空间,这句意思大概是:重用一次内容空间的话,就避免了一次内存分配的操作。直接把之前分配过的但是内容已经不需要的内存再用起来。否则的话,就是一次新的内存分配和一次对已分配内存的清理

扩展知识

主要讲两个问题:

  • 扩容策略
  • 内容泄露

扩容策略

Buffer值既可以被手动扩容,也可以进行自动扩容。并且这种扩容方式的策略是基本一致的。所以,在完全确定后续内容所需的字节数的时候手动扩容,否则让Buffer值自动扩容就好了。
在扩容的时候,是会先判断内容容器(bootstrap)的剩余容量是否够用,如果可以,会在当前的内容容器上,进行长度扩容。在源码中就是下面这几句体现的:

func (b *Buffer) grow(n int) int {
    m := b.Len()
    // 省略中间的若干代码
    b.buf = b.buf[:m+n]  // 当前内容的长度+需要的长度
    return m
}

若干内容容器的剩余容量不够了,那么扩容就会用新的内容容器去替代原有的内容容器,从而实现扩容。这里会有一步优化,如果当前内容容器的容量的一半仍然大于或等于现有长度在加上需要的字节数,那么扩容代码会复用现有的内容容器,并把容器中未读内容拷贝到它的头部位置。这样就是把已读内容都覆盖掉了,整体内容在内存里往前移。这样的复用可以省掉一次后续的扩容所带来的内存分配,以及若干字节的拷贝。
若上面的优化条件不满足,那么扩容代码就要再创建一个新的内容容器,并把原有容器中的未读内容拷贝进去,最后再用新的容器替换掉原有的容器。这个新容器的容量讲会等于原有容量的两倍再加上需要的字节数。这个策略和之前strings扩容的策略是一样的
下面是一个扩容的示例代码:

func main() {
    contents := "Good Year!"
    b1 := bytes.NewBufferString(contents)
    fmt.Printf("Lan: %d, Cap: %d.\n", b1.Len(), b1.Cap())  // 10, 16
    n := 10
    b1.Grow(n)
    fmt.Printf("Lan: %d, Cap: %d.\n", b1.Len(), b1.Cap())  // 10, 42
}

如果对处于零值状态的Buffer值来说,如果第一次扩容时需要的字节数不大于64,那么该值就会基于一个预先定义好的、长度为64的数组([64]byte)来作为内容容器。这样做的目的是为了让Buffer值在刚被真正使用的时候就可以快速的做好准备。
完成上面的步骤,对内容容器的扩容就基本完成了。不过,为了内部数据的一致性,以及避免原有的已读内容可能造成的数据混乱,扩容代码还会把已读计数置为0,并再对内容容器做一下切片操作,以掩盖掉原有的已读内容。

注意内容泄露

内容泄露:这里说的内容泄露是指,使用Buffer值的一个方法通过某种非标准的(或者说不正式的)方法得到了不该得到的内容。
比如,通过调用Buffer值的某个用于读取内容的方法,得到了一部分未读内容。但是这个Buffer值又有了一些新内容后,却可以通过当时得到的结果值,直接获得新的内容,而不需要再次调用相应的读去内容的方法。这就是典型的非标准读取方式。这种读取方式是不应该存在的,即使存在,也不应该使用。因为它是在无意中(或者说不小心)暴露出来的,其行为很可能是不稳定的。
在bytes.Buffer中,Bytes方法和Next方法都可能会造成内容的泄露。原因在于,它们都把基于内容容器的切片直接返回给了方法的调用方。通过切片,就可以直接访问和操作它的底层数组。不论这个切片是基于某个数组得来的,还是通过对另一个切片做切片操作获得的。这里的Bytes方法和Next方法返回的字节切片,都是通过对内容容器做切片操作得到的。也就是说,它们与内容容器公用了同一个底层数组,起码在一段时期之内是这样的。
以Bytes方法为例,下面是演示内容泄露的示例:

func main() {
    b1 := bytes.NewBufferString("abc")
    fmt.Printf("Lan: %d, Cap: %d.\n", b1.Len(), b1.Cap())
    s1 := b1.Bytes()
    fmt.Printf("%[1]v, %[1]s\n", s1)
    b1.WriteString("123")
    fmt.Printf("Lan: %d, Cap: %d.\n", b1.Len(), b1.Cap())
    fmt.Printf("%[1]v, %[1]s\n", s1)
    // 这里只要扩充一下切片,就读到后续内容了
    s1 = s1[:cap(s1)]
    fmt.Printf("%[1]v, %[1]s\n", s1)
    // 只是读到还不算,还能改
    s1[len(s1)-3] = ‘X‘
    fmt.Printf("%[1]v, %[1]s\n", s1)
}

这里要避免扩容,写入内容后都输出了一下容量,容量不变就是没有扩容过。那么Bytes方法返回的结果值与内容容器在此时还共用着同一个底层数组。之后就简单的做了再切片,就通过这个结果值把后面的未读内容都拿到了。这还没完,如果当时把这个值传到了外界,那么外界就可以通过该值修改里面的内容了。这个后果就很严重了,另一个Next方法,也存在相同的问题。
不过,如果经过扩容,Buffer值的内容容器或者它的底层数组就被重新设定了,那么之前的内容泄露问题就无法再进一步发展了。
这里是一个很严重的数据安全问题。一定要避免这种情况的发生。泄露的包里的方法本身的特性,无法避免,但是可以小心操作。会造成严重后果的途径是有意或无意的把这些返回的结果值传到了外界,这个问题可以避免。要在传出切片这类值之前,做好隔离。不如,先对它们进行深拷贝,然后再把副本传出去。

原文地址:http://blog.51cto.com/steed/2348657

时间: 2024-11-06 13:42:39

Go36-38,39-bytes包的相关文章

Go语言学习(十)bytes包处理字节切片

bytes包提供了对字节切片进行读写操作的一系列函数 字节切片处理的函数比較多,分为基本处理函数,比較函数,后缀检查函数,索引函数,切割函数, 大写和小写处理函数和子切片处理函数等. 1.字节切片基本处理函数api 1.1Contains()函数 //Contains()函数的功能是检查字节切片b是否包括子切片subslice,假设包括返回true,否则返回false. func Contains(b,subslice []bytes) bool 1.2Count()函数 //Count()函数

GoLang之buffer与bytes包

strings包 strings包的使用举例: package main import s "strings" import "fmt" var p = fmt.Println func main() { p("Contains: ", s.Contains("test", "es")) p("Count: ", s.Count("test", "t&quo

go语言中bytes包的常用函数,Reader和Buffer的使用

bytes中常用函数的使用: package main; import ( "bytes" "fmt" "unicode" ) //bytes包中实现了大量对[]byte操作的函数和两个最主要的Reader和Buffer两个结构 func main() { str := "aBcD"; //转为小写 fmt.Println(string(bytes.ToLower([]byte(str)))); //转为大写 fmt.Prin

golang bytes 包 详解

概况: 包字节实现了操作字节切片的函数.它类似于琴弦包的设施. 函数: func Compare(a, b []byte) int func Contains(b, subslice []byte) bool func ContainsAny(b []byte, chars string) bool func ContainsRune(b []byte, r rune) bool func Count(s, sep []byte) int func Equal(a, b []byte) bool

golang bytes包解读

golang中的bytes标准库实现了对字节数组的各种操作,与strings标准库功能基本类似. 功能列表:1.字节切片 处理函数 (1).基本处理函数(2).字节切片比较函数(3).前后缀检查函数(4).字节切片位置索引函数(5).分割函数(6).大小写处理函数(7).子字节切片处理函数2.Buffer 对象3.Reader 对象 基本处理函数Contains() :返回是否包含子切片func Contains(b, subslice []byte) bool 案例:执行结果:[email p

go bytes包

代码: package main import ("bytes""fmt"// "icode.baidu.com/baidu/gdp/automaxprocs"// "icode.baidu.com/baidu/gdp/log") // "icode.baidu.com/baidu/gdp/log" // "icode.baidu.com/baidu/gdp/log" func main

GO中常用包笔记 bytes(四)

Package bytes 对字节数组进行操作的包.功能和strings包相似. bytes包提供的功能有: 和另一个字节数组切片的关系(逐字节比较大小,是否相等/相似,是否包含/包含次数,位置搜索,是否是前缀后缀) 2.字节数组切片和字符串的关系(字符串中是否含有字节数组所包含的rune,以及在字符串中的位置) 3.字节数组切片和rune的关系(字节数组中是否含有特定的或满足特定条件的rune,以及在字节数组中的位置) 4.字节数组切片和字节的关系(包含/位置) 5.分割分组,分组连结 6.大

C#版的抓包软件

C#版的抓包软件 [创建时间:2015-09-10 22:37:04] NetAnalyzer下载地址 不好意思啊,NetAnalyzer停更有点长了,今天继续填坑^&^ NetAnalyzer实现结构 在上一篇中介绍一点VC++开发环境的配置,与基本的运行方式.因为NetAnalyzer使用的C#作为开发语言,所以在此主要介绍一些在C#环境下的开发环境的配置,与一些基本开发情况,力求在完成本篇后后,读者可以制作一个简单的抓包程序. 在开始编程前先要介绍连个.Net类库SharpPcap.dll

NetAnalyzer笔记 之 四. C#版的抓包软件

[创建时间:2015-09-10 22:37:04] NetAnalyzer下载地址 不好意思啊,NetAnalyzer停更有点长了,今天继续填坑^&^ NetAnalyzer实现结构 在上一篇中介绍一点VC++开发环境的配置,与基本的运行方式.因为NetAnalyzer使用的C#作为开发语言,所以在此主要介绍一些在C#环境下的开发环境的配置,与一些基本开发情况,力求在完成本篇后后,读者可以制作一个简单的抓包程序. 在开始编程前先要介绍连个.Net类库SharpPcap.dll与PacketDo

NetAnalyzer笔记 之 三. 用C++做一个抓包程序

[创建时间:2015-08-27 22:15:17] NetAnalyzer下载地址 经过前两篇的瞎扯,你是不是已经厌倦了呢,那么这篇让我们来点有意思的吧,什么,用C#.不,这篇我们先来C++的 Winpcap开发环境配置 完成了对Winpcap的介绍,什么,你没看到Winpcap的介绍,左转,百度(其实,真的是不想复制).我们就需要做一点有用的事情,比如写一个简单的数据采集工具.当然在此之前,我们需要配置Winpcap的开发环境. (1) 运行环境设置 Win32 平台下Winpcap应用程序