正文
go语言bytes Go语言圣经
小程序:扫一扫查出行
【扫一扫了解最新限行尾号】
复制小程序
【扫一扫了解最新限行尾号】
复制小程序
Go语言中的字节序
Go中go语言bytes的binary包实现了简单go语言bytes的数字与字节序列go语言bytes的转换以及变长值的编解码
package main
import ( "fmt" "bytes" "encoding/binary" ) func main(){ n := 0x12345678 bytesBuffer := bytes.NewBuffer([]byte{}) //BigEndian 大端顺序存储 LittleEndian小端顺序存储 binary.Write(bytesBuffer, binary.BigEndian, int32(n)) data:=bytesBuffer.Bytes() fmt.Printf("[0]: %#x addr:%#x\n",data[0],data[0]) fmt.Printf("[0]: %#x addr:%#x\n",data[1],data[1]) fmt.Printf("[0]: %#x addr:%#x\n",data[2],data[2]) fmt.Printf("[0]: %#x addr:%#x\n",data[3],data[3]) }
输出
[0]: 0x12 addr:0xc042010248 [1]: 0x34 addr:0xc042010249 [2]: 0x56 addr:0xc04201024a [3]: 0x78 addr:0xc04201024b
也可以使用下面的方式
n := 0x12345678 var data []byte = make([]byte,4) //操作的都是无符号整型 binary.BigEndian.PutUint32(data,uint32(n))
可以使用下面的方式判断当前系统的字节序类型
const INT_SIZE int = int(unsafe.Sizeof(0))
//判断go语言bytes我们系统中的字节序类型 func systemEdian() { var i int = 0x1 bs := (*[INT_SIZE]byte)(unsafe.Pointer(i)) if bs[0] == 0 { fmt.Println("system edian is little endian") } else { fmt.Println("system edian is big endian") } }
golang内存扩容
一般来说当内存空间span不足时,需要进行扩容。而在扩容前需要将当前没有剩余空间的内存块相关状态解除,以便后续的垃圾回收期能够进行扫描和回收,接着在从中间部件(central)提取新的内存块放回数组中。
需要注意由于中间部件有scan和noscan两种类型,则申请的内存空间最终获取的可能是其两倍,并由heap堆进行统一管理。中间部件central是通过两个链表来管理其分配的所有内存块:
1、empty代表“无法使用”状态,没有剩余的空间或被移交给缓存的内存块
2、noempty代表剩余的空间,并这些内存块能够提供服务
由于golang垃圾回收器使用的累增计数器(heap.sweepgen)来表达代龄的:
从上面内容可以看到每次进行清理操作时 该计数器 +2
再来看下mcentral的构成
当通过mcentral进行空间span获取时,第一步需要到noempty列表检查剩余空间的内存块,这里面有一点需要说明主要是垃圾回收器的扫描过程和清理过程是同时进行的,那么为了获取更多的可用空间,则会在将分配的内存块移交给cache部件前,先完成清理的操作。第二步当noempty没有返回时,则需要检查下empty列表(由于empty里的内存块有可能已被标记为垃圾,这样可以直接清理,对应的空间则可直接使用了)。第三步若是noempty和empty都没有申请到,这时需要堆进行申请内存的
通过上面的源码也可以看到中间部件central自身扩容操作与大对象内存分配差不多类似。
在golang中将长度小于16bytes的对象称为微小对象(tiny),最常见的就是小字符串,一般会将这些微小对象组合起来,并用单块内存存储,这样能够有效的减少内存浪费。
当微小对象需要分配空间span,首先缓存部件会按指定的规格(tiny size class)取出一块内存,若容量不足,则重新提取一块;前面也提到会将微小对象进行组合,而这些组合的微小对象是不能包含指针的,因为垃圾回收的原因,一般都是当前存储单元里所有的微小对象都不可达时,才会将该块内存进行回收。
而当从缓冲部件cache中获取空间span时, 是通过偏移位置(tinyoffset)先来判断剩余空间是否满足需求。若是可以的话则以此计算并返回内存地址;若是空间不足,则提取新的内存块,直接返回起始地址便可; 最后在对比新旧两块内存,空间大的那块则会被保留。
如何将golangbyte转换为字符串
func BytesToString(bs []byte) string {
l := len(bs)
buf := make([]string, 0, l)
for i := 0; i l; i++ {
buf = appendString(buf, bs[i])
}
return strings.Join(buf, dot)
}
func appendString(bs []string, b byte) []string {
var a byte
var s int
for i := 0; i 8; i++ {
a = b
b = 1
b = 1
switch a {
case b:
s += 0
default:
temp := 1
for j := 0; j 7 - i; j++ {
temp = temp*2
}
s += temp
}
b = 1
}
return append(bs, strconv.Itoa(s))
}
go语言string之Buffer与Builder
操作字符串离不开字符串的拼接,但是Go中string是只读类型,大量字符串的拼接会造成性能问题。
拼接字符串,无外乎四种方式,采用“+”,“fmt.Sprintf()”,"bytes.Buffer","strings.Builder"
上面我们创建10万字符串拼接的测试,可以发现"bytes.Buffer","strings.Builder"的性能最好,约是“+”的1000倍级别。
这是由于string是不可修改的,所以在使用“+”进行拼接字符串,每次都会产生申请空间,拼接,复制等操作,数据量大的情况下非常消耗资源和性能。而采用Buffer等方式,都是预先计算拼接字符串数组的总长度(如果可以知道长度),申请空间,底层是slice数组,可以以append的形式向后进行追加。最后在转换为字符串。这申请了不断申请空间的操作,也减少了空间的使用和拷贝的次数,自然性能也高不少。
bytes.buffer是一个缓冲byte类型的缓冲器存放着都是byte
是一个变长的 buffer,具有 Read 和Write 方法。 Buffer 的 零值 是一个 空的 buffer,但是可以使用,底层就是一个 []byte, 字节切片。
向Buffer中写数据,可以看出Buffer中有个Grow函数用于对切片进行扩容。
从Buffer中读取数据
strings.Builder的方法和bytes.Buffer的方法的命名几乎一致。
但实现并不一致,Builder的Write方法直接将字符拼接slice数组后。
其没有提供read方法,但提供了strings.Reader方式
Reader 结构:
Buffer:
Builder:
可以看出Buffer和Builder底层都是采用[]byte数组进行装载数据。
先来说说Buffer:
创建好Buffer是一个empty的,off 用于指向读写的尾部。
在写的时候,先判断当前写入字符串长度是否大于Buffer的容量,如果大于就调用grow进行扩容,扩容申请的长度为当前写入字符串的长度。如果当前写入字符串长度小于最小字节长度64,直接创建64长度的[]byte数组。如果申请的长度小于二分之一总容量减去当前字符总长度,说明存在很大一部分被使用但已读,可以将未读的数据滑动到数组头。如果容量不足,扩展2*c + n 。
其String()方法就是将字节数组强转为string
Builder是如何实现的。
Builder采用append的方式向字节数组后添加字符串。
从上面可以看出,[]byte的内存大小也是以倍数进行申请的,初始大小为 0,第一次为大于当前申请的最大 2 的指数,不够进行翻倍.
可以看出如果旧容量小于1024进行翻倍,否则扩展四分之一。(2048 byte 后,申请策略的调整)。
其次String()方法与Buffer的string方法也有明显区别。Buffer的string是一种强转,我们知道在强转的时候是需要进行申请空间,并拷贝的。而Builder只是指针的转换。
这里我们解析一下 *(*string)(unsafe.Pointer(b.buf)) 这个语句的意思。
先来了解下unsafe.Pointer 的用法。
也就是说,unsafe.Pointer 可以转换为任意类型,那么意味着,通过unsafe.Pointer媒介,程序绕过类型系统,进行地址转换而不是拷贝。
即*A = Pointer = *B
就像上面例子一样,将字节数组转为unsafe.Pointer类型,再转为string类型,s和b中内容一样,修改b,s也变了,说明b和s是同一个地址。但是对s重新赋值后,意味着s的地址指向了“WORLD”,它们所使用的内存空间不同了,所以s改变后,b并不会改变。
所以他们的区别就在于 bytes.Buffer 是重新申请了一块空间,存放生成的string变量, 而strings.Builder直接将底层的[]byte转换成了string类型返回了回来,去掉了申请空间的操作。
(十一)golang 内存分析
编写过C语言程序的肯定知道通过malloc()方法动态申请内存,其中内存分配器使用的是glibc提供的ptmalloc2。 除了glibc,业界比较出名的内存分配器有Google的tcmalloc和Facebook的jemalloc。二者在避免内存碎片和性能上均比glic有比较大的优势,在多线程环境中效果更明显。
Golang中也实现了内存分配器,原理与tcmalloc类似,简单的说就是维护一块大的全局内存,每个线程(Golang中为P)维护一块小的私有内存,私有内存不足再从全局申请。另外,内存分配与GC(垃圾回收)关系密切,所以了解GC前有必要了解内存分配的原理。
为了方便自主管理内存,做法便是先向系统申请一块内存,然后将内存切割成小块,通过一定的内存分配算法管理内存。 以64位系统为例,Golang程序启动时会向系统申请的内存如下图所示:
预申请的内存划分为spans、bitmap、arena三部分。其中arena即为所谓的堆区,应用中需要的内存从这里分配。其中spans和bitmap是为了管理arena区而存在的。
arena的大小为512G,为了方便管理把arena区域划分成一个个的page,每个page为8KB,一共有512GB/8KB个页;
spans区域存放span的指针,每个指针对应一个page,所以span区域的大小为(512GB/8KB)乘以指针大小8byte = 512M
bitmap区域大小也是通过arena计算出来,不过主要用于GC。
span是用于管理arena页的关键数据结构,每个span中包含1个或多个连续页,为了满足小对象分配,span中的一页会划分更小的粒度,而对于大对象比如超过页大小,则通过多页实现。
根据对象大小,划分了一系列class,每个class都代表一个固定大小的对象,以及每个span的大小。如下表所示:
上表中每列含义如下:
class: class ID,每个span结构中都有一个class ID, 表示该span可处理的对象类型
bytes/obj:该class代表对象的字节数
bytes/span:每个span占用堆的字节数,也即页数乘以页大小
objects: 每个span可分配的对象个数,也即(bytes/spans)/(bytes/obj)waste
bytes: 每个span产生的内存碎片,也即(bytes/spans)%(bytes/obj)上表可见最大的对象是32K大小,超过32K大小的由特殊的class表示,该class ID为0,每个class只包含一个对象。
span是内存管理的基本单位,每个span用于管理特定的class对象, 跟据对象大小,span将一个或多个页拆分成多个块进行管理。src/runtime/mheap.go:mspan定义了其数据结构:
以class 10为例,span和管理的内存如下图所示:
spanclass为10,参照class表可得出npages=1,nelems=56,elemsize为144。其中startAddr是在span初始化时就指定了某个页的地址。allocBits指向一个位图,每位代表一个块是否被分配,本例中有两个块已经被分配,其allocCount也为2。next和prev用于将多个span链接起来,这有利于管理多个span,接下来会进行说明。
有了管理内存的基本单位span,还要有个数据结构来管理span,这个数据结构叫mcentral,各线程需要内存时从mcentral管理的span中申请内存,为了避免多线程申请内存时不断的加锁,Golang为每个线程分配了span的缓存,这个缓存即是cache。src/runtime/mcache.go:mcache定义了cache的数据结构
alloc为mspan的指针数组,数组大小为class总数的2倍。数组中每个元素代表了一种class类型的span列表,每种class类型都有两组span列表,第一组列表中所表示的对象中包含了指针,第二组列表中所表示的对象不含有指针,这么做是为了提高GC扫描性能,对于不包含指针的span列表,没必要去扫描。根据对象是否包含指针,将对象分为noscan和scan两类,其中noscan代表没有指针,而scan则代表有指针,需要GC进行扫描。mcache和span的对应关系如下图所示:
mchache在初始化时是没有任何span的,在使用过程中会动态的从central中获取并缓存下来,跟据使用情况,每种class的span个数也不相同。上图所示,class 0的span数比class1的要多,说明本线程中分配的小对象要多一些。
cache作为线程的私有资源为单个线程服务,而central则是全局资源,为多个线程服务,当某个线程内存不足时会向central申请,当某个线程释放内存时又会回收进central。src/runtime/mcentral.go:mcentral定义了central数据结构:
lock: 线程间互斥锁,防止多线程读写冲突
spanclass : 每个mcentral管理着一组有相同class的span列表
nonempty: 指还有内存可用的span列表
empty: 指没有内存可用的span列表
nmalloc: 指累计分配的对象个数线程从central获取span步骤如下:
将span归还步骤如下:
从mcentral数据结构可见,每个mcentral对象只管理特定的class规格的span。事实上每种class都会对应一个mcentral,这个mcentral的集合存放于mheap数据结构中。src/runtime/mheap.go:mheap定义了heap的数据结构:
lock: 互斥锁
spans: 指向spans区域,用于映射span和page的关系
bitmap:bitmap的起始地址
arena_start: arena区域首地址
arena_used: 当前arena已使用区域的最大地址
central: 每种class对应的两个mcentral
从数据结构可见,mheap管理着全部的内存,事实上Golang就是通过一个mheap类型的全局变量进行内存管理的。mheap内存管理示意图如下:
系统预分配的内存分为spans、bitmap、arean三个区域,通过mheap管理起来。接下来看内存分配过程。
针对待分配对象的大小不同有不同的分配逻辑:
(0, 16B) 且不包含指针的对象: Tiny分配
(0, 16B) 包含指针的对象:正常分配
[16B, 32KB] : 正常分配
(32KB, -) : 大对象分配其中Tiny分配和大对象分配都属于内存管理的优化范畴,这里暂时仅关注一般的分配方法。
以申请size为n的内存为例,分配步骤如下:
Golang内存分配是个相当复杂的过程,其中还掺杂了GC的处理,这里仅仅对其关键数据结构进行了说明,了解其原理而又不至于深陷实现细节。1、Golang程序启动时申请一大块内存并划分成spans、bitmap、arena区域
2、arena区域按页划分成一个个小块。
3、span管理一个或多个页。
4、mcentral管理多个span供线程申请使用
5、mcache作为线程私有资源,资源来源于mcentral。
Golang bytes.buffer详解
Buffer 介绍
Buffer 是 bytes 包中的一个 type Buffer struct{…}
A buffer is a variable-sized buffer of bytes with Read and Write methods. The zero value for Buffer is an empty buffer ready to use.
(是一个变长的 buffer,具有 Read 和Write 方法。 Buffer 的 零值 是一个 空的 buffer,但是可以使用)
Buffer 就像一个集装箱容器,可以存东西,取东西(存取数据)
创建缓冲器
输出
写入到缓冲器
buffer在new的时候是空的,也是可以直接Write的
Write
结果
WriteString
结果
WriteByte
WriteRune
结果
从缓冲器中写出
读出缓冲器
Read
ReadByte
返回缓冲器头部的第一个byte
ReadRun
ReadRune方法,返回缓冲器头部的第一个rune
为什么n==3,而n1==1呢?我们看下ReadRune 的源码
ReadBytes
ReadBytes方法,需要一个byte作为分隔符,读的时候从缓冲器里找出第一个出现的分隔符,缓冲器头部开始到分隔符之间的byte返回。
相当于有一个分隔符
ReadString
和readBytes方法类似
读入缓冲器
ReadFrom方法,从一个实现io.Reader接口的r,把r的内容读到缓冲器里,n返回读的数量
从缓冲器取出
Next方法,返回前n个byte(slice),原缓冲器变
缓冲区原理介绍
go字节缓冲区底层以字节切片做存储,切片存在长度len与容量cap, 缓冲区写从长度len的位置开始写,当lencap时,会自动扩容。缓冲区读会从内置标记off位置开始读(off始终记录读的起始位置),当off==len时,表明缓冲区已全部读完
并重置缓冲区(len=off=0),此外当将要内容长度+已写的长度(即len) = cap/2时,缓冲区前移覆盖掉已读的内容(off=0,len-=off),从避免缓冲区不断扩容
go语言bytes的介绍就聊到这里吧,感谢你花时间阅读本站内容,更多关于Go语言圣经、go语言bytes的信息别忘了在本站进行查找喔。