Golang 后端面经

文章目录

Golang

golang GMP模型

Go 语言的并发模型基于 goroutine 和 Go runtime 的调度器。GMP 模型是 Go runtime 用来管理 goroutine 并发执行的核心机制,其中包含三个关键组成部分:G(Goroutine)、M(Machine)、P(Processor)。下面是对 GMP 模型的详细解释。

Goroutine(G)

G 表示 Goroutine,即 Go 语言中的轻量级线程。Goroutine 是由 Go runtime 管理的用户态线程,具有更低的创建和销毁成本。
每个 G 包含一个函数、栈、程序计数器和其他元数据。
Machine(M)

M 表示 Machine,可以理解为操作系统的线程。M 是实际执行 goroutine 的实体。
M 负责在操作系统层面管理和调度资源,如 CPU 和内存。
一个 M 可以绑定多个 G,并在合适的时候切换执行它们。
Processor(P)

P 表示 Processor,是 Go runtime 中用于调度 G 的逻辑处理器。
P 的数量由 GOMAXPROCS 环境变量决定,表示 Go 应用程序能并发执行的最大 CPU 核数。
P 管理一个运行队列,存储要在该处理器上运行的 goroutine。
GMP 关系
每个 P 绑定一个 M 执行 goroutine。
每个 M 执行一个 P 中的 goroutine。
G 被分配给 P,并由 M 执行。
GMP 模型的工作原理
创建 Goroutine

当 Go 程序创建一个新的 goroutine(如使用 go func()),G 会被分配到某个 P 的运行队列中。
调度 Goroutine

P 维护一个本地运行队列,存储要执行的 goroutine。
M 通过 P 从本地运行队列中取出 G 并执行。
如果 P 的本地运行队列为空,P 可以从全局运行队列或其他 P 的本地运行队列中窃取工作。
执行 Goroutine

M 负责实际执行 G。M 可以在执行过程中进行上下文切换(如遇到 I/O 操作、系统调用等),从而允许其他 G 继续执行。
当 G 执行完毕或被阻塞时,M 会将其放回队列,并从 P 中获取下一个 G 执行。
工作窃取

如果 P 的本地运行队列为空,P 可以从全局运行队列或其他 P 的本地运行队列中窃取 goroutine,以保证工作负载的平衡。

容量为1的channel在什么情况下会堵塞(除了常见的)

当接收者没有准备好接收数据时:即使 channel 容量为 1,当通道已满且没有 Goroutine 在接收时,发送操作将阻塞。

多线程同时读写map中不同的key,一个线程只会读写一个key,会发送什么

在 Go 中,内置的 map 不是并发安全的,即使是对不同的 key 进行操作,也会导致数据竞争,从而引发未定义的行为。

检查数据竞争

可以使用 Go 的内置工具
go run -race 或
go test -race 来检查数据竞争。

实现了一个web的server,如何设置这个server返回的response的类型,比如说是一个图片一个视频一个json

可以通过设置 HTTP 头部的 Content-Type 来指定返回内容的类型:
Json:application/json
图片:image/png
视频:video/mp4

package main

import (
	"encoding/json"
	"net/http"
)

func jsonHandler(w http.ResponseWriter, r *http.Request) {
	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(map[string]string{"hello": "world"})
}

func imageHandler(w http.ResponseWriter, r *http.Request) {
	w.Header().Set("Content-Type", "image/png")
	// Write image data to the response
}

func videoHandler(w http.ResponseWriter, r *http.Request) {
	w.Header().Set("Content-Type", "video/mp4")
	// Write video data to the response
}

func main() {
	http.HandleFunc("/json", jsonHandler)
	http.HandleFunc("/image", imageHandler)
	http.HandleFunc("/video", videoHandler)
	http.ListenAndServe(":8080", nil)
}

快速定位死锁

使用 Go 的内置工具 go run -race 可以帮助检测并报告潜在的死锁。此外,还可以使用 runtime 包中的 Goroutine 和 Mutex 调试函数来手动检查死锁。

用户从客户端访问一个页面,webserver如何主动的给这个页面推送一个通知

可以使用 WebSocket 来实现服务器主动推送通知给客户端。

package main

import (
	"fmt"
	"net/http"

	"github.com/gorilla/websocket"
)

var upgrader = websocket.Upgrader{}

func wsHandler(w http.ResponseWriter, r *http.Request) {
	conn, err := upgrader.Upgrade(w, r, nil)
	if err != nil {
		fmt.Println("Upgrade error:", err)
		return
	}
	defer conn.Close()

	for {
		_, msg, err := conn.ReadMessage()
		if err != nil {
			fmt.Println("Read error:", err)
			break
		}
		fmt.Printf("Received: %s\n", msg)
	}
}

func main() {
	http.HandleFunc("/ws", wsHandler)
	http.ListenAndServe(":8080", nil)
}

singleflght是使用什么方式去通知其他线程,其他线程怎么阻塞的

singleflight 使用一个共享的等待组(sync.WaitGroup)来阻塞并通知其他线程。当一个线程发起一个请求时,其他线程会等待,直到第一个线程完成并通知它们。

不用waitgroup怎么实现这部分

可以用通道实现

slice是[]int{1,2},把它传入一个函数,修改第一项的值为3,函数结束,原来slice值改变了吗

是的,原来的 slice 值会改变,因为 slice 是引用类型。

package main

import "fmt"

func modify(slice []int) {
	slice[0] = 3
}

func main() {
	s := []int{1, 2}
	modify(s)
	fmt.Println(s) // 输出: [3 2]
}

如果函数是新建一个list等于append原来的slice,list=append(slice,3),调用完这个函数后在函数的外部打印这个原来slice的长度是多少,新的是多少,地址改变了吗,指向的原来的数组呢

原来的 slice 长度不会改变,新建的 list 长度会增加,地址可能会改变。
如果新建的list超过原有容量,则会新建一个数组并扩容,这时地址就会改变,如果没有超过原有容量,那还是原来的地址。

a和b两个线程,a里面有defer recover,a里面新开了一个b,b没写defer recover,b发生了panic,ab两个线程会发生什么情况

b发生panic 程序直接崩溃
a无法接收b的panic

在函数参数传递一个非指针的互斥锁会发生什么事情?为什么会发生?

传递一个非指针的互斥锁会导致锁失效,因为互斥锁是不可复制的,传递时会产生副本,副本和原始锁是不同的对象。

jwt鉴权管理实现怎么做的,怎么实现的鉴权,修改了token它怎么解析,为什么这么解析,原理是什么

JWT 通常由三部分组成:

Header:标识类型和使用的签名算法(如 HMAC SHA256)。
Payload:包含声明(claims),如用户信息和其他元数据。
Signature:用于验证消息的真实性和完整性。
JWT 的基本结构如下:
header.payload.signature

服务器在用户登录成功后生成一个 JWT 并返回给客户端。这个 JWT 将用于后续的请求鉴权
JWT内容包含用户信息和有效期
每次客户端请求时都会携带 JWT,服务器需要验证该 JWT 的签名和有效期,确保其有效。
解析 JWT 时,服务器执行以下步骤:

分离 Header、Payload 和 Signature。
解码 Header 和 Payload。
使用相同的算法和密钥重新计算签名。
比较重新计算的签名与 JWT 中的签名,如果匹配则验证通过。
检查声明,如过期时间,确保 JWT 没有过期。

详细说一下令牌桶算法的实现

令牌桶算法用于流量控制。算法维护一个桶,定期向桶中添加令牌。请求到来时从桶中取走令牌,如果没有令牌则拒绝请求或等待。令牌桶算法可以通过控制令牌的添加速率和桶的容量来控制请求速率。

package main

import (
	"sync"
	"time"
)

type TokenBucket struct {
	capacity  int
	tokens    int
	mutex     sync.Mutex
	ticker    *time.Ticker
	stopCh    chan struct{}
}

func NewTokenBucket(rate int, capacity int) *TokenBucket {
	tb := &TokenBucket{
		capacity: capacity,
		tokens:   capacity,
		ticker:   time.NewTicker(time.Second / time.Duration(rate)),
		stopCh:   make(chan struct{}),
	}
	go tb.refill()
	return tb
}

func (tb *TokenBucket) refill() {
	for {
		select {
		case <-tb.ticker.C:
			tb.mutex.Lock()
			if tb.tokens < tb.capacity {
				tb.tokens++
			}
			tb.mutex.Unlock()
		case <-tb.stopCh:
			return
		}
	}
}

func (tb *TokenBucket) Stop() {
	tb.ticker.Stop()
	close(tb.stopCh)
}

func (tb *TokenBucket) Allow() bool {
	tb.mutex.Lock()
	defer tb.mutex.Unlock()
	if tb.tokens > 0 {
		tb.tokens--
		return true
	}
	return false
}

func main() {
	tb := NewTokenBucket(5, 10)
	defer tb.Stop()

	for i := 0; i < 20; i++ {
		if tb.Allow() {
			fmt.Println("Request allowed")
		} else {
			fmt.Println("Request denied")
		}
		time.Sleep(100 * time.Millisecond)
	}
}

golang 垃圾回收机制

Go 语言的垃圾回收机制(Garbage Collection, GC)是一种自动管理内存的功能,旨在自动回收不再使用的内存,以防止内存泄漏并提高程序的健壮性和效率。Go 的垃圾回收器是并发的、非阻塞的,并且自 Go 1.5 以来不断改进,以提高性能和减少对应用程序的影响。以下是 Go 垃圾回收机制的详细解释:

  1. 标记-清除算法
    Go 的垃圾回收器主要基于标记-清除(mark-and-sweep)算法。这种算法分为两个阶段:

标记阶段:从根对象(如全局变量和当前函数的栈帧)开始,递归地标记所有可以到达的对象为“存活”。
清除阶段:遍历堆中的所有对象,回收那些没有被标记为“存活”的对象。
2. 三色标记法
Go 的垃圾回收器使用三色标记法来实现标记阶段。这种方法将对象分为三种颜色:

白色:未被检查的对象,假设是不可达的。
灰色:被检查过的对象,但其引用的对象还未被检查。
黑色:被检查过的对象,并且其引用的对象也都被检查过。
垃圾回收器通过以下步骤进行标记:

初始时,所有对象都是白色的。
将根对象标记为灰色。
不断从灰色对象集中取出对象,将其引用的对象标记为灰色,并将该对象本身标记为黑色。
当灰色对象集为空时,标记阶段完成。

go 和 C++的区别,各有什么优劣?

go优点:
简单、开发效率高、原生并发编程、提供垃圾回收、跨平台编译
go缺点:
性能不如C++、缺乏一些高级特性。

c++ 面向对象 类继承多态 广泛标准库,更灵活更底层。
优点:速度快、提供底层控制、标准库多
缺点:复杂度高、手动内存管理、并发编程复杂
应用场景:
go:网络编程和微服务
C++:系统级编程 游戏服务器 、高性能计算、操作系统嵌入式等底层

MVCC

多版本并发控制(MVCC, Multi-Version Concurrency Control)是一种用于处理数据库并发访问的机制。MVCC 通过保存数据的多个版本,实现了数据库的读写操作互不阻塞,提升了并发性能。
每次对数据行进行修改(插入、更新、删除)时,都会生成一个新的版本。旧版本的数据不会立即删除,而是保留以供其他事务使用。
每个数据版本都会关联一个时间戳或事务 ID,用于标识该版本是由哪个事务生成的。通常,时间戳或事务 ID 是递增的,这样可以确定数据版本的先后顺序。
读操作根据事务的时间戳或事务 ID,读取在事务开始之前生成的最新数据版本。这样,读操作不会阻塞写操作,反之亦然。
特点:
高性能:由于读写操作互不阻塞,MVCC 提高了数据库的并发性能。
一致性读取:读操作总是读取事务开始之前的最新数据版本,确保了读取的一致性视图。
无锁操作:读操作无需加锁,从而避免了死锁问题。

go中的并发utils了解多少?

Goroutines、channel、Select、sync、context
9. TCP和UDP的区别,TCP的三次握手四次挥手。
都是传输层协议
TCP:
面向连接:必须建立连接才能数据传输;
可靠性:提供可靠的数据传输,通过序列号、确认 号、重传机制和流量控制,确保数据包按顺序、无误地到达目标。
流量控制: 使用滑动窗口机制进行流量控制,确保发送方不会发送超过接收方处理能力的数据量。
拥塞控制:拥塞控制机制(如慢启动、拥塞避免、快速重传和快速恢复)来避免网络拥塞。
数据传输顺序:TCP 保证数据包按发送顺序到达接收方。
数据开销:由于需要建立连接、确认数据包和重传等功能、开销相对较大

UDP:
无连接、不可靠传输、无流量控制 、无拥塞控制、传输速度快、开销小。

TCP的三次握手(Three-way Handshake)

TCP 的三次握手是指在建立连接时,客户端和服务器之间需要进行的三次消息交换,以确保双方准备就绪并同步初始序列号。

第一次握手(客户端 -> 服务器):

客户端发送一个带有 SYN(同步序列号)标志位的包,表示请求建立连接,同时选择一个初始序列号(Seq=x)。
第二次握手(服务器 -> 客户端):

服务器收到 SYN 包后,返回一个带有 SYN 和 ACK(确认序列号)标志位的包,表示同意连接,并确认客户端的初始序列号(Ack=x+1),同时选择自己的初始序列号(Seq=y)。
第三次握手(客户端 -> 服务器):

客户端收到 SYN+ACK 包后,返回一个带有 ACK 标志位的包,确认服务器的初始序列号(Ack=y+1)。至此,连接建立。

TCP 的四次挥手

四次挥手是指在关闭连接时,客户端和服务器之间需要进行的四次消息交换,以确保双方都能安全地关闭连接。

第一次挥手(客户端 -> 服务器):

客户端发送一个带有 FIN(结束)标志位的包,表示没有数据要发送了,请求关闭连接。
第二次挥手(服务器 -> 客户端):

服务器收到 FIN 包后,返回一个带有 ACK 标志位的包,确认客户端的 FIN 包(Ack=客户端的Seq+1)。此时,服务器进入半关闭状态,可以继续发送数据给客户端。
第三次挥手(服务器 -> 客户端):

服务器确定没有数据要发送后,发送一个带有 FIN 标志位的包,表示请求关闭连接。
第四次挥手(客户端 -> 服务器):

客户端收到 FIN 包后,返回一个带有 ACK 标志位的包,确认服务器的 FIN 包(Ack=服务器的Seq+1)。此时,客户端等待一段时间(通常是 2 * 最大报文段寿命(MSL),确保服务器收到确认包后关闭连接)。至此,连接关闭。

redis写回策略你了解哪些?

RDB(Redis Database File)持久化:
通过定时保存或者在指定时间间隔内保存数据快照到磁盘。
AOF(Append Only File)持久化:通过将写操作追加到文件末尾的方式来记录每个写操作,确保每次写操作都能被持久化。
混合持久化:混合持久化是将 RDB 和 AOF 持久化结合使用,充分利用两者的优势。
11. protobuf知道吗?thrift了解吗?什么原理?
使用 Protobuf 的核心是定义一个IDL,描述数据结构和服务接口。数据被编码为二进制格式,序列化和反序列化速度快。

Thrift:同样使用 IDL 来定义数据结构和服务接口。
支持多种序列化格式(包括二进制和 JSON)。
12. 算法题:LCA

Mysql

优化过sql吗,数据库是部署在哪里的,讲一个你认为最好的sql优化例子

在常用where条件查询上加索引
程序内使用缓存,减少查库次数

mysql部署在阿里云上,如何找到最慢的sql语句

可以使用 SHOW PROCESSLIST 查看当前运行的 SQL 语句,或者使用慢查询日志(slow query log)来记录执行时间超过一定阈值的查询。

用过gorm,如果一张上百万的数据的表,要新建一个字段的索引,如何保证线上的服务尽量少的被影响

异步索引创建和分片索引创建:
异步索引创建:在后台任务或低峰时段执行索引创建,以避免阻塞应用程序操作。
分片索引创建:将索引创建任务分成多个小批次,以减少对数据库的瞬时压力。

MYSQL 索引有什么用?索引的数据结构说一下。什么情况下会失效?

Redis

redis是单线程的还是多线程的?为什么快?适用于什么场景?

redis 是单线程的、快是因为数据存储在内存中、读写速度快
适用场景:缓存、会话存储、实时数据分析、消息队列、分布式锁

redis基本数据类型

字符串、哈希、列表、集合、有序集合、流

相关推荐

  1. Golang 端面

    2024-07-15 00:46:01       21 阅读
  2. 游戏客户端面

    2024-07-15 00:46:01       38 阅读
  3. 游戏客户端面

    2024-07-15 00:46:01       41 阅读
  4. 游戏客户客户端面

    2024-07-15 00:46:01       30 阅读
  5. 【每日前端面】2024-03-09

    2024-07-15 00:46:01       29 阅读
  6. 腾讯WXG前端面

    2024-07-15 00:46:01       44 阅读
  7. 【每日前端面】2024-03-10

    2024-07-15 00:46:01       37 阅读

最近更新

  1. docker php8.1+nginx base 镜像 dockerfile 配置

    2024-07-15 00:46:01       67 阅读
  2. Could not load dynamic library ‘cudart64_100.dll‘

    2024-07-15 00:46:01       71 阅读
  3. 在Django里面运行非项目文件

    2024-07-15 00:46:01       58 阅读
  4. Python语言-面向对象

    2024-07-15 00:46:01       69 阅读

热门阅读

  1. 印度标普基金关门,继续套利美元债LOF

    2024-07-15 00:46:01       20 阅读
  2. 基于深度学习的点云平滑

    2024-07-15 00:46:01       19 阅读
  3. 【网络编程】poll函数

    2024-07-15 00:46:01       18 阅读
  4. 19. 删除链表的倒数第 N 个结点

    2024-07-15 00:46:01       16 阅读
  5. PyMysql error : Packet Sequence Number Wrong - got 1 expected 0

    2024-07-15 00:46:01       20 阅读
  6. TCP和UDP知识点

    2024-07-15 00:46:01       21 阅读
  7. 【C++】指针学习 知识点总结+代码记录

    2024-07-15 00:46:01       19 阅读
  8. 游戏开发面试题1

    2024-07-15 00:46:01       17 阅读
  9. 利率债与信用债的区别及其与债券型基金的关系

    2024-07-15 00:46:01       19 阅读
  10. 域名信息的收集

    2024-07-15 00:46:01       19 阅读