本文发布于Cylon的收藏册,转载请著名原文链接~
数据结构
数据类型总结
Go语言将数据类型分为四类:基础类型、复合类型、引用类型和接口类型。
基础数据类型包括:
- 基础类型:
- 布尔型、整型、浮点型、复数型、字符型、字符串型、错误类型。
- 复合数据类型包括:
- 指针、数组、切片、字典、通道、结构体、接口。
什么是反射
在计算机科学领域,反射是指一类应用,它们能够自描述和自控制。
在go中,编译时不知道类型的情况下,可更新变量、运行时查看值、调用方法以及直接对他们的布局进行操作的机制,称为反射。
场景:无法透视一个未知类型的时候,这时候就需要有反射来帮忙你处理,反射使用TypeOf和ValueOf函数从接口中获取目标对象的信息,轻松完成目的。
rune与byte的区别
- byte是uint8、rune为uint32,一个仅限于ascii码的值,一个支持更多的值。rune比byte能表达更多的数。
- golang默认使用utf8编码,一个中文占用3字节,一个utf8数字占用1字节,utf8字母占用1字节
切片
切片的扩容:切片扩容,一般方式:上一次容量的2倍,超过1024字节,每次扩容上一次的1/4
切片的截取:在截取时,capacity 不能超过原slice的 capacity
new() 与 make() 的区别
new(T)
和 make(T, args)
是Go语言内建函数,用来分配内存,但适用的类型不用。
new
函数用于分配指定类型的零值对象,并返回指向其内存地址的指针。例如,new(int)
将分配一个类型为int
且值为0的对象,并返回一个指向该地址的指针。可以使用*
运算符访问指针指向的值。make
函数用于创建和初始化内置类型(如map
、slice
、channel
)的数据结构,并返回其指针。它比new
函数更加复杂很多,因为它需要知道类型的大小和结构,以便为其分配内存并初始化其字段或元素。例如,make(map[string]int)
将创建一个空的map
。它有一个string
类型的键和一个int
类型的值。
nil切片和空切片指向的地址一样吗?
- nil切片和空切片指向的地址==不一样==。nil空切片引用数组指针地址为0(无指向任何实际地址)
- 空切片的引用数组指针地址是有的,且固定为一个值
什么是Receiver
Golang的Receiver是绑定function
到特定type
成为其method
的一个参数,即一个function
加了receiver
就成为一个type的method。
构体方法跟结构体指针方法的区别(Receiver和指针Receiver的区别)
- T 的方法集仅拥有 T Receiver。
- *T 方法集则包含全部方法 (
Receiver
+*Receiver
)。
sync.once
-
是 Golang package 中使方法只执行一次的对象实现,作用与 init 函数类似。但也有所不同
-
init 函数是在文件包首次被加载的时候执行,且只执行一次
-
sync.Onc 是在代码运行中需要的时候执行,且只执行一次
-
当一个函数不希望程序在一开始的时候就被执行的时候,我们可以使用 sync.Once
-
实现:sync.Once 的源码实现非常简单,采用的是双重检测锁机制 (Double-checked Locking),是并发场景下懒汉式单例模式的一种实现方式
- 首先判断 done 是否等于 0,等于 0 则表示回调函数还未被执行
- 加锁,确保并发安全
- 在执行函数前,二次确认 done 是否等于 0,等于 0 则执行
- 将 done 置 1,同时释放锁 疑问一: 为什么不使用乐观锁 CAS 简单的来说就是 f() 的执行结果最终可能是不成功的,所以你会看到现在采用的是双重检测锁机制来实现,同时需要等 f() 执行完成才修改 done 值 疑问二: 为什么读取 done 值的方式没有统一 比较 done 是否等于 0,为什么有的地方用的是 atomic.LoadUint32,有的地方用的却是 o.done。主要原因是 atomic.LoadUint32 可以保证原子读取到 done 值,是并发安全的,而在 doSlow 中,已经加锁了,那么临界区就是并发安全的,使用 o.done 就可以来读取值就可以了
原子操作和互斥锁的区别
文档:https://zhuanlan.zhihu.com/p/147618421
GMP模型
文档:https://zhuanlan.zhihu.com/p/261590663 文档:https://juejin.cn/post/6844904104343388168
-
G:goroutine
-
M:Machine,内核线程
-
P:Logical Processor,处理器;代表了M所需要的上下文环境
- runtime.GOMAXPROCS (numLogicalProcessors)可以设置多少个处理器,go 1.5开始,默认是CPU核数;
- 实际运行时P和CPU核心数并无任何关联,P最大不超过256;P可以理解为并行度的多少,也就是说当前最多只能有P个线程在运行;(是不是很像线程池)
- P一旦初始化了,就不能修改了 三者关系
-
M的数量和P不一定匹配,可以设置很多M,M和P绑定后才可运行,多余的M处于休眠状态。
-
P包含一个LRQ(Local Run Queue)本地运行队列,这里面保存着P需要执行的协程G的队列。
-
除了每个P自身保存的G的队列外,调度器还拥有一个全局的G队列GRQ(Global Run Queue),这个队列存储的是所有未分配的协程G。
go func()执行流程
- 创建一个G对象,加入到本地队列或全局队列;
- 如果还有空闲的P,则创建一个M;
- M会启动一个底层线程,并结合P,循环执行G;
- P执行G的顺序是,先从本地队列找,没有则到全局队列找(一次性转移[全局G个数/P个数]),再到其他P中找(一次性转移一半);
- G是执行顺序是按照队列顺序的;
- P管理着G队列,但是G要运行,还需要M的绑定;
- runtime.GOMAXPROCS只会影响P的数量,不会影响M的数量;
- P和M的关系,就好比用户线程和内核线程的N:M模型;
- 没有足够的M关联P时,会创建M;在runtime执行系统监控或垃圾回收等任务的时候也会导致新的M的创建。 所以,runtime.GOMAXPROCS只是类似线程池的大小设置而已;
- 当然,go也可以通过runtime/debug.SeMaxThreads限制操作系统线程数;SetMaxThreads主要用于限制程序无限制的创造线程导致的灾难。目的是让程序在干掉操作系统之前,先干掉它自己。
- goroutine是按照抢占式调度的,一个goroutine最多执行10ms就会换作下一个;
死锁
死锁是:多个进(线)程是相互竞争的关系,并且互持资源,相互等待,这样产生的永久阻塞的现象称为死锁。
死锁产生的原因:
- 互斥
- 占有且等待
- 不可抢占
- 循环等待
死锁如何解决:死锁的发生很难通过人为干预来解决,只能避免(打破死锁产生的条件)
- 互斥:线程安全是通过互斥来实现的(无法干预)
- 占有且等待:申请资源时获取所有所需资源
- 不可抢占:占用资源的进程在进一步申请其他资源时,如申请不到主动释放已占有的资源
- 循环等待:按需预防,对所需资源进行排序,按照大小依次申请
Refer deadlock
Go中产生死锁的原因:
- 无缓冲;解决:缓冲或先读后写
- 缓冲已满(只写不读);解决,需要有消费端
- 读写互斥;(读写加锁导致一段阻塞变成死锁)
- 未初始化的channel(读,写,关闭)
- 多线程只要保证个线程的执行,可以允许死锁
- 如主进程只读不写造成阻塞,这种情况在没有子线程情况下是死锁
slice和map区别
- slice是有序的,map是无序的,在每次迭代时,无法确定其顺序
- slice有容量,map没有容量,map是由go内部控制的数据结构
- slice可以使用appen(),map不可以
- slice和map都是引用类型,当作为参数传递时共享相同地址
如何复制slice、map和interface?
这些类型的变量是内存引用类型,可以使用内置函数 copy()
来完成复制 Refer to
a := []int{1, 2}
b := []int{3, 4}
check := a
copy(a, b)
fmt.Println(a, b, check)
// Output: [3 4] [3 4] [3 4]
什么是goroutine
go的多线程是包含在运行时内的一种机制,用的模型是两级,即runtime帮助申请和释放线程,而这个gorutine可以为一对一,也可以为一对多,即操作系统中的 “两级模型”(Two-Level
)是严格意义上的多对多模型,可以为单个用户线程专门一对一绑定内核线程的能力的模型
用户线程的缺点:
- 用户级线程与操作系统的集成度不高;如用空闲线程调度进程,阻塞其线程发起 I/O 的进程,即使该进程有其他线程可以运行,以及有锁的线程取消调度进程。
- 用户级线程需要非阻塞系统调用,否则,当一个线程阻塞,即使进程中还有可运行的线程,整个进程也会在内核中阻塞。例如,如果一个线程导致页面错误,则进程阻塞。
- 用户线程和操作系统内核之间缺乏协调性;无论进程有 1 个线程还是 1000 个线程,都仅能获得一个CPU时间片。由每个线程主动将控制权交给其他线程。
- 由于进程时资源分配的最小单位,多线程情况下,每个线程得到的时间片较少,执行会较慢。
C语言的线程和Goroutine的区别主要表现在以下几个方面
- 实现方式不同:C语言的线程是由操作系统内核来实现的,而Goroutine则是通过Go语言的runtime来实现的。
- 内存分配方式不同:C语言的线程需要在内存中分配一定的栈空间,而Goroutine则通过自动扩展栈来实现。
- 调度方式不同:C语言的线程是由操作系统内核来调度的,而Goroutine是通过Go语言的runtime自己实现的调度器来调度的。
- 轻量级:Goroutine是轻量级的线程,一个Goroutine只需要2KB的栈空间,而C语言的线程需要占用更多的内存空间。
- 效率高:由于Goroutine是由Go语言的runtime来实现的,因此它具有非常高的执行效率和并发性能,比C语言的线程更加高效。
总的来说,Goroutine是Go语言的并发特性中非常重要的一部分,通过Goroutine可以非常方便地实现高效的并发程序。而C语言的线程则更多地是由操作系统来实现,对于一些需要最大化利用机器性能的场景会更为适合。
应用
Go语言创建TCP连接
- conn.dial
- conn.write/read
本文发布于Cylon的收藏册,转载请著名原文链接~
链接:https://www.oomkill.com/2021/10/interview-go/
版权:本作品采用「署名-非商业性使用-相同方式共享 4.0 国际」 许可协议进行许可。