Go内置数据结构

Go内置数据结构

Go中有四大内置的数据结构:

  • 数组
  • 切片slice
  • 映射map
  • 字符串

数组

数组是一种很常见的数据结构,就是一系列相同类型的数据集合。计算机会为其分配一块连续的内存来保存数组中的元素,通过数组元素的索引访问数组元素,时间复杂度是O(1)

和Java不太相同,在go语言中,相同类型但是数组容量不同,也被视为不同的数组类型,只有两个条件都相同才是同一类型。

[1]int != [2]int

go语言底层中,数组类型的定义如下:

// Array contains Type fields specific to array types.
type Array struct {
	Elem  *Type // element type (数组元素类型)
	Bound int64 // number of elements; <0 if unknown yet(数组容量)
}

实际使用中,数组初始化:

var array [1]int
var array = [4]int{1,2,3,4}
var array [...]int{1,2,3}   // 编译器自动推导数组容量

实际走的底层初始化:

func NewArray(elem *Type, bound int64) *Type {
	if bound < 0 {
		Fatalf("NewArray: invalid bound %v", bound)
	}
	t := New(TARRAY)
	t.Extra = &Array{Elem: elem, Bound: bound}
	t.SetNotInHeap(elem.NotInHeap())
	return t
}

所以数组类型t是通过数组容量和数组元素类型一起决定的,并且当前数组是否应该在堆栈中初始化也在编译期就确定了。

第二个需要注意的是go在编译期可以对数组进行简单的越界检查,如果索引是常量的话,但是如果索引是变量的话,例如arr[i],是检查不了的,但是在运行期如果i越界了,会抛出panic,程序异常中断,类似于java的runtimeException

访问数组的索引是非整数时,报错 “non-integer array index %v”;
访问数组的索引是负数时,报错 “invalid array index %v (index must be non-negative)";
访问数组的索引越界时,报错 “invalid array index %v (out of bounds for %d-element array)";

第三个需要注意的问题是,数组是赋值问题,函数传递数组是传递数组的拷贝,所以如果传递的数组比较大的话,内存拷贝的开销也会比较大,这时建议传递数组的头指针,但是这样函数对数组的操作会对数组造成改变

	var array4 = testArray(arr4)	// 数组是值类型,在函数中传递的是数组的拷贝,所以不会修改到原数组,如果数组较大,数组拷贝开销大
	fmt.Println(array4)		// [2 3 4]
	fmt.Println(arr4)		// [1 2 3]
	fmt.Println("=======================")
	testArray2(&arr4)
	fmt.Println(arr4)		// [2 3 4] 传递指针可以修改原数组并且效率高


func testArray(array [3]int) [3]int {
	for index,val := range(array){
		val = val + 1
		array[index] = val
	}
	return array
}

func testArray2(arrayPoint *[3]int) {
	for index,val := range(arrayPoint){
		val = val + 1
		arrayPoint[index] = val
	}
}

切片slice

切片和数组非常相似,初始化方式也非常相似,切片其实是一种动态数组,可以自动扩容,容量可以发生变化。
初始化

	var sli1 []int
	var sli3 = make([]int,5,8) // 也可以使用make初始化,5是元素个数,8是总容量

切片底层数据结构:其实底层也是数组,类似于Java的ArrayList

type SliceHeader struct {
	Data uintptr    // 底层数组头指针
	Len  int        // 切片元素数量
	Cap  int        // 切片容量
}

切片截取:

	// 切片截取
	fmt.Println(sli2)
	tmp1 := sli2[:]	   // 切片不指定max,cap取得是底层数组的max
	fmt.Println(tmp1)  // [1 2 3 4 5 6]
	// 切片截取操作的是底层数组,底层数组改变,切片会随着改变
	sli2[0] = 10
	fmt.Println(sli2)	// [10 2 3 4 5 6]
	fmt.Println(tmp1)	// [10 2 3 4 5 6]

第一个需要注意的问题就是切片截取后会生成一个新的切片,但是这个切片和原切片底层共用一个数组,所以,底层数组一旦发生改变会影响到两个切片

当切片非常大或者会发生逃逸,那么切片将会在堆上进行分配,当切片比较小且不会逃逸,可直接在栈上进行分配

访问切片元素和访问数组元素方式一样,都是通过下标直接访问

slice[1]

最后需要注意的是切片的追加和扩容

切片的追加使用的是append

	sli4 = append(sli4, 4)

底层流程:

  • 容量判断,是否需要扩容
    • 扩容,返回扩容后的切片数组头指针
  • 元素转移
  • 初始化一个新的切片
// append(slice, 1, 2, 3)
ptr, len, cap := slice
newlen := len + 3
if newlen > cap {
    ptr, len, cap = growslice(slice, newlen)
    newlen = len + 3
}
*(ptr+len) = 1
*(ptr+len+1) = 2
*(ptr+len+2) = 3
return makeslice(ptr, newlen, cap)

切片扩容:

func growslice(et *_type, old slice, cap int) slice {
	newcap := old.cap
	doublecap := newcap + newcap
	if cap > doublecap {
		newcap = cap
	} else {
		if old.len < 1024 {
			newcap = doublecap
		} else {
			for 0 < newcap && newcap < cap {
				newcap += newcap / 4
			}
			if newcap <= 0 {
				newcap = cap
			}
		}
	}

在分配内存空间之前需要先确定新的切片容量,运行时根据切片的当前容量选择不同的策略进行扩容:

  • 如果期望容量大于当前容量的两倍就会使用期望容量;
  • 如果当前切片的长度小于 1024 就会将容量翻倍;
  • 如果当前切片的长度大于 1024 就会每次增加 25% 的容量,直到新容量大于期望容量;

映射map

哈希是除了数组外最常见的数据结构,数组是元素的序列集合,哈希是键值对映射的结构

哈希表是计算机科学中的最重要数据结构之一,这不仅因为它 O(1) 的读写性能非常优秀,还因为它提供了键值之间的映射。想要实现一个性能优异的哈希表,需要注意两个关键点 —— 哈希函数和冲突解决方法。

完美的哈希函数就是能让结果尽可能均匀的分布,哈希冲突尽可能小

解决哈希冲突的方法:

  • 开放地址法
    • 开放地址法是在遇到冲突后,继续遍历寻找下一个空的位置
  • 拉链法
    • 遇到冲突会选择以链表的形式将冲突的key链接起来

gomap的基本使用:

    // 初始化
	map2 := make(map[string]int)

新增元素

	res := make(map[string]interface{})
	res["code"] = 200
	res["msg"]  = "success"

删除元素

	delete(res2,"code")

遍历元素:

	for key,value := range res2{
		fmt.Println("key: ",key)
		fmt.Println("value: ",value)
	}

Go 语言运行时同时使用了多个数据结构组合表示哈希表,其中 runtime.hmap 是最核心的结构体

type hmap struct {
    count        int               /* Map中元素数量 */
    flags        int8              /* 相关标志位 */
    B            int8              /* (1<< B * 6.5)为最多元素的数量 */
    noverflow    int16             /* 溢出桶的数量 */
    hash0        uint32            /* 哈希种子 */
    buckets      unsafe.Pointer    /* 桶指针 */
    oldbuckets   unsafe.Pointer    /* 桶指针(只有扩容的时候才使用,指向旧的桶) */
    nevacuate    uintptr           /* 用于桶搬迁 */
    extra        *mapextra         /* 当key/value非指针时使用 */
}

type mapextra struct {
    overflow      *[]*bmap         /* 溢出桶的指针 */
    oldoverflow   *[]*bmap             
    nextOverflow  *bmap                   
}

type bmap struct {
    tophash  [bucketCnt]uint8      /* bucketCnt=8,一个桶只能存放8对k/v, 低B位用来寻找桶,高8位用来寻找元素 */
}

/* 当kev/value不为指针时,溢出桶存放到mapextra结构中,overflow存放buckets中的溢出
    桶,oldoverflow存放oldbuckets中的溢出桶,nextOverflow预分配溢出桶空间 */
type mapextra struct {
    overflow        *[]*bmap       /* 以切片形式存放buckets中的每个溢出桶 */
    oldoverflow     *[]*bmap       /*  以切片形式存放oldbuckets中的每个溢出桶*/
    nextOverflow    *bmap
}

源码分析

字符串

Go 语言中的字符串只是一个只读的字节数组,下图展示了 “hello” 字符串在内存中的存储方式

所以底层实现也是字节数组和长度两个字段:

type StringHeader struct {
	Data uintptr
	Len  int
}

字符串类型转换

func main() {
	str := "1"
	fmt.Println(strconv.Atoi(str))	// int error
	fmt.Println(strconv.ParseBool("false"))
	fmt.Println(strconv.Itoa(1))
	fmt.Println([]byte("test"))// 字符串转byte数组
	// 2,byte转为string
	byte1 := []byte{116,101,115,116}
	fmt.Println(string(byte1[:]))
}

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值