golang面试题题目归纳
2021.06.01
defer
下面这段代码输出的内容
package main
import (
"fmt"
)
func main() {
defer_call()
}
func defer_call() {
defer func() { fmt.Println("打印前") }()
defer func() { fmt.Println("打印中") }()
defer func() { fmt.Println("打印后") }()
panic("触发异常")
}
打印后
打印中
打印前
panic:触发异常
ps:defer的执行顺序是先进后出,当出现panic时,会先按照defer的后进先出的顺序执行,最后才会执行panic
2021.06.02
go的调度
1.多进程/多线程产生的问题
- 高内存占用
每个任务都创建一个线程是不现实的,会消耗大量的内存 - 调度的高消耗CPU
程拥有太多的资源,进程的创建、切换、销毁,都会占用很长的时间,CPU 虽然利用起来了,但如果进程过多,CPU 有很大的一部分都被用来进行进程调度了。
2、 “内核态 “线程和” 用户态 “线程
内核线程 -> 线程
用户线程 -> 协程
映射关系
- N:1关系
N个协程绑定一个线程,优点就是协程在用户态线程即完成切换,不会陷入到内核态,这种切换非常的轻量快速。但也有很大的缺点:一个进程的所有协程都绑定在一个线程上
缺点:
- 某个程序用不了硬件的多核加速能力
- 一旦某协程阻塞,造成线程阻塞,本进程的其他协程都无法执行了,根本就没有并发的能力了
- 1:1关系
一个协程绑定一个线程,这种最容易实现。协程的调度都由CPU完成了,不存在N:1缺点
缺点:
- 协程的创建、删除和切换的代价都由CPU完成,有点略显昂贵了。
- M:N关系
M个协程绑定N个线程,是N:1和1:1类型的结合,克服了以上两种模型的缺点,但实现起来最为复杂
协程跟线程是有区别的,线程由CPU调度是抢占式的,协程由用户态是协作式的,一个协程让出CPU后,才执行下一个协程
3、Go语言的协程goroutine
Go为了提供更容易使用的并发方法,使用了goroutine和channel。goroutine来自协程的概念,让一组可复用的函数运行在一组线程之上,即便有协程阻塞,该线程的其他协程也可以被runtime调度,转移到其他可运行的线程上,最关键的是,程序员看不到这些底层细节,这就降低了变成的难度,提供了更容易的开发
Go中,协程被称为goroutine,它非常轻量,一个goroutine只占几KB,并且这几KB就足够goroutine运行完,这就能在有限的内存空间内支持大量goroutine,支持更多的并发,虽然一个goroutine的栈只占几KB,但实际上是可伸缩的,如果需要更多的内容,runtime会自动为goroutine分配。
goroutine的特点:
- 占用内存更小(几KB,可由runtime动态分配大小,支持动态调节)
- 调度更灵活(runtime调度,非用户调度,解放开发者的变成难度)
4、被废弃的goroutine调度器
Go目前使用的调度器是2012年重新设计的,因为之前的调度器性能存在问题,所以使用4年就被放弃了
G表示goroutine,M表示线程
M想要执行、放回G都必须访问全局G队列,并且M有多个,即多线程访问同一资源需要加锁进行保证互斥/同步,所以全局G队列是有互斥锁进行保护的。
老调度器有几个缺点:
- 创建、销毁、调度G都需要每个M获得锁,这就形成了激烈的锁竞争
- M转移G会造成延迟和额外的系统负载。比如当G中包含创建新协程的时候,M创建了G’,为了继续执行G,需要把G‘交给M’执行,也造成了很差的局部性,因为G‘和G是相关的,最好放在M上执行,而不是其他M‘
- 系统调用(CPU在M之间的切换)导致频繁的线程阻塞和取消阻塞操作增加了系统的开销
5、Goroutine调度器的GMP模型的设计思想
面对之前调度器的问题,Go设计了新的调度器
在新调度器中,除了M(thread)和G(goroutine),又引进了P(processor)
Processor,它包含了运行goroutine的资源,如果线程想运行goroutine,必须先获得P,P中包含了可运行的G队列
GMP模型
在Go中,线程试运行goroutine的实体,调度器的功能是把可运行的goroutine分配到工作线程上
- 全局队列(Global Queue),存放等待运行的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,不断重复下去
Goroutine调度器和OS调度器时通过M结合起来的,每个M代表了一个内核线程,IS调度器负责把内核线程分配到CPU的核上执行
有关P和M的个数问题
1.P的数量:
由启动时环境变量 G O M A X P R O C S 或 者 是 由 r u n t i m e 的 方 法 G O M A X P R O C E ( ) 决 定 。 这 意 味 着 在 程 序 执 行 的 任 意 时 刻 都 只 有 GOMAXPROCS或者是由runtime的方法GOMAXPROCE()决定。这意味着在程序执行的任意时刻都只有 GOMAXPROCS或者是由runtime的方法GOMAXPROCE()决定。这意味着在程序执行的任意时刻都只有GOMAXPROCS个goroutine在同时运行
2.M的数量:
go语言本身的限制,go程序启动时,会设置M的最大数量,默认10000,但是内核很难支持这么多的线程数,所以这个限制可以忽略
runtime/debug中的SetMaxThreads函数,设置M的最大数量
一个M阻塞了,会创建新的M
M与P的数量没有绝对关系,一个M阻塞,P就会去创建或者切换另一个M,所以,即使P的默认数量是1,也有可能会创建很多个M出来
P和M如何会被创建
- P何时创建,在确定了P的最大数量n后,运行时系统会根据这个数量创建n个P
- M何时创建,没有足够的M来关联P并运行其中的可运行的G。比如所有的M此时都阻塞住了,而P中还有很多就绪任务,就会去寻找空闲的M,而没有空闲的,就回去创建新的M
调度器的生命周期
特殊的M0和G0
M0
M0是启动程序后的编号为0的主线程,这个M对应的实例会在全局变量runtime.m0中,不需要再heap上分配,M0负责执行初始化操作和启动第一个G,在之后M0就和其他的M一样了
G0
G0是每次启动一个M都会第一个创建的goroutine,G0仅用于负责调度的G,G0不指向任何可执行的函数,每个M都会有一个自己的G0,在调度或系统调用时会使用G0的栈空间,全局变量的Go是M0的Go
示例代码:
package main
import "fmt"
func main() {
fmt.Println("Hello,world!")
}
接下来我们来针对上面的代码对调度器里面的结构做一个分析(也会经理如上面代码所示的过程)
- runtime创建最初的线程M0和goroutine G0,并把两者关联
- 调度器初始化,初始化M0、栈、垃圾回收,以及创建和初始化由GOMAXPROCS个P构成的P列表
- 示例代码中的main函数main.main,runtime中也有一个main函数——runtime.main,代码经过编译后,runtime.main会调用main.main,程序启动时会为runtime.main创建goroutine,称它为main.goroutine把,然后把main,routine加入到P的本地列表
- 启动M0,M0已经绑定了P,会从P的本地队列获取G,获取到main goroutine
- G拥有栈,M根据G中的栈信息和调度信息设置运行环境
- M运行G
- G退出,再次回到M获取可运行的G,这样重复下去,知道main.main退出,runtime.main执行defer和panic处理,或调到用runtime.exit退出程序
调度器的生命周期几乎占满了一个Go程序的一生,runtime.main和goroutine执行之前都是为了调度器做准备工作,runtime.main的goroutine运行,才是调度器的真正开始,知道runtime.main结束而结束
2021.06.06
go struct能不能比较
因为Go是强类型语言,所以不同类型的结构不能作比较,但是同一类型的实例值是可以比较的,实例不可以比较是因为指针类型
select
select常用于goroutine的完美退出
golang的select就是监听IO操作,当IO操作发生时,触发相应的动作
每个case语句里必须是一个IO操作,确切的说,应该是一个面向channel的IO操作
context包的用途
Context通常被称作上下文,他是一个比较抽象的概念,其本质,是【上下上下】存在上下层的传递,上会把内容传递给下。在Go语言中,程序单元也就指的是goroutine
client如何实现长连接
server是设置超时时间,for循环遍历
主协程如何等其余协程完再操作
使用channel、select、context、waitgroup
slice,len,cap,共享,扩容
append函数,因为slice底层数据结构是由数组、len、cap组成,所以在使用append扩容时,会查看数组后面有没有连续内存块,有就在后面添加,没有就重新生成一个大的数组
map如何顺序读取
map不能顺序读取,因为map是无序的,想要有序读取,首先的解决的问题就是,把key变为有序的,所以可以把key放入切片对切片进行排序,遍历切片,通过key取值
实现set
type inter interface{}
type Set struct {
m map[inter]bool
sync.RWMutex
}
func New() *Set {
return *Set{
m:map[inter]bool{}
}
}
func (s *Set) Add(item inter) {
s.Lock()
defer s.Unlock()
s.m[item]=true
}
2021.06.07
实现消息队列(多生产者,多消费者)
使用切片加锁可以实现
大文件排序
归并排序,分而治之,拆分为小文件
基本排序,哪些是稳定的
稳定的排序:
- 冒泡排序 o(n2)
- 鸡尾酒排序(双向冒泡排序)o(n2)
- 插入排序 o(n2)
- 桶排序 o(n);需要o(k)额外
- 计数排序 o(n+k)
- 归并排序 o(nlogn)
- 原地归并排序 o(n2)
- 二叉树排序 o(nlogn)
HTTP GET和Head
400 bad request 请求报文存在语法错误
401 unauthorized 表示发送的请求需要有通过HTTP认证的认证信息
403 forbidden 表示对请求资源的访问被服务器拒绝
404 not found 表示在服务器上没有找到请求的资源
HTTP keep-alive
client发出的HTTP请求头需要增加Connection:keep-alive字段
web-server端需要能识别connection:keep-alive字段,并且在http的response里指定connection:keep-alive字段,告诉client,我能提供keep-alive服务,并且”应允“client我暂时不会关闭socket链接
HTTP能不能一次连接多次请求,不等后端返回
HTTP本质上是使用socket链接,因此发送请求,接写入tcp缓冲,是可以多次进行的,这也是HTTP是无状态的原因
TCP和UDP的区别,UDP有点,适用场景
tcp传输的是数据流,而UDP是数据包,tcp会经过三次握手,udp不需要
2021.06.09
time-wait的作用
time-wait开始的时间为tcp四次挥手中主动关闭连接方发送完最后一次挥手,也就是ACK=1的信号结束后,主动关闭连接方所处的状态
然后time-wait的的持续时间为2MSL. MSL是Maximum Segment Lifetime,译为“报文最大生存时间”,可为30s,1min或2min。2msl就是2倍的这个时间。工程上为2min,2msl就是4min。但一般根据实际的网络情况进行确定持续时间的长度设计原因:
原因1:为了保证客户端发送的最后一个ack报文段能够到达服务器。因为这最后一个ack确认包可能会丢失,然后服务器就会超时重传第三次挥手的fin信息报,然后客户端再重传一次第四次挥手的ack报文。如果没有这2msl,客户端发送完最后一个ack数据报后直接关闭连接,那么就接收不到服务器超时重传的fin信息报(此处应该是客户端收到一个非法的报文段,而返回一个RST的数据报,表明拒绝此次通信,然后双方就产生异常,而不是收不到。),那么服务器就不能按正常步骤进入close状态。那么就会耗费服务器的资源。当网络中存在大量的timewait状态,那么服务器的压力可想而知。
原因2:在第四次挥手后,经过2msl的时间足以让本次连接产生的所有报文段都从网络中消失,这样下一次新的连接中就肯定不会出现旧连接的报文段了。也就是防止我们上一篇文章 为什么tcp是三次握手而不是两次握手? 中说的:已经失效的连接请求报文段出现在本次连接中。如果没有的话就可能这样:这次连接一挥手完马上就结束了,没有timewait。这次连接中有个迷失在网络中的syn包,然后下次连接又马上开始,下个连接发送syn包,迷失的syn包忽然又到达了对面,所以对面可能同时收到或者不同时间收到请求连接的syn包,然后就出现问题了。
数据库如何建索引
普通索引
创建索引:CREATE INDEX [indexName] ON [table_name] ([column_name])
修改表结构:ALTER table [tableName] ADD INDEX indexName
创建表时指定索引:
create table [tableName] (
ID int not null,
username varchar(16) not null,
index [indexName] (username(length))
)唯一索引
创建索引:CREATE UNIQUE INDEX [indexName] ON mytable
修改表结构:ALTER table mytable ADD UNIQUE [indexName] (username(length))
创建表时指定索引:
create table [tableName] (
ID int not null,
username varchar(16) not null,
unique [indexName] (username(length))
)
孤儿进程,僵尸进程
孤儿进程:一个父进程退出,而它的一个或多个子进程还在运行,那么那些子进程将成为孤儿进程。孤儿进程将被init进程(进程号为1)所收养,并由init进程对它们完成状态收集工作
僵尸进程:一个进程使用fork创建子进程,如果子进程退出,而父进程并没有调用wait或waitpid获取子进程的状态信息,那么子进程的进程描述符仍然保存在系统中。这种进程称之为僵死进程。
2021.06.11
死锁条件,如何避免
死锁的条件:
- 系统资源不足
- 进程运行推进的顺序不合适
- 资源分配不当等
死锁产生的四个必要条件
- 互斥
- 持有并等待
- 不可抢占
- 环路等待
如何避免:
- 打破互斥条件
- 打破不可抢占条件
- 打破占有且申请条件
- 打破循环等待条件
linux命令,查看端口占用,cpu负载,内存占用,如何发送信号给一个进程
查看端口占用:
- isof -i XXXX(端口号)
- netstat -anp|grep 9001
2.1 netstat命令解释:
netstat -ntlp // 查看当前所有tcp端口
netstat -ntulp | grep 80 // 查看所有80端口使用情况
netstat -an | grep 3306 // 查看所有3306端口使用情况
参数说明:
-t (tcp)仅显示tcp相关选项
-u (udp)仅显示udp相关选项
-n 拒绝显示别名,能显示数据的全部转化为数字
-l 仅列出在Listen(监听)的服务状态
-p 显示建立相关链接的程序名查看cpu负载:
- atop
- top
- uptime
查看内存占用:
- top
- free
- cat 、proc/meminfo
如何发送信号给一个进程
kill
2021.06.12
Slice与数组的区别,Slice底层结构
区别
数组:
- 长度不可变,初始化的时候需要声明数组的长度
Slice:
- Slice长度不定,append时会自动扩容,属于引用类型
Slice底层结构:
type slice struct {
array unsafe.Pointer
len int
cap int
}
Slice的结构由三部分组成,Pointer是一个指向数组的指针,len表示长度,cap表示容量,cap总是且必须大于len
redis的基本数据结构
string——字符串
redis最简单的数据结构;动态字符串,采用与分配冗余空间的方法
hash——字典
hashmap,哈希
list——列表
其实就是链表;Redis 还提供了操作 List 中某一段元素的 API,你可以直接查询,删除 List 中某一段的元素
set——集合
集合就是一堆不重复值的组合
sorted list——有序集合
比普通set多了一个权重score,使集合中的元素能够按照score进行有序排序
Redis的List用过吗?底层怎么实现的?
没用过,但是了解过
底层是通过链表(双向和压缩)来实现的
链表与数组最大的区别就是链表是链式存储,数组是连续存储的
redis的list底层是通过list和listnode两个结构体来实现的
listnode是一个双向链表节点,多个listnode通过prev和next指针来组成双端链表
使用list结构体来持有链表,list结构体为链表提供了表头指针head和标为指针tail,以及链表节点的数量len
2021.06.17
MySQL的索引有几种,以及其时间复杂度
全文索引、空间索引、B+ Tree索引、哈希索引
MySQL主要提供的索引是B+Tree索引和哈希索引,其时间复杂度分别是O(logN)和O(1)
InnoDB是表锁还是行锁,为什么
搜索条件有索引列的时候是行级锁,没有的话是表级锁
Go的channel(有缓冲无缓冲)
无缓冲的channel必须进出同步,不然会阻塞
有缓冲的channel可以异步,不需要生产者和消费者同步,可以给消费者一定的缓冲
用过什么消息中间件
rabbitMQ
退出程序怎么防止无缓冲channel没有消费完(channel还留有数据没有)
退出时将生产者关闭,不会产生多余的数据给消费者
go 生产者消费者
func produce(ch chan int) {
defer func() {
if err := recover(); err!= nil && err.(runtime.Error).Error() == "send on closed channel" {
fmt.Println(err)
fmt.Println("即将生产的数据:",X)
} else {
close(ch) // 不再继续生产
}
wg.Done()
}()
x := 0
for i:= 1;i<=10;i++ {
x++
ch <- x
}
}
func consumer(ch chan int) {
defer func () {
if err := recover(); err != nil {
fmt.Println(err)
close(ch)
fmt.Println(<-ch) // 触发"send on closed channel"这个错误
}
wg.Done()
}()
for x := range ch {
time.Sleep(1*time.Second)
fmt.Println(x)
if x == 3 {
panic("强制结束")
}
}
}
func main() {
fmt.Println("退出程序时,防止channel没有消费完")
ch := make(chan int)
wg.Add(2)
go Send(ch)
go Receive(ch)
wg.Wait()
fmt.Println("任务完成")
_,ok:=<- ch
fmt.Println(ok)
}
手写循环队列
在这里插入代码片