【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 时,需要注意以下几点:

  1. Channel 是引用类型,可以像 Slice 和 Map 一样传递给函数。

  2. 默认情况下,Channel 是无缓冲的,只有当有 Goroutine 准备好接收数据时,发送操作才会成功。如果发送操作没有被接收,发送的 Goroutine 将会阻塞。

  3. 通过 make() 函数创建带缓冲的 Channel 时,可以指定缓冲区的大小。在缓冲区没有被填满之前,发送操作不会阻塞。

  4. Channel 支持多路复用,可以使用 select 语句在多个 Channel 上进行选择和等待。

  5. 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 语法会在编译时,转换为 makechan64makechan

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/387586.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Miniconda快速安装conda

关注B站可以观看更多实战教学视频&#xff1a;hallo128的个人空间 安装官方网址&#xff1a;https://docs.anaconda.com/miniconda/#quick-command-line-install 1. Miniconda for Windows curl https://repo.anaconda.com/miniconda/Miniconda3-latest-Windows-x86_64.exe -o …

【LeetCode】56. 区间合并

区间合并 题目描述&#xff1a; 以数组 intervals 表示若干个区间的集合&#xff0c;其中单个区间为 intervals[i] [starti, endi] 。请你合并所有重叠的区间&#xff0c;并返回 一个不重叠的区间数组&#xff0c;该数组需恰好覆盖输入中的所有区间 。 示例 1&#xff1a; …

捷径,这世上有没有捷径

Q&#xff1a;大师&#xff0c;这个世界上有没有捷径&#xff1f; A&#xff1a;有呀&#xff0c;有捷径呀 Q&#xff1a;大师&#xff0c;那我要怎么走&#xff1f; A&#xff1a;你错啦&#xff0c;不要想着走捷径&#xff0c;因为捷径不是用来走的&#xff0c;捷径是用来飞的…

SNN系列论文阅读:梦开始的地方

论文地址:https://igi-web.tugraz.at/people/maass/psfiles/85a.pdf 1. nn分类 一开始论文将nn分为三类, 1. 最初的MP多层感知机 2. 加入非线性激活, 并可以反向传播训练的神经网络 3. SNN 注意分类不是普通的fc网络,rnn网络和snn网络 2. 理解脉冲 LIF模型,全称Leak…

CUDA_Occupancy_Calculator计算公式

CUDA_Occupancy_Calculator计算公式

【设计模式:单例模式】

单例模式的特点&#xff1a; 单例类只允许一个实例单例类必须自己创造自己的唯一实例单例类必须给所有其他对象提供这一实例 单例模式底层如何实现&#xff1a; 私有化构造函数&#xff0c;类外部无法创造类对象&#xff0c;实现了单例类只允许有一个实例对象的特点类定义中含有…

【技术支持案例】使用S32K144+NSD8381驱动电子膨胀阀

文章目录 1. 前言2. 问题描述3. 理论分析3.1 NSD8381如何连接电机3.2 S32K144和NSD8381的软件配置 4.测试验证4.1 测试环境4.2 测试效果4.3 测试记录 1. 前言 最近有客户在使用S32K144NSD8381驱动电子膨胀阀时&#xff0c;遇到无法正常驱动电子膨胀阀的情况。因为笔者也是刚开…

c#中使用数据验证器

前言 在很多情况下&#xff0c;用户的输入不一定满足我们的设计要求&#xff0c;需要验证输入是否正确&#xff0c;传统的方案是拿到控件数据进行逻辑判定验证后&#xff0c;给用户弹窗提示。这种方法有点职责延后的感觉&#xff0c;数据视图层应该很好的处理用户的输入。使用…

如何处理selenium Webdriver中的文本框?

文本框或字段在整个网页中广泛使用,本文将介绍如何在Java中使用Selenium Webdriver处理文本框。可以有各种文本字段,我们将尝试包括其中的大多数,并执行各种操作,如清除和输入文本。 我们将使用我们的Selenium游乐场网站- testkru,与各种文本框进行交互。您也可以使用同一…

后端采用SpringBoot框架开发的:ADR药物不良反应智能监测系统源码,用于监测和收集药品在使用过程中发生的不良反应的系统

ADR药物不良反应智能监测系统是一套用于监测和收集药品在使用过程中发生的不良反应&#xff08;Adverse Drug Reaction, ADR&#xff09;的系统。该系统基于医院临床数据中心&#xff0c;运用信息技术实现药品不良反应的智能监测、报告管理、知识库查询、统计分析等功能&#x…

厚积薄发,详解 IoTeX 2.0 如何推动 DePIN 赛道迈向新台阶

背 景 DePIN 是加密货币行业的一个新兴垂直领域&#xff0c;也是本轮牛市最重要的叙事之一。DePIN 通常通过发行和分配代币来激励参与者&#xff0c;用户可以通过提供资源、维护网络、参与治理等方式获得代币奖励并产生直接的经济收益&#xff0c;从而重新洗牌财富分配方…

Java线程阻塞:原因

Java线程阻塞&#xff1a;原因 1. sleep()2. suspend() 和 resume()&#xff08;不推荐&#xff09;3. yield()4. wait() 和 notify()/notifyAll() &#x1f496;The Begin&#x1f496;点点关注&#xff0c;收藏不迷路&#x1f496; 线程阻塞是一个重要的概念&#xff0c;它决…

17K star!30秒偷走你的声音,开源声音克隆工具

现在的AI发展越来越快&#xff0c;生成一段语音不是难事&#xff0c;那如果生成的是你自己的声音&#xff0c;你觉得如何&#xff1f; 今天我们分享一款开源的声音克隆工具&#xff0c;只需30秒的一般音源&#xff0c;他就可以偷走你的声音&#xff0c;它就是&#xff1a;Open…

前端开发不得不知道的那些事

文章目录 一、技能提升篇vueuseJavaScript中文网JavaScript.infoRxJsWeb安全学习书栈网码农之家 二、UI篇iconfont&#xff1a;阿里巴巴矢量图标库IconPark3dicons美叶UndrawError 404摹克 三、CSS篇You-need-to-know-cssCSS TricksAnimate.cssCSS ScanCSS Filter 四、颜色篇中…

视觉SLAM第一讲

第一讲-预备知识 SLAM是什么&#xff1f; SLAM&#xff08;Simultaneous Localization and Mapping&#xff09;是同时定位与地图构建。 它是指搭载特定传感器的主体&#xff0c;在没有环境先验信息的情况下&#xff0c;于运动过程中建立环境的模型&#xff0c;同时估计自己…

KVM虚拟机迁移

KVM虚拟机迁移 KVM虚拟机迁移&#xff0c;是将某一虚拟机上的环境和软件完全复制到另一台物理机上继续运行。KVM虚拟机迁移可以优化系统负载、重新规划KVM虚拟机布局并简化KVM虚拟机的管理维护工作。 KVM虚拟机迁移的主要应用场景如下所示。 当一台KVM宿主机的负载比较高时&am…

C++重载左移运算符

通过重载左移运算符&#xff0c;可以实现cout << p;直接输出类对象的各个属性。 其只能使用全局函数重载。 注意cout的定义如下&#xff1a; _EXPORT_STD extern "C" __PURE_APPDOMAIN_GLOBAL _CRTDATA2_IMPORT ostream cout; 也就是说我们一直用来输出的c…

公布一批脸书爬虫(facebook)IP地址,真实采集数据

一、数据来源&#xff1a; 1、这批脸书爬虫&#xff08;facebook&#xff09;IP来源于尚贤达猎头公司网站采集数据&#xff1b; ​ 2、数据采集时间段&#xff1a;2023年10月-2024年7月&#xff1b; 3、判断标准&#xff1a;主要根据用户代理是否包含“facebook”和IP核实。…

《程序猿入职必会(4) · Vue 完成 CURD 案例 》

&#x1f4e2; 大家好&#xff0c;我是 【战神刘玉栋】&#xff0c;有10多年的研发经验&#xff0c;致力于前后端技术栈的知识沉淀和传播。 &#x1f497; &#x1f33b; CSDN入驻不久&#xff0c;希望大家多多支持&#xff0c;后续会继续提升文章质量&#xff0c;绝不滥竽充数…

【C语言】指针基础知识理解【续】

1. ⼆级指针 指针变量也是变量&#xff0c;是变量就有地址&#xff0c;那指针变量的地址存放在哪⾥&#xff1f;这就是 ⼆级指针 。 1.1 引入二级指针 由于一级指针已经很熟悉&#xff0c;这里就不再赘述&#xff0c;这里我们重点探讨二级指针 下面先简单使用一个二级指针看…