[TOC]
面试基础整理
Golang语言特点
go语言有什么优点和缺点
优势:容易学习,生产力,并发,动态语法,扩展性好。
劣势:包管理,错误处理,缺乏框架。
与java的区别
最大的区别在于内存管理,java需要一个jvm来进行运行与管理,系统的资源占用较大。
go语言中的引用类型包含哪些?
切片(Slice)、字典(map)、通道(channel)、接口(interface)
Channel 整理
Golang 的channel是怎么实现的
golang的channel是个结构体,里面大概包含了三大部分:
a. 指向内容的环形缓存区,及其相关游标(环形缓冲区)
b. 读取和写入的排队goroutine链表(等待队列recvq和sendq)
c. 类型信息
d. 互斥锁
任何操作前都需要获得锁, 当写满或者读空的时候,就将当前goroutine加入到recvq或者sendq中, 并出让cpu(gopark)。
Channel 的读写阻塞条件
1. 协程读取管道时:
a. 管道无缓冲区
b. 管道的缓冲区无数据
c. 管道的值为nil
2. 协程写入管道时:
a. 管道无缓冲区
b. 管道的缓冲区已满
c. 管道的值为nil
无缓冲 Chan 的发送和接收是否同步?
ch := make(chan int) 无缓冲的channel由于没有缓冲发送和接收需要同步。
ch := make(chan int, 2) 有缓冲channel不要求发送和接收操作同步。
channel 无缓冲时,发送阻塞直到数据被接收,接收阻塞直到读到数据。
channel 有缓冲时,当缓冲满时发送阻塞,当缓冲空时接收阻塞。
常见的channel触发的panic
向已关闭的管道写数据;
关闭值为nil的管道;
关闭已经被关闭的管道。
Slice 整理
Slice 实现原理
slice依托底层数组实现,结构主要包含array、len、cap
Slice 扩容规则
如果原来容量小于1024,则扩容后容量为原来的2倍;
如果原来容量大于或者等于1024,则扩容后的容量为原来的1.25倍;
扩容后的底层数组为新的数组。
数组和Slice的区别
-
数组是值类型而切片是引用底层数组
-
数组长度不可变,初始化的时候声明长度,Slice长度可拓展
Map
map数据结构
map使用 Hash 表作为底层实现,一个 Hash 表可以有多个 bucket,而每个 bucket 保存了map中的一个或一组键值对。
map的数据结构
-
count 保存元素个数
-
B 是bucket数组大小
-
buckets 是bucket数组,数组长度为
-
oldbuckets是老旧bucket数组,用于扩容
bucket的数据结构
-
tophash 存在 Hash 的高8位
-
data存放的是 key-value 数据
-
overflow是溢出 bucket 的地址,指针指向的是下一个 bucket
什么是 Hash 负载因子
用于衡量一个 Hash 表的冲突情况
负载因子 = 键数量 / bucket数据
负载因子过大或者过小都不理想:
-
过小说明空间利用率低
-
过大说明冲突严重,存取效率低
当负载因子大于6.5时触发扩容
扩容方式:
-
增量扩容:增加 bucket,并进行值搬迁
-
等量扩容:不增加 bucket,进行数据搬迁,将松散的数据重新搬迁排列
string整理
为什么Golang不允许修改字符串
GO的实现中,==string类型不包含内存空间,只有一个内存指针==,这样做的好处是string变得非常轻量,可以方便的进行传递而不用担心内存拷贝。因为string==通常指向字符串字面量,而字符串字面量存储的位置是只读段==,而不是在堆或者栈上,所以才有了string不可修改的约定。
len(string)长度是字符串长度吗
len(string)的长度是字符串字节长度,所以在遍历字符串时,中文字符与index不一定一一对应。应该使用len([]rune(string))
字符串拼接方式
-
直接利用
+进行拼接 -
fmt.Sprintf 进行拼接
-
Join进行拼接
strings.Join([]string{hello, world}, ",")
- buffer.WriteString()
func main() {
hello := "hello"
world := "world"
for i := 0; i < 1000; i++ {
var buffer bytes.Buffer
buffer.WriteString(hello)
buffer.WriteString(",")
buffer.WriteString(world)
}
_ = buffer.String()
}
struct
结构体的比较,有几个需要注意的地方:
-
结构体只能比较是否相等,但是不能比较大小。
-
相同类型的结构体才能够进行比较,结构体是否相同不但与属性类型有关,还与属性顺序相关,sn3 与 sn1 就是不同的结构体;
sn1 := struct {
age int
name string
}{age: 11, name: "qq"}
sn3:= struct {
name string
age int4
}{age:11,name:"qq"}
- 如果 struct 的所有成员都可以比较,则该 struct 就可以通过 == 或 != 进行比较是否相等,比较时逐个项进行比较,如果每一项都相等,则两个结构体才相等,否则不相等;
可比较的有 bool、数值型、字符、指针、数组等
切片、map、函数、通道是不能比较的。 Go 说明文档
Interface的实现
interface 底层实现上用两种 struct 来表示:iface 和 eface。
eface:表示不含 method 的 interface 结构,或者叫 empty interface。对于 Golang 中的大部分数据类型都可以抽象出来 _type 结构,同时针对不同的类型还会有一些其他信息。
iface: 表示 non-empty interface 的底层实现。相比于 empty interface,non-empty 要包含一些 method。method 的具体实现存放在 itab.fun 变量里。以及对象数据data的指针。
itab 主要有三个部分,
-
代表接口自身类型的 interfacetype;
-
表示 实际对象类型 的 _type;
-
实际对象的func 队列的首地址 fun [1]uintptr;
Go接收器有哪些
值接收器和指针接收器,值接收器接收的是数据值的拷贝。
协程整理
进程、线程与协程分别是什么?
**进程:**进程是具有一定独立功能的程序关于某个数据集合上的一次运行活动,进程是系统进行资源分配和调度的一个独立单位
**线程:**线程是进程的一个实体,是CPU调度和分派的基本单位
**协程:**协程是一种用户态的轻量级调度单位,协程的调度完全由用户控制。协程拥有自己的寄存器上下文和栈。协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈,直接操作栈则基本没有内核切换的开销,可以不加锁的访问全局变量,所以上下文的切换非常快。协程仅仅是一个特殊的函数,协程它进程和线程不是一个维度的。
线程有哪些状态
New、Runnable、Running、Block、Dead
协程的优势
线程池虽然可以减少线程创建是销毁的系统开销,但是线程调度时,如果发生阻塞,则工作的work线程会被减少,系统执行效率会降低,而且过多地线程上下文切换导致系统开销过大。相较于线程,工作在用户态的协程不会让woker线程进入阻塞,协程逐个被调用到线程中执行,如果发生阻塞,则会将协程调出线程并放到全局队列等待,而且协程能够大大减少系统上下文切换的开销。
有没有必要创建协程池?
协程的创建和销毁比较稳定的时候没有必要
如果需要大量的创建协程,虽然协程可以减少阻塞线程,但是大量的协程切换也会增加上下文切换的开销,浪费很多上下文切换的资源,导致做无用功。所以设计一个Goroutine池限制Goroutine的开辟个数在大型并发场景还是必要的。
CSP(Communicating Sequential Processes)模型
M Machine,工作线程,由操作系统调度
P Process,处理器,包含运行Go代码的重要资源,也有调度groutine的能力
G groutine,Go协程,每一个go关键字都会创建一个协程。
协程队列runqueue有两种,第一个是global runqueue,第二个是P的Local runqueue,
P创建的协程会先放入本地队列,如果本地队列已满或者阻塞的协程被唤醒,则协程会被放入全局队列;处理器P除了会调度本地队列以外,还会周期性从全局队列中摘取协程来调度。
Scheduler调度策略
-
**队列轮转:**每个P维护着一个本地队列,处理器P依次将协程调度到M中执行
-
**系统调用:**协程G0发生系统调用时,会将原来协程工作线程M0的调度器P放入冗余的M1执行调度,M0由于系统调用而被阻塞,M1接替M0剩余的工作保证P不会空闲,充分利用CPU,而M0负责执行G0
-
**工作量窃取:**当某个处理器P没有需要调度的协程时,会从其他处理器中偷取协程进行调度
-
**抢占式调度:**某个协程执行时间过长,而阻塞其他协程调度时,会暂停当前协程执行,转而调度其他协程,以此来达到类似于时间片轮转的效果
gopark函数和goready函数原理分析
gopark
gopark函数在协程的实现上扮演着非常重要的角色,用于协程的切换,协程切换的原因一般有以下几种情况:
-
系统调用;
-
channel读写条件不满足;
-
抢占式调度时间片结束;
gopark函数做的主要事情分为两点:
-
解除当前goroutine的M的绑定关系,将当前goroutine状态机切换为等待状态;
-
调用一次schedule()函数,在局部调度器P发起一轮新的调度。
goready
goready函数相比gopark函数来说简单一些,主要功能就是唤醒某一个goroutine,该协程转换到runnable的状态,并将其放入P的local runqueue,等待调度。对一个协程调用goready函数,这个协程不是可以马上就执行的,而是要等待调度器的调度执行。
GOMAXPROCS是什么,如何设置,是否设置越大性能越好
GOMAXPROCS 是协程处理器(P)的可启动个数
由环境变量设置,或者runtime。GOMAXPROCS(80) 设置
一般情况M的个数略大于P,因为涉及到系统调度
一般来讲,程序运行时就将GOMAXPROCS 的大小设置为 CPU的核数,可让Go程序充分利用CPU。但是在某些I/O密集型应用中,这个值可能并不意味着性能最好,理论上某个goroutine进入系统调用时,会有一个新的 M 被启用或者创建,继续占满 CPU。但由于 GO 调度器监测到 M 被阻塞是有一定延迟的,即旧的 M 被阻塞和新的 M 得到运行之间有一定时间间隔,可以在 I/O密集型应用中不妨把 GOMAXPROCS 的值设置的大一些,或许会有更好的效果。
内存管理
Go的内存结构
**spans:**存放arena指针,指向arena的每一个page,一个指针大小为8kb
**bitmap:**用于保存arena对应的某个地址是否存在对象,以及对象是否被GC扫描过,主要用于GC
**arena:**是我们通常所说的heap,即所谓的堆,为了方便管理,arena区被划分成一个个page,每个page大小为8KB
arena即所谓的堆,spans和bitmap是为了管理arena区存在的。
span是内存管理的基本单位,每个span用于管理特定的class对象,根据对象大小,span将一个或者多个页拆分成多个块进行管理。
Go的内存分配核心思想
Go是内置运行时的编程语言(runtime),像这种内置运行时的编程语言通常会抛弃传统的内存分配方式,改为自己管理。这样可以完成类似预分配、内存池等操作,以避开系统调用带来的性能问题,防止每次分配内存都需要系统调用。
Go的内存分配的核心思想可以分为以下几点:
-
每次从操作系统申请一大块儿的内存,由Go来对这块儿内存做分配,减少系统调用
-
内存分配算法采用Google的
TCMalloc算法。算法比较复杂,究其原理可自行查阅。其核心思想就是把内存切分的非常的细小,分为多级管理,以降低锁的粒度。 -
回收对象内存时,并没有将其真正释放掉,只是放回预先分配的大块内存中,以便复用。只有内存闲置过多的时候,才会尝试归还部分内存给操作系统,降低整体开销
内存管理组件
go的内存管理组件主要有:mspan、mcache、mcentral和mheap
-
mspan为内存管理的基础单元,直接存储数据的地方。 -
mcache:每个运行期的goroutine都会绑定的一个mcache(具体来讲是绑定的GMP并发模型中的P,所以可以无锁分配mspan,后续还会说到),mcache会分配goroutine运行中所需要的内存空间(即mspan)。 -
mcentral为所有mcache切分好后备的mspan -
mheap代表Go程序持有的所有堆空间。还会管理闲置的span,需要时向操作系统申请新内存。
内存分配过程
针对分配对象大小的不同有不同的分配逻辑
(0,16B) 且不包含指针的对象:Tiny分配
(0,16B)且包含指针的对象:正常分配
[16B, 32KB] : 正常分配
(32KB, ∞):大对象分配
申请size为n的内存为例,步骤如下:
- 获取当前线程的私有缓存 mcache
- 根据size计算出合适的 class 的 ID
- 从 mcache 的 alloc[class] 链表中查询可用的 span
- 如果 mcache 没有可用的 span,则从 mcentral 申请一个新的 span 加入 mcache
- 如果 mcentral 也没有可用的 span,则从 mheap 中申请一个新的 span 加入 mcentral
- 从该 span 中获取空闲对象地址并返回
Golang垃圾回收算法
- 标记-清除法,Go采用三色标记,标记清除法的缺点就是STW(Stop the world)
灰色: 对象还在标记队列中等待
黑色: 对象已被标记,该对象在本次GC中不会被清理
白色: 对象未被标记,该对象在本次GC被清理
- gc的过程一共分为四个阶段:
- 栈扫描(开始时STW)
- 第一次标记(并发)
- 第二次标记(STW)
- 清除(并发)
| 阶段 | 说明 | 赋值器状态 |
| ----------------- | ---------------------------------------------------- | ---------- |
| GCMark | 标记准备阶段,为并发标记做准备工作,启动写屏障 | STW |
| GCMark | 扫描标记阶段,与赋值器并发执行,写屏障开启 | 并发 |
| GCMarkTermination | 标记终止阶段,保证一个周期内标记任务完成,停止写屏障 | STW |
| GCoff | 内存清扫阶段,将需要回收的内存归还到堆中,写屏障关闭 | 并发 |
| GCoff | 内存归还阶段,将过多的内存归还给操作系统,写屏障关闭 | 并发 |
Golang GC 对于STW的优化
-
写屏障(Write Barrier): goroutine 与 GC 同时运行
-
辅助 GC(Mutator Assist): 为了防止内存分配过快,在GC执行过程中,如果goroutine 需要分配内存,那么该goroutine会参与一部分GC工作。
GC 触发时机
- 内存分配达到阈值触发GC
阈值 = 上次GC内存分配量 × 内存增长率
-
定期触发GC: 默认2min触发一次,在
/runtime/proc.go:forcegcperiod变量中被声明 -
手动触发: runtime.GC()
如果内存分配速度超过了标记清除的速度怎么办?
核心思想:暂停内存分配过快的goroutine,并执行辅助标记清除
目前的 Go 实现中,当 GC 触发后,会首先进入并发标记的阶段。并发标记会设置一个标志,并在 mallocgc 调用时进行检查。当存在新的内存分配时,会暂停分配内存过快的那些 goroutine,并将其转去执行一些==辅助标记(Mark Assist)==的工作,从而达到放缓继续分配、辅助 GC 的标记工作的目的。
编译器会分析用户代码,并在需要分配内存的位置,将申请内存的操作翻译为 mallocgc调用,而 mallocgc的实现决定了标记辅助的实现,其伪代码思路如下:
funcmallocgc(t typ. Type, size uint64) {
if enableMarkAssist {
// 进行标记辅助,此时用户代码没有得到执行(...)
}
// 执行内存分配(...)
}
GC优化思路
GC 调优的核心思想:优化内存的申请速度,尽可能的少申请内存,复用已申请的内存
控制、减少、复用。
GC性能与对象数量相关,对象越多GC性能越差;减少对象数量,比如==对象复用或者大对象组合多个小对象==。
另外,内存逃逸也会产生一些隐式的内存分配,也有可能成为GC负担,==减少内存逃逸==。
逃逸分析:哪些场景下对象会逃逸
-
指针逃逸
-
栈空间不足逃逸
-
动态类型逃逸
-
闭包引用对象逃逸
函数传递指针真的比传值效率高吗?
传递指针可以减少底层值的复制,可以提高效率,但是如果复制的数据量小,由于指针传递会产生逃逸,则可能会使用堆,也可能增加 GC 负担,所以传递指针不一定是高效的。
并发控制
sync.Mutex 底层实现
type Mutex struct {
state int32 //状态标识 0(可用) 1(被锁) 2~31等待队列计数
sema uint32 //信号量,向处于Gwaitting的G发送信号
}
const (
mutexLocked = 1 << iota // 1 互斥锁是锁定的
mutexWoken // 2 唤醒锁
mutexWaiterShift = iota // 2 统计阻塞在这个互斥锁上的goroutine数目需要移位的数值
)
除了互斥锁sync.Mutex之外,Go还有哪些常用的并发模型
-
**Channel:**利用channel的读写阻塞来控制子协程
-
**WaitGroup:**使用信号量机制控制子协程
-
**Context:**使用上下文控制子协程
package main
func main() {
ctx1 := context.Background()
ctx2, _ := context.WithCancel(ctx1)
ctx3, _ := context.WithTimeout(ctx2, time.Second * 5)
ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(50 * time.Millisecond))
ctx6 := context.WithValue(ctx5, "userID", 12)
}
读写锁sync.RWMutex
利用Mutex实现
type RWMutex struct {
w Mutex // 用于控制多个写锁,获得写锁首先要获取该锁
writerSem uint32 // 写阻塞等待信号量,最后一个读者释放锁时会释放该信号量
readerSem uint32 // 读阻塞的协程等待信号量,持有写锁的协程释放锁后会释放该信号量
readerCount int32 // 记录读者个数
readerWait int32 // 记录写阻塞时读者个数
}
通常有些公共数据修改的机会很少,但其读的机会很多。并且在读的过程中会伴随着查找,给这种代码加锁会降低我们的程序效率。读写锁可以解决这个问题。
注意:写独占,读共享,写锁优先级高
sync.Map的实现原理?
type Map struct {
mu Mutex
read atomic.Value // readOnly,不存在时查询dirty
dirty map[interface{}]*entry //新写入数据存在dirty上
misses int // 统计 read 被穿透的次数(被穿透指需要读 dirty 的情况),超过一定次数同步到read
}
sync.Map 的实现原理可概括为:
- 通过 read 和 dirty 两个字段将读写分离,读的数据存在只读字段 read 上,将最新写入的数据则存在 dirty 字段上
- 读取时会先查询 read,不存在再查询 dirty,写入时则只写入 dirty
- 读取 read 并不需要加锁,而读或写 dirty 都需要加锁
- 另外有 misses 字段来统计 read 被穿透的次数(被穿透指需要读 dirty 的情况),超过一定次数则将 dirty 数据同步到 read 上
- 对于删除数据则直接通过标记来延迟删除
sync.WaitGroup的使用场景?
程序中需要并发,需要创建多个goroutine,并且一定要等这些并发全部完成后才继续接下来的程序执行.
WaitGroup的特点是Wait()可以用来阻塞直到队列中的所有任务都完成时才解除阻塞,而不需要sleep一个固定的时间来等待.
但是其缺点是无法指定固定的goroutine数目(也就是协程池功能)
sync.Pool 的作用
基于Get 和 Put 来进行GC优化,减少可复用对象的创建。
- sync.Pool的源代码里说了,pool里的对象随时都有可能被自动移除,并且没有任何通知。sync.Pool的数量是不可控制的。
- Pool调用New与线程调度有关,Pool内部有一个localPool的数组,每个P对应其中一个localPool,在当前P执行goroutine的时候,优先从当前的localPool的private变量取,取不到在从shared列表里面取,再取不到就尝试从别的P的localPool的shared里面偷一个。最后实在取不到就New一个。
死锁
死锁产生的四个必要条件:
- 互斥条件:一个资源每次只能被一个进程使用
- 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
- 不剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺。
- 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
这四个条件是死锁的必要条件,只要系统发生死锁,这些条件必然成立,而只要上述条件之一不满足,就不会发生死锁。
a. 预防死锁
可以把资源一次性分配:(破坏请求和保持条件)
然后剥夺资源:即当某进程新的资源未满足时,释放已占有的资源(破坏不可剥夺条件)
资源有序分配法:系统给每类资源赋予一个编号,每一个进程按编号递增的顺序请求资源,释放则相反(破坏环路等待条件)
b. 避免死锁
预防死锁的几种策略,会严重地损害系统性能。因此在避免死锁时,要施加较弱的限制,从而获得 较满意的系统性能。由于在避免死锁的策略中,允许进程动态地申请资源。因而,系统在进行资源分配之前预先计算资源分配的安全性。若此次分配不会导致系统进入不安全状态,则将资源分配给进程;否则,进程等待。其中最具有代表性的避免死锁算法是银行家算法。
c. 检测死锁
首先为每个进程和每个资源指定一个唯一的号码,然后建立资源分配表和进程等待表.
d. 解除死锁
当发现有进程死锁后,便应立即把它从死锁状态中解脱出来,常采用的方法有.
e. 剥夺资源
从其它进程剥夺足够数量的资源给死锁进程,以解除死锁状态.
f. 撤消进程
可以直接撤消死锁进程或撤消代价最小的进程,直至有足够的资源可用,死锁状态.消除为止.所谓代价是指优先级、运行代价、进程的重要性和价值等。
异常处理
go中recover()能救所有程序吗?
不能,Go中的错误主要有三种,一个是业务相关的错误,即 error,一种是panic,还有一种 Go 中的一些 fatal error
第一种,也就是常说的 error 类型错误,不需要 recover 拯救,处理方式自己决定
第二种,panic 和 recover 是紧密集合的,有点类似 try catch,recover 能捕获到 panic
第三种,一些 Go 语言系统级别的错误,比如发生死锁,数据竞争,这种错误程序会立刻报错, 无法recover
defer 和 return 及返回值的执行顺序
defer、return、返回值三者的执行顺序应该是:
return最先给返回值赋值;
接着defer开始执行一些收尾工作;
最后RET指令携带返回值退出函数。
defer底层实现
runtime._defer 结构体是延迟调用链表**(先入后出)**上的一个元素,所有的结构体都会通过 link 字段串联成链表
defer 关键字的实现主要依靠编译器和运行时的协作:
- 编译期;
将 defer 关键字被转换 runtime.deferproc;
在调用 defer 关键字的函数返回之前插入 runtime.deferreturn;
- 运行时:
runtime.deferproc 会将一个新的 runtime._defer 结构体追加到当前 Goroutine 的链表头;
runtime.deferreturn 会从 Goroutine 的链表中取出 runtime._defer 结构并依次执行;
版本依赖管理
Go导入包时 _和 .分别作什么用?
_ 是当导入一个包时,该包下的文件里所有init函数都会被执行,但是有时我们仅仅需要使用init函数而已并不希望把整个包导入(不使用包里的其他函数)
import "database/sql"
import _ "github.com/go-sql-driver/mysql"
db, err := sql.Open("mysql", "user:password@/dbname")
. 是为了省略包名直接调用函数
import(.“fmt”)
func main() {
Println("Hello World!")
}
这个点操作的含义就是这个包导入之后在你调用这个包的函数时,你可以省略前缀的包名,也就是前面你调用的fmt.Println(“hello world”)可以省略的写成Println(“hello world”)
其他问题
unsafe.Pointer
为了解决指针类型不可强转
package main
import (
"fmt"
"unsafe"
)
func main() {
u := uint32(32)
i := int32(1)
fmt.Println(&u, &i)
p := &i
p = (*int32)(unsafe.Pointer(&u))
fmt.Println(p)
}
字符串转为byte切片会发生内存拷贝吗?如何避免这个问题?
会发生,只要是类型转换都会发生内存拷贝,字符串的非安全指针转为StringHeader指针,再把byte指针指向StringHeader的非安全指针。
package main
import (
"fmt"
"reflect"
"unsafe"
)
func main() {
a :="aaa"
ssh := *(*reflect.StringHeader)(unsafe.Pointer(&a))
b := *(*[]byte)(unsafe.Pointer(&ssh))
fmt.Printf("%v",b)
}
单例模式实现
饿汉式和懒汉式,饿汉式可以利用sync.Once
var once = new(sync.Once)
var instance chan int
func Init() {
once.Do(func() {
instance = make(chan int, 10)
})
}
make 和 new 的区别
make 类似于创建并初始化一个值对象
make 的作用是为 slice,map 或 chan 初始化并返回实例引用(T)
new 的作用是初始化一个指向类型的指针(*T)
select可以用于什么
常用于 gorotine 的完美退出和管道监听
golang 的 select 就是监听 IO 操作,当 IO 操作发生时,触发相应的动作
每个case语句里必须是一个IO操作,确切的说,应该是一个面向channel的IO操作
client如何实现长连接
server是设置超时时间,for循环遍历的
Go的反射包怎么找到对应的方法
func callReflect(any interface{}, name string, args... interface{}) []reflect.Value{
inputs := make([]reflect.Value, len(args))
for i, _ := range args {
inputs[i] = reflect.ValueOf(args[i])
}
if v := reflect.ValueOf(any).MethodByName(name); v.String() == "<invalid Value>" {
return nil
}else {
return v.Call(inputs)
}
}
如何实现退出程序防止channel没有读取完数据
**实现方式:**利用sync.WaitGroup,写入和读取分为执行的两个Done()
package main
import (
"fmt"
"time"
"sync"
)
var waitGp sync.WaitGroup
func main() {
waitGp.Add(2)
ch := make(chan int, 10)
go writeChannel(ch)
go readChannel(ch)
waitGp.Wait()
_,ok:=<- ch
fmt.Println(ok)
}
func readChannel(ch chan int) {
for item := range ch {
fmt.Printf("%d ", item)
time.Sleep(time.Second)
}
fmt.Println()
waitGp.Done()
fmt.Println("\nRead任务结束")
}
func writeChannel(ch chan int) {
for i := 0; i < 10; i++ {
ch <- i
}
close(ch)
waitGp.Done()
fmt.Println("\nwrite任务结束")
}
用户态和内核态
系统态(内核态),操作系统在系统态运行——运行操作系统程序
用户态(也称为目态),应用程序只能在用户态运行——运行用户程序
数据竞态 Data Race问题怎么解决?能不能不加锁解决这个问题?
==同步访问共享数据==是处理数据竞争的一种有效的方法。
-
使用互斥锁访问数据
-
使用
sync/atomic进行原子访问
golang在1.1之后引入了竞争检测机制,可以使用 go run -race 或者 go build -race来进行静态检测。 其在内部的实现是,开启多个协程执行同一个命令, 并且记录下每个变量的状态。
竞争检测器基于C/C++的ThreadSanitizer 运行时库,该库在Google内部代码基地和Chromium找到许多错误。这个技术在2012年九月集成到Go中,从那时开始,它已经在标准库中检测到42个竞争条件。现在,它已经是我们持续构建过程的一部分,当竞争条件出现时,它会继续捕捉到这些错误。
竞争检测器已经完全集成到Go工具链中,仅仅添加-race标志到命令行就使用了检测器。
$ go test -race mypkg // 测试包
$ go run -race mysrc.go // 编译和运行程序
$ go build -race mycmd // 构建程序
$ go install -race mypkg // 安装程序
要想解决数据竞争的问题可以使用互斥锁sync.Mutex,解决数据竞争(Data race),也可以使用管道解决,使用管道的效率要比互斥锁高.
缓存算法(FIFO 、LRU、LFU三种算法的区别)
FIFO (First In First Out, 先入先出算法)
核心:如果一个数据最先进入缓存,那么也应该最先被删掉。最早进入缓存的数据其不再被利用的可能性比刚进入缓存的数据要大。
实现:创建一个队列(双向链表),新增记录添加到队列尾,当缓存满时,淘汰队首记录
LFU (Least Frequently Used, 最少使用算法)
核心:如果数据过去被访问多次,那么将来被访问频率也会很高。
实现:按照访问次数排序的队列,每次访问时,访问次数+1,队列重新排序,淘汰时选择访问次数最少的即可。
LRU (Least Rencently Used, 最近最少使用算法)
核心:如果数据最近被访问过,那么将来被访问的概率会很高。
实现:维护一个队列,如果某条数据被访问了,则把这条数据移到队尾,队首则是最近最少使用的数据,淘汰队首数据即可。
缓存优化的方案
1. 加速并发访问(减少goroutine执行时间)
高并发环境下,锁的争用十分频繁,因此导致性能明显下降。所以尽可能减少锁的争用,让某些情况下无锁操作,或者让锁的时间尽可能短。
BigCache 和 FreeCache 高新能的关键就是使用了无锁技巧,将数据分片(分组),每个分片内需要锁。
2. 避免GC
-
对象池。重用对象,避免频繁分配内存
-
让内存分配在栈中,避免逃逸到堆。