【Golang 面试 - 基础题】每日 5 题(八)
✍个人博客:Pandaconda-CSDN博客
📣专栏地址:http://t.csdnimg.cn/UWz06📚专栏简介:在这个专栏中,我将会分享 Golang 面试中常见的面试题给大家~
❤️如果有收获的话,欢迎点赞👍收藏📁,您的支持就是我创作的最大动力💪
36. Go Ma p 遍历为什么是无序的?
使用 range 多次遍历 map 时输出的 key 和 value 的顺序可能不同。这是 Go 语言的设计者们有意为之,旨在提示开发者们,Go 底层实现并不保证 map 遍历顺序稳定,请大家不要依赖 range 遍历结果顺序。
主要原因有 2 点:
-
map 在遍历时,并不是从固定的 0 号 bucket 开始遍历的,每次遍历,都会从一个随机值序号的 bucket,再从其中随机的 cell 开始遍历。
-
map 遍历时,是按序遍历 bucket,同时按需遍历 bucket 中和其 overflow bucket 中的 cell。但是 map 在扩容后,会发生 key 的搬迁,这造成原来落在一个 bucket 中的 key,搬迁后,有可能会落到其他 bucket 中了,从这个角度看,遍历 map 的结果就不可能是按照原来的顺序了。
map 本身是无序的,且遍历时顺序还会被随机化,如果想顺序遍历 map,需要对 map key 先排序,再按照 key 的顺序遍历 map。
func TestMapRange(t *testing.T) {m := map[int]string{1: "a", 2: "b", 3: "c"}t.Log("first range:")for i, v := range m {t.Logf("m[%v]=%v ", i, v)}t.Log("second range:")for i, v := range m {t.Logf("m[%v]=%v ", i, v)}// 实现有序遍历var sl []int// 把 key 单独取出放到切片for k := range m {sl = append(sl, k)}// 排序切片sort.Ints(sl)// 以切片中的 key 顺序遍历 map 就是有序的了for _, k := range sl {t.Log(k, m[k])}
}
37. G o Map 为什么是非线程安全的?
map 默认是并发不安全的,同时对 map 进行并发读写时,程序会 panic,原因如下:
Go 官方在经过了长时间的讨论后,认为 Go map 更应适配典型使用场景(不需要从多个 goroutine 中进行安全访问),而不是为了小部分情况(并发访问),导致大部分程序付出加锁代价(性能),决定了不支持。
场景: 2 个协程同时读和写,以下程序会出现致命错误:fatal error: concurrent map writes
package mainimport ("fmt""time"
)func main() {s := make(map[int]int)for i := 0; i < 100; i++ {go func(i int) {s[i] = i}(i)}for i := 0; i < 100; i++ {go func(i int) {fmt.Printf("map第%d个元素值是%d", i, s[i])}(i)}time.Sleep(1 * time.Second)
}
如果想实现 map 线程安全,有两种方式:
1. 使用读写锁 map
+ sync.RWMutex
package mainimport ("fmt""sync""time"
)func main() {var lock sync.RWMutexs := make(map[int]int)for i := 0; i < 100; i++ {go func(i int) {lock.Lock()s[i] = ilock.Unlock()}(i)}for i := 0; i < 100; i++ {go func(i int) {lock.RLock()fmt.Printf("map第%d个元素值是%d
", i, s[i])lock.RUnlock()}(i)}time.Sleep(1 * time.Second)
}
2. 使用 Go 提供的 sync.Map
package mainimport ("fmt""sync""time"
)func main() {var m sync.Mapfor i := 0; i < 100; i++ {go func(i int) {m.Store(i, i)}(i)}for i := 0; i < 100; i++ {go func(i int) {v, ok := m.Load(i)fmt.Printf("Load: %v, %v
", v, ok)}(i)}time.Sleep(1 * time.Second)
}
38. G o map 和 sync.Map 谁的性能好,为什么?
Go 语言的 sync.Map
支持并发读写,采取 “空间换时间” 的机制,冗余了两个数据结构,分别是:read 和 dirty。
type Map struct {mu Mutexread atomic.Value // readOnlydirty map[interface{}]*entrymisses int
}
对比原始 map:
和原始 map + RWLock 的实现并发的方式相比,减少了加锁对性能的影响。它做了一些优化:可以无锁访问 read map,而且会优先操作 read map,倘若只操作 read map 就可以满足要求,那就不用去操作 write map (dirty),所以在某些特定场景中它发生锁竞争的频率会远远小于 map + RWLock 的实现方式。
优点:
适合读多写少的场景。
缺点:
写多的场景,会导致 read map 缓存失效,需要加锁,冲突变多,性能急剧下降。
39. 介绍一下 Channel
在 Go 语言中,Channel(通道)是用于多个 Goroutine 之间进行通信的一种机制,通过它们可以安全地传递数据。
Channel 是一种类型,可以使用内置的 make() 函数来创建它们。创建 Channel 时,需要指定它们可以传输的数据类型。
使用 Channel 时,可以在 Goroutine 之间传递数据,通过它们可以进行同步和异步的操作。在使用 Channel 时,需要注意以下几点:
-
Channel 是引用类型,可以像 Slice 和 Map 一样传递给函数。
-
默认情况下,Channel 是无缓冲的,只有当有 Goroutine 准备好接收数据时,发送操作才会成功。如果发送操作没有被接收,发送的 Goroutine 将会阻塞。
-
通过 make() 函数创建带缓冲的 Channel 时,可以指定缓冲区的大小。在缓冲区没有被填满之前,发送操作不会阻塞。
-
Channel 支持多路复用,可以使用 select 语句在多个 Channel 上进行选择和等待。
-
Channel 可以用于控制 Goroutine 的执行,例如通过关闭 Channel 来通知 Goroutine 退出。
使用 Channel 可以帮助解决并发编程中的一些常见问题,例如避免竞态条件、协调不同 Goroutine 之间的操作等。
40. Go channel 的底层实现原理?
概念:
Go 中的 channel 是一个队列,遵循先进先出的原则,负责协程之间的通信(Go 语言提倡不要通过共享内存来通信,而要通过通信来实现内存共享,CSP (Communicating Sequential Process) 并发模型,就是通过 goroutine 和 channel 来实现的)
使用场景:
-
停止信号监听
-
定时任务
-
生产方和消费方解耦
-
控制并发数
底层数据结构:
通过 var 声明或者 make 函数创建的 channel 变量是一个存储在函数栈帧上的指针,占用 8 个字节,指向堆上的 hchan 结构体。
源码包中 src/runtime/chan.go
定义了 hchan 的数据结构:
hchan 结构体:
type hchan struct {closed uint32 // channel是否关闭的标志elemtype *_type // channel中的元素类型// channel分为无缓冲和有缓冲两种。// 对于有缓冲的channel存储数据,使用了 ring buffer(环形缓冲区) 来缓存写入的数据,本质是循环数组// 为啥是循环数组?普通数组不行吗,普通数组容量固定更适合指定的空间,弹出元素时,普通数组需要全部都前移// 当下标超过数组容量后会回到第一个位置,所以需要有两个字段记录当前读和写的下标位置buf unsafe.Pointer // 指向底层循环数组的指针(环形缓冲区)qcount uint // 循环数组中的元素数量dataqsiz uint // 循环数组的长度elemsize uint16 // 元素的大小sendx uint // 下一次写下标的位置recvx uint // 下一次读下标的位置// 尝试读取channel或向channel写入数据而被阻塞的goroutinerecvq waitq // 读等待队列sendq waitq // 写等待队列lock mutex //互斥锁,保证读写channel时不存在并发竞争问题
}
等待队列:
双向链表,包含一个头结点和一个尾结点。
每个节点是一个 sudog 结构体变量,记录哪个协程在等待,等待的是哪个 channel,等待发送/接收的数据在哪里。
type waitq struct {first *sudoglast *sudog
}
type sudog struct {g *gnext *sudogprev *sudogelem unsafe.Pointerc *hchan...
}
操作:
创建
使用 make(chan T, cap)
来创建 channel,make 语法会在编译时,转换为 makechan64
和 makechan
。
func makechan64(t *chantype, size int64) *hchan {if int64(int(size)) != size {panic(plainError("makechan: size out of range"))}return makechan(t, int(size))
}
创建 channel 有两种,一种是带缓冲的 channel,一种是不带缓冲的 channel。
// 带缓冲
ch := make(chan int, 3)
// 不带缓冲
ch := make(chan int)
创建时会做一些检查:
-
元素大小不能超过 64K。
-
元素的对齐大小不能超过 maxAlign 也就是 8 字节。
-
计算出来的内存是否超过限制。
创建时的策略:
-
如果是无缓冲的 channel,会直接给 hchan 分配内存。
-
如果是有缓冲的 channel,并且元素不包含指针,那么会为 hchan 和底层数组分配一段连续的地址。
-
如果是有缓冲的 channel,并且元素包含指针,那么会为 hchan 和底层数组分别分配地址。
发送
发送操作,编译时转换为 runtime.chansend
函数。
func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool
阻塞式:
调用 chansend 函数,并且 block = true。
ch <- 10
非阻塞式:
调用 chansend 函数,并且 block = false。
select {case ch <- 10:...default
}
向 channel 中发送数据时大概分为两大块:检查和数据发送,数据发送流程如下:
-
如果 channel 的读等待队列存在接收者 goroutine
-
将数据直接发送给第一个等待的 goroutine, 唤醒接收的 goroutine。
-
-
如果 channel 的读等待队列不存在接收者 goroutine
-
如果循环数组 buf 未满,那么将会把数据发送到循环数组 buf 的队尾。
-
如果循环数组 buf 已满,这个时候就会走阻塞发送的流程,将当前 goroutine 加入写等待队列,并挂起等待唤醒。
-
接收
发送操作,编译时转换为runtime.chanrecv
函数。
func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool)
阻塞式:
调用 chanrecv 函数,并且 block = true。
<ch
v := <ch
v, ok := <ch
// 当channel关闭时,for循环会自动退出,无需主动监测channel是否关闭,可以防止读取已经关闭的channel,造成读到数据为通道所存储的数据类型的零值
for i := range ch {fmt.Println(i)
}
非阻塞式:
调用 chanrecv 函数,并且 block = false。
select {case <-ch:...default
}
向 channel 中接收数据时大概分为两大块,检查和数据发送,而数据接收流程如下:
-
如果 channel 的写等待队列存在发送者 goroutine
-
如果是无缓冲 channel,直接从第一个发送者 goroutine 那里把数据拷贝给接收变量,唤醒发送的 goroutine。
-
如果是有缓冲 channel(已满),将循环数组 buf 的队首元素拷贝给接收变量,将第一个发送者 goroutine 的数据拷贝到 buf 循环数组队尾,唤醒发送的 goroutine。
-
-
如果 channel 的写等待队列不存在发送者 goroutine
-
如果循环数组 buf 非空,将循环数组 buf 的队首元素拷贝给接收变量。
-
如果循环数组 buf 为空,这个时候就会走阻塞接收的流程,将当前 goroutine 加入读等待队列,并挂起等待唤醒。
-
关闭
关闭操作,调用 close 函数,编译时转换为 runtime.closechan
函数。
close(ch)
func closechan(c *hchan)
案例分析:
package main
import ("fmt""time""unsafe"
)
func main() {// ch是长度为4的带缓冲的channel// 初始hchan结构体重的buf为空,sendx和recvx均为0ch := make(chan string, 4)fmt.Println(ch, unsafe.Sizeof(ch))go sendTask(ch)go receiveTask(ch)time.Sleep(1 * time.Second)
}
// G1是发送者
// 当G1向ch里发送数据时,首先会对buf加锁,然后将task存储的数据copy到buf中,然后sendx++,然后释放对buf的锁
func sendTask(ch chan string) {taskList := []string{"this", "is", "a", "demo"}for _, task := range taskList {ch <- task //发送任务到channel}
}
// G2是接收者
// 当G2消费ch的时候,会首先对buf加锁,然后将buf中的数据copy到task变量对应的内存里,然后recvx++,并释放锁
func receiveTask(ch chan string) {for {task := <-ch //接收任务fmt.Println("received", task) //处理任务}
}
总结 hchan 结构体的主要组成部分有四个:
-
用来保存 goroutine 之间传递数据的循环数组:buf
-
用来记录此循环数组当前发送或接收数据的下标值:sendx 和 recvx
-
用于保存向该 chan 发送和从该 chan 接收数据被阻塞的 goroutine 队列: sendq 和 recvq
-
保证 channel 写入和读取数据时线程安全的锁:lock
相关文章:

【Golang 面试 - 基础题】每日 5 题(八)
✍个人博客:Pandaconda-CSDN博客 📣专栏地址:http://t.csdnimg.cn/UWz06 📚专栏简介:在这个专栏中,我将会分享 Golang 面试中常见的面试题给大家~ ❤️如果有收获的话,欢迎点赞👍收藏…...
LeetCode 算法:在排序数组中查找元素的第一个和最后一个位置 c++
原题链接🔗:在排序数组中查找元素的第一个和最后一个位置 难度:中等⭐️⭐️ 题目 给你一个按照非递减顺序排列的整数数组 nums,和一个目标值 target。请你找出给定目标值在数组中的开始位置和结束位置。 如果数组中不存在目标…...

会话存储、本地存储,路由导航守卫、web会话跟踪、JWT生成token、axios请求拦截、响应拦截
1、会话存储、本地存储 前端浏览器中存储用户信息,会话存储、本地存储、cookie 会话存储(sessionStorage):会话期间存储,关闭浏览器后,数据就会销毁 sessionStorage.setItem("account",resp.d…...
strcmp库函数原型
int strcmp(const char *str1, const char *str2) {unsigned const char *s1 (unsigned const char *) str1;unsigned const char *s2 (unsigned const char *) str2;while (*s1 && *s1 *s2) {s1;s2;}return *s1 - *s2; }while (*s1 && *s1 *s2) 一直循环&…...
在 Vue.js 项目中延迟加载子组件
在 Vue.js 中,当父组件渲染时,子组件的生命周期钩子函数会立即执行,即使这些子组件并未显示。这是因为 Vue.js 会在渲染父组件时实例化所有引用的子组件。为了避免不必要的函数执行,我们可以通过使用 v-if 指令和异步组件延迟加载…...
何时会用到设计模式、七大设计原则介绍
以下关于b站尚硅谷相关设计模式视频的总结 设计模式的重要性: 代码重用性(相同的代码,不用编写很多次)、 可读性(编程规范,便于其他程序员阅读和理解)、 可扩展性(增加新功能时&am…...

编程语言发展历史:赋值与相等运算符的变迁历程
本文摘取自笔者书稿《编程语言发展历史》 赋值运算符是编程语言最基础的运算符,其发展历史也非常有趣。最早的赋值语句就是使用等号“”来表示,一些语言为了让赋值运算在数学形式上更加严谨(形如“x x 1”的表达式在数学上不成立࿰…...

求职Leetcode题目(2)
1.柱状图中最大的矩形 据说这是2024年字节二面的题目,我感觉这道题跟接雨水有点类似,最重要的思路还是要找到什么时候能形成矩形的这么个情况,某个范围的矩形的高度,是由最短的柱形来决定的。 我们先整理一下,解决这道…...
深入探索 Postman:使用 API 性能测试优化你的 Web 服务
引言 在当今快速发展的互联网时代,Web 服务的性能至关重要。API 作为服务之间的桥梁,其性能直接影响到整个应用的响应速度和用户体验。Postman,作为一个多功能的 API 开发工具,提供了强大的性能测试功能,帮助开发者评…...

校车购票小程序的设计
管理员账户功能包括:系统首页,个人中心,学生管理,我的乘车信息管理,车辆信息管理,座位管理,系统管理 微信端账号功能包括:系统首页,车辆信息,我的 开发系统…...

拯救数据危机!2024年最受欢迎的数据恢复软件评测
现在大家快速传输资料的方式都变成了电子档,有些数据是存储在电脑上,有些存储在手机,有的存储在U盘甚至其他一些电子设备上。电子设备存储数据方便,丢失数据也总在意料之外。很多时候我们多学会一个工具,比如转转大师数…...

记一次因为在html两个地方引入vue.js导致组件注入失败的问题
这个问题我遇到两次了,是在恼火,不对,三次了,我如果不做这个笔记,我确定我还会遇到第三次。 尾部这个去掉就行 因为头部有了 遇到这种bu g好恼火,解决了又怎么样呢?重蹈覆辙的滋味不好受...
Postman中的智慧重试:API测试用例的错误处理与重试逻辑设置
Postman中的智慧重试:API测试用例的错误处理与重试逻辑设置 在API测试过程中,错误处理和重试逻辑是确保测试准确性和可靠性的重要环节。Postman提供了多种功能来处理测试中可能出现的错误,并允许自定义重试逻辑以适应不同的测试场景。本文将…...
docker部署本地词向量模型
开源项目:GitHub - huggingface/text-embeddings-inference: A blazing fast inference solution for text embeddings models 1. 下载词向量模型 参考我的另一篇博客:langchain 加载本地词向量模型 2. 部署词向量模型 就三行命令 model/data/BAAI/…...

接口自动化中对于文件上传的处理方法
正常的接口自动化基本都是json的格式,对于文件上传是一种特殊的格式是表单格式针对这种表单格式在接口自动化中怎么处理,主要通过工作中使用的一个实际的例子进行分享 举例:web上需要导入一个文件实现相关的功能,主要通过两个接口…...

Java高频面试题分享
文章目录 1. 策略模式怎么控制策略的选取1.1 追问:如果有100种策略呢?1.2 追问:什么情况下初始化Map 2. 什么是索引?什么时候用索引?2.1 追问:怎么判断系统什么时候用量比较少2.2 追问:如何实时…...

kvm虚拟化平台部署
kvm虚拟化平台部署 kvm概念简介 kvm自linux2.6版本以后就整合到内核中,因此可以看做是一个原生架构. kvm虚拟化架构 硬件底层提供物理层面的硬件支持 linux(host),就相当于这个架构中的宿主机,上面运行了多个虚拟机。…...
利用arthas热更新class文件
利用arthas热更新class文件 背景:发现一个bug,家里难以复现,需要在现场环境更新几行代码验证。 arthas-boot version: 3.7.1 java -jar arthas-boot.jar启动arthas 1、利用arthas的sc命令查找确定类名称 sc com.**2、反编译为java文件 …...

天机学堂 第四天 高并发优化总结
前端每隔15秒就发起一次请求,将播放记录写入数据库。 但问题是,提交播放记录的业务太复杂了,其中涉及到大量的数据库操作: 如何进行优化 单机并发能力 变同步为异步 合并写请求 提高单机并发:优化SQL,尽…...

Canva收购Leonardo.ai,增强生成式AI技术能力
每周跟踪AI热点新闻动向和震撼发展 想要探索生成式人工智能的前沿进展吗?订阅我们的简报,深入解析最新的技术突破、实际应用案例和未来的趋势。与全球数同行一同,从行业内部的深度分析和实用指南中受益。不要错过这个机会,成为AI领…...

【Axure高保真原型】引导弹窗
今天和大家中分享引导弹窗的原型模板,载入页面后,会显示引导弹窗,适用于引导用户使用页面,点击完成后,会显示下一个引导弹窗,直至最后一个引导弹窗完成后进入首页。具体效果可以点击下方视频观看或打开下方…...
Android Wi-Fi 连接失败日志分析
1. Android wifi 关键日志总结 (1) Wi-Fi 断开 (CTRL-EVENT-DISCONNECTED reason3) 日志相关部分: 06-05 10:48:40.987 943 943 I wpa_supplicant: wlan0: CTRL-EVENT-DISCONNECTED bssid44:9b:c1:57:a8:90 reason3 locally_generated1解析: CTR…...

使用VSCode开发Django指南
使用VSCode开发Django指南 一、概述 Django 是一个高级 Python 框架,专为快速、安全和可扩展的 Web 开发而设计。Django 包含对 URL 路由、页面模板和数据处理的丰富支持。 本文将创建一个简单的 Django 应用,其中包含三个使用通用基本模板的页面。在此…...
<6>-MySQL表的增删查改
目录 一,create(创建表) 二,retrieve(查询表) 1,select列 2,where条件 三,update(更新表) 四,delete(删除表…...
PHP和Node.js哪个更爽?
先说结论,rust完胜。 php:laravel,swoole,webman,最开始在苏宁的时候写了几年php,当时觉得php真的是世界上最好的语言,因为当初活在舒适圈里,不愿意跳出来,就好比当初活在…...
MySQL 隔离级别:脏读、幻读及不可重复读的原理与示例
一、MySQL 隔离级别 MySQL 提供了四种隔离级别,用于控制事务之间的并发访问以及数据的可见性,不同隔离级别对脏读、幻读、不可重复读这几种并发数据问题有着不同的处理方式,具体如下: 隔离级别脏读不可重复读幻读性能特点及锁机制读未提交(READ UNCOMMITTED)允许出现允许…...
DockerHub与私有镜像仓库在容器化中的应用与管理
哈喽,大家好,我是左手python! Docker Hub的应用与管理 Docker Hub的基本概念与使用方法 Docker Hub是Docker官方提供的一个公共镜像仓库,用户可以在其中找到各种操作系统、软件和应用的镜像。开发者可以通过Docker Hub轻松获取所…...
Qt Widget类解析与代码注释
#include "widget.h" #include "ui_widget.h"Widget::Widget(QWidget *parent): QWidget(parent), ui(new Ui::Widget) {ui->setupUi(this); }Widget::~Widget() {delete ui; }//解释这串代码,写上注释 当然可以!这段代码是 Qt …...

华为OD机试-食堂供餐-二分法
import java.util.Arrays; import java.util.Scanner;public class DemoTest3 {public static void main(String[] args) {Scanner in new Scanner(System.in);// 注意 hasNext 和 hasNextLine 的区别while (in.hasNextLine()) { // 注意 while 处理多个 caseint a in.nextIn…...
C++中string流知识详解和示例
一、概览与类体系 C 提供三种基于内存字符串的流,定义在 <sstream> 中: std::istringstream:输入流,从已有字符串中读取并解析。std::ostringstream:输出流,向内部缓冲区写入内容,最终取…...