Go语言小细节
结构体
- 结构体中允许存在匿名字段,即只有类型没有具体的变量名,但是一个结构体内只允许有一个相同类型的
- 结构体中字段大写开头表示可公开访问,小写表示私有(仅在当前结构体的包中可访问)
- 在编写结构体的 Tag 时,不要在key和value中添加空格,容错能力很差
//Student 学生
type Student struct {
ID int `json:"id"` //通过指定tag实现json序列化该字段时的key
Gender string //json序列化是默认使用字段名作为key
name string //私有不能被json包访问
}
- 在结构体的方法中,如果要修改 slice、array、map 这些成员属性,要注意,因为他们的指向都是地址, GO 中函数的参数虽然是一份拷贝,但是拷贝的仍然是一份地址,如果你在方法中不进行 copy,你在外部修改源数据也会影响成员的内部
package包
一个包的初始化过程是按照代码中引入的顺序来进行的,所有在该包中声明的 init 函数将被串行调用并且仅调用一次。每一个包的初始化的时候都是先执行依赖包中的声明的 init 函数再执行当前包中的 init 函数,确保在程序 main 函数开始执行时所有的依赖包都已经初始化完成。
接口
- 一般在接口命名时会 + er 作为单词的结尾
- 在实现接口的过程中,如果是值接收者,接口类型的变量时可以被 结构体的实例化地址或者对象变量赋值的,但如果是指针接收者,那么不能将非 结构体的引用赋值给接口变量
- 接口也可以和结构体一样进行组合,只要实现新结构体中的所有方法,即认为它实现了这个新的接口
- 空接口可以不必使用 type 关键字声明,空接口可以存储任何类型的值
- 只有当有两个或两个以上的具体类型必须以相同的方式进行处理时才需要定义接口。切记不要为了使用接口类型而增加不必要的抽象,导致不必要的运行时损耗。
并发
-
串行:无法同步,只有完成了上一步才能执行下一步
-
并行:同一时刻执行多个任务
-
并发:同一个时间段执行多个任务
-
进程:程序在操作系统中的一次执行过程,系统进行资源分配和调度的一个独立单位
-
线程:操作系统基于进程开启的轻量级进程,是操作系统调度执行的最小单元
-
协程:非操作系统提供而是由用户自行创建和控制的用户态"线程",比线程更轻量级
并发模型
- 线程&锁模型
- Actor模型
- CSP 模型
- Fork&Join模型
go的并发主要是通过基于 CSP
的 goroutine 和 channel 来实现,当然也支持使用传统的多线程共享内存的并发方式
一个 goroutine 会以一个很小的栈开始它的生命周期,一般只需要 2kb。区别于操作系统线程由系统内核进行调度,goroutine 是由 Go运行时(runtime)负责调度。例如 Go 运行时会只能的将 M 个 goroutine 合理地分配给 N 个操作系统线程,实现类型 m:n 的调度机制,不再需要 Go 开发者自行在代码层面维护一个线程池。
goroutine 的调度是Go语言运行时(runtime)层面的实现,是完全由 Go 语言本身实现的一套调度系统——go scheduler。它的作用是按照一定的规则将所有的 goroutine 调度到操作系统线程上执行,目前 Go 采用的调度策略是 GMP
模型。
- main goroutine如果执行完了,其他的 goroutine 没有执行完,也会被关闭,所以我们会使用 WaitGroup 等待协程的完成
- 多个 goroutine 的调度是随机的
动态栈
操作系统的线程一般都有固定的栈内存(通常为2MB),而 Go 语言中的 goroutine 非常轻量级,一个 goroutine 的初始栈空间很小(一般为2KB),所以在 Go 语言中一次创建数万个 goroutine 也是可能的。并且 goroutine 的栈不是固定的,可以根据需要动态地增大或缩小, Go 的 runtime 会自动为 goroutine 分配合适的栈空间。
goroutine调度
单从线程调度讲,Go语言相比起其他语言的优势在于OS线程是由OS内核来调度的, goroutine 则是由Go运行时(runtime)自己的调度器调度的,完全是在用户态下完成的, 不涉及内核态与用户态之间的频繁切换,包括内存的分配与释放,都是在用户态维护着一块大的内存池, 不直接调用系统的malloc函数(除非内存池需要改变),成本比调度OS线程低很多。 另一方面充分利用了多核的硬件资源,近似的把若干goroutine均分在物理线程上, 再加上本身 goroutine 的超轻量级,以上种种特性保证了 goroutine 调度方面的性能。
GOMAXPROCS
Go运行时的调度器使用GOMAXPROCS参数来确定需要使用多少个 OS 线程来同时执行 Go 代码。默认值是机器上的 CPU 核心数。例如在一个 8 核心的机器上,GOMAXPROCS 默认为 8。Go语言中可以通过runtime.GOMAXPROCS函数设置当前程序并发时占用的 CPU逻辑核心数。(Go1.5版本之前,默认使用的是单核心执行。Go1.5 版本之后,默认使用全部的CPU 逻辑核心数。)
channel
单纯地将函数并发执行是没有意义的。函数与函数间交换数据才能体现并发执行函数的意义。
虽然可以使用共享内存进行数据交换,但是共享内存在不同的 goroutine 中容易发生竞态问题。为了保证数据交换的正确性,很多并发模型中必须使用互斥量对内存进行加锁,这种做法势必造成性能问题。
Go 使用 CSP 并发模型,提倡通过通信共享内存,而不是通过共享内存实现通信
如果说 goroutine 是Go程序并发的执行体,channel就是它们之间的连接。channel是可以让一个 goroutine 发送特定值到另一个 goroutine 的通信机制。
Go 语言中的通道(channel)是一种特殊的类型。通道像一个传送带或者队列,总是遵循先入先出(First In First Out)的规则,保证收发数据的顺序。每一个通道都是一个具体类型的导管,也就是声明 channel 的时候需要为其指定元素类型。
注意:一个通道值是可以被垃圾回收掉的。通道通常由发送方执行关闭操作,并且只有在接收方明确等待通道关闭的信号时才需要执行关闭操作。它和关闭文件不一样,通常在结束操作之后关闭文件是必须要做的,但关闭通道不是必须的。
关闭后的通道有以下特点:
- 对一个关闭的通道再发送值就会导致 panic。
- 对一个关闭的通道进行接收会一直获取值直到通道为空。
- 对一个关闭的并且没有值的通道执行接收操作会得到对应类型的零值。
- 关闭一个已经关闭的通道会导致 panic。
无缓存通道
- 它需要等待接收后才不会造成死锁,如果没有人接收就会 deadlock
- 使用无缓冲通道进行通信将导致发送和接收的 goroutine 同步化。因此,无缓冲通道也被称为同步通道。
有缓存通道
只要通道的容量大于零,那么该通道就属于有缓冲的通道,通道的容量表示通道中最大能存放的元素数量。当通道内已有元素数达到最大容量后,再向通道执行发送操作就会阻塞,除非有从通道执行接收操作。就像你小区的快递柜只有那么个多格子,格子满了就装不下了,就阻塞了,等到别人取走一个快递员就能往里面放一个。
我们可以使用内置的len函数获取通道内元素的数量,使用cap函数获取通道的容量,虽然我们很少会这么做。
单向通道
<- chan int // 只接收通道,只能接收不能发送
chan <- int // 只发送通道,只能发送不能接收
Select多路复用
- 可处理一个或多个 channel 的发送/接收操作。
- 如果多个 case 同时满足,select 会随机选择一个执行。
- 对于没有 case 的 select 会一直阻塞,可用于阻塞 main 函数,防止退出。
并发安全和锁
有时候我们的代码中可能会存在多个 goroutine 同时操作一个资源(临界区)的情况,这种情况下就会发生竞态问题(数据竞态)。这就好比现实生活中十字路口被各个方向的汽车竞争,还有火车上的卫生间被车厢里的人竞争。
互斥锁是一种常用的控制共享资源访问的方法,它能够保证同一时间只有一个 goroutine 可以访问共享资源。Go 语言中使用sync包中提供的Mutex类型来实现互斥锁。
但是一般在我们的应用中都是读多写少,我们会考虑使用读写互斥锁,当我们并发的去读取一个资源而不涉及资源修改的时候是没有必要加互斥锁的
读写锁分为两种:读锁和写锁。当一个 goroutine 获取到读锁之后,其他的 goroutine 如果是获取读锁会继续获得锁,如果是获取写锁就会等待;而当一个 goroutine 获取写锁之后,其他的 goroutine 无论是获取读锁还是写锁都会等待。
sync.WaitGroup
sync.WaitGroup内部维护着一个计数器,计数器的值可以增加和减少。例如当我们启动了 N 个并发任务时,就将计数器值增加N。每个任务完成时通过调用 Done 方法将计数器减1。通过调用 Wait 来等待并发任务执行完,当计数器值为 0 时,表示所有并发任务已经完成。
sync.Once
在某些场景下我们需要确保某些操作即使在高并发的场景下也只会被执行一次,例如只加载一次配置文件等。
Go语言中的sync包中提供了一个针对只执行一次场景的解决方案——sync.Once,sync.Once只有一个Do方法,其签名如下:
sync.Map
Go 语言中内置的 map 不是并发安全的
原子操作 atomic
atomic包提供了底层的原子级内存操作,对于同步算法的实现很有用。这些函数必须谨慎地保证正确使用。除了某些特殊的底层应用,使用通道或者 sync 包的函数/类型实现同步更好。
struct转换成map时会出现数据
类型转换错误的问题
// UserInfo 用户信息
type UserInfo struct {
Name string `json:"name"`
Age int `json:"age"`
}
u1 := UserInfo{Name: "q1mi", Age: 18}
这个如果 u1 转成 map 的话,先 json,然后序列化回来给 map, 这时这个 Age 就变成了 float 类型
- 方法一:这里使用反射遍历结构体字段的方式生成map
- 第三方库structs