darope

目录

基础面试题

1 GO

1.1 如何防止goroutin泄露

其实无论是死循环、channel 阻塞、锁等待,只要是会造成阻塞的写法都可能产生泄露。因而,如何防止 goroutine 泄露就变成了如何防止发生阻塞。为进一步防止泄露,有些实现中会加入超时处理,主动释放处理时间太长的 goroutine。

1.2 Go语言的锁

Go语言存在两种锁,排他锁和共享锁。

var (
    lock sync.Mutex // 排他锁又叫互斥锁
    rwlock sync.RWMutex // 读写锁又叫共享锁
    )

1.3 Go的IO

1、常规读写

  • os.Mkdir(name string, perm FileMode) error // 仅创建一层
  • os.MkdirAll(path string, perm FileMode) error // 创建多层
  • os.Create(name string) (file *File, err error) // 存在则覆盖
  • os.Open(name string) (file *File, err error) // 只读方式打开文件
  • os.OpenFile(name string, flag int, perm FileMode) (file *File, err error) // parm控制权限,例如0066、0777
  • file.Close() error // 关闭文件,断开程序与文件的连接
  • os.Remove(name string) error // 删除文件一层
  • os.RemoveAll(path string) error // 级联删除

2、带缓冲读写bufio

io.Reader和io.Writer

type Reader interface {
    Read(p []byte) (n int, err error)
}

type Writer interface {
    Write(p []byte) (n int, err error)
}

1.4 new和make的区别

new(T) 和 make(T,args) 是 Go 语言内建函数,用来分配内存,但适用的类型不同。

new(T) 会为 T 类型的新值分配已置零的内存空间,并返回地址(指针),即类型为 *T的值。换句话说就是,返回一个指针,该指针指向新分配的、类型为 T 的零值。适用于值类型,如数组、结构体等。

make(T,args) 返回初始化之后的 T 类型的值,这个值并不是 T 类型的零值,也不是指针 *T,是经过初始化之后的 T 的引用。make() 只适用于 slice、map 和 channel.

1.5 go中init函数

一个包中,可以包含多个 init 函数;

程序编译时,先执行依赖包的 init 函数,再执行 main 包内的 init 函数;可以用_来忽略导入包,但是执行该包的init函数

main包也可以包含不止一个Init函数,且init函数不能被其他函数显示调用,否则编译错误

1.6 golang中引用类型和值类型及内存分配

1.值类型:变量直接存储值,内存通常在栈中分配。

值类型:基本数据类型int、float、bool、string以及数组和struct

2.引用类型:变量存储的是一个地址,这个地址存储最终的值。内存通常在 堆上分配。通过GC回收。

引用类型:指针、slice、map、chan等都是引用类型。

1.7 接受者和方法参数何时使用指针类型,何时使用值类型,区别?

方法接受推荐使用指针类型。

  • 推荐在实例方法上使用指针(前提是这个类型不是一个自定义的 map、slice 等引用类型)
  • 当结构体较大的时候使用指针会更高效。一般结构体超过五个字段,用指针
  • 如果要修改结构内部的数据或状态必须使用指针
  • 当结构类型包含 sync.Mutex 或者同步这种字段时,必须使用指针以避免成员拷贝
  • 如果你不知道该不该使用指针,使用指针!

方法参数该使用什么类型?

  • map、slice 等类型不需要使用指针(自带 buf)
  • 指针可以避免内存拷贝,结构大的时候不要使用值类型
  • 值类型和指针类型在方法内部都会产生一份拷贝,指向不同
  • 小数据类型如 bool、int 等没必要使用指针传递
  • 初始化一个新类型时(像 NewEngine() *Engine)使用指针
  • 变量的生命周期越长则使用指针,否则使用值类型

1.8 值接受者和指针接受者互相调用问题

1、值类型可以调用值接受者方法,也能调用指针接收者方法,当值类型调用指针接受者方法是,编译器底层会先取地址&,再调用

2、指针类型也已调用指针接受者方法,也可以调用值接收方法,当指针类型调用值接受者方法是,编译器底层会先解引用*,再调用

特殊情况需要注意:

1、当值类型不能被寻址的时候,该值不能调用对应的指针接受者方法

2、当指针接受者方法是为了实现某个接口的时候,那么只有指针类型的实体实现了接口。用值类型,不算实现接口。如果是值接收者,实体类型的值和指针都可以实现对应的接口;

1.9 Go的调度

G: 协程,对应的数据结构为runtime.G。全局变量allgs记录着所有的g

P: 数据结构为runtime.P。拥有本地runq变量,和全局变量sched,sched对应的结构是runtime.schedt代表调度器,P会和一个M绑定,M可以直接从P这里获取待执行的G

M: 工作线程,对应的数据结构为runtime.M。全局变量allm记录着所有的m

如果P的本地队列已满,那么等待执行的G就会被放到全局队列中,M会先从关联P所持有的本地runq中获取待执行的G,如果没有的话再去全局队列中领取一些G来执行,如果全局队列中也没有多余的G,那就去别的P那里领取一些G。

1.10 go中的slice扩容规则

规则1:需要增长到的容量cap是原始容量的两倍还多,则扩容到cap

规则2.1:需要增长的容量小于原容量2倍,但是原用量小于1024,则2倍扩容

规则2.2:需要增长的容量小于原容量2倍,但是原容量大于1024,则1.25倍扩容

扩容后需要分配的内存,并不是扩容后的容量乘以数据类型字节。而是根据扩容后的容量去内存管理模块申请最匹配且覆盖的容量,根据这个容量乘以数据类型,才是最终需要分配的内存空间

1.11 go中的map扩容规则

  • go语言map的默认负载因子是6.5

规则1:count / (2 ^ B) > 6.5 时,翻倍扩容;

规则2:负载因子没超标,但是溢出桶使用较多,触发等量扩容。所谓等量扩容,就是创建和旧桶数目一样多的新桶,把旧桶中的值迁移到新桶中。

注意:等量扩容有什么用,如果负载因子没超,但是用了很多的溢出桶。那么只能说明存在很多的删除的键值对。扩容后更加紧凑,减少了溢出桶的使用

超过负载因子,翻倍扩容;溢出桶较多,等量扩容

1.12 Go的反射包怎么找到对应的方法

反射主要两个函数:

func TypeOf(i interface{}) Type  // 用来提取一个接口中值的类型信息。
func ValueOf(i interface{}) Value

返回的Type是接口类型,Type类型包含很多个方法,可以调用:

type Type interface {
    // 所有的类型都可以调用下面这些函数。下面简单列举,函数比较多

    // 此类型的变量对齐后所占用的字节数
    Align() int

    // 如果是 struct 的字段,对齐后占用的字节数
    FieldAlign() int

    // 返回类型方法集里的第 `i` (传入的参数)个方法
    Method(int) Method

    // 通过名称获取方法
    MethodByName(string) (Method, bool)

    // 获取类型方法集里导出的方法个数
    NumMethod() int

    // 类型名称
    Name() string

    // 返回类型所在的路径,如:encoding/base64
    PkgPath() string

    // 返回类型的大小,和 unsafe.Sizeof 功能类似
    Size() uintptr

    // 返回类型的字符串表示形式
    String() string

    // 返回类型的类型值
    Kind() Kind

    // 类型是否实现了接口 u
    Implements(u Type) bool

    // 是否可以赋值给 u
    AssignableTo(u Type) bool

    // 是否可以类型转换成 u
    ConvertibleTo(u Type) bool

    // 类型是否可以比较
    Comparable() bool
    
    // ......

}

Value是结构体,它包含类型结构体指针、真实数据的地址、标志位。Value 结构体定义了很多方法,通过这些方法可以直接操作 Value 字段 ptr 所指向的实际数据:

// 设置切片的 len 字段,如果类型不是切片,就会panic
 func (v Value) SetLen(n int)

 // 设置切片的 cap 字段
 func (v Value) SetCap(n int)

 // 设置字典的 kv
 func (v Value) SetMapIndex(key, val Value)

 // 返回切片、字符串、数组的索引 i 处的值
 func (v Value) Index(i int) Value

 // 根据名称获取结构体的内部字段值
 func (v Value) FieldByName(name string) Value

 // ……

Value 字段还有很多其他的方法。例如:

// 用来获取 int 类型的值
func (v Value) Int() int64

// 用来获取结构体字段(成员)数量
func (v Value) NumField() int

// 尝试向通道发送数据(不会阻塞)
func (v Value) TrySend(x reflect.Value) bool

// 通过参数列表 in 调用 v 值所代表的函数(或方法
func (v Value) Call(in []Value) (r []Value) 

// 调用变参长度可变的函数
func (v Value) CallSlice(in []Value) []Value 

// 等等....

1.13 Go的channel(有缓冲和无缓冲)

1、ch := make(chan int) 无缓冲的channel由于没有缓冲发送和接收需要同步.

2、ch := make(chan int, 2) 有缓冲channel不要求发送和接收操作同步.

3、channel无缓冲时,发送阻塞直到数据被接收,接收阻塞直到读到数据。

4、channel有缓冲时,当缓冲满时发送阻塞,当缓冲空时接收阻塞

1.14 Gin的context和Go的context

Context是由Golang官方开发的并发控制包,一方面可以用于当请求超时或者取消时候,相关的goroutine马上退出释放资源,另一方面Context本身含义就是上下文,其可以在多个goroutine或者多个处理函数之间传递共享的信息。

创建一个新的context,必须基于一个父context,新的context又可以作为其他context的父context。所有context在一起构造一个context树。

1、Context用途:

  • Context一大用处就是超时控制。
  • Context另外一个用途就是传递上下文信息。

2、Context接口一共包含四个方法:

  • Deadline:返回绑定该context任务的执行超时时间,若未设置,则ok等于false
  • Done:返回一个只读通道,当绑定该context的任务执行完成并调用cancel方法或者任务执行超时时候,该通道会被关闭
  • Err:返回一个错误,如果Done返回的通道未关闭则返回nil,如果context如果被取消,返回Canceled错误,如果超时则会返回DeadlineExceeded错误
  • Value:根据key返回,存储在context中k-v数据

3、使用Context注意事项:

  • 不要将Context作为结构体的一个字段存储,相反而应该显示传递Context给每一个需要它的函数,Context应该作为函数的第一个参数,并命名为ctx
  • 不要传递一个nil Context给一个函数,即使该函数能够接受它。如果你不确定使用哪一个Context,那你就传递context.TODO
  • context是并发安全的,相同的Context能够传递给运行在不同goroutine的函数

Gin的Context是Go中Context接口的一个实现。

1.15 go的垃圾回收

常用垃圾回收原则有引用计数和标记清扫。Go采用的是标记清扫。

Go语言中的垃圾回收采用标记清扫算法,支持主体并发增量式回收。使用插入和删除两种写屏障的混合写屏障,并发是用户程序和垃圾回收可以并发执行,增量式回收保证一次垃圾回收的STW分摊到多次。

标记清扫-三色标记:

要识别存活对象,可以把栈,数据段上的数据对象作为根root,基于他们进一步追踪,把能追踪到的数据都进行标记。剩下的追踪不到的就是垃圾了。

  • 垃圾回收开始时,所有数据都为白色,然后把直接追踪到的root节点标记为灰色,灰色代表基于当前节点展开的追踪还未完成。
  • 当基于某个root节点的追踪任务完成后,便会把该root节点标记为黑色,黑色表示它是存活数据,而且无需基于它再次追踪了。
  • 基于黑色节点找到的所有节点都被标记为灰色,表示还要基于它们进一步展开追踪
    。当没有灰色节点时,意味着标记工作可以结束了。此时有用数据都为黑色,无用数据都为白色,接下来回收这些白色对象的内存地址即可

1.16 golang中开协程的限制,底层调度?8C8G的服务器最多开多少协程?

1、计算机资源肯定是有限的,所以goroutine肯定也是有限制的,单纯的goroutine,一开始每个占用4K内存,所以这里会受到内存使用量的限制,还有goroutine是通过系统线程来执行的,golang默认最大的线程数是10000个。可以通过https://gowalker.org/runtime/debug#SetMaxThreads来修改。

2、要注意线程和goroutine不是一一对应关系,理论上内存足够大,而且goroutine不是计算密集型的话,可以开启无限个goroutine。

2 Redis

2.1 redis中缓存穿透和缓存雪崩的概念?

缓存穿透:就是查询一个数据库不存在的key,先查缓存,缓存没有再查数据库,数据库也没有也就不会缓存。那么不停的查这个不存在的key就属于恶意攻击,存在缓存击穿的概念

缓存雪崩:就是一批商品的缓存时间是一样的,如果遇到双十一,一批商品同时缓存失效,查询都会落到DB的头像。所以设置缓存尽量分散缓存过期时间,热门类目的商品缓存时间长一些,冷门类目的商品缓存时间短一些,也能节省缓存服务的资源

缓存击穿:是指一个key非常热点,在不停的扛着大并发,大并发集中对这一个点进行访问,当这个key在失效的瞬间,持续的大并发就穿破缓存,直接请求数据库,就像在一个屏障上凿开了一个洞。

2.2 redis持久化?两种持久化的方式?

RDB(Redis DataBase)和AOF(Append Only File)。

RDB其实就是把数据以快照的形式保存在磁盘上。什么是快照呢,你可以理解成把当前时刻的数据拍成一张照片保存下来。

全量备份总是耗时的,有时候我们提供一种更加高效的方式AOF,工作机制很简单,redis会将每一个收到的写命令都通过write函数追加到文件中。通俗的理解就是日志记录。

3 Mysql

3.1 Mysql事务ACID特性

  • 原子性(Atomicity):事务开始后所有操作,要么全部做完,要么全部不做,不可能停滞在中间环节。事务执行过程中出错,会回滚到事务开始前的状态,所有的操作就像没有发生一样。也就是说事务是一个不可分割的整体,就像化学中学过的原子,是物质构成的基本单位。
  • 一致性(Consistency):事务开始前和结束后,数据库的完整性约束没有被破坏 。比如A向B转账,不可能A扣了钱,B却没收到。
  • 隔离性(Isolation):同一时间,只允许一个事务请求同一数据,不同的事务之间彼此没有任何干扰。比如A正在从一张银行卡中取钱,在A取钱的过程结束前,B不能向这张卡转账。
  • 持久性(Durability):事务完成后,事务对数据库的所有更新将被保存到数据库,不能回滚。

3.2 事务的隔离级别

  • 脏读:读取未提交数据、常发生在转账与取款操作中
  • 不可重复读:前后多次读取,数据内容不一致。
  • 幻读:前后多次读取,数据总量不一致

读未提交:在这种隔离级别下,所有事务能够读取其他事务未提交的数据。读取其他事务未提交的数据,会造成脏读。因此在该种隔离级别下,不能解决脏读、不可重复读和幻读。

读已提交:在这种隔离级别下,所有事务只能读取其他事务已经提交的内容。能够彻底解决脏读的现象。但在这种隔离级别下,会出现一个事务的前后多次的查询中却返回了不同内容的数据的现象,也就是出现了不可重复读。这是大多数数据库系统默认的隔离级别,例如Oracle和SQL Server,但mysql不是。

可重复读:在这种隔离级别下,所有事务前后多次的读取到的数据内容是不变的。也就是某个事务在执行的过程中,不允许其他事务进行update操作,但允许其他事务进行add操作,造成某个事务前后多次读取到的数据总量不一致的现象,从而产生幻读。mysql的默认事务隔离级别

可串行化:在这种隔离级别下,所有的事务顺序执行,所以他们之间不存在冲突,从而能有效地解决脏读、不可重复读和幻读的现象。但是安全和效率不能兼得,这样事务隔离级别,会导致大量的操作超时和锁竞争,从而大大降低数据库的性能,一般不使用这样事务隔离级别。

隔离级别 脏读 不可重复读 幻读
read uncommitted(未提交读) T T T
read committed(提交读) F T T
repeatable read(可重复读) F F T
serializable (可串行化) F F F

3.3 InnoDb是表锁还是行锁,为什么

3.4 Mysql索引的底层数据结构是什么?为什么要使用B+树?B+树为什么要比B树稳定?

Mysql底层数据结构是B+树,B+树的查找效率更高,B+树中间层级的节点不保存数据,只保存索引,所以整体层数回更少,范围查询上B+树优势更大,原因是B+树数据节点都在最下层,且节点与节点之间有引用指向

  • 单一节点存储更多的元素,使得查询的IO次数更少;
  • 所有查询都要查找到叶子节点,查询性能稳定;
  • 所有叶子节点形成有序链表,便于范围查询。

3.5 Mysql的执行计划?执行计划中,哪些语句可以看出来该语句走了全表扫描?

其中最重要的字段为:id、type、key、rows、Extra

  • id : id相同:执行顺序由上至下 ;id不同:如果是子查询,id的序号会递增,id值越大优先级越高,越先被执行
  • select_type:查询的类型,主要是用于区分普通查询、联合查询、子查询等复杂的查询
  • type:访问类型,sql查询优化中一个很重要的指标,结果值从好到坏依次是:system > const > eq_ref > ref > range > index > ALL;一般来说,好的sql查询至少达到range级别,最好能达到ref

4 网络

4.1 time-wait和close-wait

4.2 TCP三次握手

4.3TCP的拥塞控制?拥塞控制做什么用的,通过哪种方式控制网络的传输速度?

1、慢启动阶段思路是不要一开始就发送大量的数据,先探测一下网络的拥塞程度,也就是说由小到大逐渐增加拥塞窗口的大小,在没有出现丢包时每收到一个 ACK 就将拥塞窗口大小加一(单位是 MSS,最大单个报文段长度),每轮次发送窗口增加一倍,呈指数增长,若出现丢包,则将拥塞窗口减半,进入拥塞避免阶段;

2、当窗口达到慢启动阈值或出现丢包时,进入拥塞避免阶段,窗口每轮次加一,呈线性增长;当收到对一个报文的三个重复的 ACK 时,认为这个报文的下一个报文丢失了,进入快重传阶段,要求接收方在收到一个失序的报文段后就立即发出重复确认(为的是使发送方及早知道有报文段没有到达对方,可提高网络吞吐量约20%)而不要等到自己发送数据时捎带确认;

5 Linux

5.1 孤儿进程,僵尸进程

孤儿进程:一个父进程退出,而它的一个或多个子进程还在运行,那么那些子进程将成为孤儿进程。孤儿进程将被init进程(进程号为1)所收养,并由init进程对它们完成状态收集工作。

僵尸进程:一个进程使用fork创建子进程,如果子进程退出,而父进程并没有调用wait或waitpid获取子进程的状态信息,那么子进程的进程控制块(PCB)仍然保存在系统中。这种进程称之为僵死进程

危害:

僵尸进程危害,大量的产生僵死进程,将因为没有可用的进程号而导致系统不能产生新的进程. 此即为僵尸进程的危害,应当避免。

孤儿进程是没有父进程的进程,孤儿进程这个重任就落到了init进程身上,除了init进程会忙一些,并没有什么危害

5.2 死锁条件,如何避免

死锁产生的四个必要条件:

1.互斥性:线程对资源的占有是排他性的,一个资源只能被一个线程占有,直到释放。

2.请求和保持条件:一个线程对请求被占有资源发生阻塞时,对已经获得的资源不释放。

3.不剥夺:一个线程在释放资源之前,其他的线程无法剥夺占用。

4.循环等待:发生死锁时,线程进入死循环,永久阻塞。

避免死锁的方法为破坏后三个必要条件:

1.破坏“请求和保持”条件:想办法,让进程不要那么贪心,自己已经有了资源就不要去竞争那些不可抢占的资源。比如,让进程在申请资源时,一次性申请所有需要用到的资源,不要一次一次来申请,当申请的资源有一些没空,那就让线程等待。不过这个方法比较浪费资源,进程可能经常处于饥饿状态。还有一种方法是,要求进程在申请资源前,要释放自己拥有的资源。

2.破坏“不可抢占”条件:允许进程进行抢占,方法一:如果去抢资源,被拒绝,就释放自己的资源。方法二:操作系统允许抢,只要你优先级大,可以抢到。

3.破坏“循环等待”条件:将系统中的所有资源统一编号,进程可在任何时刻提出资源申请,但所有申请必须按照资源的编号顺序(升序)提出

6 并发

6.1 三个协程ABC同时启动,不停循环打印ABC。如何实现?

  • 方法1:协程通知与切换方式
func main() {
	chA, chB, chC := make(chan string), make(chan string), make(chan string)

	go func() {
		for i := 1; ; {
			select {
			case <-chA:
				fmt.Println("[A]:", "A")
				chB <- "B"
				i += 3
			}
		}
	}()

	go func() {
		for i := 2; ; {
			select {
			case <-chB:
				fmt.Println("[B]:", "B")
				chC <- "C"
				i += 3
			}
		}
	}()

	go func() {
		for i := 3; ; {
			if i == 3 {
				chA <- "A"
			}
			select {
			case <-chC:
				fmt.Println("[C]:", "C")
				chA <- "A"
				i += 3
			}
		}
	}()

	for !false {
		fmt.Print()
	}
}
  • 方法2:协程写入,主协程打印的方式

7 算法

7.1 LRU算法

LRU 是 Least Recently Used 的缩写,这种算法认为最近使用的数据是热门数据,下一次很大概率将会再次被使用。而最近很少被使用的数据,很大概率下一次不再用到。当缓存容量满的时候,优先淘汰最近很少使用的数据。

思路:

准备两张表,一个哈希表,一个双向链表。假设A,3存入表中,则map中key还是原始的key,key=A,value加工一下,包括A和3.
对于双向链表从尾部加,从头部出。如果需要将某个元素从拿出,则此时将需要拿出的元素拿出,放到链表的最后,此时,该元素优先级最高。

7.2 TopK问题。内存2G, 文件10G,按行读取文件,求任意两行互为逆序的字符串,保存起来

Hash大法好!

8 Web

8.1 Cookie和Session

  • Cookie

Cookie通过在客户端记录信息确定用户身份,Session通过在服务器端记录信息确定用户身份。

由于HTTP是一种无状态的协议,服务器单从网络连接上无从知道客户身份。怎么办呢?就给客户端们颁发一个通行证吧,每人一个,无论谁访问都必须携带自己通行证。这样服务器就能从通行证上确认客户身份了。这就是Cookie的工作原理。

Cookie具有不可跨域名性。根据Cookie规范,浏览器访问Google只会携带Google的Cookie,而不会携带Baidu的Cookie。Google也只能操作Google的Cookie,而不能操作Baidu的Cookie。

  • Session

Session是服务器端使用的一种记录客户端状态的机制,使用上比Cookie简单一些,相应的也增加了服务器的存储压力。

  • 对比

1、cookie数据存放在客户的浏览器上,session数据放在服务器上.

2、cookie不是很安全,别人可以分析存放在本地的COOKIE并进行COOKIE欺骗考虑到安全应当使用session。

4、session会在一定时间内保存在服务器上。当访问增多,会比较占用你服务器的性能考虑到减轻服务器性能方面,应当使用cookie。

8.2 http,https

https是基于安全套接字的http协议,也可以理解为是http+ssl/tls(数字证书)的组合

8.3 单点登录,tcp粘包

单点登录,门户鉴权后,分发token,用户请求在cookie中带上token信息,就可以访问门户下的所有子系统了。

tcp粘包:

发送端为了将多个发往接收端的包,更加高效的的发给接收端,于是采用了优化算法(Nagle算法),将多次间隔较小、数据量较小的数据,合并成一个数据量大的数据块,然后进行封包。那么这样一来,接收端就必须使用高效科学的拆包机制来分辨这些数据。

TCP粘包就是指发送方发送的若干包数据到达接收方时粘成了一包,从接收缓冲区来看,后一包数据的头紧接着前一包数据的尾,出现粘包的原因是多方面的,可能是来自发送方,也可能是来自接收方。

解决方法:

1、发送方:对于发送方造成的粘包问题,可以通过关闭Nagle算法来解决,使用TCP_NODELAY选项来关闭算法。

2、接收方:接收方没有办法来处理粘包现象,只能将问题交给应用层来处理。

3、应用层:

格式化数据:每条数据有固定的格式(开始符,结束符),这种方法简单易行,但是选择开始符和结束符时一定要确保每条数据的内部不包含开始符和结束符。

发送长度:发送每条数据时,将数据的长度一并发送,例如规定数据的前4位是数据的长度,应用层在处理时可以根据长度来判断每个分组的开始和结束位置。

相关文章: