golang面试题题目归纳

golang面试题题目归纳,第1张

golang面试题题目归纳 2021.06.01defer 2021.06.02go的调度1.多进程/多线程产生的问题2、 “内核态 “线程和” 用户态 “线程映射关系 3、Go语言的协程goroutine4、被废弃的goroutine调度器5、Goroutine调度器的GMP模型的设计思想GMP模型调度器的生命周期 2021.06.06go struct能不能比较selectcontext包的用途client如何实现长连接主协程如何等其余协程完再 *** 作slice,len,cap,共享,扩容map如何顺序读取实现set 2021.06.07实现消息队列(多生产者,多消费者)大文件排序基本排序,哪些是稳定的HTTP GET和HeadHTTP keep-aliveHTTP能不能一次连接多次请求,不等后端返回TCP和UDP的区别,UDP有点,适用场景 2021.06.09time-wait的作用数据库如何建索引孤儿进程,僵尸进程 2021.06.11死锁条件,如何避免linux命令,查看端口占用,cpu负载,内存占用,如何发送信号给一个进程 2021.06.12Slice与数组的区别,Slice底层结构redis的基本数据结构Redis的List用过吗?底层怎么实现的? 2021.06.17MySQL的索引有几种,以及其时间复杂度InnoDB是表锁还是行锁,为什么Go的channel(有缓冲无缓冲)用过什么消息中间件退出程序怎么防止无缓冲channel没有消费完(channel还留有数据没有)go 生产者消费者手写循环队列

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),存放等待运行的GP的本地队列:同全局队列类似,存放的也是等待运行的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()决定。这意味着在程序执行的任意时刻都只有 GOMAXPROCSruntimeGOMAXPROCE()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个PM何时创建,没有足够的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 goroutineG拥有栈,M根据G中的栈信息和调度信息设置运行环境M运行GG退出,再次回到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负载:

atoptopuptime

查看内存占用:

topfreecat 、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)
}
手写循环队列
在这里插入代码片

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/langs/996047.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-05-21
下一篇 2022-05-21

发表评论

登录后才能评论

评论列表(0条)

保存