内置数据类型的实现
内置的扩充类型皆为结构体
string 字符串
struct string {
byte* str;
intgo len;
}通常string常量是编译器分配到只读段的(.rodata),对应的数据地址不可写入。fmt.Sprintf生成的字符串分配在堆上,对应数据地址可修改
底层结构(字符串赋值时拷贝的结构)
type StringHeader struct {
Data uintptr
Len int
}string 与 byte 相互转换
//return GoString's buffer slice(enable modify string)
func StringBytes(s string) Bytes {
return *(*Bytes)(unsafe.Pointer(&s))
}
// convert b to string without copy
func BytesString(b []byte) String {
return *(*String)(unsafe.Pointer(&b))
}
func unsafeEqual(a string, b []byte) bool {
bbp := *(*string)(unsafe.Pointer(&b))
return a == bbp
}rune 转成 string 因为内存结构的不同,因此必定会发生内存重分配
var p []byte
buf := make([]byte, 3)
for _, r := range s {
n := utf8.EncodeRune(buf, r)
p = append(p, buf[:n]...)
}
return string(p)本质上,字符串的赋值只是复制了数据地址和对应的长度,不会导致底层字节数组的赋值 字符串也支持切片操作
普通数组
有多种初始化方式
var a [3]int // 初始化为零值,3个元素
var b = [...]int{1, 2, 3} // 字面量初始化
var c = [...]int{2: 3, 1: 2} // 下标:值初始化,其余没指定的就是0slice
type slice struct {
array unsafe.Pointer
len int
cap int
}使用 for-range 的方式进行迭代性能会好一些,因为不会进行越界判断
只根据len次数去迭代,无额外内存代价
for range times {
// do something
}使用 append 支持链式操作,不过在添加的时候生成中间临时切片,可以使用下面的方式进行插入元素
a = append(a, 0)
copy(a[i+1:], a[i:]) // 往后挪元素
a[i] = x // 再插入a = a[:copy(a, a[N:])] // 删除开头N个元素判断一个切片是否为空的时候,一般通过 len 获取切片的长度来判断,一般通过 len 获取切片的长度来判断,一般很少将切片和 nil 做直接的比较
利用长度为0的切片进行原地修改
// 传入的 s 的底层数组会被 b 修改
func TrimSpace(s []byte) []byte {
b := s[:0]
for _, x := range s {
if x != ' ' {
b = append(b, x)
}
}
return b
}高效操作的要点是降低内存分配的次数,尽量保证 append 操作不会超过 cap 的容量
如果我们只是使用很大一段 slice 数组的一小部分,那么其余的部分依然不会被垃圾回收,因此更好的做法是通过 copy 返回一个真正容量上更小的 slice,使得原大 slice 可以被回收
切片强制类型转换时可以通过直接修改切片的头信息来实现转换
// 对浮点切片进行高速排序
func SortFloat64FastV2(a []float64) {
var c []int
aHdr := (*reflect.SliceHeader)(unsafe.Pointer(&a))
cHdr := (*reflect.SliceHeader)(unsafe.Pointer(&c))
*cHdr = *aHdr
sort.Ints(c)
}map
// A header for a Go map.
type hmap struct {
count int // len()返回的map的大小 即有多少kv对
flags uint8
B uint8 // 表示hash table总共有2^B个buckets
hash0 uint32 // hash seed
buckets unsafe.Pointer // 按照low hash值可查找的连续分配的数组,初始时为16个Buckets.
oldbuckets unsafe.Pointer
nevacuate uintptr
overflow *[2]*[]*bmap //溢出链 当初始buckets都满了之后会使用overflow
}buckets 是存放桶的一维数组 overflow 是桶满了之后挂载在桶中的链表
每个桶最多存 8 个KV对,还有指向下一个桶的指针,为了节约内存空间,存储顺序分别为 kkkkkkkkvvvvvvvv,不成对存储防止内存对齐导致空间浪费
增量扩容:同 redis 的实现方式类似,扩容的时候为了降低使用时的延迟,map 中会同时维护当前的桶和旧的桶,新的 kv 使用新的 hash 存到新的桶中,同时在每一次插入或者删除的时候迁移 1 到2个键值对,迁移过的 KV 会进行标记,直到所有的旧表中的数据都迁移到新表中时,解除旧引用并释放内存
插入:hash之后的值,通过低8位来寻找键对应的桶,找到空的位置来存放哈希值的高八位 查找:同样对应插入的流程,如果还未迁移到新的 map 就会回到旧的去读,会与桶里的 8 个key高8位进行循环比对,如果桶里没有,那么就去 overflow 溢出链中找
删除:不会收缩不再使用的空间,以备未来使用
map 的所谓泛型其实是在编译时确定好 maptype 和 _type 的值,编译好的程序只含有用户定义的 map 类型的实现
也同样有其他语言所有的 hash 方法和 equal 方法
type typeAlg struct {
// function for hashing objects of this type
// (ptr to object, seed) -> hash
hash func(unsafe.Pointer, uintptr) uintptr
// function for comparing objects of this type
// (ptr to object A, ptr to object B) -> ==?
equal func(unsafe.Pointer, unsafe.Pointer) bool
}每一个 go 的类型都有对应一个 typeAlg
interface 接口
type _interface struct {
dynamicTypeInfo *_implementation
dynamicValue unsafe.Pointer // 通用指针类型
}
type _implementation struct {
itype *_type // 接口类型
dtype *_type // 运行时类型,必须实现 itype
methods []*_func // 实现了对应 itype 的方法的函数指针
}为了避免其他的类型无意中适配了接口,在设计接口的时候可以定义一些特有的名字的函数来避免此问题
纯虚继承:通过嵌入匿名指针对象或者匿名接口,然后再运行时动态注入来实现
byte
byte是uint8的别名
rune
rune是int32的别名
int
比较运算
大部分的类型是可以进行比较运算的,除了 func, map, slice 。如果是接口类型,那么具体类型需要支持 == 和 != 操作