赞
踩
1、make和new区别?
make和new都是go的内置函数(builtin包下)。
func new(Type) *Type:内建函数new分配内存。其第一个实参为类型,而非值。其返回值为指向该类型的新分配的零值的指针。
func make(Type, size IntegerType) Type:内建函数make分配并初始化一个类型为切片slice、映射map、或通道channel的对象。其第一个实参为类型,而非值。make的返回类型与其参数相同,而非指向它的指针。其具体结果取决于具体的类型:
- 切片:size指定了其长度。该切片的容量等于其长度。切片支持第二个整数实参可用来指定不同的容量;
- 它必须不小于其长度,因此 make([]int, 0, 10) 会分配一个长度为0,容量为10的切片。
- 映射:初始分配的创建取决于size,但产生的映射长度为0。size可以省略,这种情况下就会分配一个
- 小的起始大小。
- 通道:通道的缓存根据指定的缓存容量初始化。若 size为零或被省略,该信道即为无缓存的。
new 只分配内存;make 只能用于 slice、map 和 channel 的初始化。
2、Go的内存管理?
内存管理会有两个问题:1)内存碎片——提高内存利用率。2)多线程——稳定性,效率问题。
内存划分:
(512GB/8KB) * 指针大小8byte = 512M
512GB / (指针大小(8 byte) * 8 / 2) = 16G
,用于表示arena区域中哪些地址保存了对象, 并且对象中哪些地址包含了指针,主要用于GC。span是内存管理的基本单位,每个span用来管子特定的size class对象,根据size class,span将若干个页分成块进行管理。
stack:Go语言中,每个goroutine采用动态扩容方式,初始2KB,按需增长,最大1G。此外GC会收缩栈空间。
BTW,增长扩容都是有代价的,需要copy数据到新的stack,所以初始2KB可能有些性能问题。
Cache:从上面我们知道go通过span来分配内存,那在哪里用span?每个P都有mcache,通过mcache管理每个G需要的内存。
alloc是span数组,长度是67 << 1,说明每种size class有2组元素。第一组span对象中包含了指针,叫做scan,表示需要gc scan;第二组没有指针,叫做noscan。提高gc scan性能。
mcache初始没有span,G先从central动态申请span,并缓存在cache。
Central——从central获取span步骤如下: 加锁 —> 从nonempty列表获取一个可用span,并将其从链表中删除 —> 将取出的span放入empty链表 —> 将span返回给线程 —> 解锁 —> 线程将该span缓存进cache。span归还步骤:加锁 —> 将span从empty链表删除 —> 将span加入nonempty列表 —> 将span返回给线程 —> 解锁。
heap:central只管理特定的size class span,所以必然有一个更上层的数据结构,管理所有的sizeclass central,这就是heap。
Go的内存分配核心思想
Go是内置运行时的编程语言(runtime),像这种内置运行时的编程语言通常会抛弃传统的内存分配方式,改为自己管理。这样可以完成类似预分配、内存池等操作,以避开系统调用带来的性能问题,防止每次分配内存都需要系统调用。
Go的内存分配的核心思想可以分为以下几点:
参考:图解golang内存分配机制 (转) - 孤独信徒 - 博客园
2、调用一个函数,它需要传入一个结构体的话,它传指针还是传值?哪个更好?
函数传递指针不一定比传值效率高。传递指针可以减少底层值的拷贝,可以提高效率,但是如果拷贝的数据量小,由于指针传递会产生逃逸,可能会使用堆,也可能会增加GC的负担,所以传递指针不一定是高效的。
所有对象默认都在栈中,当有下列情况时,对象逃逸到堆中:
3、指针逃逸分析的作用?
在编译原理中,分析指针动态范围的方法称之为逃逸分析。通俗来讲,当一个对象的指针被多个方法或线程引用时,我们称这个指针发生了“逃逸”。
逃逸总结:
栈上分配内存比在堆中分配内存有更高的效率。
栈上分配的内存不需要GC处理。
堆上分配的内存使用完毕会交给GC处理。
逃逸分析目的是决定内分配地址是栈还是堆。
逃逸分析在编译阶段完成。
4、Linux操作系统中线程有哪几种模型?
5、Go垃圾回收,stop the world。
Stop the world:停止程序意味着停止所有正在运行的 goroutine。
6、PMG模型。
P:processor ,代表一个逻辑处理器,也就是执行代码的上下文环境。
M:machine ,代表一个内核线程(OS线程),这个线程是操作系统来处理的,操作系统负责把它放置到一个 core 上去执行。
G:goroutine ,代表一个并发的代码片段。
简而言之,P 在 M 上执行 G 。
LRQ:Local RunQueue,本地等待运行的 G,存的数量有限,不超过 256 个。如果队列满了,则会把本地队列中一半的 G 移动到全局队列。
GRQ:Global RunQueue,存放等待运行的 G,LRQ都满了的时候,会放入GRQ。其中,LRQ 不加锁,GRQ加锁。
Network poller:同步调度。
线程状态:
线程的工作类型:
CPU密集型(cpu-bound):这种工作下,线程永远不会被置换到等待(waiting)状态。这种一般是进行持续性的cpu计算工作。比如计算Pi这种的就是cpu密集型工作。
IO密集型(io-bound):这种工作会让线程进入到等待(waiting)状态。这种情况线程会持续的请求资源(比如网络资源)或者是对操作系统进行系统调用。线程需要访问数据库的情况就是IO密集型工作。同步事件(例如mutexes、atomic)。
参考链接:理解golang调度之一 :操作系统调度 - 掘金
7、线程有继承关系吗?——没有,只有组合。
- package main
-
- import(
- "fmt"
- )
-
- type Human struct{
- name string
- }
-
- func(h Human)Dowork(){
- fmt.Println(h.name + "work")
- }
-
- func(h Human)Happy(){
- fmt.Println(h.name + "happy")
- }
-
- type Father interface{
- Dowork()
- }
-
- type Son interface{
- Father
- Happy()
- }
-
- func main(){
- human1 := Human{name:"张三丰"}
- human2 := Human{name:"张三"}
-
- father := human1
- son := human2
- father.Dowork()
- son.Happy()
- son.Dowork()
- }
8、什么是token认证方式?
在web应用的开发过程中,我们往往还会使用另外一种认证方式进行身份验证,那就是:Token认证。基于Token的身份验证是无状态,不需要将用户信息服务存在服务器或者session中。
基于Token认证的身份验证主要过程是:客户端在发送请求前,首先向服务器发起请求,服务器返回一个生成的token给客户端。客户端将token保存下来,用于后续每次请求时,携带着token参数。服务端在进行处理请求之前,会首先对token进行验证,只有token验证成功了,才会处理并返回相关的数据。
客户端的token存在cookies里。
9、go语言反射原理?
Go反射的实现和interface和unsafe.Pointer密切相关。
先看interface的底层实现:Go的interface是由两种类型来实现的: iface
和 eface
。
无函数的eface结构:一共有两个属性构成,一个是类型信息 _type
,一个是数据信息。其中, _type
可以认为是Go语言中所有类型的公共描述,Go语言中几乎所有的数据结构都可以抽象成 _type
,是所有类型的表现,可以说是万能类型, data
是指向具体数据的指针。
有函数的iface结构:itab是确定唯一的包含方法的interface的具体结构类型,data是指向具体方法集的指针。
Go语言中,每个变量都有唯一个静态类型(static interface type),这个类型是编译阶段就可以确定的。有的变量可能除了静态类型之外,还会有动态混合类型(dynamic concrete type)。
Go的反射法则:
Go的反射原理:在运行时,能够动态知道给定数据对象的类型和结构,并有机会修改它!
10、select有哪些特性?
select是Go语言中的一个控制语句,只用来操作的channel的读写操作(I/O操作)。
备注:golang 的 select 本质上,就是监听 IO 操作,当 IO 操作发生时,触发相应的动作。也是常用的多路复用的一种,例如poll, epoll。
select 的特性:
应用场景:
底层实现是创建select —> 注册case —> 执行case —> 释放select。结构体select如下(runtime/select.go),
- type hselect struct {
- tcase uint16 // total count of scase[] 总的case数目
- ncase uint16 // currently filled scase[] 目前已经注册的case数目
- pollorder *uint16 // case poll order 【超重要】 轮询的case序号
- lockorder *uint16 // channel lock order 【超重要】chan的锁定顺序
- // case 数组,为了节省一个指针的 8 个字节搞成这样的结构
- // 实际上要访问后面的值,还是需要进行指针移动
- // 指针移动使用 runtime 内部的 add 函数
- scase [1]scase // one per case (in order of appearance) 【超重要】保存当前case操作的chan (按照轮询顺序)
- }
-
- /**
- select 中每一个case的定义
- */
- type scase struct {
- elem unsafe.Pointer // data element 数据指针
- c *hchan // chan 当前case所对应的chan引用
- pc uintptr // return pc (for race detector / msan) 和汇编中的pc同义,表示 程序计数器,用于指示当前将要执行的下一条机器指令的内存地址
- kind uint16 // 通道的类型
- receivedp *bool // pointer to received bool, if any
- releasetime int64
- }
参考链接:【我的架构师之路】- golang源码分析之select的底层实现_GavinXujiacan的博客-CSDN博客
select和poll、epoll区别:
参考链接:select、poll、epoll之间的区别(搜狗面试) - aspirant - 博客园
11、互斥体和饥饿问题?
参考链接:https://www.jianshu.com/p/9f4376fbbe5c
12、多核情况下如何保证Cache的数据一致性?
MESI缓存一致性协议,每个缓存行都用2个bit表示四种状态,修改状态Modified,独占状态Exclusive,共享状态Shared,失效状态Invalid。
比如说,当CPU的某个内核Core加载某一个数据到Cache1里时,这个缓存行的状态就是独占状态,然后内核对数据做了修改,这里缓存行的状态就是修改状态。如果有另外一个内核读相同的数据时,Cache1监测到另外一个内核读取了数据,这时候缓存行的状态就变成了共享状态。在共享状态下,如果自己内核对数据做修改,那自己的缓存行就变成修改状态,其他所有缓存行变成失效状态。
13、实现一个dog、cat、fish循环输出程序。三个通道,利用dogCh->catCh->fishCh->dogCh->...->fishch控制顺序,WaitGroup等待这组的三个线程结束。
- package main
-
- import(
- "fmt"
- "sync"
- "sync/atomic"
- )
-
- const num = 10
-
- func main(){
- var wg sync.WaitGroup
- var dogCount uint64
- var catCount uint64
- var fishCount uint64
-
- dogCh := make(chan struct{}, 1)
- catCh := make(chan struct{}, 1)
- fishCh := make(chan struct{}, 1)
-
- wg.Add(3)
- go dog(&wg, dogCount, dogCh, catCh)
- go cat(&wg, catCount, catCh, fishCh)
- go fish(&wg, fishCount, fishCh, dogCh)
- dogCh <- struct{}{}
-
- wg.Wait()
- }
-
- func dog(wg *sync.WaitGroup, count uint64, dogCh chan struct{}, catCh chan struct{}){
- for{
- if count>=uint64(num) {
- wg.Done()
- return
- }
- <- dogCh
- fmt.Println("dog")
- atomic.AddUint64(&count, 1)
- catCh <- struct{}{}
- }
- }
-
- func cat(wg *sync.WaitGroup, count uint64, catCh chan struct{}, fishCh chan struct{}){
- for{
- if count>=uint64(num) {
- wg.Done()
- return
- }
- <- catCh
- fmt.Println("cat")
- atomic.AddUint64(&count, 1)
- fishCh <- struct{}{}
- }
- }
-
- func fish(wg *sync.WaitGroup, count uint64, fishCh chan struct{}, dogCh chan struct{}){
- for{
- if count>=uint64(num) {
- wg.Done()
- return
- }
- <- fishCh
- fmt.Println("fish")
- atomic.AddUint64(&count, 1)
- dogCh <- struct{}{}
- }
- }
14、golang中channel底层如何实现?
结构体为hchan
- type hchan struct {
- qcount uint // total data in the queue
- dataqsiz uint // size of the circular queue
- buf unsafe.Pointer // 是有缓冲的channel所特有的结构,用来存储缓存数据。是个循环链表
- elemsize uint16
- closed uint32
- elemtype *_type // element type
- sendx uint // 用于记录buf这个循环链表中的发送index
- recvx uint // 用于记录buf这个循环链表中的接收index
- recvq waitq // 接收(<-channel)的goroutine抽象出来的结构体(sudog)的队列。是个双向链表
- sendq waitq // 发送(channel <- xxx)的goroutine抽象出来的结构体(sudog)的队列。
- lock mutex // lock是个互斥锁
- }
hchan
的结构体,并返回一个ch指针,我们使用过程中channel在函数之间的传递都是用的这个指针(channel本身就是一个指针),使用 make 内建函数返回的就是指针。goroutine 是一种用户级线程,由 Go 运行时系统来创建和管理,而不是操作系统。相较于操作系统的线程更加轻量级。一个 OS thread 对应对个 goroutine。这便是 Go 语言的 M:N 调度模型。
channel的使用及原理:
参考链接:Go Chanel 使用与原理 一 - Go语言中文网 - Golang中文社区
参考链接:深入详解Go的channel底层实现原理【图解】 - 腾讯云开发者社区-腾讯云
15、进程通信方式有哪7种?
16、Mutex互斥锁底层原理?
Mutex互斥锁结构体有两个成员变量:状态(标志锁是否被持有)和信号量(阻塞和唤醒goroutine)。它有两种模式:正常模式(非公平):进入等待状态的goroutine会进入等待队列,在获取锁的时候按先进先出的顺序获取,但当唤醒一个goroutine时它不会立即获取锁而是和新来的goroutine竞争,通常是新来的更容易获得锁,因为它已经运行在CPU,所以刚被唤醒的goroutine大概率获取不到锁。饥饿模式(公平):当在正常模式等待的goroutine超过1ms没获取到锁,Mutex被切换成饥饿模式,饥饿模式下锁直接交给等待队列最前面的goroutine,新来的goroutine不会参与竞争锁也不会在那自旋,而是直接添加到队列尾部。当拥有锁的goroutine发现它是队列里的最后一个或者等待时间小于1ms又会切换回正常模式。
参考链接:mutex | 学习笔记
其他:
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。