Golang面试基础整理
1、Golang中的panic(异常)和recover(捕获异常)
panic的作用就是抛出一条错误信息,从它的参数类型可以看到它可以抛出任意类型的错误信息。在函数执行过程中的某处调用了panic,则立即抛出一个错误信息,同时函数的正常执行流程终止,但是该函数中panic之前定义的defer语句将被依次执行。之后该goroutine立即停止执行。
recover()用于将panic的信息捕捉。recover必须定义在panic之前的defer语句中。在这种情况下,当panic被触发时,该goroutine不会简单的终止,而是会执行在它之前定义的defer语句。
2、Golang 之 struct能不能比较
同一个struct的两个实例能不能比较?同一个struct的两个实例可比较也不可比较,当结构不包含不可直接比较成员变量时可直接比较,否则不可直接比较 (借助 reflect.DeepEqual 函数 来对两个变量进行比较)
两个不同的struct的实例能不能比较? 可以比较,也不可以比较 可通过强制转换来比较, 如果成员变量中含有不可比较成员变量,即使可以强制转换,也不可以比较
可比较:Integer,Floating-point,String,Boolean,Complex(复数型),Pointer,Channel,Interface,Array
不可比较:Slice,Map,Function
3、golang中Array与Slice
相同点
由相同类型的元素组合构成
元素有序排列,0为第一个元素下标
基本使用方法相同
区别
array声明时需要指定容量大小,而且无法修改
slice可通过append增加元素,当容量不够时,会自动扩容
array传递类型:值拷贝;slice传递类型:引用拷贝
slice append时会首先使用可用容量cap部分,如果cap不够扩容就会分配一个新的底层数组,并将所有元素拷贝至新地址。并且新的底层数组会按照一定策略进行扩容:在切片的容量小于1024个元素时会成倍地增加容量,一旦元素个数超过1024容量的增长因子会设为1.25,即每次增加25%的容量
4、make 与 new 的区别
首先,两者都是用作变量声明时的内存分配,两者的内存分配空间都为堆。但是 make 只能声明 slice、map 与 channel,而 new 用于类型的内存分配,并且初始化所有内容为零值。
其次,make 返回类型本身,而 new 返回的是类型的引用指针。
再则,new 不常用,一般使用 := 代替。而 make 函数无法替代,因为 slice、map与channel都需要它来声明。
5.rpc
RPC是Remote Procedure Call Protocol单词首字母的缩写,简称为:RPC,翻译成中文叫远程过程调用协议。所谓远程过程调用,通俗的理解就是可以在本地程序中调用运行在另外一台服务器上的程序的功能方法。这种调用的过程跨越了物理服务器的限制,是在网络中完成的,在调用远端服务器上程序的过程中,本地程序等待返回调用结果,直到远端程序执行完毕,将结果进行返回到本地,最终完成一次完整的调用。
需要强调的是:远程过程调用指的是调用远端服务器上的程序的方法整个过程。
B/S架构、C/S架构。B/S架构指的是浏览器到服务器交互的架构方式,另外一种是在计算机上安装一个单独的应用,称之为客户端,与服务器交互的模式。
6.内存逃逸
Golang中,内存的分配有两种方式:堆(Heap)和栈(Stack);栈是计算机内存的特定区域,一般是 CPU 自动分配释放,读写很快,更重要的是不会产生碎片;堆是应用程序在运行的时候请求操作系统分配的,需要申请和释放,会产生碎片。
那么什么是内存逃逸呢?简单说,就是指从栈上的分配变为从堆上分配。Go在编译的时候会进行逃逸分析,来决定是放栈上还是放堆上,不逃逸的对象放栈上,可能逃逸的放堆上。
验证某个函数的变量是否发生逃逸,我这里使用命名:go run -gcflags "-m -l" (-m 打印逃逸分析信息,-l 禁止内联编译);
1 指针逃逸 Go在方法内将局部变量指针返回
2 切片扩容或容量太大,栈空间不足
3 在 slice 或 map 中存储指针
4 发送指针或带有指针的值到 channel 中
5 Interface类型多态
6 闭包引用包外的值
如何避免
1 对于少量的数据,使用传值而不是传指针
2 尽量避免使用长度不固定的 slice 切片,因为在编译期无法确定切片长度,只能将切片使用堆分配
3 性能要求比较高且访问频次比较高的函数调用,谨慎使用 interface 调用方法
7.G、M、P
G goroutine 协程
P Processor处理器 p sei ser [ˈprəʊsesə(r)]
M thread 线程 [θred]
golang 中线程是运行 goroutine 的实体,调度器的作用是把可运行的 goroutine 分配到工作线程上。
全局队列:存放等待运行的 G。
P 的本地队列:同全局队列类似,存放的也是等待运行的 G,存的数量不超过 256 个。新建 G’ 时,G’ 优先加入到 P 的本地队列,如果队列满了,则会把本地队列中一半的 G 移到全局队列。
P 列表:所有的 P 都在程序启动时创建,并保存在数组中,最多有 GOMAXPROCS(可配置) 个。
M:线程想运行任务就得获取 P,从 P 的本地队列获取 G,P 队列为空时,M 也会尝试从全局队列拿一批 G 放到 P 的本地队列,或从其他 P 的本地 队列偷一半放到自己 P 的本地队列。M 运行 G,G 执行之后,M 会从 P 获取下一个 G,不断重复下去。
P 和 M 的个数问题?
P 的数量:
由启动时环境变量 $GOMAXPROCS 或者是由 runtime 的方法 GOMAXPROCS() 决定。这意味着在程序执行的任意时刻都只有 $GOMAXPROCS 个 goroutine 在同时运行。
M 的数量:
go 语言本身的限制:go 程序启动时,会设置 M 的最大数量,默认 10000。一般内核很难支持这么多线程数,这个限制可以忽略。
runtime/debug 中的 SetMaxThreads 函数,设置 M 的最大数量
一个 M 阻塞了,会创建新的 M。
M 与 P 的数量没有绝对关系,一个 M 阻塞,P 就会去创建或者切换另一个 M,所以,即使 P 的默认数量是 1,也有可能会创建很多个 M 出来。
8、进程、线程、协程、并发、并行
什么是进程,线程,协程 ?
1.进程是资源分配的单位,进程是具有一定独立功能的程序关于某个数据集合上的一次运行活动,进程是系统进行资源分配和调度的一个独立单位。每个进程都有自己的独立内存空间
2.线程是CPU调度的单位,线程是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。线程自己基本上不拥有系统资源
3.协程是一种比线程更加轻量级的存在,协程不是被操作系统内核所管理,而完全是由程序所控制,协程拥有自己的寄存器上下文和栈。
线程与进程的区别:
1) 地址空间:线程是进程内的一个执行单元,进程内至少有一个线程,它们共享进程的地址空间,而进程有自己独立的地址空间
2) 资源拥有:进程是资源分配和拥有的单位,同一个进程内的线程共享进程的资源
3) 线程是处理器调度的基本单位,但进程不是
4) 二者均可并发执行
5) 每个独立的线程有一个程序运行的入口、顺序执行序列和程序的出口,但是线程不能够独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制
协程与线程的区别:
1) 一个线程可以多个协程,一个进程也可以单独拥有多个协程。
2) 线程进程都是同步机制,而协程则是异步。
3) 协程能保留上一次调用时的状态,每次过程重入时,就相当于进入上一次调用的状态。
4)线程是抢占式,而协程是非抢占式的,所以需要用户自己释放使用权来切换到其他协程,因此同一时间其实只有一个协程拥有运行权,相当于单线程的能力。
5)协程并不是取代线程, 而且抽象于线程之上, 线程是被分割的CPU资源, 协程是组织好的代码流程, 协程需要线程来承载运行, 线程是协程的资源, 但协程不会直接使用线程, 协程直接利用的是执行器(Interceptor), 执行器可以关联任意线程或线程池, 可以使当前线程, UI线程, 或新建新程.。
6)线程是协程的资源。协程通过Interceptor来间接使用线程这个资源。
协程的优点:一是可以提高CPU利用率,避免系统内核级的线程间频繁切换造成的资源浪费;二是可以节约内存,一个进程几G、一个线程几M、一个协程几KB;三是稳定性好一些,线程可以通过内存共享数据,但是一个线程挂了,进程中所有线程会一起崩溃。
协程缺点:协程本质是个单线程,它不能同时使用单个 CPU 的多个核;一旦协程出现阻塞,将会阻塞整个线程。
并发/并行
多线程程序在单核心的 cpu 上运行,称为并发;多线程程序在多核心的 cpu 上运行,称为并行。
并发与并行并不相同,并发主要由切换时间片来实现“同时”运行,并行则是直接利用多核实现多线程的运行,Go程序可以设置使用核心数,以发挥多核计算机的能力。
9.goroutine
goroutine 是 Golang 并发的核心。一个 goroutine 本质上是一个协程
goroutine和协程的区别
goroutine 的本质是协程。但与协程不同的是,goroutine 不完全是用户控制,一定程度上由 go 运行时(runtime)管理,好处是:当某 goroutine 阻塞时,会让出 CPU 给其他 goroutine。
当启动多个 goroutine 并同时操作同一个资源时会发生竞态问题(数据竞态)。
何为竞态:当多个线程竞争同一个资源时,如果对资源的访问顺序敏感,就称存在竞态条件。导致竞态条件发生的代码区称作临界区。数据竞态问题会导致计算结果不可预期。
如何解决:加锁。共享资源在同一时间只能由一个线程访问(加锁),其他线程想要访问必须等当前线程访问完释放锁。
sync [sɪŋk] mu tex
1、互斥锁mutex
它能够保证同时只有一个 goroutine 可以访问共享资源。Golang 中可以使用 sync 包的 Mutex 来实现互斥锁。
互斥锁能保证同一时间有且只有一个 goroutine 进入临界区,其他的 goroutine 都在等待锁;当互斥锁释放后,等待的 goroutine 才能获取锁然后进入临界区,唤醒多个同时等待的 goroutine 的策略是随机的。
2、读写锁
不同于互斥锁的完全互斥,读写锁比较适用于读多写少的场景,当并发的去读取一个资源无需对资源修改时是没有必要加锁的,此时读写锁是更好的选择。
读写锁的特点:
读锁定时,一个 goroutine 获得了读锁,其他 goroutine 还可以继续获得读锁,但是无法获得写锁
写锁定时,一个 goroutine 获得了写锁,其他 goroutine 此时既不能获得读锁,也不能获得写锁
对未被读锁定的读写锁进行读解锁,会引发 Panic
对未被写锁定的读写锁惊醒写解锁,会引发 Panic
sync.WaitGroup
Golang 中可以使用 sync.WaitGroup 来实现并发任务的同步,这比在代码中使用 time.Sleep 要灵活的多
sync.WaitGroup 内部维护一个计数器,当启动 n 个 goroutine 时,可以通过 Add 方法将计数器加 n,当一个 goroutine 任务结束时可以通过 Done 方法将计数器减一,最后通过 Wait() 方法等待并发任务全部执行完,当计数器值为0时,表示所有并发任务已完成。
10、管道channel
函数在并发执行时有时需要在并发的函数之间传递数据,通常可以通过共享内存来实现,但是这种方式在不同的 goroutine 中容易发生竞态问题,要保证数据准确性就需要加锁,但是加锁又会使性能下降。golang 中提倡使用通信来共享内存而不是通过共享内存来实现通信,管道(channel)就是 golang 中不同的 goroutine 用来通信的机制,它能够保证在同一时间只有一个 goroutine 才能访问里面的数据。channel 是引用类型,它像一个队列,总是遵循先入先出的规则,这样能够保证收发数据的顺序。每一个 channel 都是一个具体类型的导管,即声明 channel 时需要为其指定元素类型。
关闭后的channel有以下特点:
对关闭的channel再进行关闭操作或引发panic
对关闭的channel发送数据会引发panic
对关闭的channel进行接收会一直获取值直到channel为空
对关闭的且已没有值的channel再获取值会得到对应类型的零值
无缓冲通道 无缓冲通道又称阻塞通道。 无缓冲通道不能存值,只有在有接收者的时候才能通过这个通道发送值
有缓冲通道 有缓冲通道能存值,能存多少容量你自己定,但是往里存的值不能超过设定的容量。
当通道关闭后再往通道发送值会引发panic
11、golang垃圾回收
golang的垃圾回收算法是三色标记法,其中三个颜色分别为:灰色、黑色、白色,其对应了垃圾回收过程中变量的三种状态:
灰色:对象还在标记队列中等待
黑色:对象已经被标记,该对象不会在本次GC中被回收
白色:对象为被标记,该对象会在本地GC中被回收
GC的原理简单来讲就是标记内存中哪些还在使用,哪些不被使用,而不被使用的部分就是GC的对象。
GC触发机制
内存分配量达到阈值
定时触发
手动触发:runtime.GC()
GC调优
控制内存分配的速度,限制 Goroutine 的数量,从而提高赋值器对 CPU 的利用率
减少并复用内存,例如使用 sync.Pool 来复用需要频繁创建临时对象,例如提前分配足够的内存来降低多余的拷贝
需要时,增大 GOGC 的值,降低 GC 的运行频率