当前位置:   article > 正文

Golang基础面试题汇总_go 基础面试题

go 基础面试题

Go

Go语言优势

底层原生的多核并发技术。

Go为什么需要指针

因为函数参数都是值传递,如果对实参进行修改,值类型只是复制了一份值,对参数没有影响,指针就可以,而且指针指向的是一个地址,内存占用小,其实go里面不存在引用类型,比如map,slice,interface底层都是指针类型。

内存泄漏场景

由于逻辑问题导致资源一直无法被释放,增加阻塞。

1.Goroutine

func mainI(){
	ch := make(chan int)
	go func(){
		ch <- 1
	}()
	//<-ch
	time.Sleep(time.Second)
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

解释:对于非缓冲管道,必须有接收者才能将数据放入管道,否则就会阻塞,从而导致内存泄漏。
2.Http

http请求resp.Body,需要defer resp.Body.Close()
  • 1
除了加Mutex锁以外还有哪些方式安全读写共享变量
  1. 通过 Channel 进行安全读写共享变量。
  2. 通过原子性操作(atomic)
Channel有无缓冲的区别

无缓冲:发送和接收需要同步。
有缓冲:不要求发送和接收同步,缓冲满时发送阻塞。

CSP并发模型

以通信的方式来共享内存,是通过Goroutine和Channel来实现的。

Goroutine

goroutine是golang实际并发的实体,底层使用coroutine(协程)实现并发,coroutine是一种运行在用户态的用户线程,底层使用coroutine,是因为:
用户空间,避免了内核态和用户态的切换导致的成本 可以由语言和框架层进行调度。
更小的栈空间允许创建大量的实例。
特性:GMP
G:Goroutine M:线程 P:CPU
运行
CPU开启一个线程执行Goroutine。单核情况,所有goroutine运行在同一个线程中,每个线程维护一个上下文,一个上下文只有一个goroutine,其他goroutine在runqueue中等待。多核情况,碰到goroutine阻塞,会再开启一个线程执行goroutine,以充分利用cpu资源。

停止Goroutine

1.runtime.Goexit()。
2.通过channel传递退出信号(<-ch)(适用于单个goroutine)。
3.使用waitgroup(Add、Wait、Done)。
4.context手动cannel或超时控制(contex应用)。

常用的并发模型

channle:无缓冲,同步。
sync.WaitGroup:为了防止main函数结束结束goroutine,waitgroup会等待所有goroutine完成。
context:上下文。包括程序的运行环境、现场和快照,主要处理多个goroutine之间共享数据及多个goroutine的管理。

Channel为什么是线程安全的

原子性,底层实现互斥锁来保证线程安全,出队入队都加了锁。

nil slice和空slice

nil slice:未初始化的slice,不能赋值。
空slice:初始化的slice。

进程、线程、协程

进程: 系统进行资源分配和调度的基本单位,拥有独立的内存空间,不同进程通过进程间通信来通信,上下文切换开销(栈、寄存器、虚拟内存、文件句柄)大,相对比较安全。
线程:轻量级进程,不拥有系统资源,共享进程全部资源,线程间通信主要通过共享内存,上下文切换开销小,相比进程容易丢失数据。
协程:一种用户态的轻量级线程,由go运行时(runtime)管理,协程的调度完全由用户控制。协程拥有自己的寄存器上下文和栈。
从单进程到多进程提高了 CPU利用率;从进程到线程,降低了上下文切换的开销;从线程到协程,进一步降低了上下文切换的开销,使得高并发的服务可以使用简单的代码写出来。
(进程拥有自己独立的堆和栈,既不共享堆,亦不共享栈,进程由操作系统调度。线程拥有自己独立的栈和共享的堆,共享堆,不共享栈,线程亦由操作系统调度(标准线程是的)。 协程和线程一样共享堆,不共享栈,协程由程序员在协程的代码里显示调度)

协程如何调度的

协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈。进程和线程的操作是由程序触发系统接口,最后执行者是系统,协程的操作执行者则是用户自身程序。

Buffer和Cache区别

Buffer:用于存放将要输出到disk(块设备)的数据,进行流量整形,把突发的大数量较小规模的IO整理成平稳的小数量较大规模的IO,以减少响应次数。
Cache:用于存放从disk上读出的数据,为了弥补高速设备和低速设备的鸿沟而引入的中间层,最终起到加速访问速度的作用。
两者都是为提高IO性能而设计的。

垃圾回收GC

垃圾回收:指内存中不再使用的内存区域,自动发现和释放这种内存区域的过程就是垃圾回收。
三色标记原理
初始所有对象白色。
从根(root)出发(全局指针和goroutine栈上的指针)扫描所有可达对象,标记为灰色,放入待处理队列。
从队列取出灰色对象,将其引用对象标记为灰色放入队列,自身标记为黑色。
重复 3,直到灰色对象队列为空。此时白色对象即为垃圾,进行回收。
原理
WB(write barrier:写屏障,go1.7Dijkstra写屏障,只监控堆上指针数据的变动(增量式垃圾回收时,新增的指针对象会被初始为白色,需要标记为黑色)re-scan。go1.8混合写屏障,只监控堆上指针数据的变动,无需re-scan)-STW(stop the world:暂停所有正在执行的用户线程/协程)-mark(三色标记,标记协程是并行的)-WB(关闭写屏障)-STW(挂起stw协程)-sweep(清除,唤醒清扫垃圾协程,该协程在应用启动后创建,创建后立即进入睡眠,被唤醒后把白色对象挨个清理掉,清扫协程和应用协程是并发进行的,清扫完成后再次进入睡眠状态)
回收方式
非增量式:垃圾回收需要STW,在STW期间完成所有垃圾对象的标记,STW结束后慢慢的执行垃圾对象的处理。
增量式:垃圾回收需要STW,在STW期间完成部分垃圾对象的标记,然后结束STW继续执行用户线程,一段时间后再次执行STW再标记部分垃圾对象,这个过程会多次重复执行,直到所有垃圾对象标记完成。
GC触发条件
辅助GC:在分配内存时,会判断当前的Heap内存分配量是否达到了触发一轮GC的阈值(每轮GC完成后,该阈值会被动态设置),如果超过阈值,则启动一轮GC。
调用runtime.GC()强制启动一轮GC。
sysmon是运行时的守护进程,当超过 forcegcperiod (2分钟)没有运行GC会启动一轮GC。

常用的分布式锁

数据库、缓存(redis、memcached、tair)、zookeeper。

切片数据结构

指向底层数组的指针、长度、容量。

切片和数组区别

数组:固定长度。
切片:底层是指向数组的指针,动态。

Slice扩容策略:

首先判断,如果新申请容量大于2倍的旧容量,最终容量就是新申请的容量。
否则判断,如果旧切片的长度小于1024,则最终容量就是旧容量的两倍。
否则判断,如果旧切片长度大于等于1024,则最终容量从旧容量开始循环增加原来的 1/4, 直到最终容量大于等于新申请的容量。
如果最终容量计算值溢出,则最终容量就是新申请容量。

扩容前后的Slice是否相同?
情况一:
原数组还有容量可以扩容(实际容量没有填充完),这种情况下,扩容以后的数组还是指向原来的数组,对一个切片的操作可能影响多个指针指向相同地址的Slice。
情况二:
原来数组的容量已经达到了最大值,再想扩容, Go 默认会先开一片内存区域,把原来的值拷贝过来,然后再执行 append()操作。这种情况丝毫不影响原数组。 要复制一个Slice,最好使用Copy函数。

new和make区别

new:new对象,分配零值内存,返回指针。
make:make slice、map、channel,分配非零值内存(并初始化成员结构),返回对象。

map底层实现

底层实现是一个散列表,主要有两结构体,hmap(a header for a go map)和bmap(a bucket for a go map,bucket),bucket中存储key和value。哈希值按照用途一分为二:低位和高位,低位用于寻找当前key属于hmap中哪个bucket,高位用于寻找bmap中的key对应的value。

map扩容

当Go的map长度增长到大于加载因子所需的map长度时,Go语言就会将产生一个新的bucket数组,然后把旧的bucket数组移到一个属性字段oldbucket中。
注意:并不是立刻把旧的数组中的元素转到新的bucket当中,而是,只有当访问到具体的某个bucket的时候,会把bucket中的数据转移到新的bucket中。

map并发安全

原生map并发是非安全的,是因为map是指针类型,并发写时,多个协程同时操作一个内存,类似于多线程操作同一个资源会发生竞争关系,共享资源会遭到破坏,因此golang出于安全的考虑,抛出致命错误:fatal error: concurrent map writes。
解决并发非安全
1.sync.map(同步锁,golang自带)
2.mutex(互斥锁)
3.rwMutex(读写锁)
如何选择解决方案:
1.(一亿)压测写入效率对比(优<差):rwMutex(207.8 ns/op) < mutex( 237.1 ns/op) < sync.map(509.3 ns/op)
2.(一亿)压测查找效率对比(优<差):sync.map(53.39 ns/op) <rwMutex(60.49 ns/op) < mutex(166.7 ns/op)
3.(一亿)压测删除效率对比(优<差):sync.map(41.54 ns/op) < mutex(168.3 ns/op) < rwMutex(188.5 ns/op)
分析:读多写少用sync.map
sync.map查询效率高的原因:
结构sync
read作为缓存层,提供了快路径(fast path)的查找。同时其结合 amended 属性,配套解决了每次读取都涉及锁的问题,实现了读这一个使用场景的高性能。
sync.map写入效率低的原因:
1.写入一定要会经过 read,无论如何都比别人多一层,后续还要查数据情况和状态,性能开销相较更大。
2.当初始化或者 dirty 被提升(需要赋值给read)后,会从 read 中复制全量的数据,若 read 中数据量大,则会影响性能。
sync.map删除效率高的原因:
先检查read是否存在元素,存在即标记为删除,不存在且dirty不为空即上互斥锁标记为删除(软删)

defer

go语言会将defer后面的语句进行延迟处理。编译时将defer后的语句追加到goroutine_defer链表最前面,运行goroutine_defer时从前到后依次执行。return后执行defer。
用途:
关闭文件句柄。
锁资源释放。
数据库连接释放。
经典例子

func test(){
		i := 0
		defer fmt.Println("defer:",i)
		i   ++
		return i
}
输出:defer:0 1
func test(){
		i := 0
	   defer func(){
			fmt.Println("defer:",i)	
		}()
		i   ++
		return i
}
输出:defer:1 1
func returnValues()int {
		var result int
		defer func() {
			result ++
			fmt.Println("defer:",result)
		}()
		return result
}
输出:defer:1 0
func reReturnValues() (result int) {
		defer func() {
			result ++
			fmt.Println("defer:",result)
		}()
		return result
}
输出:defer:1 1
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
sync.Once和init

init:文件包首次被加载的时候执行,且只执行一次。
sync.Once:需要的时候执行,且只执行一次。

反射

反射机制是指在程序运行时动态地捕获甚至改变变量的类型信息和值,但是在编译时并不知道这些变量的具体类型。
使用场景:
1.有时你需要编写一个函数,但是并不知道传给你的参数类型是什么,可能是没约定好;也可能是传入的类型很多,这些类型并不能统一表示。这时反射就会用的上了。
2.有时候需要根据某些条件决定调用哪个函数,比如根据用户的输入来决定。这时就需要对函数和函数的参数进行反射,在运行期间动态地执行函数。
IDE代码自动补全、json序列化、fmt函数、DeepEqual等

缺点
1.与反射相关的代码,经常是难以阅读的。在软件工程中,代码可读性也是一个非常重要的指标。
2.Go 语言作为一门静态语言,编码过程中,编译器能提前发现一些类型错误,但是对于反射代码是无能为力的。所以包含反射相关的代码,很可能会运行很久,才会出错,这时候经常是直接panic,可能会造成严重的后果。
3.反射对性能影响还是比较大的,比正常代码运行速度慢一到两个数量级。所以,对于一个项目中处于运行效率关键位置的代码,尽量避免使用反射特性。
interface: 存储实体的类型信息。
reflect: TypeOf()函数返回一个接口,这个接口定义了一系列方法,利用这些方法可以获取关于类型的所有信息; ValueOf()函数返回一个结构体变量,包含类型信息以及实际值。
sss

捕获异常
func f(){
	defer func(){
		if r:=recover();r!=nil{
			fmt.Println("recovered in f:",r)
		}
	}()
	//异常
	g(0)
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

注:defer recover机制只针对当前函数以及直接调用的函数可能产生的panic,无法处理调用产生的其他协程的panic,这一点和try catch机制不一样。

逃逸分析

局部变量被分配到堆上。
编译时分析变量是否被外部引用,被外部引用分配在堆上,否则分配在栈上。
为什么要有堆栈:堆上动态内存分配比栈上静态内存分配,开销大很多。
堆栈回收:堆上一般手动回收,一般语言有自动垃圾回收机制, 栈上由操作系统自动(分配)回收。

分布式和微服务的区别

微服务:将一个系统分别多个业务模块。
分布式:将一个系统分为多个业务模块,业务模块分别部署在不同的机器上,各个业务模块之间通过接口进行数据交互。

RabbitMQ和ActiveMQ

作用: 异步、解耦、销峰。
ActiveMQ:
基于JMS(Java Message Service),java语言。
提供P2P和Pub/Sub(发布/订阅)两种消息模型。
提供多种消息类型(TextMessage、MapMessage、BytesMessage、StreamMessage、ObjectMessage)。
跨平台性差。
RabbitMQ:
基于AMQP,erlang语言(并发强)。
提供direct exchange、fanout exchange、topic exchange、headers exchange、system exchange 五种消息模型。
只有byte[]消息类型。
AMQP具有跨平台、跨语言特性。

处理高并发

系统拆分:将一个系统拆分为多个子系统,dubbo。
缓存:读多写少情况。
MQ(消息队列):多写情况。
分库分表:一个数据库拆分为多个库,一个表拆分为多个表,每个表的数据量保持少一点,提高sql跑的性能。
读写分离:读多写少情况。主从架构,主库写入,从库读取。 SolrCloud(solr云):是Solr提供的分布式搜索方案,可以解决海量数据的分布式全文检索,因为搭建了集群,因此具备高可用的特性,同时对数据进行主从备份,避免了单点故障问题。可以做到数据的快速恢复。并且可以动态的添加新的节点,再对数据进行平衡,可以做到负载均衡。

服务器雪崩效应及解决方案

雪崩效应:由于网络或自身原因,服务不能保证100%可用,服务出现问题,调用这个服务的服务就会出现线程阻塞的情况,此时若有大量的请求涌入,就会出现多条线程阻塞等待,进而导致服务瘫痪。并且由于服务与服务之间的依赖,故障传播从而导致所有服务瘫痪。其实就是某个微小服务挂了,导致整一大片服务不可用,相当于生活中由于落下的最后一片雪花引发了雪崩情况。
容错方案:
1.隔离机制:线程多分。
2.超时机制:在上游服务调用下游服务的时候,设置一个最大响应时间,超过这个时间,下游未作出反应,便断开请求,释放掉线程。
限流机制:限制系统的输入和输出流量以达到保护系统的目的。一旦达到限制的阈值需要限制流量并采取少量措施以完成限制流量的目的。
熔断机制:互联网系统中,当下游服务因访问压力过大而响应变慢或失败,上游服务为了保护系统整体的可用性,可以暂时切断对下游服务的调用,牺牲局部,保全整体。(熔断状态:关闭状态、开启状态、半熔断状态(尝试恢复服务调用))
降级机制:为服务提供一个兜底方案,一旦服务无法正常调用,就使用兜底方案。

Beego和Gin区别

MVC:Beego支持完整的MVC;Gin不支持完整的MVC,需要开发者自己实现MVC。
路由和Session:Beego支持正则路由,Gin不支持正则路由;Beego支持Session,Gin不支持,需要安装另外的包。
适用场景:Beego适用快速开发或者业务相对复杂的项目;Gin适用性能要求高或者业务相对简单的项目。

数据库

Etcd分布式锁原理

raft算法。

Etcd分布式锁实现原理

1.在etcd系统里创建一个key。
2.如果创建失败,key存在,则监听key的变化事件,直到该key被删除,回到1。
3.如果创建成功,则认为获得了锁。

Etcd工作原理

http server接受请求并转发给store进行处理,如果涉及节点修改,则交给raft进行状态变更、日志记录,然后同步给其他节点以确认提交,最后提交数据,并再次同步。其中etcd使用wal来进行持久化存储。

Redis持久化存储方式

rdb:(默认)定时快照存储。
aof:即时存储。

Redis队列

类型:list。
新增:lpush。
查询:blpop。

Memcached和Redis比较

memcached:数据结构单一,只能缓存数据不能持久化,适用多读少写。
redis:数据结构丰富,两个持久化方案,并可以数据恢复。

Leveldb

基于本地文件存储,数据存量是物理内存的3-5倍,将一部分分热区数据.log保存在内存,持久化数据.sst保存在磁盘上。

Mysql索引

采用b+tree,b+tree采用二分查找法。

SQL优化和索引优化

sql优化:
避免索引失效,全表扫描。
适当使用in和exists(in先执行子查询,exists先执行外层,所以in适合外表大而内表小的情况,exitst适合外表小而内表大的情况)。
避免返回不必要的列(*)和数据(用limit)。
尽量避免子查询,使用join 用小表驱动大表,减少Nested Loop的循环提升性能。
尽量用inner join,少用left join,inner join 会自动选择小表去驱动大表,left join 一般使用场景是大表驱动小表。join 连接最好不超过2个。
索引优化:
避免索引失效:
索引列上做操作,计算、函数、类型转换。
!= 或<>,会导致后面索引失效 。
null 。
like 通配符前缀%,会导致后面索引失效。
字符串不加单引号。

Mysql InnoDB和MyIsam存储引擎区别

innoDb:
支持外键 。
聚集索引,数据文件和索引绑在一起,必须要有主键,通过主键索引效率很高。
不保存表的具体行数,不保存表的具体行数,执行select count(8) from table时需要全表扫描 。
不支持全文索引。
支持事务 。
myIsam:
不支持外键 。
非聚集索引,数据文件和索引是分离的,索引保存数据文件的指针。
保存表的具体行数,用一个变量保存了整个表的行数,执行select count(*) from table语句时只需要读出该变量即可,速度很快 。
支持全文索引,查询效率高 。
不支持事务。

持续更新…

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/菜鸟追梦旅行/article/detail/445677
推荐阅读
相关标签
  

闽ICP备14008679号