Go语言入门
type
status
date
slug
summary
tags
category
icon
password
语言处理系统
解释型
- python
- javascript
image-20240218091032596
编译型
- go
- c++
混合体
- java
变量与类型
变量的内涵
执行环境中的数据的存储区域
强类型与弱类型
变量的声明与赋值
变量名
Go变量名以数字、字母或下划线组成,允许任何被视为字母或数字的Unicode字符
所以π也可以
变量生命周期
和C++差不多,略
作用域
函数外部和函数内部有同名变量时,就近原则
运算符优先级
运算符种类几乎和C++一样,略
截屏2024-02-18 09.49.42
程序控制结构
选择结构
if-else
注意,else一定要跟在
}
后面switch-case
循环结构
完整C风格for循环
只有条件判断的for循环
无限循环
for-range循环
Break&Continue
Label
image-20240218103659076
函数
函数声明
函数递归
斐波那契数列-尾递归
高阶函数
闭包
内层函数可以访问外层变量
复合类型
数组
切片
哈希表
哈希碰撞-拉链法
哈希碰撞-开放寻址法
哈希表使用实例
defer
- 延迟执行
- 参数预计算
a=2
- LIFO执行顺序
start 5->start 1
panic 函数
panic特性
执行结果:
虽然发生了panic异常信息,还是输出了defer语句中的信息,这说明panic的发生,还是会执行defer操作;先把defer栈清空再panic(死也要清白地死)
recover函数
输出结果:
关于panic、recover、defer的执行顺序:
https://www.jb51.net/article/272258.htm
接口
多态
嵌套
类型断言
image-20240218205120634
空接口的类型断言
反射
在Go语言的反射机制中,任何接口值都由是
一个具体类型
和具体类型的值
两部分组成的。在Go语言中反射的相关功能由内置的reflect包提供,任意接口值在反射中都可以理解为由reflect.Type
和reflect.Value
两部分组成,并且reflect包提供了reflect.TypeOf
和reflect.ValueOf
两个函数来获取任意对象的Value和Type。TypeOf
在反射中关于类型还划分为两种:
类型(Type)
和种类(Kind)
。因为在Go语言中我们可以使用type关键字构造很多自定义类型,而种类(Kind)
就是指底层的类型,但在反射中,当需要区分指针、结构体等大品种的类型时,就会用到种类(Kind)
。Go语言的反射中像数组、切片、Map、指针等类型的变量,它们的
.Name()
都是返回空
。在
reflect
包中定义的Kind类型如下:ValueOf
reflect.ValueOf()
返回的是reflect.Value
类型,其中包含了原始值的值信息。reflect.Value
与原始值之间可以互相转换。reflect.Value
类型提供的获取原始值的方法如下:方法 | 说明 |
Interface() interface {} | 将值以 interface{}
类型返回,可以通过类型断言转换为指定类型 |
Int() int64 | 将值以 int
类型返回,所有有符号整型均可以此方式返回 |
Uint() uint64 | 将值以 uint
类型返回,所有无符号整型均可以此方式返回 |
Float() float64 | 将值以双精度(float64)类型返回,所有浮点数(float32、float64)均可以此方式返回 |
Bool() bool | 将值以 bool 类型返回 |
Bytes() []bytes | 将值以字节数组 []bytes 类型返回 |
String() string | 将值以字符串类型返回 |
E.g:
想要在函数中通过反射修改变量的值,需要注意函数参数传递的是值拷贝,必须传递变量地址才能修改变量值。而反射中使用专有的
Elem()
方法来获取指针对应的值。结构体反射
任意值通过
reflect.TypeOf()
获得反射对象信息后,如果它的类型是结构体,可以通过反射值对象(reflect.Type
)的NumField()
和Field()
方法获得结构体成员的详细信息。reflect.Type
中与获取结构体成员相关的的方法如下表所示。方法 | 说明 |
Field(i int) StructField | 根据索引,返回索引对应的结构体字段的信息。 |
NumField() int | 返回结构体成员字段数量。 |
FieldByName(name string) (StructField,
bool) | 根据给定字符串返回字符串对应的结构体字段的信息。 |
FieldByIndex(index []int)
StructField | 多层成员访问时,根据 []int
提供的每个结构体的字段索引,返回字段的信息。 |
FieldByNameFunc(match func(string) bool)
(StructField,bool) | 根据传入的匹配函数匹配需要的字段。 |
NumMethod() int | 返回该类型的方法集中方法的数目 |
Method(int) Method | 返回该类型方法集中的第i个方法 |
MethodByName(string)(Method, bool) | 根据方法名返回该类型方法集中的方法 |
E.g:
Go 并发编程
一个线程可以对应多个协程
协程vs线程
调度方式
协程依附于线程而存在
截屏2024-02-18 21.11.53
上下文切换速度
协程0.2微秒,线程1~2微秒
调度策略
线程:抢占式
协程:协作式
栈大小
线程默认的栈比较大(例如2MB)
而Go语言中的协程栈默认大小为2KB,且可以动态扩容
goroutine
sync.WaitGroup
可以用
sync.WaitGroup
来让主线程在其他用户线程之后退出动态栈
操作系统的线程一般都有固定的栈内存(通常为2MB),而 Go 语言中的
goroutine 非常轻量级,一个 goroutine
的初始栈空间很小(一般为2KB),所以在 Go 语言中一次创建数万个 goroutine
也是可能的。并且 goroutine
的栈不是固定的,可以根据需要动态地增大或缩小, Go 的 runtime 会自动为
goroutine 分配合适的栈空间
goroutine调度
操作系统内核在调度时会挂起当前正在执行的线程并将寄存器中的内容保存到内存中,然后选出接下来要执行的线程并从内存中恢复该线程的寄存器信息,然后恢复执行该线程的现场并开始执行线程。从一个线程切换到另一个线程需要完整的上下文切换。因为可能需要多次内存访问,索引这个切换上下文的操作开销较大,会增加运行的cpu周期。
区别于操作系统内核调度操作系统线程,goroutine
的调度是Go语言运行时(runtime)层面的实现,是完全由 Go
语言本身实现的一套调度系统——go
scheduler。它的作用是按照一定的规则将所有的 goroutine
调度到操作系统线程上执行。
在经历数个版本的迭代之后,目前 Go 语言的调度器采用的是
GPM
调度模型。gpm
其中:
- G:表示 goroutine,每执行一次
go f()
就创建一个 G,包含要执行的函数和上下文信息。
- 全局队列(Global Queue):存放等待运行的 G。
- P:表示 goroutine 执行所需的资源,最多有 GOMAXPROCS 个。
- P 的本地队列:同全局队列类似,存放的也是等待运行的G,存的数量有限,不超过256个。新建 G 时,G 优先加入到 P 的本地队列,如果本地队列满了会批量移动部分 G 到全局队列。
- M:线程想运行任务就得获取 P,从 P 的本地队列获取 G,当 P 的本地队列为空时,M 也会尝试从全局队列或其他 P 的本地队列获取 G。M 运行 G,G 执行之后,M 会从 P 获取下一个 G,不断重复下去。
- Goroutine 调度器和操作系统调度器是通过 M 结合起来的,每个 M 都代表了1个内核线程,操作系统调度器负责把内核线程分配到 CPU 的核上执行。
单从线程调度讲,Go语言相比起其他语言的优势在于OS线程是由OS内核来调度的,
goroutine
则是由Go运行时(runtime)自己的调度器调度的,完全是在用户态下完成的,
不涉及内核态与用户态之间的频繁切换,包括内存的分配与释放,都是在用户态维护着一块大的内存池,
不直接调用系统的malloc函数(除非内存池需要改变),成本比调度OS线程低很多。
另一方面充分利用了多核的硬件资源,近似的把若干goroutine均分在物理线程上,
再加上本身 goroutine 的超轻量级,以上种种特性保证了 goroutine
调度方面的性能。
- Work stealing
所有P放满了才会放到全局队列,如果某个M没东西了,会从其他P偷一个G过来,·
其他P没得偷从全局偷
- handoff
如果某P的本地队列中的某G使得M阻塞,P会移动到另一个唤醒/创建的M,原来的M在G运行完之后睡眠或者销毁
GOMAXPROCS
Go运行时的调度器使用
GOMAXPROCS
参数来确定需要使用多少个
OS 线程来同时执行 Go 代码。默认值是机器上的 CPU 核心数。channel
Go语言采用的并发模型是
CSP(Communicating Sequential Processes)
,提倡通过通信共享内存而不是通过共享内存而实现通信。只读通道:
v <-chan int
只写通道:
v chan<- int
注意:一个通道值是可以被垃圾回收掉的。通道通常由发送方执行关闭操作,并且只有在接收方明确等待通道关闭的信号时才需要执行关闭操作。它和关闭文件不一样,通常在结束操作之后关闭文件是必须要做的,但关闭通道不是必须的。
关闭后的通道有以下特点:
- 对一个关闭的通道再发送值就会导致 panic。
- 对一个关闭的通道进行接收会一直获取值直到通道为空。
- 对一个关闭的并且没有值的通道执行接收操作会得到对应类型的零值。
- 关闭一个已经关闭的通道会导致 panic。
无缓冲通道:
c := make(chan int)
有缓冲通道:
cc := make(chan int, 3)
对无缓冲通道发送消息而不接收会造成死锁,我们要创建一个goroutine来接收通道中的内容:
我们可以用多返回值模式来判断通道是否关闭:
其中:
- value:从通道中取出的值,如果通道被关闭则返回对应类型的零值。
- ok:通道ch关闭时返回 false,否则返回 true。
Worker Pool
E.g:三个线程干五个活
select
Select 语句具有以下特点。
- 可处理一个或多个 channel 的发送/接收操作。
- 如果多个 case 同时满足,select 会随机选择一个执行。
- 对于没有 case 的 select 会一直阻塞,可用于阻塞 main 函数,防止退出
并发安全和锁
原子锁
梦回操作系统
互斥锁
互斥锁是一种常用的控制共享资源访问的方法,它能够保证同一时间只有一个
goroutine 可以访问共享资源。Go
语言中使用
sync
包中提供的Mutex
类型来实现互斥锁。sync.Mutex
提供了两个方法供我们使用。方法名 | 功能 |
func (m *Mutex) Lock() | 获取互斥锁 |
func (m *Mutex) Unlock() | 释放互斥锁 |
读写互斥锁
互斥锁是完全互斥的,但是实际上有很多场景是读多写少的,当我们并发的去读取一个资源而不涉及资源修改的时候是没有必要加互斥锁的,这种场景下使用读写锁是更好的一种选择。读写锁在
Go 语言中使用
sync
包中的RWMutex
类型。sync.RWMutex
提供了以下5个方法。方法名 | 功能 |
func (rw *RWMutex) Lock() | 获取写锁 |
func (rw *RWMutex) Unlock() | 释放写锁 |
func (rw *RWMutex) RLock() | 获取读锁 |
func (rw *RWMutex) RUnlock() | 释放读锁 |
func (rw *RWMutex) RLocker() Locker | 返回一个实现Locker接口的读写锁 |
读写锁分为两种:读锁和写锁。当一个 goroutine 获取到读锁之后,其他的
goroutine 如果是获取读锁会继续获得锁,如果是获取写锁就会等待;而当一个
goroutine 获取写锁之后,其他的 goroutine
无论是获取读锁还是写锁都会等待。
在读多写少(相差一个数量级)的情况下,使用读写互斥锁比起使用互斥锁可以提高执行效率
sync.WaitGroup
方法名 | 功能 |
func (wg * WaitGroup) Add(delta
int) | 计数器+delta |
(wg *WaitGroup) Done() | 计数器-1 |
(wg *WaitGroup) Wait() | 阻塞直到计数器变为0 |
sync.Once
在某些场景下我们需要确保某些操作即使在高并发的场景下也只会被执行一次,例如只加载一次配置文件等。
Go语言中的
sync
包中提供了一个针对只执行一次场景的解决方案——sync.Once
,sync.Once
只有一个Do
方法sync.Map
Go 语言中内置的 map
不是并发安全的,当并发地写map时会报错
fatal error: concurrent map writes
,因此我们可以使用sync.Map
方法名 | 功能 |
func (m *Map) Store(key, value
interface{}) | 存储key-value数据 |
func (m *Map) Load(key interface{})
(value interface{}, ok bool) | 查询key对应的value |
func (m *Map) LoadOrStore(key, value
interface{}) (actual interface{}, loaded bool) | 查询或存储key对应的value |
func (m *Map) LoadAndDelete(key
interface{}) (value interface{}, loaded bool) | 查询并删除key |
func (m *Map) Delete(key
interface{}) | 删除key |
func (m *Map) Range(f func(key, value
interface{}) bool) | 对map中的每个key-value依次调用f |
Cond
感谢李文周大佬的教程
GO并发模式
Ping-Pong模式
Fan-in模式
Fan-out模式
Worker Pool是一个典型的例子
image-20240218220726384
Sub-worker模式
对Fan-out的扩展
Pipeline模式
其实就是linux的管道,把前一个的输出作为后一个的输入
Step1. generator
Step2. multiply
Step3. add
例子-筛选素数:
依赖管理
### GOPATH
可以用
go env
来查看GO Modules
用GOPATH是很痛苦的,当我们需要切换版本的时候,就要切换GOPATH
而GO Modules解决了这一问题
其他
构造函数
不同于C++,Go本身是没有构造函数的,但我们可以人为地写一个
入门篇到此为止,测试、调试、泛型等内容不在入门篇做介绍
- Waline
Last update: 2024-02-24
type
status
date
slug
summary
tags
category
icon
password
使用Notion全新构建!欢迎访问
关注taffy喵~谢谢喵~