【每日八股】Golang篇(二):关键字(上)

目录

  • [make 和 new 的区别?](#make 和 new 的区别?)
  • [struct 能不能比较?](#struct 能不能比较?)
  • [为什么 slice 之间不能直接比较?](#为什么 slice 之间不能直接比较?)
  • [slice 的底层实现?](#slice 的底层实现?)
  • [slice 和数组的区别?](#slice 和数组的区别?)
  • [slice 的扩容机制?](#slice 的扩容机制?)
  • [slice 是线程安全的吗?](#slice 是线程安全的吗?)
  • [slice 之间如何比较?](#slice 之间如何比较?)
  • [map 之间如何进行比较?](#map 之间如何进行比较?)
  • [map 如何实现顺序读取?](#map 如何实现顺序读取?)
  • [map 的底层数据结构?](#map 的底层数据结构?)
  • [bucket 是如何工作的?](#bucket 是如何工作的?)
  • [map 的查找过程?](#map 的查找过程?)
  • [map 的扩容过程?](#map 的扩容过程?)
  • [如何实现一个线程安全的 map?](#如何实现一个线程安全的 map?)

make 和 new 的区别?

概念

make :make 能够分配并初始化类型所需的内存空间和结构返回引用类型的本身 ;make 具有使用范围的局限性,仅支持 channel、map、slice 三种类型;make 函数会对三种类型的内部数据结构(长度、容量等)赋值。

new :new 能够分配类型所需的内存空间 ,返回指针引用(指向内存的指针);new 可以被替代,可以通过字面值快速初始化。

例子

make

go 复制代码
// 仅用于 slice/map/channel 的初始化
// 注意, make 返回的是初始化后的类型实例, 而不是指针
obj := make(T, args...)

// 示例:
slice := make([]int, 5, 10)	// 长度为 5, 容量为 10 的切片
m := make(map[string]int)	// 初始化可用的 map
ch := make(chan int, 3)		// 缓冲容量为 3 的通道

new

go 复制代码
// 为类型 T 分配零值内存, 返回 *T 类型指针
ptr := new(T)

// 示例:
p := new(int)	// *int 类型, 指向零值 0
s := new([]int)	// *[]int 类型, 指向 nil slice

struct 能不能比较?

不同类型的结构体,如果成员变量类型、变量名和顺序都相同,而且结构体没有不可比较字段时,那么进行显式类型转换后就可以比较,反之不能比较。

同类型之间的 struct 进行比较时分两种情况:

  • struct的所有成员都是可以比较的,则该strcut的不同实例可以比较;
  • struct中含有不可比较的成员,则该struct不可以比较;

当需要比较两个struct内容时,最好使用 reflect.DeepEqual 方法进行比较,无论什么类型均可满足比较要求。

不可比较的类型

  • slice,因为 slice 是引用类型,只能和 nil 比较;
  • map,和 slice 同理,如果想要 map 当中的元素,只能通过循环;
  • 函数类型不可比较,没有可比性。

为什么 slice 之间不能直接比较?

因为 slice 的元素是间接引用 的,一个 slice 甚至可以包含自身,slice 的变量实际是一个指针,使用 == 其实在判断地址。

slice 的底层实现?

切片的底层是一个结构体,包含三个成员,第一个是 unsafe.Pointer 指针,指向一个具体的底层数组,一个是 cap,表示切片的容量,一个是 len,表示当前切片的真实长度(即其所包含元素的个数)。

由于切片是基于数组实现,因此它底层的内存是连续分配的,效率非常高,可以通过索引获得数据

切片本身不是动态数组或数组指针,而是基于其所设定的属性,将数据读写操作限定在一个指定区域内(可以理解为 C++ 当中的 vector)。

切片本身是一个只读对象(因为对它进行的修改是直接修改了底层的数组,可以将其视为其底层数组的视图,需要注意的是,从视图的角度来理解切片本身具有一定的误导性,因为切片本身确实比数组灵活很多,比如它的长度是不固定的,而数组一旦长度确定,就不可以再改变了。应该这样正确地理解:切片是对其底层数组的视图,但是底层数组是可以切换的,比如当底层数组的容量不能满足切片追加数据的需求时,切片将创建一个容量更大的数组,并以它作为新的底层数组),其工作机制类似于数组指针的一种封装。

如果 make 函数初始化了一个过大的切片,该切片就会逃逸到堆区;如果分配了一个较小的切片,那么内存就会被分配在栈区。切片大小的临界值默认为 64 KB,因此 make([]int64, 1023)make([]int64, 1024) 的内存布局是完全不同的。

go 复制代码
type slice struct {
	array unsafe.Pointer
	len int
	cap int
}

slice 和数组的区别?

切片是指针类型,数组是值类型

传递数组是通过拷贝的方式,而传递切片是通过传递引用的方式。

数组的长度固定,而切片可以动态扩容

数组是一组内存空间连续的数据,一旦初始长度大小固定了,就不会再改变了,切片的长度可以拓展,当切片底层的数组容量不够时,切片会创建新的底层数组。

切片比数组多一个属性容量 cap

数组的容量是有上限的,而切片的容量是可以动态扩容的,其扩容机制与 C++ 当中的 vector 类似。

slice 的扩容机制?

扩容主要分为两个过程:第一步是分配新的内存空间(即,新建一个容量更大的数组作为切片的底层数组);第二步是将原有切片的内容进行复制(即,将原先底层数组当中的数据复制到新的底层数组当中)。

分配新空间需要估计大致的容量,然后再确定容量。容量的选择具有不同的策略:

  • 如果期望容量大于当前容量的两倍,则直接使用期望容量;
  • 如果当前切片的长度小于 1024,容量就会翻倍;
  • 如果当前切片的长度大于 1024,每次扩容 25%,直到新容量大于期望容量;
  • 在进行 125% 的扩容过程中,如果最终容量发生溢出,即超过了 int 的最大范围,则最终的容量就是新申请的容量。

【总结:期望容量大于当前容量的两倍,则直接采用期望容量;当前切片长度小于 1024,则翻倍;大于 1024,则每次增加 25%;溢出则最终的容量就是新的容量】

对于切片的扩容:

  • 当切片较小时,采用较大的扩容倍速进行扩容,以避免频繁扩容 ,从而减少内存分配的次数和数据拷贝的代价
  • 切片较大时,采用较小的扩容倍速,主要是为了避免空间浪费。

slice 是线程安全的吗?

不是。slice 底层结构并没有加锁等方式,因此不支持并发读写,因此 slice 不是线程安全的。

使用多个 goroutine 对 slice 进行操作时,每次输出的值大概率会不一样,与预期不符。

slice 在并发执行时不会报错,但是可能会引起数据丢失。

实现线程安全的方法

互斥锁、读写锁、原子操作、sync.oncesync.atomicchannel

slice 之间如何比较?

go 复制代码
func equal(x, y []int) bool {
	if len(x) != len(y) {
		return false
	}
	for i := range x {
		if x[i] != y[i] {
			return false
		}
	}
	return true
}

map 之间如何进行比较?

go 复制代码
for equal(x, y map[string]int) bool {
	if len(x) != len(y) {
		return false
	}
	for k, xv := range x {
		if yv, ok := y[k]; !ok || yv != xv {
			return false
		}
	}
	return true
}

map 如何实现顺序读取?

map 不能顺序读取,因为它本身是无序的,想要有序读取,应该把 key 变为有序的,可行办法是把 key 放入 slice,对 slice 进行排序,再通过遍历 slice 的方式读取 value:

go 复制代码
package main

import (
	"fmt"
	"sort"
)

func main() {
	var m = map[string]int {
		"hello": 	0,
		"morning": 	1,
		"keke":		2,
		"jame":		3,
	}
	var keys []string
	for k := range m {
		keys = append(keys, k)
	}
	sort.Strings(keys)
	for _, k := range keys {
		fmt.Println("Key:", k, "Value:", m[k])
	}
}

map 的底层数据结构?

底层数据结构

map 的底层由 hmap 和 bmap 两个结构体实现,map 在实际存储键值对结构时用到了数组和链表。map 高效的原因也正是其结合了顺序存储结构和链式存储结构两种数据结构。数组是 map 的骨干,在数组下有一个类型为链表的元素。

hmap

go 复制代码
type hmap struct {
    count     int    	//元素的个数
    flags     uint8  	//状态标志
    B         uint8  	//可以最多容纳 6.5 * 2 ^ B 个元素,6.5为装载因子
    noverflow uint16 	//溢出的个数
    hash0     uint32 	//哈希种子

    buckets    unsafe.Pointer //指向一个桶数组
    oldbuckets unsafe.Pointer //指向一个旧桶数组,用于扩容
    nevacuate  uintptr        //搬迁进度,小于nevacuate的已经搬迁
    overflow *[2]*[]*bmap     //指向溢出桶的指针
}

hmap 是 map 最外层的数据结构,包括了 map 的各种基础信息,如大小、bucket 等。buckets 这个参数存储的是指向 buckets 数组第一个元素的指针。

bmap

go 复制代码
type bmap struct {
    //元素hash值的高8位代表它在桶中的位置,如果tophash[0] < minTopHash,表示这个桶的搬迁状态
    tophash [bucketCnt]uint8
    //接下来是8个key、8个value,但是我们不能直接看到;为了优化对齐,go采用了key放在一起,value放在一起的存储方式,
    keys     [8]keytype   //key单独存储
	values   [8]valuetype //value单独存储
	pad      uintptr
	overflow uintptr	  //指向溢出桶的指针
}

bucket 是如何工作的?

bucket 的工作原理有些超纲了,它在 csview 被标记为"了解",我目前完全理解不了,故此处我先暂时略过,答案详见 csview

map 的查找过程?

同上

map 的扩容过程?

同上

如何实现一个线程安全的 map?

通过三种方式实现:

  • 加读写锁;
  • 分片加锁;
  • sync.Map;

加读写锁、分片加锁,这两种方案都比较常用,后者的性能更好,因为它可以降低锁的粒度,提高访问此 map 对象的吞吐。前者并发性能虽然不如后者,但是加锁的方式更加简单。sync.Map 是 Go 1.9 增加的一个线程安全的 map ,虽然是官方标准,但反而是不常用的,原因是 map 要解决的场景很难描述。

相关推荐
用户108386386807 分钟前
95%开发者不知道的调试黑科技:Apipost让WebSocket开发效率翻倍的秘密
前端·后端
Tomorrow'sThinker10 分钟前
Python零基础学习第三天:函数与数据结构
开发语言·windows·python
元媛媛13 分钟前
Python - 轻量级后端框架 Flask
开发语言·python·flask
疏狂难除24 分钟前
基于Rye的Django项目通过Pyinstaller用Github工作流简单打包
后端·python·django
钢板兽28 分钟前
Java后端高频面经——Spring、SpringBoot、MyBatis
java·开发语言·spring boot·spring·面试·mybatis
钢板兽33 分钟前
Java后端高频面经——JVM、Linux、Git、Docker
java·linux·jvm·git·后端·docker·面试
爱吃柠檬呀37 分钟前
《C陷阱与缺陷》读书笔记(一)
c语言·开发语言·算法·《c陷阱与缺陷》·编写程序
行码棋1 小时前
【Python】omegaconf 用法详解
开发语言·python
awonw1 小时前
[java][基础] 悲观锁 vs 乐观锁
java·开发语言
未完结小说1 小时前
声明式远程调用:OpenFeign 基础教程
后端