Golang知识点五、数据类型

数据类型

  从本篇文章开始,记录Golang数据类型相关的内容,数据类型相关的知识点包括slice、map、string三个部分。

1. 切片

1.1. 从数组说起

  数组是具有固定长度具有零个或者多个相同数据类型元素的序列。

  由于数组长度固定,在Go里很少直接使用。

  1. 定义数组
// 3种方式,声明,初始,省略号

// 变量arr1类型为[5]int
var arr1 [5]int

// 变量arr2类型为[3]int,同时初始化赋值
var arr2 [5]int = [5]int{1,2,3}

// 让编译器自己数,结果为[3]int
arr3 := [...]int{1,2,3}

// 错误例子,因为[3]int和[4]int是两种类型
arr3 := [4]int{1,2,3,4}

  • 数组长度是数组类型的一部分[3]int[5]int就是两种类型。
  • 数组长度必须是常量表达式,即这个表达式的值在编译阶段就可以确定。
  1. 查看数组
arr1 := [...]{1,2,3}
lenth := len(arr1)
  1. 数组遍历
arr1 := [3]{1,2,3}
for i,val := range arr1 {
    
}
  1. Go中的数组是值类型,换句话说,如果你将一个数组赋值给另外一个数组,那么,实际上就是将整个数组拷贝一份。
  2. 如果Go中的数组作为函数的参数,那么实际传递的参数是一份数组的拷贝,而不是数组的指针。
  3. 注意!!!在其他语言中,数组是隐式的使用引用传递;Golang传参时,传入的参数会创建一个副本,使用这种方式传递大的数组会变的很低效。

1.2. slice底层数组

  切片用于表示一个拥有相同类型元素可变长的序列,看上去像是没有长度的数组类型。

  1. 切片是引用类型,因此在当传递切片时将引用同一指针,修改值将会影响其他的对象。
  2. 切片可能会在堆上分配内存,本身不是动态数组或者数组指针,内部是通过指针引用底层数组,切片本身是一个只读对象,本身没有数据,底层数组才有数据,类似于数组指针的一种封装,是引用类型
  3. 使用for range 遍历slice的时候,拿到的value其实是切片的值拷贝,每次打印出来的value地址不变
  4. slice扩容时,当cap小于1024的时候,每次扩容都会变成原来容量的2倍;当大于1024的时候,每次变为之前的1.25倍
  • 所谓切片是引用类型,是指切片作为函数的参数,操作的是同一个底层数组。(由于引用类型数据存放在堆上,值类型数据存放在栈上,因此切片数据存放在堆上,栈上保存堆在内存中的地址)

切片拷贝:

  1. 对于切片直接使用=拷贝,实际上是浅拷贝,只拷贝了切片在堆上的内存地址;sliceA = sliceB
  2. 对于切片深拷贝的需求,可以使用copy内置函数完成;copy(sliceA,sliceB)
	func nonempty(strings []string) []string {
	    i := 0
	    for _, s := range strings {
	        if s != "" {
	            strings[i] = s
	            i++
	        }
	    }
	    return strings[:i]
	}
  • golang中函数传递都是值传递,map、channel、slice都是引用传递,会传递指针值。参数传递时,切片进行浅拷贝(指针、cap、len),在函数内部发生append扩容等操作时,会进行深拷贝,会为切片分配新的内存https://zhuanlan.zhihu.com/p/111796041
	//定义一个函数,给切片添加一个元素
	func addOne(s []int) {
	    s[0] = 4  // 可以改变原切片值
	    s = append(s, 1)  // 扩容后分配了新的地址,原切片将不再受影响
	    s[0] = 8 
	}
	var s1 = []int{2}   // 初始化一个切片
	addOne(s1)          // 调用函数添加一个切片
	fmt.Println(s1)     // 输出一个值 [4]

1.3. 切片扩容

  1. 预估扩容后的newCap
// 预估规则:
if oldCap * 2  < newCap
	直接分配内存
else 
	if oldLen < 1024  newCap = oldCap * 2
	if oldLen > 1024 newCap = oldCap *1.25
  1. newCap个元素需要多大的内存?
// 预估到的newCap只是扩容后元素的个数,具体分配多大的内存呢?
// newCap * sizeof(T)吗?
// 事实上,许多编程语言中,申请分配内存,并不是直接和操作系统交涉,而是和语言自身实现的内存管理模块。内存管理模块会提前申请一批常用的内存,管理起来,需要申请 内存时内存管理模块会帮我们匹配到最接近的规格
  1. 匹配到合适的内存规格

2. map

2.1. hash表

  键值对的存储,我们首先会想到hash表,hash表通常会由一堆桶来存储键值对。一个键值对来了,如何确定哪个桶呢?

  • 首先通过hash函数把“键”处理一下,得到一个hash值,根据这个hash值从m个桶中选择一个,桶编号区间为[0,m-1]
  • 两个方法确定将这个值存到哪个桶。1. 取模法 h a s h hash hash % m m m    2. h a s h hash hash& ( m − 1 ) (m-1) (m1),注意一定是2的整数次幂,否则一定会出现有些桶不会被选中的情况。
  • 如果两个hash选择了同一个桶,称发生了hash冲突。解决hash冲突的办法有两种:顺位后移即开放地址法;添加链表即拉链法。

2.2. hash冲突

  hash冲突的发生会影响hash表的读写效率,选择散列均匀的哈希函数可以减少哈希冲突的发生,适时的对hash表进行扩容也是保障读写效率的有效手段。

  扩容时通常会把 c o u n t / m count/m count/m作为是否需要扩容的判断依据, l o a d = c o u n t / m load = count/m load=count/m 称为“负载因子”。需要扩容时,就要分配更多的桶,它们就是新桶,需要把旧桶里存储的键值对都迁移到新桶里。

  如果需要迁移的数据过多,通常会在哈希表扩容时,先分配足够多的新桶,然后用一个字段记录旧桶的位置,再增加一个字段记录旧桶的迁移进度,直到所有数据全部迁移。

  把键值对迁移的时间分摊到多次哈希表中操作的方式,就是“渐进式扩容”,可以避免一次性扩容带来的性能瞬间抖动。

扩容规则:

  • Golang中负载因子loadFactor > 6.5 时,就会发生翻倍扩容,分配新桶的数目是旧桶的2倍。每个旧桶的键值都会分流到两个新桶中。
  • 如果loadFactor没有超标,但使用的溢出桶较多,也会发生扩容,即等量扩容。即创建和旧桶数目一样多的新桶,然后把原来的键值对,迁移到新桶中。
  • 既然是等量为什么还要迁移呢?先思考什么情况下会导致负载因子没有超标,而使用溢出桶较多?答案是很多键值对被删除的情况下。同样数目的键值对,迁移到新桶中能够排列的更紧凑,从而减少溢出桶的使用,这便是等量扩容的意义所在。

2.3. Golang中map

  Golang中map类型的底层实现就是哈希表。

	type hmap struct {
		// Note: the format of the hmap is also encoded in cmd/compile/internal/gc/reflect.go.
		// Make sure this stays in sync with the compiler's definition.
		count     int // # live cells == size of map.  Must be first (used by len() builtin)
		flags     uint8
		B         uint8  // log_2 of # of buckets (can hold up to loadFactor * 2^B items)
		noverflow uint16 // approximate number of overflow buckets; see incrnoverflow for details
		hash0     uint32 // hash seed
	
		buckets    unsafe.Pointer // array of 2^B Buckets. may be nil if count==0.
		oldbuckets unsafe.Pointer // previous bucket array of half the size, non-nil only when growing
		nevacuate  uintptr        // progress counter for evacuation (buckets less than this have been evacuated)
	
		extra *mapextra // optional fields
	}

  hmap结构体最后有一个extra字段,执行一个mapextra结构体,里面记录的都是溢出桶相关的信息。如编号为2的桶满了,就会在后面链一个溢出桶,nextoverflow就执行下一个空闲桶。

	type mapextra struct {
		// If both key and elem do not contain pointers and are inline, then we mark bucket
		// type as containing no pointers. This avoids scanning such maps.
		// However, bmap.overflow is a pointer. In order to keep overflow buckets
		// alive, we store pointers to all overflow buckets in hmap.extra.overflow and hmap.extra.oldoverflow.
		// overflow and oldoverflow are only used if key and elem do not contain pointers.
		// overflow contains overflow buckets for hmap.buckets.
		// oldoverflow contains overflow buckets for hmap.oldbuckets.
		// The indirection allows to store a pointer to the slice in hiter.
		overflow    *[]*bmap
		oldoverflow *[]*bmap
	
		// nextOverflow holds a pointer to a free overflow bucket.
		nextOverflow *bmap
	}

  每个桶里可以放8个键值对,但是为了让内存排列更加紧凑,8个key放一起,8个value放一起,8个key前是8个top hash,每个top hash对应hash值的高8位。

	type bmap struct {
		// tophash generally contains the top byte of the hash value
		// for each key in this bucket. If tophash[0] < minTopHash,
		// tophash[0] is a bucket evacuation state instead.
		tophash [bucketCnt]uint8
		// Followed by bucketCnt keys and then bucketCnt elems.
		// NOTE: packing all the keys together and then all the elems together makes the
		// code a bit more complicated than alternating key/elem/key/elem/... but it allows
		// us to eliminate padding which would be needed for, e.g., map[int64]int8.
		// Followed by an overflow pointer.
	}

在这里插入图片描述
  溢出桶: 为了减少扩容次数而引入溢出桶,当一个桶存满了,还有可用的溢出桶时,就会在后面链一个溢出桶。实际上,如果hash表要分配的桶的数目大于 2 4 2^{4} 24,就认为使用到溢出桶的几率比较大,就会分配 2 B − 4 2^{B-4} 2B4个溢出桶备用,这些溢出桶和常规桶在内存中是连续的。

3. 字符串

  Golang中字符串类型用关键字string来标识。首先从数值型说起,计算机中数值表示的最小单位是bit,1bit可以表示0或1两个数字;1个字节可以表示0到255共256个数字;2个字节可以表示0到65535共65536个数字。那么一堆二进制0和1,怎么表示成字母A呢?
在这里插入图片描述

3.1. 字符

  为了用一堆二进制0和1,表示字符。一种直接的方法就是给字符编号,于是有了字符集的概念。ASCII码字符集收录了128个字符,其扩展字符也只有256个,GB2312增加了汉字,BIG5增加繁体字。

  上述字符集都有一定的局限性,不同国家语言存在差异性,于是,便有了Unicode全球化编码字符集,Unicode字符集自1990年开始研发,1994年正式公布,实现了跨语言,跨平台的文本转换与处理。字符集使字符与二进制之间可以互相转换

3.2. UTF8编码

  单个字符容易转化,那如何表示字符串呢?如要把“eggo 世界”这个字符串用二进制表示,最直接的方法是直接转换。
在这里插入图片描述
  问题是,我怎么知道这么长的二进制数字,如何进行划分?于是,主要问题变成了,划分字符边界。由于Unicode字符集中,一个字符最多占4字节,一种直接的方法是,4字节表示一个字符,高位补0,这种方法固然可以,但是会浪费内存,称为定长编码。

  于是,有了传说中的UTF8编码方式,因此Unicode只是一个字符集,UTF8是一种编解码方式,主要为了解决如何划分Unicode二进制码的字符边界问题,同时保证了内存的合理利用。
在这里插入图片描述

3.3. 字符串

  var str string = "hello"在内存中结构如下,问题是如何知道字符串结束的位置呢?C语言的做法是,在结尾处放一个特定标识符,这就限制了内容中不能再出现这个标识符,Golang中没有采用了这种做法。
在这里插入图片描述

  1. Golang中字符串
	s1 := "eggo世界"

在这里插入图片描述
2. 字符串读写

  • 可以使用fmt.Printf("%c\n",s1[2])这样的方式来读取,但是不可以使用s1[2] = 'b'这样的方式修改。
  • Golang规定,字符串的内容不能够进行修改,于是把字符串内容分配到只读内存段,这是由于字符串变量可以共用底层字符串内容,如果允许修改只读内存段的话,会影响到其他的字符串。

  如果真要修改字符串某个字符,可以强制转换为字节类型的slice后,再进行修改,从而脱离只读内存的限制。

	bs := ([]byte)(s1)
	bs[2] = 'b'

  如上代码,会为bs重新分配一段内存,并拷贝s1中的内容。

4. 参考文献

[1] https://www.bilibili.com/video/BV1Sp4y1U7dJ?from=search&seid=1014416151836055424

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值