系列一
1. Go 的垃圾回收机制? GMP 模型?
- Go 使用一个并发的、三色标记、写屏障的垃圾收集器。
- GMP 是 Goroutine、M (OS 线程) 和 P (处理器) 的缩写,它描述了 Go 运行时调度的模型。其中 P 表示可执行 goroutine 队列和相关资源的逻辑处理器,M 是系统线程,G 是 goroutine。
2. 如何优雅关闭一个 channel?
- 只有发送者应该关闭 channel,接收者只读取数据。
- 在所有数据都发送完毕之后,通过调用
close(chan)
来关闭 channel。
3. Go 里面的 map 解决 hash 冲突的方法?
- Go 的 map 实现使用链地址法来解决哈希冲突,即在同一个桶位置上形成一个链表存放所有键值对。
4. slice 是引用传递还是值传递?
- Slice 在传递时是值传递,但这个值包含指向底层数组的指针,因此函数内部对 slice 的修改会影响原 slice。
5. Go 读写锁的概念?
sync.RWMutex
是 Go 语言的读写锁。- 读锁之间不互斥,读写和写写之间互斥。
- 默认情况下,Go 的读写锁没有明确的读优先或写优先规则,执行顺序依赖于调度和锁请求的顺序。
6. context 的应用场景?
- 用于控制子协程的生命周期(取消信号)、传递请求级别的数据、超时控制等。
7. select 的作用?
- 用于同时等待多个通道操作,可以处理异步 IO、超时等情况。
8. 数组和切片的区别?
- 数组是固定大小,内存分配时其长度就确定了;切片是可变长,底层引用一个数组,并提供容量和长度的动态管理。
9. map 是否是线程安全的,如何实现线程安全的 map?
- Go 中的 map 不是线程安全的。为了线程安全,可以使用
sync.Map
或者自己在 map 上加sync.RWMutex
锁。
10. sync.map 的原理?
sync.Map
使用了一个特殊的算法,它与普通的 map 相比,可以安全地被多个协程并发访问和修改,而无需额外的锁定或协调。
11. Go 数据类型有哪些?
- 基本类型:布尔型、数值型、字符串
- 复合类型:数组、切片、结构体、指针、函数、接口、map、channel
12. 如何判断两个 interface{} 相等?
- 使用
==
进行比较,但需要注意的是,如果 interface{} 内部存储的是指针,则比较的是地址。
13. Go map 中删除一个 key 的内存是否会立即释放?
- 不会立即释放,具体取决于垃圾回收器的行为。
14. init() 方法的特性?
init()
函数在包初始化时调用,用于初始化操作,每个包可以有多个init()
函数。
15. switch-case 语句,强制执行下一个 case?
- Go 的
switch-case
默认不贯穿(fallthrough),需要显式使用fallthrough
关键字。
16. encoding/json 包解码 JSON 数据时,默认将数字解析为 float64 类型?
- 是的,如果你使用
interface{}
接收 JSON 中的数字,它会被转换为float64
。
17. Go 里面的类型断言?
- 类型断言用于检查接口值是否保存了特定的类型,例如
value, ok := i.(int)
。
18. Go 静态类型声明?
- Go 是静态类型语言,变量在编译时已经确定了类型,不能改变。
19. sync 包使用?
- 提供基础的同步原语,如互斥锁 (
sync.Mutex
), 读写锁 (sync.RWMutex
), WaitGroup, Once 等。
20. gin 的并发请求、错误处理、路由处理?
- Gin 是一个高性能的 HTTP web 框架,支持并发请求处理;
- 错误处理可以通过中间件和
c.Error()
方法实现; - 路由处理通过定义路由和关联的处理函数(Handlers)。
21. CSP 并发模型?
- Communicating Sequential Processes (CSP) 是 Go 语言并发模型的基础,通过 goroutines (并行执行单元) 和 channels (通信机制) 实现。
22. 逃逸分析的介绍?
- 逃逸分析是编译器用于决定变量分配位置(栈或堆)的过程。在 Go 中,使用
-gcflags "-m"
查看编译器关于逃逸分析的信息。
23. 对关闭的 channel 进行读写会发生什么?
- 对关闭的 channel 读取会返回零值;
- 对关闭的 channel 写入会导致 panic。
24. 字符串转 byte 数组会发生内存拷贝么?
- 是的,将 string 转换为 []byte 会发生内存拷贝。
25. 如何实现字符串转切片无内存拷贝(unsafe)?
- 使用 unsafe 包可以避免内存拷贝,但这种做法可能不安全,需要谨慎处理。
26. Go 语言 channel 的特性? channel 阻塞情况? channel 底层实现?
- Channel 是 Go 中的通信机制,可以在协程之间传递数据。
- 如果 channel 满了,发送者会阻塞;如果空了,接收者会阻塞。
- Channel 的底层实现基于队列结构,涉及到一些同步原语。
系列二
哪些数据结构是线程不安全的
在许多编程语言中,基本的数据结构通常是线程不安全的,因为它们没有内置的同步机制。在 Go 语言中,以下是线程不安全的数据结构:
- Slices
- Maps
- Built-in types (int, float, bool, etc.)
Map为什么是线程不安全的
Go 中的 map 是线程不安全的,因为当多个 goroutines 同时读写一个 map(尤其是有一个进行写操作)时,可能会发生并发冲突,导致运行时异常如“fatal error: concurrent map writes”。
Channel阻塞可以实现什么场景(计数,令牌桶)
Channel 阻塞可以用于多种场景,例如:
- 计数信号量:通过有缓冲的 channel 实现计数,限制同时运行的 goroutines 数量。
- 令牌桶算法:用于流量控制,通过 channel 来限制事件的发生速率。
Mysql什么时候是行锁什么时候是表锁
MySQL 会根据使用的存储引擎和查询类型来决定使用行锁或表锁。例如:
- MyISAM 默认使用表锁。
- InnoDB 支持行锁和表锁,默认情况下使用行锁。
MySQL有几种错误读(脏读、幻读等等)
在事务中,错误的读取类型主要有三种:
- 脏读:读取到另一个未提交事务的数据。
- 不可重复读:在同一事务中多次读取同样记录的结果不一致。
- 幻读:在同一事务中,一个范围查询的结果改变了,因为另一个事务插入或删除了符合该范围的行。
MySQL默认事务隔离级别是什么
MySQL 默认的事务隔离级别是 REPEATABLE READ
。
假如有个sql联了多个表还有子查询,怎么优化
优化方法可能包括:
- 确保所有被连接的字段都有索引。
- 分析查询执行计划(EXPLAIN)以寻找性能瓶颈。
- 重新设计查询,使其更高效,比如将子查询替换为 JOIN 操作。
- 使用临时表或物化视图来存储中间结果。
你平常是怎么优化MySQL的
优化 MySQL 的常见方法:
- 创建合适的索引,并定期检查它们的有效性。
- 优化查询语句,避免全表扫描。
- 调整 MySQL 配置,如缓存大小和缓冲池。
- 使用分区表提高大表的管理效率和查询性能。
- 定期维护数据库,进行碎片整理。
Kafka为什么快?
Kafka 之所以快,主要得益于:
- 顺序 I/O:Kafka 把消息顺序写入磁盘,这种方式比随机 I/O 快得多。
- 零拷贝技术:直接从文件系统缓存传输数据到网络缓冲区,减少数据复制。
- 分区和多副本:提供并行处理能力和负载均衡。
- 批处理:在网络和磁盘 I/O 中进行消息批处理,降低系统调用的开销。
Kafka怎么实现消息不丢失
Kafka 可以通过以下方式来确保消息不丢失:
- 在生产者端设置
acks
参数为all
,确保所有副本都收到消息后才认为消息已提交。 - 使用幂等生产者和/或事务确保消息不重复。
- 在消费者端正确处理偏移量,确保消息被成功处理后再提交偏移量。
Kafka是顺序写还是随机写
Kafka 是顺序写的,这是它快速高效的关键原因之一。
Go协程是怎么扩展内存的(找P要)
Go 协程开始时有一个很小的栈(通常是2KB),当需要更多空间时,栈会自动增长。当栈不够用时,Go 运行时会检测到这一点,并分配一个更大的栈空间,然后把原来的数据复制过去。这个过程称为栈扩展。
讲一下你对Docker和K8s的理解
- Docker:是一个开源的应用容器引擎,让开发者可以打包他们的应用及依赖到一个可移植的容器中,然后发布到任何支持 Docker 的 Linux 机器上,也可以实现虚拟化。
- Kubernetes (K8s):是一个强大的容器编排工具,用于自动化部署、扩展和管理容器化应用程序。提供服务发现、负载均衡、自我修复、自动滚动更新等功能。
说一下某种集群的leader选举策略(举例了redis)
Redis Sentinel 系统用于管理多个 Redis 服务器,包括故障转移和leader选举。Sentinel 通过发送心跳包进行监控,并在 master 宕机时自动进行leader选举,选择新的 master。
Redis中什么是主观下线,什么是客观下线。
- 主观下线:当 Sentinel 判定一个 Redis 实例无法访问时,它会对该实例进行主观下线。
- 客观下线:当多个 Sentinel 都报告同一个实例不可达时,那么该实例会被客观下线,并触发故障转移。
聊一聊你对GRPC的理解。
gRPC 是一个高性能、开源和通用的 RPC 框架,由 Google 主导开发。它支持多种语言,在 HTTP/2 上实现请求/响应、流式传输,利用 Protocol Buffers 作为接口描述语言。
为什么gRPC传输比JSON快
gRPC 通常比 JSON 快,因为:
- 它使用 Protocol Buffers,这是一种轻量、高效的二进制格式。
- 它基于 HTTP/2,支持多路复用、服务器推送等高效特性。
大量定时任务需要在凌晨1点都准时开始执行
要处理大量定时任务,有几种方法可以保证它们能够准时并且高效地执行:
-
使用分布式任务调度框架:
- 例如 Apache Airflow, Celery (对于 Python),或者 Go 的分布式调度器如 dkron。
- 这些框架会负责触发任务,并且可以扩展到多个服务器以分散负载。
-
负载均衡与批处理:
- 如果所有任务必须同时开始,可以将它们分成小批次,每个批次被不同的工作服务器处理。
- 可以采用消息队列(如 Kafka、RabbitMQ)来管理这些批次的调度。
-
资源分配与限流:
- 确保系统有足够的资源来同时执行这些任务,或者基于当前系统负载动态调整可执行的任务数量。
- 对数据库和其他共享资源进行访问限制,避免因为一下子过多的请求导致崩溃。
-
预热:
- 在执行之前预热应用实例,确保代码已经加载到内存中,连接池已建立等。
-
监控与报警:
- 实时监控任务执行状态,出现问题及时报警并自动恢复。
-
冗余设计:
- 确保任务调度系统的高可用性,比如通过设置主备调度器。
-
时间同步:
- 确保所有参与任务调度的服务器时间完全同步,可以使用 NTP 服务保持时间一致。
消息发送过多导致大量堆积怎么处理
面临消息堆积问题时,可以从以下几个方向寻找解决办法:
-
增加消费者:
- 增加更多的消费者进程或者线程来处理消息队列中的消息,从而提升处理速度。
-
优化处理逻辑:
- 分析并优化消息的处理逻辑,提高单个消费者的处理速度。
-
水平扩展:
- 增加更多的服务器以扩展系统的消费能力。
-
优先级队列:
- 如果有些消息比其他消息更紧急,可以考虑实现优先级队列,确保重要的消息首先被处理。
-
消息压缩:
- 如果消息内容太大,考虑对消息进行压缩。
-
消息削峰:
- 在消息发送方实施限流策略,比如令牌桶或漏桶算法,以平滑发送速率。
-
监控与报警:
- 对消息队列进行实时监控,一旦出现堆积,立即通知相关人员进行处理。
-
临时存储:
- 如果是暂时的高峰期造成的堆积,可以考虑将消息写入快速的临时存储,如 Redis,稍后再慢慢处理。
-
消息持久化:
- 确保所有未处理的消息都被持久化存储,防止系统崩溃导致数据丢失。
-
稳定性优先:
- 如果系统无法处理过多的消息,可以退回部分消息或拒绝接收新消息,保证系统稳定运行。
处理消息堆积的关键在于监控系统状况,并根据实际情况调整资源,以及优化处理逻辑来应对。此外,系统设计时也应考虑到可能出现的高峰情况,做好相应的扩展和容错设计。
有缓冲channel和无缓冲channel的区别
在 Go 语言中,channel 是协程间通信的一种机制,它们可以是有缓冲的(buffered)或无缓冲的(unbuffered)。
-
无缓冲channel:当你向无缓冲channel发送数据时,发送者会阻塞直到另一个 goroutine 读取了这些数据。同样,如果从无缓冲channel读取,读取者也会阻塞直到有数据被写入。因此,无缓冲channel提供了一种同步交换数据的方式。
-
有缓冲channel:有缓冲的channel具有固定的容量,只有在缓冲区满时,尝试向其发送数据的操作才会阻塞。同理,只有在缓冲区为空时,尝试从中读取数据的操作才会阻塞。有缓冲channel允许发送者和接收者在缓冲区不满和不空的情况下异步工作。
选择使用有缓冲还是无缓冲channel取决于你的具体需求,比如是否需要强同步或者希望减少因等待锁而产生的延迟。
了解gin的中间件吗,讲一下你对他的了解
Gin 是一个用 Go (golang) 编写的 HTTP web 框架。它内置了一组丰富的中间件功能,使得用户可以轻松地为请求添加处理逻辑。
中间件是 Gin 路由过程中的处理函数,它可以:
- 在处理请求之前执行某些操作(例如身份验证、日志记录、限流)。
- 修改请求和响应。
- 终止请求链并直接返回响应。
Gin 的中间件支持全局注册和单个路由注册,也允许创建自定义中间件。中间件的执行顺序按照它们被添加到路由器上的顺序。
select 满足多个case的时候怎么执行的
当 select
语句中存在多个 case
同时满足时,Go 运行时会随机选择一个执行。这保证了每个可运行的 case
都有相同的机会被选中,避免了总是倾向于优先选择特定 case
而导致的偏差。
如果有一个全局变量怎么保证并发安全
要保证全局变量的并发安全,可以采用以下方法:
- 使用互斥锁(Mutex)来保护对全局变量的访问。
- 使用原子操作,如
sync/atomic
包中的函数,来进行简单的变量更新操作。 - 将全局变量封装进一个协程,并通过 channel 与该协程通信来安全地读写全局变量。
CPU高问题如何解决?
解决CPU使用率高的问题通常包括以下步骤:
- 分析:首先需要确定是什么导致了CPU使用率高。这通常涉及性能监控和分析工具,比如
pprof
来识别高CPU消耗的代码部分。 - 优化:根据分析结果,可以通过算法优化、减少不必要的计算、并发执行等方法来降低CPU使用率。
- 扩展:如果应用程序因为负载增加而导致CPU使用率高,可能需要水平扩展(增加更多服务器)或垂直扩展(升级服务器配置)。
- 代码审查:检查代码中是否存在无效循环或密集型操作,以及是否所有并发执行都经过适当同步。
知道哀设计模式?
设计模式是软件设计中普遍存在、反复出现问题的解决方案。在面向对象的设计中,常见的设计模式包括但不限于:
- 创建型模式:如 Singleton(单例)、Factory Method(工厂方法)、Abstract Factory(抽象工厂)、Builder(建造者)、Prototype(原型)。
- 结构型模式:如 Adapter(适配器)、Composite(组合)、Proxy(代理)、Flyweight(享元)、Facade(外观)、Bridge(桥接)、Decorator(装饰者)。
- 行为型模式:如 Strategy(策略)、Observer(观察者)、Command(命令)、State(状态)、Visitor(访问者)、Mediator(中介者)、Iterator(迭代器)、Chain of Responsibility(责任链)、Memento(备忘录)。
这些设计模式有助于编写易于维护和扩展的代码,同时也便于团队之间的沟通。
Linux 查看磁盘占用
在 Linux 中,有几个命令可以帮助你检查磁盘使用情况:
-
df (disk free): 显示文件系统的总空间、已用空间、可用空间和挂载点。
df -h
-h
参数让输出以人类可读的格式显示(例如使用 MB、GB 等单位)。 -
du (disk usage): 报告目录或文件的磁盘使用空间。
du -sh /path/to/directory
-s
参数表示汇总每个参数的大小;-h
使其以易读格式显示。 -
ncdu (NCurses Disk Usage): 是
du
的一种图形化版本,它提供了一个交互式界面来浏览系统,并查找占用大量空间的文件。ncdu /path/to/directory
如果系统中没有安装 ncdu,可以使用包管理器安装,如
sudo apt-get install ncdu
。 -
ls: 列出文件和目录,结合
-l
和-h
参数可以显示文件的大小。ls -lh
-
iotop or atop: 实时监控磁盘 I/O 和磁盘占用。
-
baobab (Disk Usage Analyzer): GNOME 桌面环境下的磁盘使用分析工具,提供了一个图形化界面。
-
find: 查找超过特定大小的文件。
find / -type f -size +100M
这将会列出系统上所有超过 100MB 的文件。
通过这些工具,你可以有效地管理和监控 Linux 系统的磁盘空间,确保关键任务的顺利运行。
GC(垃圾回收)的过程描述
不同的编程语言和环境有不同的垃圾回收机制。以下是大多数现代垃圾回收算法通用的高层次步骤:
-
标记阶段:
- 垃圾回收器遍历所有从根对象(通常是全局变量和活跃线程的执行栈上的变量)可达的对象。
- 遍历期间,所有可达的对象被标记为活动的,这意味着它们当前正在使用中,不能被回收。
-
清除/删除非活动对象:
- 将未标记的对象认为是垃圾,并进行回收。这些对象占用的内存将被释放并返回到内存池。
- 在某些回收算法中,比如标记-清除(Mark-Sweep),此步骤直接将未标记的内存释放。
- 在其他算法中,如标记-整理(Mark-Compact)或复制(Copying),会移动活动对象以消除内存碎片,然后回收剩余的全部内存区域。
-
压缩阶段 (可选):
- 特定的回收器可能会包括一步用于压缩内存的阶段,即整理存活的对象,使它们在内存中连续排列,减小碎片化,优化后续分配的性能。
在实际应用程序中,垃圾回收可能更复杂,因为它涉及到如何识别垃圾、如何处理并发执行、如何最小化停顿时间等问题。
例如,在 Java 虚拟机(JVM)中,垃圾回收器可以采用几种不同的策略,如串行回收、并行回收、CMS(Concurrent Mark-Sweep)、G1(Garbage-First)等,每种都有其优缺点和适用场景。
在 Go 语言中,垃圾回收器是并发执行的,并且设计成尽量减少程序运行的暂停时间。Go 的GC算法主要基于三色标记法,并且在近年的迭代中一直在改进以减少STW(stop-the-world)事件的频率和持续时间。
Channel 的使用场景
在 Go 语言中,channel 是用于协程之间通信的强大工具。以下是一些常见的 channel 使用场景:
- 消息传递:在 goroutines 之间安全地传递数据。
- 同步操作:使用无缓冲 channel 实现多个 goroutine 之间的同步。
- 信号广播:通过关闭 channel 来广播一个信号,告诉其他 goroutine 一个事件已发生。
- 任务队列:使用有缓冲 channel 创建一个工作队列,并由多个 worker 并发处理。
- 限制并发数:用有缓冲 channel 控制同时运行的 goroutines 数量,以避免过载。
- 数据流管道:将一系列处理阶段连接起来,每个阶段由一个或多个 goroutines 执行,并且它们之间使用 channel 传递数据(类似 Unix 管道)。
channel关闭之后再读和再关闭会发生什么?
-
关闭之后再读:关闭一个 channel 后,已经发送到 channel 中的数据仍然可以被接收,直到 channel 中的数据都被读取完。当所有数据被读取后,任何对该 channel 的进一步读取都将立即返回而不会阻塞,并且返回的值为该类型的零值。这意味着你可以安全地从一个已关闭的 channel 中读取数据,但需要考虑如何正确处理零值。
-
关闭之后再关闭:一个 channel 只能被关闭一次。再次尝试关闭同一个 channel 将会导致 panic。因此,在关闭 channel 之前应确保只有一个 goroutine 负责关闭,并且没有其他 goroutine 会尝试重复关闭它。
map中的数据delete之后内存会回收吗?
在 Go 中,当你使用 delete()
函数从 map 中删除键值对时,该键值对所占用的内存确实可能被垃圾回收器回收。但这不是立即发生的,回收的时间依赖于垃圾回收器的调度。
delete()
函数只是将键值对从 map 中移除,使其不再可访问。垃圾收集器稍后将检查不再被任何引用的内存块,并回收它
Kafka的消息丢失和消息重复消费:
- 消息丢失可能发生在生产者发送失败、Kafka集群故障或消费者未能成功提交offset导致的重启后重新消费而跳过某些消息。
- 消息重复消费可能由网络问题导致生产者重试发送、消费者处理完消息但提交offset失败,重启后重新消费相同消息。
Kafka和RabbitMQ的区别:
- 架构:Kafka基于发布订阅模型,设计为高吞吐量分布式系统;RabbitMQ是传统的消息队列,更强调消息的可靠性和多种消息模式。
- 推拉模式:Kafka主要是消费者拉取(polling)消息,而RabbitMQ支持推送(push)消息给消费者。
拉的模式有什么好处:
- 控制消费速度:消费者可以根据自己的处理能力来拉取消息,避免被动接收导致的处理瓶颈。
使用分布式锁的过程中应用挂了:
- 优雅启停+defer:确保释放锁的操作在defer语句中执行,即使应用异常终止也能保证资源的释放。
- 使用过期时间+自动续期:设置锁的过期时间并定期续期,如果应用挂掉,锁将因超时而自动释放。
对象存储和文件存储的主要区别:
- 对象存储提供通过API访问非结构化数据,无文件层次结构,每个对象包含数据、元数据和全局唯一标识。适合大数据和云存储。
- 文件存储则是基于文件系统提供文件或文件夹的存取,有层次结构,适合传统应用。
分片上传实现:
- 分片上传通过将大文件分成小块独立上传,最后再合并这些块。
- 文件合并后进行hash一致性校验,以确认数据完整性。
- 秒传可以通过比对已存在的文件hash值来实现,如果文件已存在,则不需要重新上传,直接创建索引即可。
邮箱验证码功能实现:
- 使用Redis存储验证码及其过期时间,结合邮箱服务组件发送验证码到用户邮箱。
JWT的格式:
- JWT由三部分组成:Header(头部,包含加密算法)、Payload(负载,包含内容)、Signature(签名,用于验证)。
defer的原理:
- defer语句将其后面跟随的函数推迟到当前函数返回之前执行。
map的底层结构:
- Go语言的map底层是一个哈希表,使用数组和链表组合实现。
map中hash冲突解决:
- 使用链表存储具有相同哈希值的不同键值对。
- 当链表过长时,转换为平衡树(如红黑树)以提高查找效率。
Go性能调优案例:
- 使用pprof工具分析CPU和内存使用情况,通过分析线程日志找出性能瓶颈。
看线程在日志里的状态:
- 线程日志通常会标记线程状态,展示是否在循环或等待事件。
MySQL的事务隔离级别:
- Read Uncommitted
- Read Committed
- Repeatable Read
- Serializable
可重复读:
- 指在同一个事务中,多次读取同一数据结果都是一致的,即使其他事务正在修改该数据。
事务实现的底层原理:
- 事务通过锁机制、MVCC(多版本并发控制)等手段来实现ACID特性。
Redis持久化机制:
- RDB:定期快照存储数据状态。
- AOF:记录所有写操作命令并在重启时重放。
持久化时为什么是fork子进程:
- 避免阻塞主进程,提高数据安全性,利用操作系统的写时复制(Copy-On-Write)技术减少内存使用。
Docker基本原理:
- Docker使用Linux容器技术,通过cgroups和namespace实现资源隔离和限制,UnionFS为容器提供轻量级的、可写的文件系统。
其他容器运行时:
- 例如Podman, containerd, rkt等。
K8s的组件:
- 节点组件:kubelet, kube-proxy, Container Runtime
- 控制面组件:kube-apiserver, kube-controller-manager, kube-scheduler, etcd
构建deployment的control:
- Kubernetes中的Deployment对象通常是通过kubectl命令行工具或者使用YAML配置文件定义的,然后由kube-apiserver处理并存储到etcd中,由控制器管理器中的deployment控制器进行实际的构建和维护。