go面试题整理(附带部分自己的解答)
解答题:go相关
go的调度
答:
go的调度原理是基于GMP模型,G代表一个goroutine,不限制数量;M=machine,代表一个线程,最大1万,所有G任务还是在M上执行;P=processor代表一个处理器,每一个允许的M都会绑定一个G,默认与逻辑CPU数量相等(通过runtime.GOMAXPROCS(runtime.NumCPU())设置)。
go调用过程:
创建一个G对象
如果还有空闲的的P,创建一个M
M会启动一个底层线程,循环执行能找到的G
G的执行顺序是先从本地队列找,本地没找到从全局队列找。
一次性转移(全局G个数/P个数)个,再去其它P中找(一次性转移一半)
以上的G任务是按照队列顺序执行(也就是go函数的调用顺序)。
另外在启动时会有一个专门的sysmon来监控和管理,记录所有P的G任务计数schedtick。如果某个P的schedtick一直没有递增,说明这个P一直在执行一个G任务,如果超过一定时间就会为G增加标记,并且该G执行非内联函数时中断自己并把自己加到队尾。
go struct能不能比较
答:
可以能,也可以不能。
因为go存在不能使用==判断类型:map、slice,如果struct包含这些类型的字段,则不能比较。
这两种类型也不能作为map的key。
go defer(for defer)
答:
类似栈操作,后进先出。
因为go的return是一个非原子性操作,比如语句 return i
,实际上分两步进行,即将i值存入栈中作为返回值,然后执行跳转,而defer的执行时机正是跳转前,所以说defer执行时还是有机会操作返回值的。
select可以用于什么
答:
- goroutine超时设置,防止goroutine一直执行导致内存不释放等问题。
- 判断channel是否已满或空。如实现一个池线程,当channel已被写满,暂无空闲worker在进行读取,进入default,返回一个暂无可分配资源错误。
select的case的表达式必须是一个channel类型,所有case都会被求值,求值顺序自上而下,从左至右。如果多个case可以完成,则会随机执行一个case,如果有default分支,则执行default分支语句。如果连default都没有,则select语句会一直阻塞,直到至少有一个IO操作可以进行。
break关键字可跳出select的执行。
context包的用途
答:
goroutine管理、信息传递。context的意思是上下文,在线程、协程中都有这个概念,它指的是程序单元的一个运行状态、现场、快照,包含。context在多个goroutine中是并发安全的。
应用场景:
- 超时:
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
ch := make(chan int)
t := time.Now().UnixNano()
t = t / 1000 / 1000
fmt.Println(t)
select {
case <-ch:
case <-ctx.Done():
//如果超过1秒钟ch未读取到信息,自动执行超时
fmt.Println(ctx.Err())
}
fmt.Println("耗时:",time.Now().UnixNano()/1000/1000 - t,"ms")
- 通信并达到取消并发goroutine继续执行的目的。
- 数据传递
- client如何实现长连接
例子参考:go context应用场景 - 简书
主协程如何等其余协程完再操作
waitgroup
wg := sync.WaitGroup{}
wg.Add(10)
for i := 0; i < 10; i++ {
go func() {
wg.Done()
}()
}
wg.Wait()
fmt.Println("123")
channel
ch := make(chan int,5)
for i := 0; i < 10; i++ {
go func(i int) {
ch <- i
}(i )
}
for i:=0;i<10;i++{
<-ch
}
slice,len,cap,共享,扩容
答:
len:切片的长度,访问时间复杂度为O(1),go的slice底层是对数组的引用。
cap:切片的容量,扩容是以这个值为标准。默认扩容是2倍,当达到1024的长度后,按1.25倍。
扩容:每次扩容slice底层都将先分配新的容量的内存空间,再将老的数组拷贝到新的内存空间,因为这个操作不是并发安全的。所以并发进行append操作,读到内存中的老数组可能为同一个,最终导致append的数据丢失。
共享:slice的底层是对数组的引用,因此如果两个切片引用了同一个数组片段,就会形成共享底层数组。当sliec发生内存的重新分配(如扩容)时,会对共享进行隔断。详细见下面例子:
a := []int{1, 2, 3}
b := a[:2]
a[0] = 10
//此时a和b共享底层数组
fmt.Println(a, "a cap:", cap(a), "a len:", len(a))
fmt.Println(b, "b cap:", cap(b), "b len:", len(b))
fmt.Println("-----------")
b = append(b, 999)
//虽然b append了1,但是没有超出cap,所以未进行内存重新分配
//等同于b[2]=999,因此a[2]一并被修改
fmt.Println(a, "a cap:", cap(a), "a len:", len(a))
fmt.Println(b, "b cap:", cap(b), "b len:", len(b))
fmt.Println("-----------")
a[2] = 555 //同上,未重新分配,所以,a[2] b[2]都被修改
fmt.Println(a, "a cap:", cap(a), "a len:", len(a))
fmt.Println(b, "b cap:", cap(b), "b len:", len(b))
fmt.Println("-----------")
b = append(b, 777) //超出了cap,这时候b进行重新分配,b[3]=777,cap(b)=6
a[2] = 666 //这时候a和b不再共享
fmt.Println(a, "a cap:", cap(a), "a len:", len(a))
fmt.Println(b, "b cap:", cap(b), "b len:", len(b))
make([]Type,len,cap)
a :=make([]int,1)=make([]int,1,1);结果将创建一个长度为1,容量为1的int切片,a[0]=0
map如何顺序读取
答:
map的底层是hash table(hmap类型),对key值进行了hash,并将结果的低八位用于确定key/value存在于哪个bucket(bmap类型)。再将高八位与bucket的tophash进行依次比较,确定是否存在。出现hash冲撞时,会通过bucket的overflow指向另一个bucket,形成一个单向链表。每个bucket存储8个键值对。
如果要实现map的顺序读取,需要使用一个slice来存储map的key并按照顺序进行排序。
m := make(map[int]int, 10)
keys := make([]int, 0, 10)
for i := 0; i < 10; i++ {
m[i] = i
keys = append(keys, i)
}
//降序
sort.Slice(keys, func(i, j int) bool {
if keys[i] > keys[j] {
return true
}
return false
})
for _, v := range keys {
fmt.Println(m[v])
}
实现set
答:
利用map,如果要求并发安全,就用sync.map
要注意下set中的delete函数需要使用 delete(map)
来实现,但是这个并不会释放内存,除非value也是一个子map。当进行多次delete后,可以使用make来重建map。
实现消息队列(多生产者,多消费者)
答:
使用sync.Map来管理topic,用channel来做队列。
参考:
https://studygolang.com/articles/8231
大文件排序
答
多路归并法:
<pre class="vditor-reset" placeholder="" contenteditable="true" spellcheck="false"><p data-block="0">(1)假设有K路<a href="https://baike.baidu.com/item/%E6%95%B0%E6%8D%AE%E6%B5%81/3002243">数据流</a>,流内部是有序的,且流间同为升序或降序;
</p><p data-block="0">(2)首先读取每个流的第一个数,如果已经EOF,pass;
</p><p data-block="0">(3)将有效的k(k可能小于K)个数比较,选出最小的那路mink,输出,读取mink的下一个;
</p><p data-block="0">(4)直到所有K路都EOF。
</p></pre>
假设文件又1个G,内存只有256M,无法将1个G的文件全部读到内存进行排序。
第一步:
可以分为10段读取,每段读取100M的数据并排序好写入硬盘。
假设写入后的文件为A,B,C...10
第二步:
将A,B,C...10的第一个字符拿出来,对这10个字符进行排序,并将结果写入硬盘,同时记录被写入的字符的文件指针P。
第三步:
将刚刚排序好的9个字符再加上从指针P读取到的P+1位数据进行排序,并写入硬盘。
重复二、三步骤。
go读文件的两种方式
filePath := "1G.txt"
filePath2 := "1G.txt"
//只读的形式打开文件。
//ps: os.OpenFile可以以读写方式打开
file, err := os.Open(filePath)
if err != nil {
fmt.Println(err)
return
}
file2, err := os.Open(filePath2)
if err != nil {
fmt.Println(err)
return
}
defer file.Close()
//分片读取
//1024bit*8=1024byte=1kb
//1kb*1024=1m
//读取100M的内容
buffer := make([]byte, 1024*1024*100)
i := 0
for {
n, err := file.Read(buffer)
if err != nil && err != io.EOF {
fmt.Println("错误", err)
return
}
if n == 0 {
fmt.Println("结束")
break
}
i += n / 1024
fmt.Println("大小", i/1024, "KB")
}
fmt.Println("文件大小:", i/1024, "KB")
//流处理:
scanner := bufio.NewScanner(file2)
for scanner.Scan() {
fmt.Println(scanner.Bytes())
}
go文件读写参考:
基本排序,哪些是稳定的
答:
保证排序前两个相等的数其在序列的前后位置顺序和排序后它们两个的前后位置顺序相同的排序叫稳定排序。
快速排序、希尔排序、堆排序、直接选择排序不是稳定的排序算法。
基数排序、冒泡排序、直接插入排序、折半插入排序、归并排序是稳定的排序算法。
参考:
https://www.jianshu.com/p/abe27f16b7b5
网络、架构相关
http get跟head
答:
head只请求页面的首部。多用来判断网页是否被修改和超链接的有效性。
get请求页面信息,并返回实例的主体。
参考:
https://blog.csdn.net/ysh1042436059/article/details/80985574
http 401,403
401:未授权的访问。
403: 拒绝访问。
http keep-alive
普通的http连接是客户端连接上服务端,然后结束请求后,由客户端或者服务端进行http连接的关闭。下次再发送请求的时候,客户端再发起一个连接,传送数据,关闭连接。这么个流程反复。但是一旦客户端发送connection:keep-alive头给服务端,且服务端也接受这个keep-alive的话,两边对上暗号,这个连接就可以复用了,一个http处理完之后,另外一个http数据直接从这个连接走了。减少新建和断开TCP连接的消耗。这个可以在Nginx设置,
keepalive_timeout 65;
这个keepalive_timout时间值意味着:一个http产生的tcp连接在传送完最后一个响应后,还需要hold住keepalive_timeout秒后,才开始关闭这个连接。
特别注意TCP层的keep alive和http不是一个意思。TCP的是指:tcp连接建立后,如果客户端很长一段时间不发送消息,当连接很久没有收到报文,tcp会主动发送一个为空的报文(侦测包)给对方,如果对方收到了并且回复了,证明对方还在。如果对方没有报文返回,重试多次之后则确认连接丢失,断开连接。
tcp的keep alive可通过
sysctl -a|grep tcp_keepalive
#打印
net.ipv4.tcp_keepalive_intvl = 75
net.ipv4.tcp_keepalive_probes = 9
net.ipv4.tcp_keepalive_time = 7200
net.ipv4.tcp_keepalive_intvl = 75 // 当探测没有确认时,重新发送探测的频度。缺省是75秒。
net.ipv4.tcp_keepalive_probes = 9 //在认定连接失效之前,发送多少个TCP的keepalive探测包。缺省值是9。这个值乘以tcp_keepalive_intvl之后决定了,一个连接发送了keepalive之后可以有多少时间没有回应
net.ipv4.tcp_keepalive_time = 7200 //当keepalive起用的时候,TCP发送keepalive消息的频度。缺省是2小时。一般设置为30分钟1800
修改:
vi /etc/sysctl.conf
## 键入
net.ipv4.tcp_keepalive_intvl = 30
net.ipv4.tcp_keepalive_probes = 5
net.ipv4.tcp_keepalive_time = 1800
## 生效
sysctl -p
http能不能一次连接多次请求,不等后端返回
可以
tcp与udp区别,udp优点,适用场景
tcp是面向连接的,upd是无连接状态的。
udp相比tcp没有建立连接的过程,所以更快,同时也更安全,不容易被攻击。upd没有阻塞控制,因此出现网络阻塞不会使源主机的发送效率降低。upd支持一对多,多对多等,tcp是点对点传输。tcp首部开销20字节,udp8字节。
udp使用场景:视频通话、im聊天等。
time-wait的作用
time-wait表示客户端等待服务端返回关闭信息的状态,closed_wait表示服务端得知客户端想要关闭连接,进入半关闭状态并返回一段TCP报文。
time-wait作用:
- 防止上一次连接的包迷路后重复出现,影响新连接
- 可靠的关闭TCP连接。在主动关闭方发送的最后一个 ack(fin) ,有可能丢失,这时被动方会重新发fin, 如果这时主动方处于 CLOSED 状态 ,就会响应 rst 而不是 ack。所以主动方要处于 TIME_WAIT 状态,而不能是 CLOSED 。另外这么设计TIME_WAIT 会定时的回收资源,并不会占用很大资源的,除非短时间内接受大量请求或者受到攻击。
解决办法:
#对于一个新建连接,内核要发送多少个 SYN 连接请求才决定放弃,不应该大于255,默认值是5,对应于180秒左右时间
net.ipv4.tcp_syn_retries=2
#net.ipv4.tcp_synack_retries=2
#表示当keepalive起用的时候,TCP发送keepalive消息的频度。缺省是2小时,改为300秒
net.ipv4.tcp_keepalive_time=1200
net.ipv4.tcp_orphan_retries=3
#表示如果套接字由本端要求关闭,这个参数决定了它保持在FIN-WAIT-2状态的时间
net.ipv4.tcp_fin_timeout=30
#表示SYN队列的长度,默认为1024,加大队列长度为8192,可以容纳更多等待连接的网络连接数。
net.ipv4.tcp_max_syn_backlog = 4096
#表示开启SYN Cookies。当出现SYN等待队列溢出时,启用cookies来处理,可防范少量SYN攻击,默认为0,表示关闭
net.ipv4.tcp_syncookies = 1
#表示开启重用。允许将TIME-WAIT sockets重新用于新的TCP连接,默认为0,表示关闭
net.ipv4.tcp_tw_reuse = 1
#表示开启TCP连接中TIME-WAIT sockets的快速回收,默认为0,表示关闭
net.ipv4.tcp_tw_recycle = 1
##减少超时前的探测次数
net.ipv4.tcp_keepalive_probes=5
##优化网络设备接收队列
net.core.netdev_max_backlog=3000
close_wait:
被动关闭,通常是由于客户端忘记关闭tcp连接导致。
https://blog.csdn.net/yucdsn/article/details/81092679
数据库如何建索引
根据业务来啊~
重要指标是cardinality(不重复数量),这个数量/总行数如果过小(趋近于0)代表索引基本没意义,比如sex性别这种。
另外查询不要使用select *,根据select的条件+where条件做组合索引,尽量实现覆盖索引,避免回表。
孤儿进程,僵尸进程
僵尸进程:
即子进程先于父进程退出后,子进程的PCB需要其父进程释放,但是父进程并没有释放子进程的PCB,这样的子进程就称为僵尸进程,僵尸进程实际上是一个已经死掉的进程。
孤儿进程:
一个父进程退出,而它的一个或多个子进程还在运行,那么那些子进程将成为孤儿进程。孤儿进程将被init进程(进程号为1)所收养,并由init进程对它们完成状态收集工作。
子进程死亡需要父进程来处理,那么意味着正常的进程应该是子进程先于父进程死亡。当父进程先于子进程死亡时,子进程死亡时没父进程处理,这个死亡的子进程就是孤儿进程。
但孤儿进程与僵尸进程不同的是,由于父进程已经死亡,系统会帮助父进程回收处理孤儿进程。所以孤儿进程实际上是不占用资源的,因为它终究是被系统回收了。不会像僵尸进程那样占用ID,损害运行系统。
原文链接:https://blog.csdn.net/Eunice_fan1207/article/details/81387417
死锁条件,如何避免
产生死锁的四个必要条件:
(1) 互斥条件:一个资源每次只能被一个进程使用。
(2) 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
(3) 不剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺。
(4) 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
避免方法:
- 为表添加合理的索引。可以看到如果不走索引将会为表的每一行记录添加上锁,死锁的概率大大增加。
- 多个程序尽量约定以相同的顺序访问表(这也是解决并发理论中哲学家就餐问题的一种思路),以固定的顺序访问表和行。比如两个更新数据的事务,事务A更新数据的顺序为1,2;事务B更新数据的顺序为 2 ,1,这样更可能会造成死锁。
- 同一个事务尽可能做到一次锁定所需要的所有资源。
https://blog.csdn.net/IT_10/article/details/97301736
linux命令,查看端口占用,cpu负载,内存占用,如何发送信号给一个进程
端口占用:lsof -i:端口号 或者 nestat
cpu、内存占用:top
发送信号:kill -l 列出所有信号,然后用 kill [信号变化] [进程号]来执行。如kill -9 453。强制杀死453进程
git文件版本,使用顺序,merge跟rebase
git log:查看提交记录
git diff :查看变更记录
git merge:目标分支改变,而源分支保持原样。优点:保留提交历史,保留分支结构。但会有大量的merge记录
git rebase:将修改拼接到最新,复杂的记录变得优雅,单个操作变得(revert)很简单;缺点:
- Rebase 后 feature 分支间的上下文模糊了
- 在团队里 rebasing 公共分支是高风险的事
- 工作变多了:feature 分支需要经常更新
git revert:反做指定版本,会新生成一个版本
git reset:重置到某个版本,中间版本全部丢失
项目实现爬虫的流程
爬虫如何做的鉴权吗
怎么实现的分布式爬虫
电商系统图片多会造成带宽过高,如何解决
micro服务发现
mysql底层有哪几种实现方式
channel底层实现
java nio和go 区别
读写锁底层是怎么实现的
go-micro 微服务架构怎么实现水平部署的,代码怎么实现
micro怎么用
怎么做服务发现的
etcd、Consul
raft算法是那种一致性算法,有什么特点
https://www.cnblogs.com/xybaby/p/10124083.html
当go服务部署到线上了,发现有内存泄露,该怎么处理
pprof
https://www.cnblogs.com/weiweng/p/12497274.html
go的GC原理
http://alblue.cn/articles/2020/07/07/1594131614114.html
mysql
mysql索引为什么要用B+树?
节省空间(非叶子节点不存储数据,相对b tree的优势),减少I/O次数(节省的空间全部存指针地址,让树变的矮胖),范围查找方便(相对hash的优势)。
mysql语句性能评测?
explain
其他的见:
https://blog.csdn.net/chengxuyuanyonghu/article/details/61431386
程序代码题
1.以下代码的打印结果和为什么
func max() {
runtime.GOMAXPROCS(1)
wg := sync.WaitGroup{}
wg.Add(20)
for i:=0;i<10;i++{
go func() {
fmt.Println(i)
wg.Done()
}()
}
for i:=0;i<10;i++{
go func(i int) {
fmt.Println(i)
wg.Done()
}(i)
}
wg.Wait()
}
func main(){
max()
}
runtime2.go 中关于 p 的定义: 其中 runnext 指针决定了下一个要运行的 g,根据英文的注释大致意思是说:
- 1 . 如果 runnext 不为空且是就绪的,那么应该优先运行该 runnext 指向的 g 而不是运行队列中到达运行时间的 g。
- 2 . 此时要被运行的 g 将继承当前 g 剩余的时间片继续执行。
- 3 . 之所以这样做是为了消除将最后的 g 添加到队列末尾导致的潜在的调度延迟。
所以当设置 runtime.GOMAXPROCS(1) 时,此时只有一个 P,创建的 g 依次加入 P, 当最后一个即 i==9 时,加入的最后 一个 g 将会继承当前主 goroutinue 的剩余时间片继续执行,所以会先输出 9, 之后再依次执行 P 队列中其它的 g。
https://studygolang.com/articles/23333?fr=sidebar
写sql问题
按半小时统计日志数量
SELECT time, COUNT( * ) AS num
FROM
(
SELECT Duration,
DATE_FORMAT(
concat( date( TimeStart ), ' ', HOUR ( TimeStart ), ':', floor( MINUTE ( TimeStart ) / 30 ) * 30 ),
'%Y-%m-%d %H:%i'
) AS time
FROM tarck
WHERE Flag = 0 AND Duration >= 300
) a
GROUP BY DATE_FORMAT( time, '%Y-%m-%d %H:%i' )
ORDER BY time;
————————————————
版权声明:本文为CSDN博主「小白咸菜」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/u010946448/java/article/details/83752984
删除除了主键id不相同,其他字段均相同的数据。
delet from table where id not in(select id from(select max(id) as id from table group by 字段... )as t1);
获取每科分数均高于80分的学生姓名
drop table if EXISTS tmp_1;
create table tmp_1
(`Id` INT NOT NULL AUTO_INCREMENT,
name varchar(10),
subject varchar(10),
score int,
PRIMARY KEY(`Id`));
insert into tmp_1 (name,subject,score) values ('李云龙','语文',79);
insert into tmp_1 (name,subject,score) values ('李云龙','数学',81);
insert into tmp_1 (name,subject,score) values ('楚云飞','语文',81);
insert into tmp_1 (name,subject,score) values ('楚云飞','数学',89);
insert into tmp_1 (name,subject,score) values ('张大彪','语文',79);
insert into tmp_1 (name,subject,score) values ('张大彪','数学',90);
select `name` from table group by `name` having sum(score>80)>科目数;
行转列
CREATE TABLE IF NOT EXISTS tb_amount(
`Id` INT NOT NULL AUTO_INCREMENT,
`Year` CHAR(4),
`Month` CHAR(2),
`Amount` DECIMAL(5,2),
PRIMARY KEY(`Id`)
);
INSERT INTO `tb_amount`(`Year`, `Month`, `Amount`) VALUES('1991', '1', '1.1');
INSERT INTO `tb_amount`(`Year`, `Month`, `Amount`) VALUES('1991', '2', '1.2');
INSERT INTO `tb_amount`(`Year`, `Month`, `Amount`) VALUES('1991', '3', '1.3');
INSERT INTO `tb_amount`(`Year`, `Month`, `Amount`) VALUES('1991', '4', '1.4');
INSERT INTO `tb_amount`(`Year`, `Month`, `Amount`) VALUES('1992', '1', '2.1');
INSERT INTO `tb_amount`(`Year`, `Month`, `Amount`) VALUES('1992', '2', '2.2');
INSERT INTO `tb_amount`(`Year`, `Month`, `Amount`) VALUES('1992', '3', '2.3');
INSERT INTO `tb_amount`(`Year`, `Month`, `Amount`) VALUES('1992', '4', '2.4');
方法一:
select `year`,
(select amount from `tb_amount` as m where month = 1 and m.`year`=tb_amount.`year`) as m1,
(select amount from `tb_amount` as m where month = 2 and m.`year`=tb_amount.`year`) as m2,
(select amount from `tb_amount` as m where month = 3 and m.`year`=tb_amount.`year`) as m3,
(select amount from `tb_amount` as m where month = 4 and m.`year`=tb_amount.`year`) as m4
from tb_amount group by `year`;
方法二:
select `year`,
max(case month when 1 then amount else 0 end) as m1,
max(case month when 2 then amount else 0 end) as m2,
max(case month when 3 then amount else 0 end) as m3,
max(case month when 4 then amount else 0 end) as m4
from tb_amount group by year;
温度高低问题
[图片上传失败...(image-4ef445-1594976286098)]
方法1:to_days,返回给的日期从0开始算的天数。
select w1.id from weather as w1,weather as w2 where to_days(w2.recorddate)-to_days(w1.recorddate) = 1 and w2.temperature>w1.temperature;
方法2:data_add。向日期添加指定时间间隔
select w1.id from weather as w1,weather as w2 where date_add(w1.recorddate,interval 1 day)=(w1.recorddate) = 1 and w2.temperature>w1.temperature;
mysql查出数据表中连续出现三次或三次以上的数据
[图片上传失败...(image-b67b10-1594976286098)]
select * from number where id in (
select distinct n1.id from number n1,number n2,number n3
where (n1.num = n2.num and n2.num = n3.num and (
(n1.id + 1= n2.id and n2.id +1 = n3.id)or
(n3.id + 1= n2.id and n2.id +1 = n1.id)or
(n3.id + 1= n1.id and n1.id +1 = n2.id)
)
)
order by n1.id )
golang 100题
赞赏码
非学,无以致疑;非问,无以广识