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. 某个程序用不了硬件的多核加速能力
  2. 一旦某协程阻塞,造成线程阻塞,本进程的其他协程都无法执行了,根本就没有并发的能力了
  • 1:1关系
    一个协程绑定一个线程,这种最容易实现。协程的调度都由CPU完成了,不存在N:1缺点
    缺点:
  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()决定。这意味着在程序执行的任意时刻都只有 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如何会被创建

  1. P何时创建,在确定了P的最大数量n后,运行时系统会根据这个数量创建n个P
  2. 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

死锁条件,如何避免

死锁的条件:

  • 系统资源不足
  • 进程运行推进的顺序不合适
  • 资源分配不当等

死锁产生的四个必要条件

  1. 互斥
  2. 持有并等待
  3. 不可抢占
  4. 环路等待

如何避免:

  1. 打破互斥条件
  2. 打破不可抢占条件
  3. 打破占有且申请条件
  4. 打破循环等待条件

linux命令,查看端口占用,cpu负载,内存占用,如何发送信号给一个进程

查看端口占用:

  1. isof -i XXXX(端口号)
  2. 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负载:

  1. atop
  2. top
  3. uptime

查看内存占用:

  1. top
  2. free
  3. 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)
}

手写循环队列

在这里插入代码片
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值