Skip to content

面试八股-golang

golang

部分内容参考《go 程序员面试笔试宝典》

go 有什么特点

特性

  • 协程
  • 静态编译,编译出的可执行文件体积小
  • 有 runtime、垃圾回收、反射
  • 指针

和其他语言相比

  • go 作为静态编译语言,和 C/C++相比,go 有 runtime,有垃圾回收,有反射
  • go 作为有 runtime 的语言,和 java 相比,是 go 静态编译的
  • go 支持指针操作

引用类型

  • 说明,go 中实际上没有引用这个机制,go 只有值传参,没有所谓的引用传参,我们只是用“引用”来形容一种现象,即传参后,对参数的修改会影响到原变量
  • 6 种,slice、map、channel、指针、函数、interface
  • 引用类型的零值是 nil
  • 数组和结构体不是引用类型,它们的零值是其所有元素和成员的零值

切片(slice)

  • 切片和数组
  • 切片是对数组的引用,切片的底层数据结构是数组。
    • 切片的长度是切片引用部分的长度,切片的容量是从引用起点到底层数组末尾的长度
  • 切片能扩容,数组不能
  • 切片如何扩容
    • 追加元素时发现数组已满会引起扩容
    • 扩容是给切片重新分配一个更大的数据,并将数据迁移过去
    • 扩容后会预留一定的 buffer,以免每次追加元素都扩容
    • 扩容策略:

      当原 slice 容量小于 1024 的时候,新 slice 容量变成原来的 2 倍;原 slice 容量超过 1024,新 slice 容量变成原来的 1.25 倍以上(有一个计算公式)。

  • 切片实际上上是一个结构体,因此,把切片作为参数传递的时候,如果修改它的底层数组,会影响原切片,但是如果切片发生了扩容,是直接把底层数组给换了,是不会影响到原切片的
  • 切片不能比较

map

  • map 底层是一个 hash 表,链式 hash,每个 hash 位置指向一个链表,每个链表节点长度为 8(能放 8 个 key-value 对)
  • map 实际上是一个指向底层哈希表的指针,因此把 map 作为参数传递后,对参数做的修改都会影响原 map
  • map 遍历是无序的,每次遍历的顺序不一样(因为对 hash 位置的遍历是随机的)
  • map 扩容
  • 扩容不是一个立即完成的操作,而是会持续一段时间,每次最多搬运两个 hash 链
  • 两种扩容场景
    1. 所有的节点都快满了,这时要双倍扩容(扩容后的 hash 空间是原来的两倍),并且要重新计算元素的 hash 值(rehash)
    2. 元素总数很少,节点数量过多,map 退化为链表,这时要等量扩容,扩容后 hash 空间数量和原来相等,只是把数据存储变紧凑了。
  • 不要用 float 做 key(语法上是可以的,因为 float 可比较),浮点数的精度问题会导致很多诡异的情况。
  • map 不是并发安全的,sync.map 是并发安全的
  • 无法对 map 的 key 或 value 进行取址
  • 不能直接比较两个 map,只能通过遍历比较

接口

  • 接口的作用
  • 用于实现继承和多态
  • T*T是两个类型,用T实现了一个接口,并不能认为*T也实现了
  • 结论,实现了接收者是值类型的方法,相当于自动实现了接收者是指针类型的方法;而实现了接收者是指针类型的方法,不会自动生成对应接收者是值类型的方法
  • 换句话说,值类型实现了接口 A,相当于指针类型也实现了接口 A;指针类型实现了接口 A,不会使值类型也实现了接口 A
  • 类型断言的作用
  • 提取动态值
  • 转换接口类型
  • 判断动态值是否能转换为某个类型(利用类型断言返回的 bool)
  • 放在switch语句中简化判断结构

go runtime

  • go runtime 位于应用程序和操作系统之间
  • 负责应用程序的内存分配、垃圾回收、协程调度、channel 通信
  • 负责创建 os 线程、调用 system call

go 的协程(goroutine)

  • 协程是什么
  • 和线程、进程一样是一种并发单位
  • 和系统线程相比
  • 更细粒度,线程的栈空间是固定的,而且比较大(linux 下为 8MB),而 goroutine 初始栈大小为 2KB,按需扩缩容,无上限
  • 上下文切换开销小,线程运行在内核态,而协程运行在内核态
  • go 协程没有供程序员访问的标识,而线程都有线程号,go 不鼓励类似 threadlocal 这样的东西
  • goroutine 是如何调度的
  • goroutine 由 go runtime 的 scheduler 调度
  • goroutine 运行在系统线程上,是一种 m:n 模型
  • 线程数通过 GOMAXPROCS 变量调整,默认的系统线程数是物理机的 cpu 核数

goroutine 调度

  • go 调度器的最终目的是把 g 尽量均匀的分配到 m 上执行,兼顾公平和效率
  • gmp 模型,go 使用三种基础的结构体实现 goroutine 的调度。
  • g 代表一个 goroutine,包含:goroutine 栈、状态、指令地址(PC 寄存器的值)
  • m 代表一个内核线程,包含正在运行的 goroutine
  • p 代表一个虚拟的 processer,维护一个 runnable 状态的 g 队列,m 需要获得 p 才能运行 g
  • 存放 g 的队列有两类,GRQ(全局可运行队列)和 LRQ(本地可运行队列),每个 p 都要关联一个 LRQ。
  • go 设计的早期没有 p 和 LRQ,只有 g、m 和 GRQ,每次调度执行一个 g 都要对 GRQ 上锁,效率低下。p 和 LRQ 的设计是为了提高调度的效率。
  • 工作窃取,如果一个 p 的 LRQ 中已经没有 g,GPQ 中也没有 g,p 会从别的 p 中“偷”一些 g 过来执行
  • os 的线程调度是抢占式调度,而 goroutine 的调度是协作式调度(无法硬件中断),但调度是由 runtime 负责的,对用户来说,仍可将 goroutine 的调度视为抢占式调度

    协作式调度依靠被调度方主动放弃执行;抢占式调度则依靠调度器强制中断被调度方的执行

  • g 的三种状态,和线程类似
  • waiting,等待状态(有时候被称为阻塞状态),由等待网络数据、硬盘 io、等待获取锁、系统调用、sleep
  • runnable,可执行状态(就绪状态)
  • running,运行状态
  • m 只有两种状态,自旋状态(工作状态)和非自旋状态(休眠状态),m 会因为找不到工作、gc 等原因进入非自旋状态,其他时间都处于自旋状态。
  • main goroutine 退出时会调用 exit(0)退出进程

channel

  • channel 解决什么问题
  • channel 用来在两个 goroutine 之间传递数据,类似一个FIFO队列
  • 解决协程之间的并发同步问题
  • 原则,不通过共享内存来通信,而是通过通信共享内存
  • 是一种比锁层次更高的同步工具
  • 从一个关闭且缓冲区数据读完的 channel 中仍能取到数据,只不过是零值
  • 操作 channel 的情况总结 |操作| nil channel |closed channel |not nil, not closed channel| |---|---|---|---| |close |panic |panic |正常关闭 |读 <- ch| 阻塞| 缓冲区如果还有数据,会正常读取数据,否则读到对应类型的零值| 阻塞或正常读取数据。|缓冲型 channel 为空或非缓冲型 channel 没有等待发送者时会阻塞| |写 ch <-| 阻塞| panic| 阻塞或正常写入数据。非缓冲型 channel 没有等待接收者或缓冲型 channel buf 满时会被阻塞
  • 使用时要注意防止资源泄露,比如有若干 goroutine 处于阻塞状态,而 channel 的状态一直没有得到改变
  • channel 的应用
  • 解耦生产者和消费者
  • 广播停止信号
  • 定时
  • 控制并发数,把令牌放入缓冲区
  • channel的底层实现是一个循环链表,记录了两个指针,读指针和写指针,利用一个mutex保证并发安全,保存两个协程队列,一个记录接收协程队列,一个记录发送协程队列
  • 如果有协程需要读写channel时,先加锁,完成数据copy,然后解锁
  • 如果buffer满了,有goroutine需要写怎么办,channel会调用调度器,让出goroutine(G1)在p上的执行权,并且将G1的指针和数据挂到发送队列上去,一旦有goroutine读数据,channel就会将等待队列的G1拿出,将其数据copy到buffer中,并通知调度器唤醒G1,然后调度器就把G1放到runnable队列中去

context

  • context 是什么
  • context 一般翻译成上下文,用来在 goroutine 之间传递上下文信息,包括停止信号、超时时间、截止时间、key-value 数据
  • context 几乎是并发控制和超时控制的标准做法,很多接口都加上了 context 参数
  • context 是一个接口,标准库实现了一些具体的 context 类型,包括 emptyCtx、valueCtx、cancelCtx、timerCtx,还有两个全局变量 background 和 todo
  • 所有 context 会组成一个树状结构(只不过指向和树是相反的,子 context 指向父 context)
  • 如何取消 context
  • 父 context 持有一个 channel,可以对所有子 context 广播取消信号,一般会让每个协程携带一个 context,这样就可以把协程也组织成树状结构,父协程可以在请求超时、请求失败时及时向子协程发出终止信号,子协程通过调用 Done()方法获取 channel,并监听 channel 的取消信号
  • 如何查找 value
  • 从自己开始,顺着父 context,层层向上找

reflect

  • 什么是反射
  • 反射是一种程序在运行过程中观察自己、修改自己的一种机制,而且这种观察和修改和编译期间做不到的
  • 什么时候用反射
  • 不知道参数类型的时候
  • 慎用反射的理由
  • 反射会让代码可读性降低
  • 反射性能差

指针

  • 普通指针(*T),不能参与运算,不同类型不能比较,不能相互转换
  • unsafe.Pointer 相当于一个通用指针,能在不同类型之间转换。
  • uintptr 可以对指针进行算术运算,但是不会对指向的变量有引用语义,换言之,即使 uintptr 还存在,它指向的变量还是有可能被 gc(他就像 c 里面的指针)

go 堆和栈

go 对程序员隐藏了堆和栈的概念,变量分配在栈上还是堆上不取决于变量定义的位置和方法。编译器会对程序进行逃逸分析,如果一个变量在函数之外被引用了,那这个变量会被创建在堆上,否则在栈上

垃圾回收(GC)

  • gc是回收堆内存的,而不是栈内存,栈内存在函数调用结束后就释放掉了不用专门回收
  • 垃圾回收机制有两种,追踪式 GC 和引用计数式 GC,go 是追踪式 GC。
  • 引用计数式GC,C++的shared_ptr就是一个典型的引用计数式资源回收
    • 好处是不需要额外的gc任务,并且程序不用暂停
    • 坏处是会出现循环引用,而且对象增加和减少引用时会增加大量计算
  • 追踪式GC,是判断一个对象是否可达,如何判断一个对象是否可达,第一步,找出所有全局变量和函数栈中的变量,标记为可达,然后通过回溯的方式把所有可达的对象标记出来,那剩下的就是不可达的
  • go 的 GC 算法是三色标记法
  • 三色标记法是一种追踪式gc,它主要是为了解决gc会让整个程序停止的问题。其原则是它把堆中的对象根据它们的颜色分到不同集合里面。
    • 黑白灰三色对象,初始全部都为白色对象(表示未检查过,是潜在垃圾)
    • 每检查到一个白色对象,将其变为灰色,表示已经检查了这个对象,但是没有检查它的子对象
    • 如果一个对象的所有直接子对象都被检查到了,就把它变成黑色
    • 直到所有的灰色对象被清空,这时剩下的白色对象就可以被回收了
  • 三色标记的特点是可以并发执行,依靠的是go的写屏障计数,这像是一个钩子方法,能够在创建对象的同时将其标记为灰色(保证不会让一个黑色对象直接指向一个白色对象)
  • 内存泄漏有两种
  • 对不再使用的内存一直有引用
  • goroutine 泄露(goroutine 一直阻塞在一个 channel 上)

go 工具

  • GOROOT 和 GOPATH 的区别
  • GOROOT 是 go 安装的位置
  • GOPATH 的作用是提供一个可以寻找 go 源码的路径,GOPATH 目录下需要包含 src、pkg、bin 三个目录,src 存放源文件,pkg 存放编译产出(库文件),bin 存放可执行文件

gin框架

  • 主要概念
  • engine、handelr、router、conetxt、bind
  • engine,是一个gin框架的全局实例,RouterGroup(定义了一个路由组,保存了该路由组的中间件和请求路径)、Trees(包含所有路由前缀树,查找路由,根据请求方法对树分类。并保存了每个路由对应的handler)
  • context -是handler会传入一个gin context指针,一般命名为c
  • gin context实现了go的Context接口的四个函数,Done、Value、Deadline、Err,所以gin context也是一个context
  • gin context定义了一个key map,调用Value时首先去这个key map里面找,还带了一个读写锁用来保护这个key map,可以并发读写key map(go原生的valuectx不能并发,只能被一个goroutine操作,除非你自己给它上锁)
  • gin context定义了一个copy函数,启动新goroutine的时候可以拷贝
  • request body和一个struct绑定以后会被清空,不能再次使用
  • 多次绑定可以使用ShouldBindBodyWith
  • http server
  • 不要直接调用r.Run(),而是把r作为handler传入一个http.Server变量,调用http.Server的ListenAndServe()启动服务
  • 如果想自定义端口、超时等配置,给http.Server的对应成员赋值即可
  • 利用goroutine、信号、和http.Server的Shutdown()函数实现优雅退出
  • gin handler(gin 中间件)
  • 一个gin handler就是一个处理函数,gin在请求处理的某个阶段调用这些handler,handler相当于是请求处理的钩子函数
  • handler负责打日志、鉴权、异常恢复等工作
  • (请求处理函数本身其实也是个handler)
  • handler分局部和全局的,局部handler对一个路由生效,全局handler对所有路由生效
  • handler可以在请求处理函数之前执行,也可以在请求处理函数之后执行
  • 在handler中调用next方法的作用是,调用next之前的代码在请求处理之前执行(前置),调用next之后的代码在请求处理之后执行(后置)。(这个不难理解,next的作用是执行其他handler,如果其中一个handler中没有调用next,那它整个是前置的,如果其中一个handler也调了next,就继续递归处理其他handler,最后一个handler是请求处理函数,请求处理函数执行完毕后,递归调用返回时,触发所有handler的后置部分)