1. 进程、线程、协程
进程:资源分配和CPU调度的基本单位
线程:CPU调度的基本单位,线程除了有一些自己的必要的堆栈空间之外,其它的资源都是共享的线程中的,共享的资源包括:
1.所有线程共享相同的虚拟地址空间,即它们可以访问同样的代码段、数据段和堆栈段。
2.文件描述符:进程打开的文件描述符是进程级别的资源,所以同一个进程中的线程可以共享打开的文件描述符,这意味着它们可以同时读写同一个文件。
3.全局变量:全局变量是进程级别的变量,因此可以被同一个进程中的所有线程访问和修改。
4.静态变量:静态变量也是进程级别的变量,在同一个进程中的线程之间共享内存空间。
5.进程ID、进程组ID
独占的资源:
1、线程ID
2、寄存器组的值
3、线程堆栈
4、错误返回码
5、信号屏蔽码
6、线程的优先级
协程:用户态的线程,可以通过用户程序创建、删除。协程切换时不需要切换内核态。
1.1 协程与线程的区别:
1.线程是操作系统的概念,而协程是程序级的概念。线程由操作系统调度执行,每个线程都有自己的执行上下文,包
括程序计数器、寄存器等。而协程由程序自身控制。
2.多个线程之间通过切换执行的方式实现并发。线程切换时需要保存和恢复上下文,涉及到上下文切换的开销。而协
程切换时不需要操作系统的介入,只需要保存和恢复自身的上下文,切换开销较小。
3.线程是抢占式的并发,即操作系统可以随时剥夺一个线程的执行权。而协程是合作式的并发,协程的执行权由程序
自身决定,只有当协程主动让出执行权时,其他协程才会得到执行机会。
1.2 线程的优点
1.创建一个新线程的代价要比创建一个新进程小的多
2.线程之间的切换相较于进程之间的切换需要操作系统做的工作很少
3.线程占用的资源要比进程少很多
4.能充分利用多处理器的可并行数量
5.等待慢速 IO操作结束以后,程序可以执行其他的计算任务
1.3 缺点:
- 性能损失( 一个计算密集型线程是很少被外部事件阻塞的,无法和其他线程共享同一个处理器,当计算密集型的线程的数量比可用的处理器多,那么就有可能有很大的性能损失,这里的性能损失是指增加了额外的同步和调度开销,二可用资源不变。)
- 健壮性降低(线程之间是缺乏保护性的。在一个多线程程序里,因为时间上分配的细微差距或者是共享了一些不应该共享的变量而造成不良影响的可能影响是很大的。)
- 缺乏访问控制( 因为进程是访问控制的基本粒度,在一个线程中调用某些OS函数会对整个进程造成影响 。)
- 编程难度提高(编写和 调试一个多线程程序比单线程困难的多。)
1.4 有栈协程和无栈协程
- 有栈协程:把局部变量放入到新开的空间上,golang的实现,类似于内核态线程的实现,不同协程间切换还是要切换对应的栈上下文,只是不用陷入内核
- 无栈协程:直接把局部变量放入系统栈上,js、c++、rust那种await、async实现,主要原理就是闭包+异步,换句话说,其实就是协程的上下文都放到公共内存中,协程切换时,使用状态机来切换,就不用切换对应的上下文了,因为都在堆里的。比有栈协程都要轻量许多。
4. Context原理
context在很大程度上利用了通道在close时会通知所有监听它的协程这一特性来实现。每一个派生出的子协程都会创建一个新的退出通道,组织好context之间的关系即可实现继承链上退出的传递。
context使用场景:
- RPC调用
- PipeLine
- 超时请求
- HTTP服务器的request互相传递数据
5. golang内存对齐机制
为了能让CPU可以更快的存取到各个字段,Go编译器会帮你把struct结构体做数据的对齐。所谓的数据对齐,是指内存地址是所存储数据大小(按字节为单位)的整数倍,以便CPU可以一次将该数据从内存中读取出来。编译器通过在结构体的各个字段之间填充一些空白已达到对齐的目的。
不同硬件平台占用的大小和对齐值都可能是不一样的,每个特定平台上的编译器都有自己的默认“对齐系数”,32位系统对齐系数是4,64位系统对齐系数是8 不同类型的对齐系数也可能不一样,使用Go 语言中的unsafe.Alignof函数可以返回相应类型的对齐系数,对齐系数都符合2^n这个规律,最大也不会超过8
5.1 对齐原则:
- 结构体变量中成员的偏移量必须是成员变量大小和成员对齐系数两者最小值的整数倍
- 整个结构体的地址必须是最大字节和编译器默认对齐系数两者最小值的整数倍(结构体的内存占用是1/4/8/16 byte…)
- struct{}放在结构体中间不进行对齐,放在结构体最后一个字段则要根据最大字节和编译器默认对齐系数两者最小值来进行字段对齐
type C struct {
a struct{}
b int64
c int64
}
type D struct {
a int64
b struct{}
c int64
}
type E struct {
a int64
b int64
c struct{}
}
type F struct {
a int32
b int32
c struct{}
}
func main() {
fmt.Println(unsafe.Sizeof(C{})) // 16
fmt.Println(unsafe.Sizeof(D{})) // 16
fmt.Println(unsafe.Sizeof(E{})) // 24
fmt.Println(unsafe.Sizeof(F{})) // 12
}
6. Golang中new和make的区别?
var
声明值类型的变量时,系统会默认为他分配内存空间,并赋该类型的零值
如果是指针类型或者引用类型的变量,系统不会为它分配内存,默认是nil
。
make
仅用来分配及初始化类型为slice
、map
、chan
的数据;new 可以分配任意类型的数据。new
分配返回的是指针,即类型*Type
。make 返回类型本身,即Type
。new
分配的空间被清零。make
分配空间后,会进行初始化;
7. Golang中,array和slice的区别
注意:Go的slice不是线程安全的。 切片是基于数组实现的,底层是数组,可以理解为对底层数组的抽象
- 数组长度不同
- 数组初始化必须指定长度,并且长度就是固定的
- 切片的长度是不固定的,可以追加元素,在追加时可能使切片的容量增大
- 函数传参不同
- 数组是值类型,将一个数组赋值给另一个数组时,传递的是一份深拷贝,函数传参操作都会复制整个数组数据,会占用额外的内存,函数内对数组元素值的修改,不会修改原数组内容。
- 切片是引用类型,将一个切片赋值给另一个切片时,传递的是一份浅拷贝,函数传参操作不会拷贝整个切片,只会复制len和cap,底层共用同一个数组,不会占用额外的内存,函数内对数组元素值的修改,会修改原数组内容。
- 计算数组长度方式不同
- 数组需要遍历计算数组长度,时间复杂度为O(n)
- 切片底层包含len字段,可以通过len计算切片长度,时间复杂度为O(1) Golang Slice的底层实现
切片是基于数组实现的,它的底层是数组,它自己本身非常小,可以理解为对底层数组的抽象。因为基于数组实现,所以它的底层的内存是连续分配的,效率非常高,还可以通过索引获得数据,可以迭代以及垃圾回收优化。 切片本身并不是动态数组或者数组指针。它内部实现的数据结构通过指针引用底层数组,设定相关属性将数据读写操作限定在指定的区域内。切片本身是一个只读对象,其工作机制类似数组指针的一种封装。
切片对象非常小,是因为它是只有3个字段的数据结构:
- 指向底层数组的指针
- 切片的长度
- 切片的容量
[
这3个字段,就是Go语言操作底层数组的元数据。
8. Golang Slice的扩容机制,有什么注意点?
扩容前后的Slice是否相同?
情况一:
原数组还有容量可以扩容(实际容量没有填充完),这种情况下,扩容以后的数组还是指向原来的数组,对一个切片的操作可能影响多个指针指向相同地址的Slice。
情况二:
原来数组的容量已经达到了最大值,再想扩容, Go 默认会先开一片内存区域,把原来的值拷贝过来,然后再执行 append() 操作。这种情况丝毫不影响原数组。
要复制一个Slice,最好使用Copy函数。 详细见 slice扩容
9. Golang的map特点
Go中的map是一个指针,占用8个字节,指向hmap结构体,map底层是基于哈希表+链地址法存储的。
map的特点:
1.键不能重复
2.键必须可哈希(目前我们已学的数据类型中,可哈希的有:int/bool/float/string/array)
3.无序
9.1 Golang的map为什么是无序的?
使用range多次遍历map时输出的key和vabue 的顺序可能不同。这是Go语言的设计者们有意为之,旨在提示开发者们,Go底层实现并不保证map遍历顺序稳定,请大家不要依赖range遍历结果顺序
主要原因有2点:
- map在遍历时,并不是从固定的0号bucket开始遍历的,每次遍历,都会从一个随机值序号的bucket,再从其中随机的cell开始遍历
- map遍历时,是按序遍历bucket,同时按序遍历bucket中和其overflow bucket中的cell。但是map在扩容后,会发生key的搬迁,这造成原来落在一个buket中的Key,搬迁后,有可能会落到其他bucket中了,从这个角度看,遍历map的结果就不可能是按照原来的顺序了
map本身是无序的,且遍历时顺序还会被随机化,如果想顺序遍历map,需要对 map key先排序,再按照key 的顺序遍历map。
see map
9.2 Map的负载因子是6.5?
什么是负载因子?
负载因子(load factor),用于衡量当前哈希表中空间占用率的核心指标,也就是每个bucket桶存储的平均 元素个数。
负载因子=哈希表存储的元素个数/桶个数
10. 空 struct{} 占用空间么?用途是什么?
空结构体 struct{} 实例不占据任何的内存空间。
用途: 1.将 map 作为集合(Set)使用时,可以将值类型定义为空结构体,仅作为占位符使用即可。 2.不发送数据的信道(channel) 使用 channel 不需要发送任何的数据,只用来通知子协程(goroutine)执行任务,或只用来控制协程并发度。 3.结构体只包含方法,不包含任何的字段
11. golang值接收者和指针接收者的区别
golang函数与方法的区别是,方法有一个接收者。
如果方法的接收者是指针类型,无论调用者是对象还是对象指针,修改的都是对象本身,会影响调用者
如果方法的接收者是值类型,无论调用者是对象还是对象指针,修改的都是对象的副本,不影响调用者
12. golang中指针的作用
- 传递大对象
- 修改函数外部变量
- 动态分配内存
- 函数返回指针
13. go struct能不能比较
答:
可以能,也可以不能。
因为go存在不能使用==
判断类型:map
、slice
,如果struct
包含这些类型的字段,则不能比较。
这两种类型也不能作为map的key。
14. Golang的方法有什么特别之处
函数的定义声明没有接收者。
方法的声明和函数类似,他们的区别是:方法在定义的时候,会在func和方法名之间增加一个参数,这个参数就是接收者,这样我们定义的这个方法就和接收者绑定在了一起,称之为这个接收者的方法。
Go语言里有两种类型的接收者:值接收者和指针接收者。
使用值类型接收者定义的方法,在调用的时候,使用的其实是值接收者的一个副本,所以对该值的任何操作,不会影响原来的类型变量。——-相当于形式参数
如果我们使用一个指针作为接收者,那么就会其作用了,因为指针接收者传递的是一个指向原值指针的副本,指针的副本,指向的还是原来类型的值,所以修改时,同时也会影响原来类型变量的值。
15. Golang可变参数
函数方法的参数,可以是任意多个,这种我们称之为可以变参数,比如我们常用的fmt.Println()
这类函数,可以接收一个可变的参数。
可以变参数,可以是任意多个。我们自己也可以定义可以变参数,可变参数的定义,在类型前加上省略号…即可。
func main() {
print("1", "2", "3")
}
func print(a ...interface{}) {
for _, v := range a {
fmt.Print(v)
}
fmt.Println()
}
16. Go import三种方式
除了直接import,常见的import有三种特殊形式,分别是前面加下划线(_),加点(.),加别名。
**加下划线:**有些时候我们并不需要把整个包都导入进来,仅仅是是希望它执行init()函数而已。这个时候就可以使用 import _ 引用该包。即:使用【import _ 包路径】只是引用该包,仅仅是为了调用init()函数,所以无法通过包名来调用包中的其他函数。
**加点(.):**import和引用的包名之间加点(.)操作的含义就是这个包导入之后在调用这个包的函数时,可以省略前缀的包名。
**别名:**别名操作顾名思义可以把包命名成另一个用起来容易记忆的名字。
16.1 包前是下划线_
当导入一个包时,该包下的文件里所有init函数都会被执行,但是有时我们仅仅需要使用init函数而已并不希望把整个包导入(不使用包里的其他函数)
每个包都可以有任意多个init函数,这些init函数都会在main函数之前执行。init函数通常用来做初始化变量、设置包或者其他需要在程序执行前的引导工作。比如上面我们讲的需要使用_
空标志符来导入一个包的目的,就是想执行这个包里的init
函数。
我们以数据库的驱动为例,Go语言为了统一关于数据库的访问,使用databases/sql抽象了一层数据库的操作,可以满足我们操作MYSQL、Postgre等数据库,这样不管我们使用这些数据库的哪个驱动,编码操作都是一样的,想换驱动的时候,就可以直接换掉,而不用修改具体的代码。
这些数据库驱动的实现,就是具体的,可以由任何人实现的,它的原理就是定义了init函数,在程序运行之前,把实现好的驱动注册到sql包里,这样我们就使用使用它操作数据库了。
package mysql
import (
"database/sql"
)
func init() {
sql.Register("mysql", &MySQLDriver{})
}
因为我们只是想执行这个mysql包的init方法,并不想使用这个包,所以我们在导入这个包的时候,需要使用_
重命名包名,避免编译错误。
import "database/sql"
import _ "github.com/go-sql-driver/mysql"
db, err := sql.Open("mysql", "user:password@/dbname")
16.2 包前是点.
import(. "fmt")
这个点操作的含义就是这个包导入之后在你调用这个包的函数时,你可以省略前缀的包名,也就是前面你调用的fmt.Println("hello world")
可以省略的写成Println("hello world")
17. Golang的参数传递、引用类型
Go语言中所有的传参都是值传递(传值),都是一个副本,一个拷贝。因为拷贝的内容有时候是非引用类型(int、string、struct等这些),这样就在函数中就无法修改原内容数据;有的是引用类型(指针、map、slice、chan等这些),这样就可以修改原内容数据。
Golang的引用类型包括 slice、map 和 channel。它们有复杂的内部结构,除了申请内存外,还需要初始化相关属性。内置函数 new 计算类型大小,为其分配零值内存,返回指针。而 make 会被编译器翻译成具体的创建函数,由其分配内存和初始化成员结构,返回对象而非指针。
18. Golang接口接收规则
实体类型以值接收者实现接口的时候,不管是实体类型的值,还是实体类型值的指针,都实现了该接口。 实体类型以指针接收者实现接口的时候,只有指向这个类型的指针才被认为实现了该接口
Methods Receivers | Values |
---|---|
(t T) | T and *T |
(t *T) | *T |
19. Golang中除了加Mutex锁以外还有哪些方式安全读写共享变量?
Golang
中Goroutine
可以通过 Channel
进行安全读写共享变量。