- 作用:context 用来解决 goroutine 之间
退出通知
、元数据传递
的功能 - context 使用注意事项:
- 不要将 Context 塞到结构体里。直接将 Context 类型作为函数的第一参数,而且一般都命名为 ctx。
- 不要向函数传入一个 nil 的 context,如果你实在不知道传什么,标准库给你准备好了一个 context:todo。
- 不要把本应该作为函数参数的类型塞到 context 中,context 存储的应该是一些共同的数据。例如:登陆的 session、cookie 等。
- 同一个 context 可能会被传递到多个 goroutine,别担心,context 是并发安全的。
- 相关函数: 这些函数都是幂等的,也就是说连续多次调用同一个方法,得到的结果都是相同的
Once同时使用了原子操作和互斥锁,原子操作操作done的值,互斥锁用来锁定临界区代码(如果是对代码块进行保护,还需要用锁)
Once过程:
Do方法在一开始就会通过调用atomic.LoadUint32函数来获取该字段的值,并且一旦发现该值为1,就会直接返回。这也初步保证了“Do方法,只会执行首次被调用时传入的函数”。不过,单凭这样一个判断的保证是不够的。因为,如果有两个 goroutine 都调用了同一个新的Once值的Do方法,并且几乎同时执行到了其中的这个条件判断代码,那么它们就都会因判断结果为false,而继续执行Do方法中剩余的代码。在这个条件判断之后,Do方法会立即锁定其所属值中的那个sync.Mutex类型的字段m。然后,它会在临界区中再次检查done字段的值,并且仅在条件满足时,才会去调用参数函数,以及用原子操作把done的值变为1。如果你熟悉 GoF 设计模式中的单例模式的话,那么肯定能看出来,这个Do方法的实现方式,与那个单例模式有很多相似之处。它们都会先在临界区之外,判断一次关键条件,若条件不满足则立即返回。这通常被称为“快路径”,或者叫做“快速失败路径”。如果条件满足,那么到了临界区中还要再对关键条件进行一次判断,这主要是为了更加严谨。这两次条件判断常被统称为(跨临界区的)“双重检查”。由于进入临界区之前,肯定要锁定保护它的互斥锁m,显然会降低代码的执行速度,所以其中的第二次条件判断,以及后续的操作就被称为“慢路径”或者“常规路径”
Mutex定义:
互斥锁看作是针对某一个临界区或某一组相关临界区的唯一访问令牌
互斥锁一个结构体类型,属于值类型中的一种.
互斥锁虽然可以保证临界区中代码的串行执行,但却不能保证这些代码执行的原子性(atomicity)
Mutex使用注意事项:
- 不要重复锁定互斥锁;
- 不要忘记解锁互斥锁,必要时使用defer语句;
- 不要对尚未锁定或者已解锁的互斥锁解锁;
- 不要在多个函数之间直接传递互斥锁。 对一个已经被锁定的互斥锁进行锁定,是会立即阻塞当前的 goroutine 的。这个 goroutine 所执行的流程,会一直停滞在调用该互斥锁的Lock方法的那行代码上。直到该互斥锁的Unlock方法被调用
条件变量定义:
当共享资源的状态不满足条件的时候,想操作它的线程再也不用循环往复地做检查了,只要等待通知就好了。
条件变量的初始化离不开互斥锁,并且它的方法有的也是基于互斥锁的
方法:等待通知(wait)、单发通知(signal)和广播通知(broadcast)
规则:
利用条件变量可以实现单向的通知,而双向的通知则需要两个条件变量。这也是条件变量的基本使用规则。
Wait: wait方法总会把当前的 goroutine 添加到通知队列的队尾
Signal: signal方法总会从通知队列的队首开始,查找可被唤醒的 goroutine。所以,因Signal方法的通知,而被唤醒的 goroutine 一般都是最早等待的那一个。这两个方法的行为决定了它们的适用场景。如果你确定只有一个 goroutine 在等待通知,或者只需唤醒任意一个 goroutine 就可以满足要求,那么使用条件变量的Signal方法就好了。
互斥锁虽然可以保证临界区中代码的串行执行,但却不能保证这些代码执行的原子性(atomicity)
在众多的同步工具中,真正能够保证原子性执行的只有原子操作
原子操作在进行的过程中是不允许中断的,这个特性由底层CPU提供芯片级别的支持
原子操作支持的类型:
数据类型有:
int32、int64、uint32、uint64、uintptr
unsafe包中的Pointer以及 Value的类型(它可以被用来存储任意类型的值)
原子操作支持的方法:
加法(add)
比较并交换(compare and swap,简称 CAS)
- 对于每一个case表达式,都至少会包含一个代表发送操作的发送表达式或者一个代表接收操作的接收表达式,同时也可能会包含其他的表达式。比如,如果case表达式是包含了接收表达式的短变量声明时,那么在赋值符号左边的就可以是一个或两个表达式,不过此处的表达式的结果必须是可以被赋值的。当这样的case表达式被求值时,它包含的多个表达式总会以从左到右的顺序被求值。
- select语句包含的候选分支中的case表达式都会在该语句执行开始时先被求值,并且求值的顺序是依从代码编写的顺序从上到下的。结合上一条规则,在select语句开始执行时,排在最上边的候选分支中最左边的表达式会最先被求值,然后是它右边的表达式。仅当最上边的候选分支中的所有表达式都被求值完毕后,从上边数第二个候选分支中的表达式才会被求值,顺序同样是从左到右,然后是第三个候选分支、第四个候选分支,以此类推。
- 对于每一个case表达式,如果其中的发送表达式或者接收表达式在被求值时,相应的操作正处于阻塞状态,那么对该case表达式的求值就是不成功的。在这种情况下,我们可以说,这个case表达式所在的候选分支是不满足选择条件的。
- 仅当select语句中的所有case表达式都被求值完毕后,它才会开始选择候选分支。这时候,它只会挑选满足选择条件的候选分支执行。如果所有的候选分支都不满足选择条件,那么默认分支就会被执行。如果这时没有默认分支,那么select语句就会立即进入阻塞状态,直到至少有一个候选分支满足选择条件为止。一旦有一个候选分支满足选择条件,select语句(或者说它所在的 goroutine)就会被唤醒,这个候选分支就会被执行。
- 如果select语句发现同时有多个候选分支满足选择条件,那么它就会用一种伪随机的算法在这些分支中选择一个并执行。注意,即使select语句是在被唤醒时发现的这种情况,也会这样做。
- 一条select语句中只能够有一个默认分支。并且,默认分支只在无候选分支可选时才会被执行,这与它的编写位置无关。
- select语句的每次执行,包括case表达式求值和分支选择,都是独立的。不过,至于它的执行是否是并发安全的,就要看其中的case表达式以及分支中,是否包含并发不安全的代码了。
panic从引发到终止的过程:
某个函数中的某行代码有意或无意地引发了一个 panic。这时,初始的 panic 详情会被建立起来,并且该程序的控制权会立即从此行代码转移至调用其所属函数的那行代码上,也就是调用栈中的上一级。这也意味着,此行代码所属函数的执行随即终止。紧接着,控制权并不会在此有片刻的停留,它又会立即转移至再上一级的调用代码处。控制权如此一级一级地沿着调用栈的反方向传播至顶端,也就是我们编写的最外层函数那里。这里的最外层函数指的是go函数,对于主 goroutine 来说就是main函数。但是控制权也不会停留在那里,而是被 Go 语言运行时系统收回。
- 作用:解决多参数配置问题
- 优点:
- API 具有可扩展性,高度可配置化,新增参数不会破坏现有代码;
- 参数列表非常简洁,并且可以使用默认的参数;
- option 函数使参数的含义非常清晰,易于开发者理解和使用;
- 如果将 options 结构中的参数设置为小写,还可以限制这些参数的权限,防止这些参数在 package 外部使用。
- 具体实施:
- 我们要对 schedule 结构进行改造,把可以配置的参数放入到options 结构中。定义options 支持的配置的功能模块。
- map的结构体:
// A header for a Go map.
type hmap struct {
// Note: the format of the hmap is also encoded in cmd/compile/internal/reflectdata/reflect.go.
// Make sure this stays in sync with the compiler's definition.
count int // # live cells == size of map. Must be first (used by len() builtin)
flags uint8
B uint8 // log_2 of # of buckets (can hold up to loadFactor * 2^B items)
noverflow uint16 // approximate number of overflow buckets; see incrnoverflow for details
hash0 uint32 // hash seed,哈希函数
buckets unsafe.Pointer // array of 2^B Buckets. may be nil if count==0.
oldbuckets unsafe.Pointer // previous bucket array of half the size, non-nil only when growing
nevacuate uintptr // progress counter for evacuation (buckets less than this have been evacuated)
extra *mapextra // optional fields
}
在 Go 中,interface 和 struct 都是非常重要的类型,但它们的使用场景有所不同。
使用 interface 的场景:
-
抽象类型:当我们需要定义一个抽象类型时,可以使用 interface。抽象类型是指只定义了方法集合,但没有具体实现的类型。
-
多态:当我们需要实现多态时,可以使用 interface。多态是指同一种类型的对象,在不同的情况下表现出不同的行为。
-
解耦:当我们需要解耦时,可以使用 interface。解耦是指将两个或多个模块之间的耦合度降低,从而提高代码的复用性和可维护性。
-
主要用来约束协议和实现多态 使用 struct 的场景:
-
封装数据:当我们需要封装数据时,可以使用 struct。封装数据是指将数据和相关的操作封装在一起,从而提高代码的可读性和可维护性。
-
定义数据结构:当我们需要定义一种数据结构时,可以使用 struct。数据结构是指一种数据的组织方式,可以用来表示实际问题中的对象。
-
定义对象:当我们需要定义一种对象时,可以使用 struct。对象是指一种具有状态和行为的实体,可以用来表示实际问题中的对象。