golang面试 - 深拷贝与浅拷贝

深拷贝是指两个变量,如果其内部引用其他的地址,那么深拷贝会将其内部的指针所指向的值重新分配地址用于存储这个值,因此他们内部的一些引用指针会变化。 浅拷贝是指两个变量,实际上其内部引用部分的地址并没有变化。 常见的浅拷贝,像slice的赋值,map的赋值 s := []int{1,2,3} s1 = s 如上所示,s1实际上就是对s的浅拷贝 对于深拷贝,我们在代码里应该这么使用 s := []int{1,2,3} var s1 []int copy(s, &s1)

November 4, 2022 · nobject

golang面试 - 米哈游第三方编制一面复盘

代码题 以n个协程,去输出[]string{“a”,“b”,“c”,“d”,“e”, “f”, “g”}。 代码题真得很简单,自己手生的很,竟然没写出来。 func main() { var wg sync.WaitGroup sem := make(chan struct{}, 5) // 创建一个大小为5的信号量 letters := []string{"a", "b", "c", "d", "e", "f", "g"} for _, letter := range letters { sem <- struct{}{} // 尝试写入信号量,如果已满,则阻塞 wg.Add(1) go func(l string) { defer wg.Done() fmt.Println(l) time.Sleep(5 * time.Second) <-sem // 认领信号量 }(letter) } wg.Wait() 八股文 go的map可不可以一边遍历,一边delete。为什么?怎么解决 在Go语言中,不能够同时遍历和修改(包括删除)map,因为在遍历过程中如果对map进行了修改或删除,会导致遍历结果不确定性,甚至会导致程序崩溃。 如果一边遍历一边删除map中的元素,会导致遍历器遍历到的元素和map中实际存在的元素不一致,可能会漏掉一些元素或者重复遍历某些元素,从而影响程序的正确性。 为了解决这个问题,可以采用以下方案: 遍历map时,将需要删除的key保存到一个slice中,然后遍历slice进行删除操作。 m := map[int]string{1:"a", 2:"b", 3:"c"} keys := make([]int, 0, len(m)) for k := range m { keys = append(keys, k) } for _, k := range keys { delete(m, k) } 使用并发安全的map,如sync....

November 4, 2022 · nobject

golang 面试 - gin

协程和线程的区别 goroutine初始栈大小,上下文如何切换,存在哪里 - 初始栈:2kb GMP说一下,P有多少个,G有多少个,M有多少个。系统调用的时候M会如何,网络IO的时候M会怎样。 - P的个数默认等于cpu个数,可以通过GOMAXPROCS环境变量更改 - M的默认值10000,go程序启动时,会设置M的最大数量,默认10000.runtime/debug中的SetMaxThreads函数,设置M的最大数量,实际上是按需创建 网络io的时候g的状态为变成Gwaiting状态,M可继续执行下一个状态为Grunable的g,如果没有,则为sleeping 系统调用的时候,g的状态为Gsyscall,M为变为sleep状态 -------------------------------------------------------------------- Go的GC说一下,扫描从哪里发起? golang 的所有类型都对应一个 _type 结构,可以在 runtime/type.go 里面找到 -------------------------------------------------------------------- Go的内存分配说一下 Go有哪些坑?for range为什么会有坑? - 因为使用 Go的map是如何实现的,是并发安全的吗?sync.map是如何实现的? - 不是并发安全的 --------------------------------------------------------------------- CAS知道吗,在Go的哪些地方用到了? 原子操作主要由硬件提供支持,锁一般是由操作系统提供支持,比起直接使用锁,使用CAS这个过程不需要形成临界区和创建互斥量,所以会比使用锁更加高效。 从硬件层面来实现原子操作,有两种方式: 1、总线加锁:因为CPU和其他硬件的通信都是通过总线控制的,所以可以通过在总线加LOCK#锁的方式实现原子操作,但这样会阻塞其他硬件对CPU的访问,开销比较大。 2、缓存锁定:频繁使用的内存会被处理器放进高速缓存中,那么原子操作就可以直接在处理器的高速缓存中进行而不需要使用总线锁,主要依靠缓存一致性来保证其原子性。 缺点:只能对一个变量原子性操作 sync.Mutex库中使用到对应的操作CAS ———————————————— sync.pool是如何实现的 channel的优点是什么,如何实现的(腾讯云) Go 通过 channel 实现 CSP 通信模型,主要用于 goroutine 之间的消息传递和事件通知。 操作 nil channel closed channel not nil, not closed channel close panic panic 正常关闭 读 <- ch 阻塞 读到对应类型的零值 阻塞或正常读取数据。缓冲型 channel 为空或非缓冲型 channel 没有等待发送者时会阻塞 写 ch <- 阻塞 panic 阻塞或正常写入数据。非缓冲型 channel 没有等待接收者或缓冲型 channel buf 满时会被阻塞 ———————————————— 原文作者:JaguarJack 转自链接:https://learnku....

November 4, 2022 · nobject

golang 面试 - 互斥锁, 互斥锁的前世今生

坑:Unlock 方法可以被任意的 goroutine 调用释放锁,即使是没持有这个互斥锁的 goroutine,也可以进行这个操作。这是因为,Mutex 本身并没有包含持有这把锁的 goroutine 的信息,所以,Unlock 也不会对此进行检查。Mutex 的这个设计一直保持至 今。 互斥锁是怎么实现的 如果锁是空闲状态且没等待的goroutine,直接获取锁 正常模式: 当前的`goroutine`会与被唤醒的`goroutine`进行抢锁,如果锁未抢到,则会进入自旋状态,自旋多次后,还未竞争到锁,如果是第1次未获取到锁,则加入到等待队列的尾部,如果超过阈值1毫秒,那么,将这个Mutex设置为饥饿模式。 饥饿模式:饥饿模式下,`mutex`将锁直接交给等待队列的最前面的`goroutine`,新来的`goroutine`不会尝试获取锁,即使锁没有被持有,也不会去抢,也不会`spin`,会加入到等待队列的尾部. 如果当前等待的`goroutine`是最后一个`waiter`,没有其他等待的`goroutine` 或者此`goroutine`等待的时间小于1ms,退出饥饿模式。 初版(最普通的做法) 数据结构 type Mutex struct { key int32 // 锁是否被持有的标识 sema int32 // 信号量专用,用以阻塞/唤醒goroutine } 具体实现 // 保证成功在val上增加delta的值 func xadd(val *int32, delta int32) (new int32) { for { v := *val if cas(val, v, v+delta) { return v + delta } } panic("unreached") } // 请求锁 func (m *Mutex) Lock() { if xadd(&m....

November 4, 2022 · nobject

golang 面试 - 内存模型

通俗定义 内存模型描述的是并发环境中多goroutine 读相同变量的时候,变量的可见性条件,具体点说,就是指,在什么条件下, goroutine 在读取一个变量的值的时候,能够看到其它 goroutine 对这个变量进行的写的结果。 Go内存模型限定了一些条件 满足这些条件 才能让变量 安全地在不同的goroutine之间读写。换句话说就是如何保证在一个 goroutine中看到在另一个goroutine修改的变量的值,如果程序中修改数据时有其他 goroutine 同时读取,那么必须将读取串行化。为了串行化访问,请使用 channel 或其他同步原语,例如 sync 和 sync/atomic 来保护数据。 happens before 谁前谁后 多个go routine的可见性问题,如果存在多个go routine访问数据的时候,必须序列化访问 单个go routine当中编写 的代码 总是会按照我们的编写代码的顺序来执行 当然这是符合我们的习惯的 当然这并不表示编辑器在编译的时候不会对我们的程序进行指令重排 而是说只会在不影响语言规范的情况下对go routine的行为定义的时候,编辑器和CPU才会对读取与写入的顺序进行重排 happens before(先行发生)的定义 当下面条件满足时,对变量 v 的读操作 r 是被允许看到对 v 的写操作 w 的: 1. r 不先行发生于 w 2. 在 w 后 r 前没有对 v 的其他写操作 为了保证对变量 v 的读操作 r 看到对 v 的写操作 w,要确保 w 是 r 允许看到的唯一写操作。即当下面条件满足时,r 被保证看到 w: 1....

November 4, 2022 · nobject

golang 面试 - 读写锁

golang中读写锁的原理 Go 标准库中的 RWMutex 设计是 Write-preferring(写优先) 方案。 如果已经有一个 writer 在等待请求锁的话,它会阻止新来的请求锁的 reader获取到锁,所以优先保障 writer。 当然,如果有一些 reader 已经请求了锁的话,新请求的 writer 也会等待已经存在的 reader 都释放锁之后才能获取。 所以,写优先级设计中的优先权是针对新来的请求而言的。这种设计主要避免了 writer 的饥饿问题。 Go RwMutex使用了readerCount记录目前读请求的总数量 将readerCount进行取反操作 这也是此字段除了标记reader数量的第二个功能,进行写锁标记 此时将取反的r值交给readerWait代表仍需要等待释放锁的reader的数量 如果该数量为0 那么代表不需要等待则直接获取写锁即可,否则就将writer挂起阻塞直至readerWait中的所有读请求全部释放掉,然后RUlock唤醒该写请求 写锁释放时,会将readerCount再加回来,所以负的代表有写请求在,正的代表只有读请求 如果你遇到可以明确区分 reader 和 writer goroutine 的场景,且有大量的并发读、少量的并发写,并且有强烈的性能需求,你就可以考虑使用读写锁 RWMutex 替换 Mutex。 Read-preferring:读优先的设计可以提供很高的并发性,但是,在竞争激烈的情况下可能会导致写饥饿。这是因为,如果有大量的读,这种设计会导致只有所有的读都释放了锁之后,写才可能获取到锁。 Write-preferring:写优先的设计意味着,如果已经有一个 writer 在等待请求锁的话,它会阻止新来的请求锁的 reader获取到锁,所以优先保障 writer。当然,如果有一些 reader 已经请求了锁的话,新请求的 writer 也会等待已经存在的 reader 都释放 锁之后才能获取。所以,写优先级设计中的优先权是针对新来的请求而言的。这种设计主要避免了 writer 的饥饿问题。 不指定优先级:这种设计比较简单,不区分 reader 和 writer 优先级,某些场景下这种不指定优先级的设计反而更有效,因为第一类优先级会导致写饥饿,第二类优先级可能 Go 标准库中的 RWMutex 设计是 Write-preferring 方案。一个正在阻塞的 Lock 调用会排除新的 reader 请求到锁。 数据结构 type RWMutex struct { w Mutex // 互斥锁解决多个writer的竞争 writerSem uint32 // writer信号量 readerSem uint32 // reader信号量 readerCount int32 // reader的数量 readerWait int32 // writer等待完成的reader的数量 } const rwmutexMaxReaders = 1 << 30 源码...

November 4, 2022 · nobject

golang面试 - gc

相关文章 https://draveness.me/golang/docs/part3-runtime/ch07-memory/golang-garbage-collector/ https://www.cnblogs.com/luozhiyun/p/14564903.html https://www.kancloud.cn/aceld/golang/1958308#Go_V13mark_and_sweep_21 https://www.bilibili.com/video/BV1wz4y1y7Kd?p=14&spm_id_from=pageDriver https://cloud.tencent.com/developer/article/1756163 https://blog.haohtml.com/archives/26358 https://mp.weixin.qq.com/s/5xjH-LJ53XiNm2sMNQZiGQ GCMark 标记准备阶段,为并发标记做准备工作,启动写屏障 STW GCMark 扫描标记阶段,与赋值器并发执行,写屏障开启 并发 GCMarkTermination 标记终止阶段,保证一个周期内标记任务完成,停止写屏障 STW GCoff 内存清扫阶段,将需要回收的内存归还到堆中,写屏障关闭 并发 GCoff 内存归还阶段,将过多的内存归还给操作系统,写屏障关闭 并发 Go 语言中对 GC 的触发时机存在两种形式: 主动触发,通过调用 runtime.GC 来触发 GC,此调用阻塞式地等待当前 GC 运行完毕。 被动触发,分为两种方式: 使用系统监控,当超过两分钟没有产生任何 GC 时,强制触发 GC。 使用步调(Pacing)算法,其核心思想是控制内存增长的比例。

November 4, 2022 · nobject

golang面试 - gmp

相关文章 https://mp.weixin.qq.com/s/jIWe3nMP6yiuXeBQgmePDg https://tonybai.com/2017/06/23/an-intro-about-goroutine-scheduler/ https://tonybai.com/2020/03/21/illustrated-tales-of-go-runtime-scheduler/ https://draveness.me/golang/docs/part3-runtime/ch06-concurrency/golang-goroutine/ https://www.kancloud.cn/aceld/golang/1958305#GoroutineGMP_164 https://segmentfault.com/a/1190000023869478 https://www.luozhiyun.com/archives/448 https://zboya.github.io/post/go_scheduler/ https://www.bilibili.com/video/BV19r4y1w7Nx?p=2&spm_id_from=pageDriver https://mp.weixin.qq.com/s/XiqVIR3U5ZmRD7xwJZKipA 进程,线程,协程的区别 进程: 进程是程序的一次执行过程,是程序在执行过程中的分配和管理资源的基本单位,每个进程都有自己的地址空间,进程是系统进行资源分配和调度的一个独立单位。 每个进程都有自己的独立内存空间,由于进程比较重量,占据独立的内存,所以上下文进程间的切换开销(栈、寄存器、虚拟内存、文件句柄等)比较大,但相对比较稳定安全。 线程:在同一个进程中的不同线程共享相同的地址空间(虚拟内存,代表切换不需要切换页表),每个线程有独立的栈区,程序计数器(执行指令的信息),栈指针以及函数运行的寄存器。 初始栈大小8M,ulimit - s可以查看。调度由系统调度。从用户态切换到内核态,上下文切换消耗相对大 协程:用户态线程,不由内核管理,由go的底层调度器控制,协程初始栈大小2K。由用户调度 goroutine初始栈大小,上下文如何切换,存在哪里 协程初始栈大小2K,上下文由g0去调度找到对应可以运行的goroutine,存在gorountine的gobuf中 组织下面试的语言 gmp是go为了提高并发能力实现的协程调度模型 g代表我们常说的协程,代码里的体现就是go关键字 m代表操作系统的线程,系统最大值默认为10000 p代表逻辑处理器,默认与cpu的核数想同,当然也可以通过runtime.GOMAXPROCS()来设置p的数量,m一般需要与p关联才能执行g。 调度策略: p会切换到g0栈上,执行调度函数,查找到可执行的goroutine,然后调用gogo函数,切换到goroutine的执行流程上。 1. 每隔61次调度轮回从全局队列找,避免全局队列中的g被饿死。 2. 从p.runnext获取g,从p的本地队列中获取。 3. 调用 findrunnable 找g,找不到的话就将m休眠,等待唤醒。 findrunnable 查找G的过程: 1. 调用 runqget ,尝试从P本地队列中获取G,获取到返回 2. 调用 globrunqget ,尝试从全局队列中获取G,获取到返回 3. 从网络IO轮询器中找到就绪的G,把这个G变为可运行的G 4. 如果不是所有的P都是空闲的,最多四次,随机选一个P,尝试从这P中偷取一些G,获取到返回 5. 上面都找不到G来运行,判断此时P是否处于 GC mark 阶段,如果是,那么此时可以安全的扫描和黑化对象和返回 gcBgMarkWorker 来运行, gcBgMarkWorker 是GC后代标记的goroutine。 6. 再次从全局队列中获取G,获取到返回 7. 再次检查所有的P,有没有可以运行的G 8. 再次检查网络IO轮询器 9. 实在找不到可运行的G了,那就调用 stopm 休眠吧 源码文件 runtime/asm_amd64....

November 4, 2022 · nobject

golang 可重入锁

可重入锁的应用场景,用同一段代码的多次调用(递归调用),方法体之内有锁嵌套。golang官方并不推荐使用可重入锁。但一些面试官比较喜欢让人去手撕可重入锁的代码。一般是java面试官,因为在java中,锁的概念非常丰富, 在golang中相对来说还是比较简单且实用的 什么是可重入锁 可重入锁又称为递归锁,是指在同一个线程在外层方法获取锁的时候,在进入该线程的内层方法时会自动获取锁,不会因为之前已经获取过还没释放而阻塞。 举个简单的例子: var mu = &sync.Mutex{} func A() { mu.Lock() fmt.Println("A") B() mu.Unlock() } func B() { mu.Lock() fmt.Println("B") mu.Unlock() } 以下的代码,在目前golang使用mutex的时候,会导致死锁的情况 fatal error: all goroutines are asleep - deadlock! 可重入锁的思路 记住持有锁的线程(golang中对应的是协程) 累计重入的次数 相关代码 type ReentrantMutex struct { mutex sync.Mutex recursion int64 // 这个goroutine 重入的次数 host int64 // 当前持有锁的goroutine id } func GoRoutineID() int64 { var buf [64]byte var s = buf[:runtime.Stack(buf[:], false)] s = s[len("goroutine "):] s = s[:bytes....

November 4, 2022 · nobject

golang 基础面试题

Go语言中是如何实现继承的? go语言没有像java,c++等传统的语言一样,有extends关键字用于继承 go语言实现继承可使用组合模式 type User struct { Name string } type Man struct { User } go struct 能不能比较 需要具体情况具体分析,如果struct中含有不能被比较的字段类型,就不能被比较,如果struct中所有的字段类型都支持比较,那么就可以被比较。 不可被比较的类型: ① slice,因为slice是引用类型,除非是和nil比较 ② map,和slice同理,如果要比较两个map只能通过循环遍历实现 ③ 函数类型 其他的类型都可以比较。 还有两点值得注意: 结构体之间只能比较它们是否相等,而不能比较它们的大小。 只有所有属性都相等而属性顺序都一致的结构体才能进行比较。 深拷贝和浅拷贝 深拷贝: 拷贝的是数据本身,创造一个新对象,新创建的对象与原对象不共享内存,新创建的对象在内存中开辟一个新的内存地址,新对象值修改时不会影响原对象值。 实现深拷贝的方式: copy(slice2, slice1); 遍历slice进行append赋值 浅拷贝∶拷贝的是数据地址,只复制指向的对象的指针,此时新对象和老对象指向的内存地址是一样的,新对象值修改时老对象也会变化。 实现浅拷贝的方式:引用类型的变量,默认赋值操作就是浅拷贝 如slice2 := slice1 make 与 new 的区别 new()对类型进行内存分配,入参为类型,返回为类型的指针,指向分配类型的内存地址 make()也是用于内存分配的,但是和new不同,它只用于channel、map以及切片的内存创建,而且它返回的类型就是这三个类型本身,而不是他们的指针类型,因为这三种类型就是引用类型,所以就没有必要返回他们的指针了。 注意,因为这三种类型是引用类型,所以必须得初始化,但是不是置为零值,这个和new是不一样的。 通过make创建对象 make只能创建slice 、channel、 map。 new和make对比: 1)make 只能用来分配及初始化类型为 slice、map、chan 的数据。new 可以分配任意类型的数据; 2)new 分配返回的是指针,即类型 *Type。make 返回引用,即 Type; 3)new 分配的空间被清零。make 分配空间后,会进行初始化; 4)make 函数只用于 map,slice 和 channel,并且不返回指针 array 与 slice的区别 go语言的引用类型有什么? 切片(slice)类型, map类型 ,管道(channel)类型 , 接口(interface)类型 获取不到锁会一直等待吗? 如果锁未被持有,直接获取锁 正常模式: 当前的goroutine会与被唤醒的goroutine进行抢锁,如果锁未抢到,则会进入自旋状态,自旋多次后,还未竞争到锁,如果是第1次未获取到锁,则加入到等待队列的尾部,如果超过阈值1毫秒,那么,将这个Mutex设置为饥饿模式。 饥饿模式:饥饿模式下,mutex将锁直接交给等待队列的最前面的goroutine,新来的goroutine不会尝试获取锁,即使锁没有被持有,也不会去抢,也不会spin,会加入到等待队列的尾部....

November 4, 2022 · nobject

golang 设计模式 - 单例模式

November 4, 2022 · nobject

Leetcode 5 最长回文子串

给你一个字符串 s,找到 s 中最长的回文子串。 示例 1: 输入:s = "babad" 输出:"bab" 解释:"aba" 同样是符合题意的答案。 示例 2: 输入:s = "cbbd" 输出:"bb" 说明: 1 <= s.length <= 1000 s 仅由数字和英文字母组成 1.暴力解法 // 暴力法 func longestPalindrome(s string) string { var left, right, length int for i := 0; i <= len(s)-1; i++ { for j := i + 1; j <= len(s)-1; j++ { if s[i] != s[j] { continue } isPalindrome := checkIsPalindrome(s, i, j) // 记录初始值与长度 if isPalindrome && (j-i+1) > length { left = i right = j length = j-i+1 } } } return string([]byte(s)[left:right+1]) } func checkIsPalindrome(s string, start int, end int) bool { for start < end { if s[start] !...

October 26, 2022 · nobject