Dig101-Go之灵活的slice

Dig101: dig more, simplified more and know more

Slice作为go常用的数据类型,在日常编码中非常常见。
相对于数组的定长不可变,slice使用起来就灵活了许多。

0x01 slice 到底是什么?

首先我们看下源码中slice结构的定义

1
2
3
4
5
6
// src/runtime/slice.go
type slice struct {
array unsafe.Pointer
len int
cap int
}

slice数据结构如上,Data指向底层引用的数组内存地址, len是已用长度,cap是总容量。
为验证如上所述,我们尝试声明一个slice a,获取 a的sliceHeader头信息,并用%p获取&a, sh, a, a[0]的地址
看看他们的地址是否相同。

1
2
3
4
5
6
7
8
a := make([]int, 1, 3)
//reflect.SliceHeader 为 slice运行时数据结构
sh := (*reflect.SliceHeader)(unsafe.Pointer(&a))
fmt.Printf("slice header: %#v\naddress of a: %p &a[0]: %p | &a: %p sh:%p ",
sh, a, &a[0],&a, sh)

//slice header: &reflect.SliceHeader{Data:0xc000018260, Len:1, Cap:3}
//address of a: 0xc000018260 &a[0]: 0xc000018260 | &a: 0xc00000c080 sh:0xc00000c080

结果发现a和&a[0]地址相同。 这个好理解,切片指向地址即对应底层引用数组首个元素地址
&a和sh及sh.Data指向地址相同。这个是因为这三个地址是指slice自身地址。
这里【slice自身地址不同于slice指向的底层数据结构地址】, 清楚这一点对于后边的一些问题会更容易判断。

Read More

Dig101-Go之for-range排坑指南

好久没写了,打算今年做个Dig101系列,挖一挖技术背后的故事。

Dig101: dig more, simplified more and know more

golang常用的遍历方式,有两种: for 和 for-range。
而for-range使用中有些坑常会遇到,今天我们一起来捋一捋。

update: go 1.22中for遍历中变量已不再使用同一个变量共享, 新版本中文中for遍历变量共享问题已不存在。

0x01 遍历取不到所有元素指针?

如下代码想从数组遍历获取一个指针元素切片集合

1
2
3
4
5
6
7
8
arr := [2]int{1, 2}
res := []*int{}
for _, v := range arr {
res = append(res, &v)
}
//expect: 1 2
fmt.Println(*res[0],*res[1])
//but output: 2 2

答案是【取不到】
同样代码对切片[]int{1, 2}map[int]int{1:1, 2:2}遍历也不符合预期。
问题出在哪里?

Read More

golang的Mutex锁如何实现

概述

golang中锁实现的整体思路是利用类似信号量的P,V操作 (互斥锁:Mutex)
来对有限个goroutine争夺资源的共享。

在获取资源accquire是执行P,对锁标志位sema-1,其他获取资源时发现资源位为0则需阻塞等待
释放资源release是执行V,对锁标志位sema+1。
此时若无阻塞等待的goroutine则直接释放,否则需唤醒一个阻塞goroutine。

为实现上诉操作定义了以下结构

1
2
3
4
5
6
7
8
9
10
11
type Mutex struct {
state int32 //倒数(右到左)三位分别是 锁、唤醒、饥饿的标志位;剩余29位是当前阻塞等待锁的goroutine个数
sema uint32 //信号量标记位
}
const (
mutexLocked = 1 << iota // mutex is locked
mutexWoken
mutexStarving
mutexWaiterShift = iota
starvationThresholdNs = 1e6 //饥饿模式阈值:超1ms获取不到锁则进入饥饿模式
)

其中饥饿标志位引入是为了解决goroutine过长时间阻塞问题

互斥锁有正常模式和饥饿模式两种

1.正常模式下
阻塞排队的goroutine先进先出,被唤醒的goroutine和新来的goroutine争夺锁,新来的goroutine
因已经在cpu执行过而占有优势,更容易获得锁。

2.饥饿模式下
阻塞超过1ms还没抢到锁则转入饥饿模式
此模式下,新来的goroutine只能排队尾,不争夺锁,不自旋等待;
锁释放后会直接移交给队首goroutine。

3.恢复正常模式
满足以下任意一个条件时
1)等候时间小于1ms
2)阻塞队列为空时

Read More

go源码分析之内存分配

go源码分析之内存池

概述

go自带内存管理,主要是内存池和垃圾回收两部分。因为其对内存的管理在性能和空间利用率上的高效,go内存大多数情况不需要用户自己去管理内存,让程序员减少了很多在内存管理的心智成本。虽然如此,本文还是借着源码,分析下go的内存管理中内存池分配时如何实现的,希望对大家了解有所帮助。如有问题,欢迎探讨指正。
(源码基于go 1.10.3)

首先,内存分配模型基于tcmalloc,Tcmalloc是Google gperftools里的组件之一。全名是 thread cache malloc(线程缓存分配器),其内存管理分为线程内存和中央堆两部分。在并行程序下分配小对象(<=32k)的效率很高。
Tcmalloc核心思想是把内存分成多级来降低锁的粒度。每个线程都会有一个cache,用于无锁分配小对象,当内存不足分配小对象,就去central申请,在不足就去heap申请,heap最终是向操作系统申请。
这样的分配模型,维护一个用户态的内存池,不仅提高了内存在频繁分配、释放时的效率,而且有效地减少内存碎片。

下面我们依次看下go中内存如何划分,主要的一些内存结构,最后在结合源码看一下主要的内存分配流程。

Read More

go学习之堆栈中的变量

关于变量分配在堆和栈的问题一直不太了解,直到看了附录几篇文章,有了一个初步的认识。

先看官网怎么说

go变量分配到堆还是栈

从正确的角度来看,你不需要知道。Go中的每个变量只要有引用就会存在。实现选择的存储位置与语言的语义无关。

存储位置对编写高效程序有影响。如果可能的话,Go编译器将为该函数的栈帧(stack frame)中分配本地变量。但是,如果编译器无法证明变量在函数返回后未被引用,则编译器必须在存在垃圾回收的堆(garbage-collected heap)上分配变量以避免指针错误。另外,如果局部变量非常大,将其存储在堆而不是栈上可能更有意义。

在当前的编译器中,如果一个变量的地址被占用,那么该变量就成为堆中的一个候选对象。但是,一些基本的逃逸分析可以识别出某些情况下,这些变量不会在函数的返回后还存在,就可以驻留在栈中。

Read More

Go Memory Model

在了解golang的内存模型后,对一些读写顺序如何保证有了了解,记录在这里
官网原文

内存模型

Happens before

Go内存操作中定义读写执行的顺序,没有明确的执行先后,则不能保证其结果顺序

对V的写入w可被读取r的条件

1.r 不happen before w
2.没有其他的写入w’ happen after w 且 happen before r

保证对V的写入w是读取r唯一可获取的条件(无读写并发)
1.w happen before r
2.其他的写入w happen after r 或 happen before w

Read More