本文目录如下:
- 四、面向对象
- 五、语言类库
- 六、并发编程(协程)
- 线程 和 进程 的区别?
- 协程 (goroutine) 和 线程的区别?
- 说说 Go语言 中的 协程 (goroutine)?
- 谈谈你对 CSP并发模型 的理解?
- 浅谈一下GMP (Go语言的调度模型)?
- Select 有什么作用?
- 浅谈一下 Select 机制?
- 说说 channel 特性?
- channel 使用场景?
- 缓冲 channel 和 无缓冲 channel 的区别?
- nil channel有什么用?
- channel 如何关闭?
- 什么情况会导致 Goroutine 发生阻塞?
- 如果 Goroutine 一直占用资源怎么办,GMP模型怎么处理这个问题?
- 如何优雅的关闭一个 Goroutine?
- 谈一下 Go语言中的 WaitGroup?
- Go语言中有哪几种锁?
- 说说go语言的 同步锁 (锁)?
- 谈谈 Go语言 中的 原子操作,和加锁实现有什么区别?
- 说说你对 Go语言 中 panic 和 recover 的理解?
- defer语句 的常见使用场景?
- defer、return 的执行顺序?
- defer 能否修改 return 的值?
- 七、IO
- 八、通信/网络
- 八、内存分配、垃圾回收
四、面向对象
如何理解面向对象编程?
- 1.万物皆对象,任何事物 在代码中都可以用 对象 表达出来。
- 2.对象包含自己的 属性 和 行为。编程的过程:就是创建对象的描述,设计对象的行为。
- 3.面向对象编程三大特性 – 封装、继承、多态。点击查看
Go语言里如何实现面向对象 (实现三大特性)?
Go语言 里可以使用
struct
和interface
实现 面向对象。
封装:
type Animal struct {
name string
}
func (p *Animal) SetName(name string) {
p.name = name
}
func (p *Animal) GetName() string {
return p.name
}
继承:
type Animal struct {
Name string
}
type Cat struct {
Animal
FeatureA string
}
type Dog struct {
Animal
FeatureB string
}
多态:
type AnimalSounder interface {
say()
}
func (c *Cat) say() {
fmt.Println("喵喵喵~")
}
func (c *Dog) say() {
fmt.Println("汪汪汪·~")
}
// 攻击函数,传入 Animal 对象
func attack(animalSounder AnimalSounder) {
animalSounder.say()
}
func main() {
c1 := Cat{}
d1 := Dog{}
attack(c1)
attack(d1)
}
简述一下Go语言里的 interface?
Interface
类型 是一种 类型,并且是 引用类型。- Interface类型 可以定义一组 方法,不需要具体实现。(interface 不能包含 变量)
- interface 的 重要作用 在于实现 多态。
简述一下 空接口 (null interface)?空接口的应用?
空接口 (null interface)
: 没有定义 任何方法的接口 就是 空接口。- 任何类型 都实现了 空接口, 所以 空接口变量 可以存储 任意类型的值!
空接口 的应用(
interface{}
):
- 1.空接口 类型可以作为 函数的参数 (可以接收 任意类型 的参数)。
- 2.空接口可以作为 map 的 value (可以存储任意值的 map) 。
Go语言中 2 个 interface 可以比较吗 ?
Go 语言 中,interface 可以使用
==
或!=
比较。2 个 interface 相等有以下 2 种情况:
- 两个 interface 均等于 nil(此时 V 和 T 都处于 unset 状态)
- 类型 T 相同,且对应的值 V 相等。
Go语言中 2 个 stuct 可以比较吗 ?
相同类型的 struct 可以比较,使用
reflect.DeepEqual
方法进行比较。。
说几个常用的设计模式?
简单说说 静态代理 和 动态代理 (简单说说 代理模式)?
Java代理模式: 静态代理-动态代理 【依赖 接口
实现】
代理模式的优点:(客户-微商代理[
代理类
]-商家[委托类
])
- 可以隐藏 委托类 的实现;
- 可以实现客户与委托类间的解耦,在不修改委托类代码的情况下能够做一些额外的处理。
静态代理
: 代理类 在程序运行前就已经存在。
- 缺点: 通过 静态代理 可以在每个方法中都添加 相应的逻辑,假如 Sell接口 中包含上百个方法, 时候使用 静态代理 就会编写许多 冗余代码。
动态代理
: 代理类 在程序运行时创建。
// 动态代理的关键: 位于代理类与委托类之间的中介类,这个中介类被要求实现 InvocationHandler接口
/**
当我们调用代理类对象的方法时,这个 "调用" 会转送到invoke方法中,
代理类对象作为 proxy参数 传入,参数 method 标识了我们具体调用的是代理类的哪个方法,args为这个 方法的参数。
*/
/**
这样一来,我们对代理类中的所有方法的调用都会变为对invoke的调用,
这样我们可以在 invoke方法 中添加统一的处理逻辑(也可以根据method参数对不同的代理类方法做不同的处理)。
*/
// 调用处理程序
public interface InvocationHandler {
Object invoke(Object proxy, Method method, Object
}
// 中介类
public class DynamicProxy implements InvocationHandler {
//obj为委托类对象;
private Object obj;
public DynamicProxy(Object obj) {
this.obj = obj;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("before");
Object result = method.invoke(obj, args);
System.out.println("after"); return result;
}
}
五、语言类库
Go语言中 Context 定义?
Context
的主要作用是在 Goroutine 之间传递 上下文 信息, 包括: 取消信号、超时时间、优雅退出Groutine等。- 当一个上下文 (Context) 被关闭时,它派生的所有 上下文 也被关闭 (调用的 Groutine 也被 自动关闭)。
简单说一下 Go语言 的 unsafe包?
unsafe包
提供了 非类型安全 的指针:unsafe.Pointer
。- 任何类型的 指针 和 unsafe.Pointer 可以 相互转换。
简单说一下 reflect.DeepEqual函数?
- 对于 array、slice、map、struct等类型,要比较两个值是否相等,使用
==
处理起来十分麻烦,在对效率没有太高要求时,可以使用reflect
包 中的DeepEqual
函数。
反射 的作用?
- 通过
TypeOf
函数 和ValueOf
函数 获得存储在 interface 中的 类型 和 值。- 将
ValueOf
函数 获取的信息通过Interface()
函数反向转变成 interface变量。
六、并发编程(协程)
线程 和 进程 的区别?
进程
是 程序运行 和 资源分配 的 基本单位。线程
是 进程 的一个实体,是 CPU调度 的基本单位,多个 线程 共享 进程 的资源。
协程 (goroutine) 和 线程的区别?
线程
是 CPU调度 的基本单位。协程
可以说是 轻量级线程,多个 协程 共享 线程 的资源, 上下文切换代价小,所以效率更高。
- 一个 操作系统线程 对应 用户态 多个 goroutine。
- Go程序 可以同时使用多个 OS线程。
- goroutine 和 OS线程 是 多对多 的关系,即 m:n。
说说 Go语言 中的 协程 (goroutine)?
- Go语言 中,在函数前添加
go关键字
即可实现 go的协程;- Go语言 中通过
channel
来进行 协程 间的通信;
谈谈你对 CSP并发模型 的理解?
CSP并发模型 的核心: 不要以 共享内存 的方式来 通信,而是要通过 通信 来 共享内存。
- 【什么是: 以 共享内存 的方式来 通信】: 构建一个 全局共享变量,通过 加锁机制 来保证 共享数据 在 并发环境 下的 线程安全,从而实现并发线程间的 通信。
- 【什么是: 通过 通信 来 共享内存】: 协程 之间通过 channel 进行通信, 协程只需要关注两件事情: 1.往channel中发送数据, 2.从channel中取出数据, 而不用关注另一个 并发实体,这使得并发实体间实现了 完全解耦,这两个并发原语之间没有 从属关系。
浅谈一下GMP (Go语言的调度模型)?
Goroutine(G)
调度器P
和 OS 调度器是通过M
结合起来的,每个 M 都代表了 1 个内核线程,OS 调度器负责把内核线程分配到 CPU 的核上执行。
G
---- goroutine: 即Go协程,每个go关键字都会创建一个协程。M
---- 内核级线程,所有的G都要放在M上才能运行。P
---- 处理器,调度 G 到 M 上,其维护了一个 队列,存储了所有需要它来调度的 G。
P
(调度器) 的 个数 默认等于 CPU核数,每个 M 必须持有一个 P 才可以执行 G,一般情况下 M 的个数会略大于 P 的个数。
Select 有什么作用?
Go语言 中的 select 的作用就是: 监听 针对 channel 的 读写操作。
浅谈一下 Select 机制?
- 每个 case 都必须是一个 通信。
- 如果 只有一个 通信可以进行,它就 执行。
- 如果有 多个 case 可以运行,则 随机执行 一个。否则执行 default 语句。
- 如果没有 default 语句,select将阻塞,直到某个 通信 可以运行 (也可以用
select{}
进行 程序阻塞)。
说说 channel 特性?
- 给一个 nil channel 读写数据,造成永远阻塞。
- 给一个已经 关闭的 channel 发送数据,引起
panic
。- 从一个已经 关闭的 channel 接收数据,如果缓冲区中为空,则返回一个 零值。
- 无缓冲 的 channel 是 同步的,而 有缓冲 的 channel 是 非同步的。
channel 使用场景?
channel
可以用于协程goroutine
间的通信
。定时任务
超时处理
控制并发数
(协程池 里的任务队列、结果队列
)
缓冲 channel 和 无缓冲 channel 的区别?
无缓冲 channel
: channel 发送和接收动作是同时发生的。
(例如: ch := make(chan int) ,如果没有 接收者<-ch
,那么 发送者ch<-
就会 一直阻塞)缓冲 channel
: 类似一个 队列,只有队列满了才可能发生 阻塞。
nil channel有什么用?
nil channel
会 阻塞 对该 channel 的所有 读写操作。- 关闭 nil channel (或未初始化的 channel) 会引起
panic
。
channel 如何关闭?
close(ch1)
什么情况会导致 Goroutine 发生阻塞?
- 对一个 nil channel 进行 读写操作,造成永远阻塞。
- 死锁。多个协程由于竞争资源导致死锁。
如果 Goroutine 一直占用资源怎么办,GMP模型怎么处理这个问题?
如果有一个 Goroutine 一直占用资源的话,GMP模型 会从 正常模式 转为 饥饿模式,强制让前面的 Goroutine 去分配使用。
如何优雅的关闭一个 Goroutine?
- 使用
channel
实现。- 使用
Context
实现 (协程 内调用 协程: 传入 ctx 参数): 【LOOP + ctx.Done()
】
谈一下 Go语言中的 WaitGroup?
WaitGroup
相当于是一个 计数器,可以用来 记录 并 维护 运行的 goroutine。如果 WaitGroup > 0,Wait() 方法就会阻塞。
- Add(): 设置 WaitGroup 的 计数值,可以理解为 子任务的数量。
- Done(): 将 WaitGroup 的 计数值 减一,可以理解为完成一个子任务。
- Wait(): 用于 阻塞调用者,直到 WaitGroup 的 计数值 为0,即 所有子任务都完成。
Go语言中有哪几种锁?
Mutex
:互斥锁。RWMutex
:读写锁,RWMutex 基于 Mutex 实现。
说说go语言的 同步锁 (锁)?
- 1.当一个 协程 获得了
Mutex
后,会阻止其他 协程 的 读写操作。- 2.
RWMutex
在 读锁 占用的情况下,会阻止 写,但不阻止 读。- 3.
RWMutex
在 写锁 占用情况下,会阻止 读写操作。
谈谈 Go语言 中的 原子操作,和加锁实现有什么区别?
Go语言 的原子操作由
atomic
包 提供。
- 加锁操作 涉及到 内核态 的 上下文切换,比较耗时,代价高。
- 原子操作 在 用户态 就可以完成,因此 性能 比 加锁 实现更好。
说说你对 Go语言 中 panic 和 recover 的理解?
panic
用来 抛出异常,recover
用来 捕获异常 并 处理异常。recover
必须配合defer
使用 (放在 panic 之前)。- 程序执行到
panic
,会终止执行后面的 代码,但不会阻止defer
程序 的执行。- recover 没有传入 参数,但是有 返回值,返回值就是 panic 传递的值。
defer语句 的常见使用场景?
通信连接
释放数据库连接
释放文件资源
释放
defer、return 的执行顺序?
- 多个
defer
之间的执行顺序是 后进先出。- return 最先执行,先将结果写入返回值中 (即赋值), 接着执行 defer 语句。
defer 能否修改 return 的值?
- 匿名返回值 (int): 不会影响返回值。
- 命名返回值 (res int) : 会影响返回值。
七、IO
八、通信/网络
八、内存分配、垃圾回收
简述一下 Go语言中的 栈内存 (也称调用栈)?
栈内存
往往用来存储 函数参数、局部变量 和 函数调用帧,它们 随着函数
的创建而分配,随着函数
的退出而释放 (由 编译器 自动进行这些操作)。- 一个
goroutine
对应一个 栈。
简述一下 Go语言中的 堆内存?
可以理解为:在 GO语言 中的
内存管理
是针对堆内存
而言的。
- 与 栈 不同的是,一个 应用程序 在运行时只会存在 一个
堆
。- 程序 在 运行期间 可以主动从 堆 上 申请内存,这些内存由
内存分配器
进行分配,并由垃圾收集器
进行回收。
简述 堆 和 栈 有什么区别?
性能
- 栈内存 性能好:栈内存 的 分配 与 释放 非常高效的。
- 堆内存 性能差:堆内存回收 需要通过 标记清除阶段,例如 三色标记法。
加锁
- 栈 不需要加锁:栈内存 是每个 goroutine 独有的,所以 栈内存 中的 操作 不需要加锁。
- 堆 有时需要加锁:堆内存 有时需要 加锁 防止 多线程冲突。
什么是逃逸分析?
- 逃逸分析 的基本思想:检查 变量 的 生命周期 是否是完全可知的(如果变量 被外部引用, 则生命周期不可知),如果通过检查,则在 栈 上分配。否则,就是所谓的 逃逸,必须在 堆 上进行分配。
- 也就是说: 编译器 通过
逃逸分析
技术去选择变量分配在 堆 还是 栈 上。
逃逸分析的原则 (了解原则后, 尽量写分配到 栈 上的代码)?
- Go的 逃逸分析 是在 编译期 完成的:编译期 无法确定的参数类型 必定放到堆中;
- 如果变量在 函数外部存在引用,则必定放在 堆 中, 否则放到 栈 中
- 如果变量 占用内存较大 时,则优先放到 堆 中;
简述垃圾回收(GC)方法-三色标记法?
- 第一步: 新创建的对象, 默认标记为 白色。
- 第二步: GC回收 开始, 从 根节点 遍历所有对象,把遍历到的对象从 白色集合 放入 灰色集合。
- 第三步: 遍历 灰色集合,将 灰色对象 引用的对象从白色集合放入 灰色集合,之后将此 灰色对象 放入 黑色集合。
- 第四步: 重复第三步, 直到灰色集合中无任何对象。
- 第五步: 回收所有的 白色标记 的对象. 也就是回收垃圾。
三色标记法的优缺点?
优点: 不需要
STW
(Stop The World)
缺点:
- 因为没有 STW, 可能会导致 对象丢失, (在时间间隙内:①"对象A"可能会被"对象2"引用)
Go语言 的 内存模型,为什么小对象多了会造成gc压力。
通常 小对象 过多会导致 GC三色法 消耗过多的 GPU。优化思路是,减少对象分配。