Golang 针对并发进行了优化,并且在规模上运行良好
自动垃圾收集明显比 Java 或 Python 更有效,因为它与程序同时执行
布尔类型
数字类型
整型
根据有符号分为:有符号、无符号类型;
根据占据的空间分为:8,16,32,64;
浮点型
float32:32位浮点型数;float64:64位浮点型数;
complex64:32位实数和虚数;complex128:64位实数和虚数;
其他
byte:类似于uint8,代表了 ASCII 码的一个字符
rune:类似于int32,表示的是一个 Unicode字符
uint:长度取决于 CPU,如果是32位CPU就是4个字节,如果是64位就是8个字节
uintptr:无符号整型,用于存放一个指针
字符串类型
数组类型
结构体类型
指针类型
Channel类型
切片类型
Map类型
函数类型
接口类型
他们都是分配内存空间的内置函数
make
用于创建切片、映射和通道(slice、map、channel)等引用类型的数据结构。
它返回一个已初始化并且可以使用的引用类型变量,通常用于创建动态大小的数据结构。
new
主要用于创建值类型的变量,如结构体、整数、浮点数等,而不是引用类型。
new
用于创建并返回一个指向新分配的零值的指针。
拷贝的是数据本身,创造一个新对象,新创建的对象与原对象不共享内存,新创建的对象在内存中开辟一个新的内存地址,新对象值修改时不会影响原对象值
值类型:对于 Go 中的基本数据类型(如整数、浮点数、字符串、结构体,数组等),赋值或传递参数时会进行深拷贝。这意味着创建一个新的值,而不是共享数据。
a := 10
b := a // 深拷贝
arr1 := [3]int{1, 2, 3}
arr2 := arr1 // 深拷贝
使用copy
赋值的数据是深拷贝
slice1 := []int{1, 2, 3}
slice2 := []int{0, 0, 0}
copy(slice2, slice1)
slice2[0] = 10
fmt.Printf("slice1:%v\n", slice1) //slice1:[1 2 3]
fmt.Printf("slice2:%v\n", slice2) //slice2:[10 2 3]
拷贝的是数据地址,只复制指向的对 象的指针,此时新对象和老对象指向的内存地址是一样的,新对象值修改时老对象也会变化
引用数据类型默认赋值操作就是浅拷贝:slice2 := slice1
arr1 := []int{1, 2, 3}
arr2 := arr1 // 浅拷贝
arr2[0] = 100
fmt.Printf("slice1:%v\n", arr1) //slice1:[100 2 3]
fmt.Printf("slice2:%v\n", arr2) //slice2:[100 2 3]
interface 是方法声明的集合
任何类型的对象实现了在interface 中声明的全部方法,则表明该类型实现了该接口
interface可以作为一种数据类型,实现了该接口的任何对象都可以给对应的接口类型变量赋值
举实例,
true
channel主要用于进程内各goroutine间的通信
type hchan struct {
qcount uint // 当前队列中剩余元素个数
dataqsiz uint // 环形队列长度,即可以存放的元素个数
buf unsafe.Pointer // 环形队列指针
elemsize uint16 // 每个元素的大小
closed uint32 // 标识关闭状态
elemtype *_type // 元素类型
sendx uint // 队列下标,指示元素写入时存放到队列中的位置
recvx uint // 队列下标,指示元素从队列的该位置读出
recvq waitq // 等待读消息的goroutine队列
sendq waitq // 等待写消息的goroutine队列
lock mutex // 互斥锁,chan不允许并发读写
}
环形队列作为其缓冲区,队列长度是创建channel时候指定的
从channel读数据,如果channel缓冲区为空或者没有缓冲区,当前goroutine会被阻塞。
向channel写数据,如果channel缓冲区已满或者没有缓冲区,当前goroutine会被阻塞。
被阻塞的goroutine将会挂在channel的等待队列中:
创建channel的过程实际上是初始化hchan结构。其中类型信息和缓冲区长度由make语句传入,buf的大小则与元素大小和缓冲区长度共同决定。
特点:
无缓冲的 channel 是同步的,有缓冲的 channel 是非同步的,缓冲满时发送阻塞
channel无缓冲时,发送阻塞直到数据被接收,接收阻塞直到读到数据;
channel有缓冲时,当缓冲满时发送阻塞,当缓冲空时接收阻塞。
如果给一个nil的 channel 发送数据,会造成永远阻塞。
如果从一个nil的 channel 中接收数据,会造成永久阻塞。
关闭值为nil的channel
关闭已经被关闭的channel
向已经关闭的channel写数据
Slice依托数组实现,底层数组对用户屏蔽,在底层数组容量不足时可以实现自动重分配并生成新的Slice。
type slice struct {
array unsafe.Pointer //array指向底层数组
len int //len代表切片长度
cap int //cap是底层数据的长度
}
1.8版本之前
上面描述的是一次追加一个元素 append(a,1),如果是一次追加多个元素append(a,1,2,3),容量扩容到大于切片长度的最小的偶数
a := make([]int, 0)
a = append(a, 1, 2, 3, 4, 5)
fmt.Printf("len(a):%v,cap(a):%v\n", len(a), cap(a))
//len(a):5,cap(a):6
切片添加追加一个元素之后,如果切片的cap不会发生扩容,那么底层指向的还是原来的那个数组
// 情况一:切片扩容后仍然指向原数组
originalSlice := make([]int, 0, 5)
originalSlice = append(originalSlice, 1)
originalSlice = append(originalSlice, 2)
originalSlice = append(originalSlice, 3)
modifiedSlice := originalSlice
// 添加元素到切片
modifiedSlice = append(modifiedSlice, 4)
fmt.Println("Original Slice:", originalSlice)
fmt.Println("Modified Slice:", modifiedSlice)
fmt.Printf("Original Slice Address: %p\n", &originalSlice[0])
fmt.Printf("Modified Slice Address: %p\n", &modifiedSlice[0])
//运行结果
//Original Slice: [1 2 3]
//Modified Slice: [1 2 3 4]
//Original Slice Address: 0xc00000e390
//Modified Slice Address: 0xc00000e390
如果切片的cap发生扩容,那么底层指向的已经不是原来那个数组,而是对数组进行了拷贝
originalSlice := make([]int, 0, 2)
originalSlice = append(originalSlice, 1)
originalSlice = append(originalSlice, 2)
modifiedSlice := originalSlice
// 添加元素到切片
modifiedSlice = append(modifiedSlice, 3)
fmt.Println("Original Slice:", originalSlice)
fmt.Println("Modified Slice:", modifiedSlice)
fmt.Printf("Original Slice Address: %p\n", &originalSlice[0])
fmt.Printf("Modified Slice Address: %p\n", &modifiedSlice[0])
//运行结果
//Original Slice: [1 2]
//Modified Slice: [1 2 3]
//Original Slice Address: 0xc00001c0c0
//Modified Slice Address: 0xc0000141e0
Slice底层结构并没有使用加锁等方式,不支持并发读写,所以并不是线程安全的,使用多个goroutine 对类型为 slice 的变量进行操作,每次输出的值大概率都不会一样,与预期值不一致;slice在并发执行中不会报错,但是数据会丢失
Golang的map使用哈希表作为底层实现,一个哈希表里可以有多个哈希表节点,也即bucket,而每个bucket就保存了map中的一个或一组键值对。
type hmap struct {
count int // 当前保存的元素个数
...
B uint8
...
buckets unsafe.Pointer // bucket数组指针,数组的大小为2^B
...
}
一个拥有4个bucket的map:
type bmap struct {
tophash [8]uint8 //存储哈希值的高8位
data byte[1] //key value数据:key/key/key/.../value/value/value...
overflow *bmap //溢出bucket的地址
}
tophash:是长度为8的数组,哈希值低位相同的键存入当前bucket时,会将哈希值的高位存放到该数组中,以方便后续匹配
data区存放的是key-value数据,存放顺序是key/key/key/…value/value/value,如此存放是为了节省字节对齐带来的空间浪费。
overflow 指针指向的是下一个bucket,据此将所有冲突的键连接起来。
当有两个或以上数量的键被哈希到了同一个bucket时,我们称这些键发生了冲突
Go使用链地址法来解决键冲突。由于每个bucket可以存放8个键值对,所以同一个bucket存放超过8个键值对时就会再创建一个键值对,用类似链表的方式将bucket连接起来。
bucket数据结构指示下一个bucket的指针称为overflow bucket,意为当前bucket盛不下而溢出的部分。事实上哈希冲突并不是好事情,它降低了存取效率
负载因子用于衡量一个哈希表冲突情况,公式为:
负载因子 = 键数量/bucket数量
例如,对于一个bucket数量为4,包含4个键值对的哈希表来说,这个哈希表的负载因子为1
哈希表需要将负载因子控制在合适的大小,超过其阀值需要进行rehash,也即键值对重新组织:
每个哈希表的实现对负载因子容忍程度不同,比如Redis实现中负载因子大于1时就会触发rehash,而Go则在在负载因子达到6.5时才会触发rehash,因为Redis的每个bucket只能存1个键值对,而Go的bucket可能存8个键值对,所以Go可以容忍更高的负载因子。
为了保证访问效率,当新元素将要添加进map时,都会检查是否需要扩容,扩容实际上是以空间换时间的手段。
触发扩容的条件有二个:
所谓的等量扩容并不是扩大容量。buckets数量不变,重新做一遍类似于增量扩容的搬迁动作,把松散的键值对重新排列一次,使得bucket的使用效率更高,进而保证更快的存取
map在遍历时,并不是从固定的0号bucket开始遍历的,每次遍历,都会从一个随机值序号的bucket,然后再从该桶中随机选择一个单元格(cell)开始遍历
在Go语言中,普通的map(即map
数据类型)是非线程安全的。这意味着在多个goroutine之间并发访问和修改同一个map时,可能会导致竞态条件和未定义的行为。
为了在多线程或多goroutine环境中安全地使用map,你有以下几种选项:
使用sync.Mutex
进行同步:你可以在每次访问map之前使用sync.Mutex
进行加锁和解锁操作,以确保一次只有一个goroutine能够访问map。
m = make(map[keyType]valueType)
var mu sync.Mutex
// 在读取或写入map之前加锁
mu.Lock()
m[key] = value
mu.Unlock()
使用sync.Map
:Go语言提供了sync.Map
类型,它是一种并发安全的map实现,可以安全地在多个goroutine之间进行读取和写入操作。
var m sync.Map
// 写入数据
m.Store(key, value)
// 读取数据
val, ok := m.Load(key)
M必须拥有P才可以执行G中的代码,P含有一个包含多个G的队列,P可以调度G交由M执行。
数量,调度过程:P维护调度(注意专业词汇)(了解G的生命周期)
work stealing
)hand off
缺点是需要STW,程序出现卡顿
不存在黑色对象引用到白色对象,当黑色节点新增了白色节点的引用时,将对应的白色节点改为灰色(满足强三色不变式)
黑色节点允许引用白色节点,但是该白色节点有其他灰色节点间接的引用(满足弱三色不变式)
满足弱三色不变式
GoV1.3- 普通标记清除法,整体过程需要启动STW,效率极低。
GoV1.5- 三色标记法, 堆空间启动写屏障,栈空间不启动,全部扫描之后,需要重新扫描一次栈(需要STW),效率普通
GoV1.8-三色标记法,混合写屏障机制, 栈空间不启动,堆空间启动。整个过程几乎不需要STW(在标记准备每个栈单独暂停),效率较高
defer语句用于延迟函数的调用,每次defer都会把一个函数压入栈中,函数返回前再把延迟的函数取出并执行。
错误处理:
可预测,不可预测,panic(执行过程,里面涉及到recover)