Nina's Blog

Good Good Study, Day Day Up


  • Home

  • Archives

redis学习笔记一:数据结构

Posted on 2022-11-14 | In redis

一些(并不)欢乐的自学时间,用了几年了也只停留在用这个层面果然还是不行啊
ref:https://www.w3cschool.cn/hdclil/r489eozt.html

字符串

自己构建了一种名为简单动态字符串(simple dynamic string,SDS)的抽象类型

  • 键值对的键是一个字符串对象, 对象的底层实现是一个保存着字符串 "msg" 的 SDS 。
  • 键值对的值也是一个字符串对象, 对象的底层实现是一个保存着字符串 "hello world" 的 SDS 。
1
2
3
4
5
6
7
8
9
10
11
12
13
struct sdshdr {

// 记录 buf 数组中已使用字节的数量
// 等于 SDS 所保存字符串的长度
int len;

// 记录 buf 数组中未使用字节的数量
int free;

// 字节数组,用于保存字符串
char buf[];

};

和C一样,字符串的buf最后是’\0’结束符,所以可以直接用printf打印出来

字符串:自动扩展缓冲区和内存预分配

缓冲区一般除非手动调用api不然不会被彻底释放,而是作为惰性空间放在free里

链表

1
2
3
4
5
6
7
8
9
10
11
12
typedef struct listNode {

// 前置节点
struct listNode *prev;

// 后置节点
struct listNode *next;

// 节点的值
void *value;

} listNode;

双向链表,list管理链表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
typedef struct list {

// 表头节点
listNode *head;

// 表尾节点
listNode *tail;

// 链表所包含的节点数量
unsigned long len;

// 节点值复制函数
void *(*dup)(void *ptr);

// 节点值释放函数
void (*free)(void *ptr);

// 节点值对比函数
int (*match)(void *ptr, void *key);

} list;

字典

整个Redis数据库的所有的键和值就组成了一个全局的字典,对数据库的增删改查操作都是构建在字典的操作之上的

Redis 的字典使用哈希表作为底层实现

哈希表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
typedef struct dictht {

// 哈希表数组
dictEntry **table;

// 哈希表大小
unsigned long size;

// 哈希表大小掩码,用于计算索引值
// 总是等于 size - 1
unsigned long sizemask;

// 该哈希表已有节点的数量
unsigned long used;

} dictht;

table是一个数组,每个数组都指向一个dictEntry的指针,每个 dictEntry 结构保存着一个键值对。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
typedef struct dictEntry {

// 键
void *key;

// 值
union {
void *val;
uint64_t u64;
int64_t s64;
} v;

// 指向下个哈希表节点,形成链表
struct dictEntry *next;

} dictEntry;

同时dictEntry有next组成一个链表,就和普通哈希逻辑一样,用链表防撞

就这样这个哈希表就是字典的基础结构

字典结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
typedef struct dict {

// 类型特定函数
dictType *type;

// 私有数据
void *privdata;

// 哈希表
dictht ht[2];

// rehash 索引
// 当 rehash 不在进行时,值为 -1
int rehashidx; /* rehashing not in progress if rehashidx == -1 */

} dict;

type和privdata是用来适配不同的数据做不同的数据处理的:

  • type 属性是一个指向 dictType 结构的指针, 每个 dictType 结构保存了一簇用于操作特定类型键值对的函数, Redis 会为用途不同的字典设置不同的类型特定函数。
  • 而 privdata 属性则保存了需要传给那些类型特定函数的可选参数。

ht 属性是一个包含两个项的数组, 数组中的每个项都是一个 dictht 哈希表, 一般情况下, 字典只使用 ht[0] 哈希表, ht[1] 哈希表只会在对 ht[0] 哈希表进行 rehash 时使用。

除了 ht[1] 之外, 另一个和 rehash 有关的属性就是 rehashidx : 它记录了 rehash 目前的进度, 如果目前没有在进行 rehash , 那么它的值为 -1 。

哈希算法

  1. 用hash函数计算出hash值,算法:MurmurHash2
  2. hash值&sizemask = index

解决键冲突:链表方案

rehash

这也就是为什么字典的哈希表需要有两个dictht,当哈希表的大小过大或不够的时候,需要进行rehash对哈希表进行缩/扩容,其实就是重新确定sizeMask。

  1. 根据ht[0]使用量确定ht[1]的sizemask大小
  2. 将保存在 ht[0] 中的所有键值对 rehash 到 ht[1] 上面,也就是重新计算hash值和索引值
  3. 释放ht[0]并把ht[1]设置为ht[0]

rehash触发条件:负载因子

1
2
# 负载因子 = 哈希表已保存节点数量 / 哈希表大小
load_factor = ht[0].used / ht[0].size

当哈希表的负载因子小于 0.1 时, 程序自动开始对哈希表执行收缩操作。

渐进式rehash

其实就是当redis里的数据量太太太大的时候,不要一次性做rehash,而是多次渐近地完成

在字典结构中维持一个rehashidx,从0开始,每次对字典操作的时候就把rehashidx 对应的所有键值对都rehash掉并且对rehashidx++,直到全都rehash完了之后把rehashidx设置为-1

在这个过程中的crud都会在两个ht中执行,比如先在0找,找不到的话再去1

跳跃表

跳表之前在db里应该学过,就是针对有序链表查询的时候,不去按顺序查,而是按照一定的逻辑快速跳到接近结果的地方,从算法上看有一丝丝接近我们的二分法,把时间复杂度从n降到logn

在redis对外能了解的用到跳表的地方就是zset:有序集合,具体有序集合的用法自己去看吧

1
2
3
4
5
6
zskiplist 结构, 该结构包含以下属性:

header :指向跳跃表的表头节点。
tail :指向跳跃表的表尾节点。
level :记录目前跳跃表内,层数最大的那个节点的层数(表头节点的层数不计算在内)。
length :记录跳跃表的长度,也即是,跳跃表目前包含节点的数量(表头节点不计算在内)。

跳表

跳跃表节点

跳跃表节点的实现由 redis.h/zskiplistNode 结构定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
typedef struct zskiplistNode {

// 后退指针
struct zskiplistNode *backward;

// 分值
double score;

// 成员对象
robj *obj;

// 层
struct zskiplistLevel {

// 前进指针
struct zskiplistNode *forward;

// 跨度
unsigned int span;

} level[];

} zskiplistNode;

成员对象和分值

用来排序的依据

  • 在同一个跳跃表中,多个节点可以包含相同的分值,但每个节点的成员对象必须是唯一的,跳跃表中的节点按照分值大小进行排序,当分值相同时,节点按照成员对象的大小进行排序;

后退指针

好像没什么好说的就是回到前一个

层

  • 每个跳跃表节点新建的时候,根据幂次定律 ( power law,越大的数出现的概率越小)随机生成一个介于 1 和 32 之间的值作为 level 数组的大小

跨度

层的跨度(level[i].span 属性)用于记录两个节点之间的距离:

  • 两个节点之间的跨度越大, 它们相距得就越远。
  • 指向 NULL 的所有前进指针的跨度都为 0 , 因为它们没有连向任何节点。

跨度是用来计算节点位置的

TBD

后面是一些自己的总结,第一是要去学一下跳表的增删改查逻辑。第二是要去查一下为什么不用平衡树而是跳表

平衡树:

  1. 主要原因:范围查找比较麻烦,跳表的范围查找相对要简单非常多
  2. 增删改更麻烦:引发子树调整
  3. 算法实现比较麻烦
  4. 内存可控性跳表>平衡树,根据调整新建层数p大致可以估算并调整占用内存。(虽然跳表的内存占用比树要大

整数集合

如果一个集合键中,其存储的元素都是整数值时,那么这个整数键的底层实现就会是整数集合

1
2
3
4
5
6
7
8
9
10
11
12
typedef struct intset {

// 编码方式
uint32_t encoding;

// 集合包含的元素数量
uint32_t length;

// 保存元素的数组
int8_t contents[];

} intset;

这里的contents虽然声明了int8_t,但是实际上是根据编码来的,但实际上 contents 数组并不保存任何 int8_t 类型的值 —— contents 数组的真正类型取决于 encoding 属性的值。

升级

实际上这堆东西都不是在创建时就定死的,比如之前添加的是 6,8,10,突然来了一个10000000,之前使用int8存现在就要使用int16(吧?),那么这个时候整数集合就要先升级,步骤如下:

  1. 根据新插入的整数大小,确定新的类型和需要分配的内存,之前是8位*3,现在是16*4
  2. 将之前的数字转换成int16类型,并且放在正确的位置上
  3. length+1,把新增的数字放到contents的最后一位
  4. encoding改为INTSET_ENC_INT16

升级的好处

  • 提升整数集合的灵活性,很好理解嘛,比起C++这种一定要规定了字段类型分配内存再来插入数据的方案,这样的灵活性强很多
  • 另一个是尽可能地节约内存。

整数类型不支持降级

压缩列表

为了节约内存而被开发的存储结构,用处:

数组,哈希和有序集合(对应上面的链表,哈希表和跳表),在一定条件(比如数据量较小)的场合都会被存储为压缩列表,压缩列表的新增、删除的操作平均时间复杂度为O(N),以哈希为例,在数据量不大的情况下这个O(N)和O(1)可以被忽略

表 7-1 压缩列表各个组成部分的详细说明

属性 类型 长度 用途
zlbytes uint32_t 4 字节 记录整个压缩列表占用的内存字节数:在对压缩列表进行内存重分配, 或者计算 zlend 的位置时使用。
zltail uint32_t 4 字节 记录压缩列表表尾节点距离压缩列表的起始地址有多少字节: 通过这个偏移量,程序无须遍历整个压缩列表就可以确定表尾节点的地址。
zllen uint16_t 2 字节 记录了压缩列表包含的节点数量: 当这个属性的值小于 UINT16_MAX (65535)时, 这个属性的值就是压缩列表包含节点的数量; 当这个值等于 UINT16_MAX 时, 节点的真实数量需要遍历整个压缩列表才能计算得出。
entryX 列表节点 不定 压缩列表包含的各个节点,节点的长度由节点保存的内容决定。
zlend uint8_t 1 字节 特殊值 0xFF (十进制 255 ),用于标记压缩列表的末端。

节点

每个压缩列表节点可以保存一个字节数组或者一个整数值

每个压缩列表节点都由 previous_entry_length 、 encoding 、 content 三个部分组成

previous_entry_length

节点的 previous_entry_length 属性以字节为单位, 记录了压缩列表中前一个节点的长度。

根据 previous_entry_length ,实现压缩列表从表尾到表头的遍历。

  1. 根据zltail算出指向表尾节点的指针p
  2. p减去表尾节点的previous_entry_length,就得到指向前一个节点的起始地址的指针

encoding

节点的 encoding 属性记录了节点的 content 属性所保存数据的类型以及长度:

  • 字节数组:00,01,10开头,分别代表了不同大小的字节数组,一个节点一字节。二字节或五字节长
  • 整数:11开头

去除前两位的encoding可以用来计算字节数组长度或者整数类型(int16还是32还是64)

连锁更新

前面说过, 每个节点的 previous_entry_length 属性都记录了前一个节点的长度:

  • 如果前一节点的长度小于 254 字节, 那么 previous_entry_length 属性需要用 1 字节长的空间来保存这个长度值。
  • 如果前一节点的长度大于等于 254 字节, 那么 previous_entry_length 属性需要用 5 字节长的空间来保存这个长度值。

假设有一排小于254字节的节点的压缩列表,每个节点的 previous_entry_length 都是一个字节,但是如果插入一个新的大于254字节的节点作为压缩列表表头的话,最前面那个节点的 previous_entry_length 需要变为5个字节,就需要给压缩列表头重新分配大小。

这个时候会遇到一个比较特殊的场景:有一排253字节大小的节点,那么当它在表头插入新的大于254字节的节点作为压缩列表表头时->重新分配大小->节点1大小从253变为257->下一个节点重新分配 previous_entry_length ,最差的情况下需要把所有的节点都重新分配一次大小,也就是标题所说的连锁更新

but 要注意的是, 尽管连锁更新的复杂度较高, 但它真正造成性能问题的几率是很低的:

  • 首先, 压缩列表里要恰好有多个连续的、长度介于 250 字节至 253 字节之间的节点, 连锁更新才有可能被引发, 在实际中, 这种情况并不多见;
  • 其次, 即使出现连锁更新, 但只要被更新的节点数量不多, 就不会对性能造成任何影响: 比如说, 对三五个节点进行连锁更新是绝对不会影响性能的;

节约内存

为什么压缩列表能节约内存:将一系列数据与其编码信息存储在一块连续的内存区域,这块内存物理上是连续的,逻辑上被分为多个组成部分,其目的是在一定可控的时间复杂读条件下尽可能的减少不必要的内存开销,从而达到节省内存的效果

本质就是物理内存的连续性可以节省内存以及减少内存碎片

golang学习笔记八:error panic 和 recover

Posted on 2020-03-03 | Edited on 2022-11-14 | In go

error

error的基础接口:

1
2
3
type error interface {
Error() string
}

当然也就可以套一个复杂一点的壳子,比如os.PathError

1
2
3
4
5
6
7
8
9
10
11
// PathError records an error and the operation and
// file path that caused it.
type PathError struct {
Op string // "open", "unlink", etc.
Path string // The associated file.
Err error // Returned by the system call.
}

func (e *PathError) Error() string {
return e.Op + " " + e.Path + ": " + e.Err.Error()
}

PathError‘s Error generates a string like this:

1
open /etc/passwx: no such file or directory

这个思路就很简单,如果捕捉到有error,就可以执行一些比如回滚之类的代码来恢复或者重新执行。

panic

当然也有一些错是希望直接抛出并且停止执行的(怎么那么像throw)

1
2
3
4
5
6
7
8
9
10
11
12
13
// A toy implementation of cube root using Newton's method.
func CubeRoot(x float64) float64 {
z := x/3 // Arbitrary initial value
for i := 0; i < 1e6; i++ {
prevz := z
z -= (z*z*z-x) / (3*z*z)
if veryClose(z, prevz) {
return z
}
}
// A million iterations has not converged; something is wrong.
panic(fmt.Sprintf("CubeRoot(%g) did not converge", x))
}

当然panic也像throw一样是不被提倡的,比起抛出并且停止程序倒不如好好走完(不然也不知道会出什么问题)。

recover

当函数发生 panic 时,它会终止运行,在执行完所有的延迟函数后,程序控制返回到该函数的调用方。这样的过程会一直持续下去,直到当前协程的所有函数都返回退出,然后程序会打印出 panic 信息,接着打印出堆栈跟踪,最后程序终止。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func fullName(firstName *string, lastName *string) {  
defer fmt.Println("deferred call in fullName")
if firstName == nil {
panic("runtime error: first name cannot be nil")
}
if lastName == nil {
panic("runtime error: last name cannot be nil")
}
fmt.Printf("%s %s\n", *firstName, *lastName)
}

func main() {
defer fmt.Println("deferred call in main")
firstName := "Elon"
fullName(&firstName, nil)
fmt.Println("returned normally from main")
}

两个函数里面的defer就是延迟函数,所以遇到panic之后的输出会变成:”defer call in fullName” -> “deferred call in main”->”runtime error”,利用先执行延迟函数的特征,recover出现了。在延迟函数内调用 recover,可以取到 panic 的错误信息,并且停止 panic 续发事件(Panicking Sequence),程序运行恢复正常。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
func recoverName() {  
if r := recover(); r!= nil {
fmt.Println("recovered from ", r)
}
}

func fullName(firstName *string, lastName *string) {
defer recoverName()
if firstName == nil {
panic("runtime error: first name cannot be nil")
}
if lastName == nil {
panic("runtime error: last name cannot be nil")
}
fmt.Printf("%s %s\n", *firstName, *lastName)
}

func main() {
defer fmt.Println("deferred call in main")
firstName := "Elon"
fullName(&firstName, nil)
fmt.Println("returned normally from main")
}

上面的程序的输出流程就会变成:”recovered from” -> “returned normally from main” -> “deferred call in main”


到此为止effective go 这本书就算看完了,个人认为不算是一本绝佳的入门教程,但是胜在短小并且覆盖的面还蛮全面的,学到这个份儿上还不自己撸代码就有点对不起这段时间的学习了,后面的目标就是边学go的web框架边给自己定个奇奇怪怪的小目标写个实例代码吧(就聊天室吧)

golang学习笔记七:interface

Posted on 2020-03-03 | Edited on 2022-11-14 | In go

接口的基本介绍和使用在这里就不写了,作为 go 最重要的组成部分之一,多态等都是通过它来实现的。

使用

接口嵌套

想要使用该子接口的话,必须将父接口和子接口的所有方法都实现。

1
2
3
4
5
6
7
8
type stringer interface{ 
string()string
}

type tester interface{
stringer // 嵌入其他接口
test()
}

插句题外话,如果structure想要像interface一样进行嵌套的话需要进行初始化。初始化方式仍旧是自己初始化and依赖注入两种。

类型转换

1
2
3
4
5
6
7
8
9
10
11
12
13
14
type Sequence []int
// Method for printing - sorts the elements before printing.
func (s Sequence) String() string {
s = s.Copy() // Make a copy; don't overwrite argument.
sort.Sort(s)
str := "["
for i, elem := range s { // Loop is O(N²); will fix that in next example.
if i > 0 {
str += " "
}
str += fmt.Sprint(elem)
}
return str + "]"
}

虽然我没搞懂为什么是 $O(N^2)$,但是loop浪费时间是真的,而利用类型转换就可以:

1
2
3
4
5
func (s Sequence) String() string {
s = s.Copy()
sort.Sort(s)
return fmt.Sprint([]int(s))
}

value.(type) 算是interface类型判断和转换的方法,利用switch可以进行类型判断

1
2
3
4
5
6
7
8
9
10
11
type Stringer interface {
String() string
}

var value interface{} // Value provided by caller.
switch str := value.(type) {
case string:
return str
case Stringer:
return str.String()
}

类型转换则是在type里输入想转换的值,失败时在上面的情况下会抛出panic,所以也提供了第二个断言值来判断转换是否成功:

1
2
3
4
5
6
str, ok := value.(string)
if ok {
fmt.Printf("string value is: %q\n", str)
} else {
fmt.Printf("value is not a string\n")
}

接口继承

OO的说法摆在这里有点奇怪。。。所有的struct只要实现了interface里定义的方法,就可以被认为是这个interface的继承,这也是GO语言实现多态的方法。

如果一个类型只是用来实现接口,并且除了该接口以外没有其它被导出的方法,那就不需要导出这个类型。只导出接口,清楚地表明了其重要的是行为,而不是实现,并且其它具有不同属性的实现可以反映原始类型的行为。这也避免了对每个公共方法实例进行重复的文档介绍。

1
2
3
4
5
6
7
8
9
10
11
12
13
type Block interface {
BlockSize() int
Encrypt(src, dst []byte)
Decrypt(src, dst []byte)
}

type Stream interface {
XORKeyStream(dst, src []byte)
}

// NewCTR returns a Stream that encrypts/decrypts using the given Block in
// counter mode. The length of iv must be the same as the Block's block size.
func NewCTR(block Block, iv []byte) Stream

在上面的加解密的例子里,无论我们传入的是哪种加密算法都无所谓,只要实现方法是只有interface内的,就可以利用NewCTR生成对应的stream。

利用接口实现多态,而实现OO的继承需要使用到反射(明天再说)。

接口详解

可以先回忆一下OO编程中的 is-a 和 has-a。

is-a就是继承,简单来说就和前面的接口继承一样,有一个基类,可以派生出非常多种子类来。再简单一点解释就是基类鸟:有翅膀,有脚,会飞,吃虫子。那么我创建的所有鸟都必须有翅膀,有脚,会飞,吃虫子。

继承是强耦合的,如果哪天定义变了,我们认为鸟不吃虫子改吃鱼了(当然实际上鸟两个都吃忘记这个失败的比方),那么所有的子类鸟都必须吃鱼。

has-a就是组合,我们有翅膀,脚,心脏,血液等等类,鸟就是由这些类组合出来的,has-a是低耦合关系,哪天血液类发生了改变,血的颜色不是红的变成绿的了,对鸟这个类本身没有什么影响。

duck typing

那么我们回到 GO 语言,现在先不讲has-a,而是is-a的问题,传统OO编程的is-a是由基类产生派生类,派生类直接继承基类的所有元素,可以在基类的基础上改进or覆盖(说的就是虚函数这种面试官的最爱)。GO语言的继承和多态的概念和前面就有点微妙的不太一样,在 GO 语言中当定义了一个鸟类接口后,我们认为,所有有翅膀,有脚,会飞吃虫子的,都是鸟。

这个is-a就变得不再是严格的属于关系,而是通过特征来判定一个东西的归属情况,这种想法我们叫它 duck typing

鸭子类型(英语:duck typing)是动态类型的一种风格。在这种风格中,一个对象有效的语义,不是由继承自特定的类或实现特定的接口,而是由”当前方法和属性的集合”决定。

duck typing 在C++中的实现我已经不熟悉了,在GO语言中当然就是通过接口+方法的方式来实现,倒不如说GO语言本身已经没有继承这种强耦合的关系形式了。

一个抄来的 duck typing 的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
type Reader interface {
Read(p []byte) (n int, err error)
}

type Writer interface {
Write(p []byte) (n int, err error)
}

type Closer interface {
Close() error
}

type Seeker interface {
Seek(offset int64, whence int) (int64, error)
}

type File struct { // ...
}
func (f *File) Read(buf []byte) (n int, err error)
func (f *File) Write(buf []byte) (n int, err error)
func (f *File) Seek(off int64, whence int) (pos int64, err error)
func (f *File) Close() error

var file1 Reader = new(File)
var file2 Writer = new(File)
var file3 Closer = new(File)
var file4 Seeker = new(File)

因为在File实现了4个接口,因此可以将File对象赋值给任何一个接口。

接口值接收(Receiver)

一个类型可以实现任意数量的接口,每个类型都实现了一个空接口interface{}。
接口是一系列接口的集合,是一种抽象数据类型,接口变量可以引用任何实现了接口的全部方法的具体数据类型的值。

接口变量存储了两部分信息,一个是分配给接口变量的具体值(接口实现者的值),一个是值的类型的描述器(接口实现者的类型),形式是(value, concrete type),而不是(value, interface type)。

下面这是一个定义了GET方法的接口,函数 f 也就是调用了接口内的 GET 方法并输出。

1
2
3
4
5
6
7
8
9
10
11
12
package main

import "fmt"

type Retriever interface {
Get(url string) string
}

func f(r Retriever) {
s := r.Get('\args')
fmt.Println(s)
}

pointer receiver

首先来看一下指针接收者的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
type RealRetriever struct {
Contents string
}

// 指针接收者
func (r *RealRetriever) Get(url string) string {
return r.Contents
}

func main(){
retriever := RealRetriever{}
f(&retriever) //pointer
f(retriever) //value
}

f(&retriever) 执行完全没有问题,而 f(retriever) 会报错,简单来说就是 f(retriever) 是个call by value,调用的时候会对 retriever 进行复制,拷贝之后的r并不支持GET的接收者是一个指针的概念。

value receiver

1
2
3
4
5
6
7
8
type MockRetriever struct {
Contents string
}

// 值接收者
func (r MockRetriever) Get(url string) string {
return r.Contents
}

同样执行上面的代码,发现两个都是可以的,当传入的是一个引用的时候,go内部的隐式转换可以通过地址找到Get方法,而之前call by value的情况下,通过值是无法找到地址在哪儿的。

空接口

空接口类型interface{}一个方法签名也不包含,所以所有的数据类型都实现了空接口。
空接口类型可以用于存储任意数据类型的实例。

空接口给了我一种指针的感觉😂,可以指向任何一个interface。

如果定义一个函数参数是 interface{} 类型,这个函数应该可以接受任何类型作为它的参数。但是对于函数内部来讲,传入的永远是一个interface类型。

数组的空接口则不能简单地通过等号来赋值,打个比方:

1
2
t := []int{1, 2, 3, 4}
var s []interface{} = t

同理,当使用 []interface{} 作为参数类型的时候,像下面这样直接传进去也是没有用的。

1
2
3
4
5
6
7
8
9
10
func printAll(vals []interface{}) { //1
for _, val := range vals {
fmt.Println(val)
}
}

func main(){
names := []string{"stanley", "david", "oscar"}
printAll(names)
}

具体为什么不自动转换有个博客提到了wiki上占用存储空间的解释(我本人的怀疑就是转换代价太高)https://github.com/golang/go/wiki/InterfaceSlice

当然可以通过手动添加地址空间来做:

1
2
t := []int{1, 2, 3, 4}
s := make([]interface{}, len(t))

判断interface的类型可以用之前类型转换里面的类型断言来做。

golang学习笔记六:new 和 make

Posted on 2020-02-24 | Edited on 2022-11-14 | In go

来吧认真抄一下effective go

new

使用new会给变量分配一个地址空间,但是并不会去初始化,而是全部定位零(zeros it)同时new返回了一个指向地址空间的指针。

composit literal(创建的语法?中文翻译成谜)

1
2
3
4
5
6
7
func NewFile(fd int, name string) *File {
if fd < 0 {
return nil
}
f := File{fd, name, nil, 0}
return &f
}

当然就可以简化为 return &File{fd, name, nil, 0},而且初始化的时候可以无视顺序:

1
return &File{fd: fd, name: name}

没填的两个参数会默认置空/零。

这样的语法可以用在很多地方,比如创建数组或者slice的时候

1
2
3
a := [...]string   {Enone: "no error", Eio: "Eio", Einval: "invalid argument"}
s := []string {Enone: "no error", Eio: "Eio", Einval: "invalid argument"}
m := map[int]string{Enone: "no error", Eio: "Eio", Einval: "invalid argument"}

make

哇我感觉我活的像一个汉化组。。。

  • 首先,像前面说的,make只负责创建新的slice,map和channel
  • make返回的是一个引用,所以它需要被初始化
1
2
3
// Unnecessarily complex:
var p *[]int = new([]int)
*p = make([]int, 100, 100)

虽然上面那段代码被effective go认为是没必要这么复杂,但是还挺有意思的,充分地展示了new 返回指针make返回引用的特质,p就是一个初始化长度是100,容量也是100的切片。

当然常用写法还是下面这个:

1
v := make([]int, 100)

golang学习笔记五:defer简介

Posted on 2020-02-24 | Edited on 2022-11-14 | In go

也是不知道应该写在哪儿不如新开一个文件系列。

简介

1
2
3
4
5
6
7
8
func Contents(filename string) (string, error) {
f, err := os.Open(filename)
if err != nil {
return "", err
}
defer f.Close() // f.Close will run when we're finished.
return nil
}

也就是在函数执行结束之后会执行defer调用的函数内容,就像析构函数一样做一些清理之类的操作。

使用

defer后面只能跟一个函数调用!

好处

  • defer可以保证即使函数因为panic等意外退出的时候也可以将一些必要的东西执行
  • 书写好看,比如上面的打开一个文件,defer可以让关闭的操作挨着打开。

调用

defer是什么时候被调用呢,抄的博客:https://www.jianshu.com/p/5b0b36f398a2

里面研究了return的汇编:

return指令的执行分三步,第一步拷贝return值到返回值内存地址,第二步会调用runtime.deferreturn去执行前面注册的defer函数,第三部再执行ret汇编指令。

执行顺序

defer的执行默认是先进后出(LIFO),所以如果一个函数里出现以下代码:

1
2
3
for i := 0; i < 5; i++ {
defer fmt.Printf("%d ", i)
}

最后输出的顺序会变成 4,3,2,1,0

golang学习笔记四:分号和换行

Posted on 2020-02-24 | Edited on 2022-11-14 | In go

不知道应该扔在哪儿不如新开一个。。。

还是来自于 effective go

go 和 C 一样都是用分号来判断一个语句的结束,但是 go (可能是在编译的时候)增加了一个自动添加分号的方法,go会在一行是以以下语句结束的时候自动添加分号:

  • 当输入被断开为标记时,如果行末标记为
    • 一个标识符
    • 一个整数、浮点数、虚数、字符或字符串文字
    • 关键字break、continue、fallthrough或return中的一个
    • 运算符和分隔符++、--、)、]或}中的一个

如果是一把一段复合语句扔到一行里去的话,比如:

1
go func() { for { dst <- <-src } }()

也并不用分号,因为go会自动在收大括号前增加分号。

根据这个方法,go解决了一个多年的争端,大括号是否换行的问题哈哈哈哈哈哈哈哈哈哈

1
2
3
if i < f() {
g()
}

上面这样没问题,但是如果换行的话f()后面就会多了一个分号哈哈哈哈哈哈哈哈哈哈哈

1
2
3
4
if i < f()  // wrong!
{ // wrong!
g()
}

备注:当然循环里面用来区别initializer,condition的分号还是不能丢呀

golang学习笔记三:Context

Posted on 2020-02-20 | Edited on 2022-11-14 | In go

学习完全没有顺序,就是写server的时候看到不懂的就去学,使用导向型学习。。。

http server太经常看到context了但是又不是特别了解,就去翻了一下官网和各种奇怪的博客😂

官网文档:https://pkg.go.dev/golang.org/x/net/context

官方博客:https://blog.golang.org/context

为什么有context

内容是抄的😂,原文某博客:https://www.cnblogs.com/qcrao-2018/archive/2019/06/12/11007503.html

讲得格外清楚,没有动力再去写一次。

简单来说,当有一个request过来的时候,我们会开很多goroutine去做不同的处理,比如获取数据库等等,context就是给这些goroutine间的共享值传递提供一个方法。

without context

with context

文中举了两个场景,一个是各种goroutine需要同一个token,也就是传统意义的共享值,另一个就是当某项操作卡住时,如果没有超时控制,goroutine就会随着request的到来越开越多越开越多,这个时候就需要一个公共的deadline做超时取消的操作。

简介

回到官方文档。

接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// A Context carries a deadline, cancelation signal, and request-scoped values
// across API boundaries. Its methods are safe for simultaneous use by multiple
// goroutines.
type Context interface {
// Done returns a channel that is closed when this Context is canceled
// or times out.
Done() <-chan struct{}

// Err indicates why this context was canceled, after the Done channel
// is closed.
Err() error

// Deadline returns the time when this Context will be canceled, if any.
Deadline() (deadline time.Time, ok bool)

// Value returns the value associated with key or nil if none.
Value(key interface{}) interface{}
}

这四个也就是一个context所需要有的基本函数,Done返回一个被关闭的channel,为什么要用一个被关闭的channel?是因为goroutine从一个被关闭的channel里是可以取出零值的,所以一旦goroutine从里面取出零值,就意味着它可以停止并且开始做一些收尾工作了。同时Err函数会返回错误原因。

API介绍

Background和TODO

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
type emptyCtx int

func (*emptyCtx) Deadline() (deadline time.Time, ok bool) {
return
}

func (*emptyCtx) Done() <-chan struct{} {
return nil
}

func (*emptyCtx) Err() error {
return nil
}

func (*emptyCtx) Value(key interface{}) interface{} {
return nil
}

background和todo就是根据上面的实现

1
func Background() Context

创建的就是一个空的context,因为里面的Done返回的channel是空,所以其实它永远也不会被cancel,TODO就是在你不知道要传什么进去的时候先拿来占个位置。

WithCancel

很多时候超时的情况下我们并不希望停止所有的coroutine,打个比方获取db超时了,那我们希望退出db的goroutine,把一个超时的error给到server,server再返回这个error,也就是子goroutine(上面的goroutineb,c…)停止的情况下我们不希望父goroutine(上面的goroutine a)也给停了。这个时候就有了withcancel。

1
func WithCancel(parent Context) (ctx Context, cancel CancelFunc)

使用withcancel,会根据父context创建一个deadline不比父context晚的子context,子context里面会有一个新的channel,这个channel被关闭有以下三种情况:

  • ddl超了
  • 返回的那个CancelFunc被引用了
  • 父context的Done channel被关了。

WithTimeout

1
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)

也就是如果程序运行超时,就自己cancel掉,不过有一点需要注意:

1
2
3
4
5
func slowOperationWithTimeout(ctx context.Context) (Result, error) {
ctx, cancel := context.WithTimeout(ctx, 100*time.Millisecond)
defer cancel() // releases resources if slowOperation completes before timeout elapses
return slowOperation(ctx)
}

这里的defer cancel() 是当整个函数正常进行并且退出(也就是没有超时的情况下),需要在退出之前清理一下(defer的具体情况详见go error)

WithValue

1
func WithValue(parent Context, key interface{}, val interface{}) Context

通过context传值的过程,官方文档的说法:返回父context中key对应的值,也就是value,打个比方获取appid之类的?

其实根据那个博客的源代码描述,其实每次获取value的时候,如果这个context存了就用自己的,没有就往上找,所以存不同的值也不是不可以,但是在实际使用时非常容易陷入混乱,所以官方文档最后提了一个真诚的建议:Use context Values only for request-scoped data that transits processes and APIs, not for passing optional parameters to functions. 翻译过来就是只用来传request范围内的值,别去当函数传参那么用😂。

使用

官方文档里的一段话,翻译一下:

  • 针对每一个来的request需要创建一个Context
  • 函数调用链需要传递一个Context
  • 不要把Context存在一个结构体里,让它作为外部参数在需要的函数里传递,Context需要作为第一个参数,名字通常用ctx表示
  • 不要传递一个空(nil)Context,你不知道传什么的时候就用TODO
  • 不要把本应该作为函数参数的类型塞到 context 中,context 存储的应该是一些共同的数据。例如:登陆的 session、cookie 等。
  • 同一个Context可以被传递到不同的goroutine里,当它被多个goroutine同时使用的时候是安全的

源码分析

canceler接口

前面已经放了Context接口,这里首先把canceler接口放一下:

1
2
3
4
type canceler interface {
cancel(removeFromParent bool, err error)
Done() <-chan struct{}
}

WithCancel的实现

然后是根据Context接口派生(go语言似乎不能这么说但是比较好理解)的cancelContext结构:

1
2
3
4
5
6
7
8
9
type cancelCtx struct {
Context

// 保护之后的字段
mu sync.Mutex
done chan struct{}
children map[canceler]struct{}
err error
}

然后是Done的实现

1
2
3
4
5
6
7
8
9
func (c *cancelCtx) Done() <-chan struct{} {
c.mu.Lock()
if c.done == nil {
c.done = make(chan struct{})
}
d := c.done
c.mu.Unlock()
return d
}

反正就是返回一个空的新channel,这个channel没地方去写入,所以除非关闭不然就会一直卡住。

然后就是cancel方法的书写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
func (c *cancelCtx) cancel(removeFromParent bool, err error) {
// 必须要传 err
if err == nil {
panic("context: internal error: missing cancel error")
}
c.mu.Lock()
if c.err != nil {
c.mu.Unlock()
return // 已经被其他协程取消
}
// 给 err 字段赋值
c.err = err
// 关闭 channel,通知其他协程
if c.done == nil {
c.done = closedchan
} else {
close(c.done)
}

// 遍历它的所有子节点
for child := range c.children {
// 递归地取消所有子节点
child.cancel(false, err)
}
// 将子节点置空
c.children = nil
c.mu.Unlock()

if removeFromParent {
// 从父节点中移除自己
removeChild(c.Context, c)
}
}

抄的还是第一章的博客,写的可清楚了懒得再加东西。具体究竟什么时候从父节点中移除自己什么时候不移除,主要还是取决于遍历删除子节点里面的函数逻辑,希望后面改改就没这个参数算了😂

WithValue的实现

1
2
3
4
type valueCtx struct {
Context
key, val interface{}
}

这不重要重要的是后面Value()函数的实现:

1
2
3
4
5
6
7
8
9
10
func (c *valueCtx) String() string {
return fmt.Sprintf("%v.WithValue(%#v, %#v)", c.Context, c.key, c.val)
}

func (c *valueCtx) Value(key interface{}) interface{} {
if c.key == key {
return c.val
}
return c.Context.Value(key)
}

如果这个值目前取不到的话就会从父Context里面取。

gin框架学习笔记一:简单使用

Posted on 2020-02-20 | Edited on 2022-11-14 | In go , gin

只是一个使用方式的memo,防止以后记不住还要查(其实就是个简易版翻译。。。毕竟是个垃圾的自己)

git地址:https://github.com/gin-gonic/gin

API列表:https://godoc.org/github.com/gin-gonic/gin

安装

1
go get -u github.com/gin-gonic/gin

要装好多东西。。。而且网。。。慢慢等就是了

运行基础代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func main() {
// Creates a gin router with default middleware:
// logger and recovery (crash-free) middleware
router := gin.Default()

router.GET("/someGet", getting)
router.POST("/somePost", posting)
router.PUT("/somePut", putting)
router.DELETE("/someDelete", deleting)
router.PATCH("/somePatch", patching)
router.HEAD("/someHead", head)
router.OPTIONS("/someOptions", options)

// By default it serves on :8080 unless a
// PORT environment variable was defined.
router.Run()
// router.Run(":3000") for a hard coded port
}

router

默认路由

gin.Default()

go gin在内部封装好了一套路由管理机制,主要介绍一下里面获取参数的方式吧。(其实就是翻译git文档)

路径匹配参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
func main() {
router := gin.Default()

// This handler will match /user/john but will not match /user/ or /user
router.GET("/user/:name", func(c *gin.Context) {
name := c.Param("name")
c.String(http.StatusOK, "Hello %s", name)
})

// However, this one will match /user/john/ and also /user/john/send
// If no other routers match /user/john, it will redirect to /user/john/
router.GET("/user/:name/*action", func(c *gin.Context) {
name := c.Param("name")
action := c.Param("action")
message := name + " is " + action
c.String(http.StatusOK, message)
})

// For each matched request Context will hold the route definition
router.POST("/user/:name/*action", func(c *gin.Context) {
c.FullPath() == "/user/:name/*action" // true
})

router.Run(":8080")
}

GET 传入的参数

query string: /welcome?firstname=Jane&lastname=Doe

1
2
3
4
5
6
router.GET("/welcome", func(c *gin.Context) {
firstname := c.DefaultQuery("firstname", "Guest")
lastname := c.Query("lastname") // shortcut for c.Request.URL.Query().Get("lastname")

c.String(http.StatusOK, "Hello %s %s", firstname, lastname)
})

其中DefaultQuery:DefaultQuery(key, defaultValue string) string,有默认值的获取参数。

POST表单获取参数

1
2
3
4
5
6
7
8
9
10
router.POST("/form_post", func(c *gin.Context) {
message := c.PostForm("message")
nick := c.DefaultPostForm("nick", "anonymous")

c.JSON(200, gin.H{
"status": "posted",
"message": message,
"nick": nick,
})
})

用map的方式获取query里的数组

request:

1
2
3
4
POST /post?ids[a]=1234&ids[b]=hello HTTP/1.1
Content-Type: application/x-www-form-urlencoded

names[first]=thinkerou&names[second]=tianou

server代码:

1
2
3
4
5
6
7
8
9
10
11
12
func main() {
router := gin.Default()

router.POST("/post", func(c *gin.Context) {

ids := c.QueryMap("ids")
names := c.PostFormMap("names")

fmt.Printf("ids: %v; names: %v", ids, names)
})
router.Run(":8080")
}

获取文件

request:

1
2
3
curl -X POST http://localhost:8080/upload \
-F "file=@/Users/appleboy/test.zip" \
-H "Content-Type: multipart/form-data"

server代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func main() {
router := gin.Default()
// Set a lower memory limit for multipart forms (default is 32 MiB)
// router.MaxMultipartMemory = 8 << 20 // 8 MiB
router.POST("/upload", func(c *gin.Context) {
// single file
file, _ := c.FormFile("file")
log.Println(file.Filename)

// Upload the file to specific dst.
// c.SaveUploadedFile(file, dst)

c.String(http.StatusOK, fmt.Sprintf("'%s' uploaded!", file.Filename))
})
router.Run(":8080")
}

参数类型绑定

针对json,xml,YAML等其他类型的request或者传统query,我们把request body绑定到某一特定类型上,再对这种类型的数据进行进一步处理。

bind分为两类:mustbind 和shouldbind,区别是当mustbind出错的时候,返回error并且强制终止后续处理并返回error 400。shouldbind当出错的时候只会返回error,是否继续执行由编写者自己决定。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// Binding from JSON
type Login struct {
User string `form:"user" json:"user" xml:"user" binding:"required"`
Password string `form:"password" json:"password" xml:"password" binding:"required"`
}
// 如果输入user没输入password,在这种情况下会报错。
// 如果把上面password的binging改成:binding:"-"就不会报错了
router.POST("/loginJSON", func(c *gin.Context) {
var json Login
if err := c.ShouldBindJSON(&json); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}

if json.User != "manu" || json.Password != "123" {
c.JSON(http.StatusUnauthorized, gin.H{"status": "unauthorized"})
return
}

c.JSON(http.StatusOK, gin.H{"status": "you are logged in"})
})

利用bind+validator可以实现鉴权,用到再说。

Html 渲染

Using LoadHTMLGlob() or LoadHTMLFiles()

1
2
3
4
5
6
7
8
9
10
11
func main() {
router := gin.Default()
router.LoadHTMLGlob("templates/*")
//router.LoadHTMLFiles("templates/template1.html", "templates/template2.html")
router.GET("/index", func(c *gin.Context) {
c.HTML(http.StatusOK, "index.tmpl", gin.H{
"title": "Main website",
})
})
router.Run(":8080")
}

templates/index.tmpl

1
2
3
4
5
<html>
<h1>
{{ .title }}
</h1>
</html>

渲染这部分用到的频率一直不算太高就这样吧

不含middleware的路由

也就是不像default一样封装好包括log等等,具体也等用到了再说

用gin运行多个server

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
package main

import (
"log"
"net/http"
"time"

"github.com/gin-gonic/gin"
"golang.org/x/sync/errgroup"
)

var (
g errgroup.Group
)

func router01() http.Handler {
e := gin.New()
e.Use(gin.Recovery())
e.GET("/", func(c *gin.Context) {
c.JSON(
http.StatusOK,
gin.H{
"code": http.StatusOK,
"error": "Welcome server 01",
},
)
})

return e
}

func router02() http.Handler {
e := gin.New()
e.Use(gin.Recovery())
e.GET("/", func(c *gin.Context) {
c.JSON(
http.StatusOK,
gin.H{
"code": http.StatusOK,
"error": "Welcome server 02",
},
)
})

return e
}

func main() {
server01 := &http.Server{
Addr: ":8080",
Handler: router01(),
ReadTimeout: 5 * time.Second,
WriteTimeout: 10 * time.Second,
}

server02 := &http.Server{
Addr: ":8081",
Handler: router02(),
ReadTimeout: 5 * time.Second,
WriteTimeout: 10 * time.Second,
}

g.Go(func() error {
err := server01.ListenAndServe()
if err != nil && err != http.ErrServerClosed {
log.Fatal(err)
}
return err
})

g.Go(func() error {
err := server02.ListenAndServe()
if err != nil && err != http.ErrServerClosed {
log.Fatal(err)
}
return err
})

if err := g.Wait(); err != nil {
log.Fatal(err)
}
}

Graceful Restart and Stop

随手查了一下,优雅重启和优雅关闭实际上是一种策略,就是在shutdown的时候先执行完手中所有的当下所有的请求再关闭,优雅重启就是要在重启的过程中更加平滑(说了等于没说

下面是抄的别的博客:

一种GracefulRestart的方法是,通过部署系统配合nginx来完成。由于大部分业务系统都是挂在nginx之后通过nginx进行反向代理的,因此在重启某台机器的进程A时,可以把该机器IP从nginx的upstream中摘除掉,等一段时间比如1分钟,该进程差不多也处理完了所以请求,实际上已经处于空闲状态了。这时就可以kill掉该进程并重启,等重启成功之后,再把该机器的IP加回到nginx对应的upstream中去。
这种方式是语言、平台无关的一种技术方案,但是缺点也很明显:

  • 首先就是复杂,需要部署系统和网关(nginx)恰到好处地配合。开发人员点击部署时,部署系统需要通知nginx摘掉某个upstream的某个IP;然后等进程重启成功之后,部署系统需要通知nginx在某个upstream中加上某个IP。这一整套系统的开发测试还是有一定复杂性的。
  • 其次是等待时间的未知性。当把机器A摘掉以后过多久进程才能处理完请求?10秒?1分钟?谁也不知道…间隔短了,会出问题,因为部分请求被卡断了;间隔长了,上线又慢,而且你还是不能确定是否请求都处理完了(其实基本上没问题,但是理论上无法保证)。
  • 另一个问题是压力陡增。对于大公司动辄几百台的集群,摘一两台无关紧要。但是对于小公司,比如某个服务只有两台机器,并且每台机器压力都挺大。这时如果直接摘一台,所有流量到另一台机器上,使得那台机器承受不住,那么可能会导致整个服务不可用。

因此这里引出第二种实现方式——fd继承

后面的不抄了,等有时间慢慢整理一下。

golang学习笔记二:concurrency

Posted on 2020-02-18 | Edited on 2022-11-14 | In go

Go在我个人理解里最独特的东西,对并发支持有着重大的作用。

线程,协程和goroutine

  • 进程:拥有自己独立的堆和栈,既不共享堆,亦不共享栈,进程由操作系统调度。
  • 线程:拥有自己独立的栈和共享的堆,共享堆,不共享栈,线程亦由操作系统调度。
  • 协程 :和线程一样共享堆,不共享栈,协程由程序员在协程的代码里显示调度。

进程懒得说了,线程按照道理应该在JAVA说的,协程按照道理应该在swoole里面说的(又欠了点东西要学)

简单说一下就是,协程全程都是用户态的,操作系统对于协程没有任何的感知,它所有的调度都由用户完成。goroutine其实就是不需要用户把所有的协程调度都写下来,而是用Go语言在runtime,系统调用等多方面对协程调度进行了封装和处理。

goroutine

其实就是个effective go的学习笔记(中文翻译):

https://golang.org/doc/effective_go.html#concurrency

https://github.com/golang/go/wiki/LearnConcurrency

中心思想:

Do not communicate by sharing memory; instead, share memory by communicating.

CSP

Communicating Sequential Processes (CSP)

Channel

channel是 goroutine 之间通信的一种方式,可以类比成 Unix 中的进程的通信方式管道。

使用

channel 使用内置的 make 函数创建,下面声明了一个 chan int 类型的 channel:

1
ch := make(chan int)

golang 提供了内置的 close 函数对 channel 进行关闭操作。

1
2
3
ch := make(chan int)

close(ch)

有关 channel 的关闭,你需要注意以下事项(抄的):

  • 关闭一个未初始化(nil) 的 channel 会产生 panic
  • 重复关闭同一个 channel 会产生 panic
  • 向一个已关闭的 channel 中发送消息会产生 panic
  • 从已关闭的 channel 读取消息不会产生 panic,且能读出 channel 中还未被读取的消息,若消息均已读出,则会读到类型的零值。从一个已关闭的 channel 中读取消息永远不会阻塞,并且会返回一个为 false 的 ok-idiom,可以用它来判断 channel 是否关闭
  • 关闭 channel 会产生一个广播机制,所有向 channel 读取消息的 goroutine 都会收到消息

buffer

简单来说,无缓存的channel像多线程的synchronize,向空channel读取消息或者向有数据的channel里发送数据都会阻塞,有缓存的 channel,当缓存未满时,向 channel 中发送消息时不会阻塞,当缓存满时,发送操作将被阻塞,直到有其他 goroutine 从中读取消息;相应的,当 channel 中消息不为空时,读取消息不会出现阻塞,当 channel 为空时,读取操作会造成阻塞,直到有 goroutine 向 channel 中写入消息。

有缓存的channel有点像semaphore,像下面的代码,sem buffer的大小决定了同时有几个协程在执行handle。

1
2
3
4
5
6
7
8
9
10
11
12
13
var sem = make(chan int, MaxOutstanding)

func handle(r *Request) {
sem <- 1 // Wait for active queue to drain.
process(r) // May take a long time.
<-sem // Done; enable next request to run.
}
func Serve(queue chan *Request) {
for {
req := <-queue
go handle(req) // Don't wait for handle to finish.
}
}

当然还有另一种不需要用channel缓存去限制的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
func handle(queue chan *Request) {
for r := range queue {
process(r)
}
}

func Serve(clientRequests chan *Request, quit chan bool) {
// Start handlers
for i := 0; i < MaxOutstanding; i++ {
go handle(clientRequests)
}
<-quit // Wait to be told to exit.
}

可以应用的算法

channel嵌套

来自effective go,但其实没有太理解书上说的 safe, parallel demultiplexing 是什么意思。

简单解释一下,代码接上面的最后一段代码,当解决多个不同返回值的request问题的时候,利用结构体和result channel可以让handle变成一个通用函数。

1
2
3
4
5
type Request struct {
args []int
f func([]int) int
resultChan chan int
}

由client自己提供需要解决的函数和它所有的参数,并且定一个result channel用来接收返回值,这样每个request都有属于自己的response路径。

1
2
3
4
5
6
7
8
9
10
11
12
func sum(a []int) (s int) {
for _, v := range a {
s += v
}
return
}

request := &Request{[]int{3, 4, 5}, sum, make(chan int)}
// Send request
clientRequests <- request
// Wait for response.
fmt.Printf("answer: %d\n", <-request.resultChan)

当request都定义好之后,handler就只要这样写就可以了而完全不需要针对每个不同的request进行修改。

1
2
3
4
5
func handle(queue chan *Request) {
for req := range queue {
req.resultChan <- req.f(req.args)
}
}
并行算法

这个和mpi几乎一模一样不想写了

leaky buffer

这个的是基于漏桶作出的设计,漏桶是在通信拥塞控制(congestion control)中使用的算法。(congestion control 就比如之前学的慢启动,快速恢复等等。)像漏斗一样,无论流入的流量多大,经过漏斗之后流出的流量速率都是稳定的。

为什么叫leaky是因为当漏斗满了之后的数据就会被丢弃,然后经过其他的处理。

effective go里写的漏桶算法不太清楚我找了另一个

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
package leakybuf

type LeakyBuf struct {
bufSize int // size of each buffer
freeList chan []byte
}

// NewLeakyBuf creates a leaky buffer which can hold at most n buffer, each
// with bufSize bytes.
func NewLeakyBuf(n, bufSize int) *LeakyBuf {
return &LeakyBuf{
bufSize: bufSize,
freeList: make(chan []byte, n),
}
}

// Get returns a buffer from the leaky buffer or create a new buffer.
func (lb *LeakyBuf) Get() (b []byte) {
select {
case b = <-lb.freeList:
default:
b = make([]byte, lb.bufSize)
}
return
}

// Put add the buffer into the free buffer pool for reuse. Panic if the buffer
// size is not the same with the leaky buffer's. This is intended to expose
// error usage of leaky buffer.
func (lb *LeakyBuf) Put(b []byte) {
if len(b) != lb.bufSize {
panic("invalid buffer size that's put into leaky buffer")
}
select {
case lb.freeList <- b:
default:
}
return
}

简单来说就是用带buffer的channel freeList来做这个漏斗,当漏斗空了的时候get函数会创建一个新的buffer(具体干什么我也不知道),而对put来说如果漏斗满了就会进入default(也就是抛弃当前buffer),这个算法是想证明利用channel来实现一些算法会更加简单易懂。

一些注意事项

协程参数调用问题

首先回到最前面buffer的功能里提到的可以当semaphore用的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var sem = make(chan int, MaxOutstanding)

func handle(r *Request) {
sem <- 1 // Wait for active queue to drain.
process(r) // May take a long time.
<-sem // Done; enable next request to run.
}

func Serve(queue chan *Request) {
for {
req := <-queue
go handle(req) // Don't wait for handle to finish.
}
}

当然这个代码有问题,虽然同时只有 MaxOutstanding个协程会执行,但是在高并发场景下会同时创建非常非常多个协程。为了避免这样的问题做了一定的改进:

1
2
3
4
5
6
7
8
9
func Serve(queue chan *Request) {
for req := range queue {
sem <- 1
go func() {
process(req) // Buggy; see explanation below.
<-sem
}()
}
}

把sem这个channel用来控制协程的创建,这样解决了上面的问题,但是这就涉及到标题的参数调用问题。

其实和多线程是一样的,上面创建的所有协程的process(req)指向的req都是同一个地址,在循环的过程中req不断地变化,就会导致process的req并不一定是想执行的那一个,就多线程的经验来说循环次数少的话很有可能第一个协程创建好的时候req已经循环到了最后一个。(多线程踩过的坑再踩一遍)

所以又多了第二次改进:

1
2
3
4
5
6
7
8
9
func Serve(queue chan *Request) {
for req := range queue {
sem <- 1
go func(req *Request) {
process(req)
<-sem
}(req)
}
}

把req作为参数传进去就没问题了,按照c的经验会给它在本地复制一块地址空间的。当然也可以在循环一开始就:req := req,但是看着很奇怪。

golang学习笔记一:基础篇

Posted on 2020-02-18 | Edited on 2022-11-14 | In go

运行

1
2
$ go run hello.go
Hello, World!
1
2
3
4
5
$ go build hello.go 
$ ls
hello hello.go
$ ./hello
Hello, World!

注释

注释和C++一样,都是/**/ 或者 //,可以用go doc指令来查看代码中的注释,比如我们想查找parse相关的内容:

1
$ go doc -all regexp | grep -i parse

变量

变量声明

一般形式

1
var identifier type
1
var identifier1, identifier2 type

根据值自动判断类型

1
var v_name = value

用:=替代var

用:=声明的变量可以被多次声明

1
v_name := value

初始化

可以在创建的时候初始化值:

1
var a int = 0

不初始化就默认零:

  • 数值类型(包括complex64/128)为 0

  • 布尔类型为 false

  • 字符串为 “”(空字符串)

  • 以下几种类型为 nil:

    1
    2
    3
    4
    5
    6
    7
    >   var a *int
    > var a []int
    > var a map[string] int
    > var a chan int
    > var a func(string) int
    > var a error // error 是接口
    >

new 和 make

Go语言中的 new 和 make 主要区别如下:

  • make 只能用来分配及初始化类型为 slice、map、chan 的数据。new 可以分配任意类型的数据;
  • new 分配返回的是指针,即类型 *Type。make 返回引用,即 Type;
  • new 分配的空间被清零。make 分配空间后,会进行初始化;

包引入

fmt

个人理解:和iostream类似,都是格式化I/O的包,简单格式:

Printf

就是个输出,用法和C的printf还有python的format有着微妙的相似性,都是第一个变量定义输出格式,后面填入输出变量。

  1. %v 占位符可以打印任何 Go 的值,“加号”标记(%+v)会添加字段名
  2. %T 可以打印出变量的类型
  3. %#v 相应值的Go语法表示
  4. %% 字母上的百分号,并非值的占位符
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import (
"fmt"
)

type Sample struct {
a int
str string
}

func main() {
s := Sample{a: 1, str: "hello"}

fmt.Printf("%v\n", s) //{1, hello}
fmt.Printf("%+v\n", s) //{a:1, str:hello}
fmt.Printf("%#v\n", s) //main.Sample{a:1, str:"hello"}
fmt.Printf("%T\n", s) // main.Sample
fmt.Printf("%%\n", s.a) // % %!(EXTRA int=1)
}

(抄一个例子),其他普通的%d什么的就懒得写了。

其他特殊用法:

指定宽度

1
2
3
fmt.Printf("%10d\n", 353)  // will print "       353"
//将宽度指定为 * 来将宽度当作 Printf 的参数:
fmt.Printf("%*d\n", 10, 353) // will print " 353"

多次引用一个变量

1
2
// 如果你在一个格式化的字符串中多次引用一个变量,你可以使用 %[n],其中 n 是你的参数索引(位置,从 1 开始)
fmt.Printf("The price of %[1]s was $%[2]d. $%[2]d! imagine that.\n", "carrot", 23)

Sprintf

Sprintf 则格式化并返回一个字 符串而不带任何输出。

1
s := fmt.Sprintf("是字符串 %s ","string")

Fprintf

和Printf的区别:格式化并输出到指定的地方

1
fmt.Fprintf(os.Stdout, "%s\n", "hello world!")

这样就是和Printf一样从os.stdout输出,看下面一个例子:

1
2
3
4
buf := bufio.NewWriter(os.Stdout)
// and now so does buf.
fmt.Fprintf(buf, "%s\n", "hello world! - buffered")
buf.Flush()

就变成了把输出扔进队列里。

Println

输出但是无法格式化。

自定义包

抄博客时间:

  • 文件名与包名没有直接关系,不一定要将文件名与包名定成同一个。
  • 文件夹名与包名没有直接关系,并非需要一致。
  • 同一个文件夹下的文件只能有一个包名,否则编译报错。

文件结构:

1
2
3
4
5
6
Test
--helloworld.go

myMath
--myMath1.go
--myMath2.go

测试代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// helloworld.go
package main

import (
"fmt"
"./myMath"
)

func main(){
fmt.Println("Hello World!")
fmt.Println(mathClass.Add(1,1))
fmt.Println(mathClass.Sub(1,1))
}
// myMath1.go
package mathClass
func Add(x,y int) int {
return x + y
}
// myMath2.go
package mathClass
func Sub(x,y int) int {
return x - y
}

常量

常量,也就是在运行时不会被修改的量,常量中的数据类型只可以是布尔型、数字型(整数型、浮点型和复数)和字符串型。常量可以用len(), cap(), unsafe.Sizeof()函数计算表达式的值。

  • 显式类型定义: const b string = "abc"
  • 隐式类型定义: const b = "abc"

itoa

iota,特殊常量,可以认为是一个可以被编译器修改的常量。const 中每新增一行常量声明将使 iota 计数一次(iota 可理解为 const 语句块中的行索引)。

第一个 iota 等于 0,每当 iota 在新的一行被使用时,它的值都会自动加 1;所以 a=0, b=1, c=2 可以简写为如下形式:

1
2
3
4
5
const (
a = iota
b
c
)

继续抄例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package main

import "fmt"

func main() {
const (
a = iota //0
b //1
c //2
d = "ha" //独立值,iota += 1
e //"ha" iota += 1
f = 100 //iota +=1
g //100 iota +=1
h = iota //7,恢复计数
i //8
)
fmt.Println(a,b,c,d,e,f,g,h,i)
}

运算符

go的运算符倒是和C的没什么太大区别,要用自己查。

语句

整体来讲和常用的语言没什么太大区别,说点不一样的吧

if

Go 对 if 语句做了稍微修改,支持在条件语句被求值之前先进行初始化:

1
2
3
if err := process(); err != nil {
return err
}

这也是一种非常常见的 go 的编写方式,虽然 err 不能在 if 语句之外使用,但他可以在任何 else if 或者 else 之内使用。

这样的写法和下面的写法相比要简介方便非常非常多:

1
2
3
4
5
6
7
8
9
10
f, err := os.Open(name)
if err != nil {
return err
}
d, err := f.Stat()
if err != nil {
f.Close()
return err
}
codeUsing(f, d)

for

go语言没有while,也是for。。。

1
for condition { }

函数

格式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func function_name( [parameter list] ) [return_types] {
函数体
}
// example
/* 函数返回两个数的最大值 */
func max(num1, num2 int) int {
/* 声明局部变量 */
var result int

if (num1 > num2) {
result = num1
} else {
result = num2
}
return result
}

返回值

可以返回多个值,默认情况下,Go 语言使用的是值传递,即在调用过程中不会影响到实际参数。

除了普通的返回值外,当给返回值命名时,它会被直接初始化,并且绑定给一个返回值,这样在函数内部使用的时候可以直接把它当作参数来用。

1
2
3
4
5
6
7
8
9
func ReadFull(r Reader, buf []byte) (n int, err error) {
for len(buf) > 0 && err == nil {
var nr int
nr, err = r.Read(buf)
n += nr
buf = buf[nr:]
}
return
}

返回地址问题:

因为go在执行返回的时候会先给返回内容分配地址,所以返回引用并不会有问题,也就是:return &f

数组

还是和C一样,go语言初始化数组时需要申明数组的长度:

1
2
3
4
5
var 数组变量名 [元素数量]Type
// 例子
var a [3]int // 定义三个整数的数组
// “...”省略号,则表示数组的长度是根据初始化值的个数来计算
q := [...]int{1, 2, 3}

但是9102年了还用这么死板的语言肯定是不行的,所以出现了切片:

切片

切片(slice)是建立在数组之上的更方便,更灵活,更强大的数据结构。切片并不存储任何元素而只是对现有数组的引用。

切片个人感觉和动态数组差不多,具体的运算逻辑和原因就后面再说,这里只提使用方法。你可以声明一个未指定大小的数组来定义切片:

1
var identifier []type

切片不需要说明长度。

或使用make()函数来创建切片:

1
2
3
4
5
var slice1 []type = make([]type, len)

也可以简写为

slice1 := make([]type, len)

向切片中增加新元素:append(oldslice, newelement)

指针

和C的还是无比相似,累了放弃。

结构和方法

结构的定义方法

1
2
3
4
5
6
type struct_variable_type struct {
member definition
member definition
...
member definition
}

关于结构:(抄的)

  1. 用于定义复杂的数据结构
  2. struct里面可以包含多个字段(属性),字段可以是任意类型
  3. struct类型可以定义方法(注意和函数的区别)
  4. struct类型是值类型
  5. struct类型可以嵌套
  6. Go语言没有class类型,只有struct类型

初始化方法:

  1. 指针类型
    var 变量 = new (结构体名称)
    var 变量 = &结构体名称{}
    var 变量 = &结构体名称{成员A:值,成员B:值}
  2. 值类型
    var 变量= 结构体{成员A:值,成员B:值}
1
2
3
4
5
6
7
8
9
10
11
12
type Books struct {
title string
author string
subject string
book_id int
}
func main() {
var Book1 Books
var Book2 *Books = &Book{}
var Book3 *Books = new(Books)
var struct_pointer *Books = &Book1 //指向book1结构体的指针
}

换个例子(还是抄的)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
type Student struct {
Name string
Age int
Source int
Next *Student
}

func creat() {
var stu_head = Student{Name: "li", Age: 20, Source: 100, Next: nil}
var stu_head1 = new(Student)
var stu_head3 = &Student{
Name: "wang",
Age: 12,
Next: stu_head1,
}
fmt.Println(stu_head)
fmt.Println(*stu_head1)
fmt.Println(stu_head3.Next)
}
func main() {
creat()
}

方法

方法这种东西总算回到了我们熟悉的OO编程的世界。。。

1
2
3
4
/* 实现接口方法 */
func (struct_name_variable struct_name) method_name1() [return_type] {
/* 方法实现 */
}

Go 没有面向对象,而我们知道常见的 Java,C++ 等语言中,实现类的方法做法都是编译器隐式的给函数加一个 this 指针,而在 Go 里,这个 this 指针需要明确的申明出来,其实和其它 OO 语言并没有很大的区别。

例:

1
2
3
4
func (c Circle) getArea() float64 {
//c.radius 即为 Circle 类型对象中的属性
return 3.14 * c.radius * c.radius
}

Map

像Python的dict的键值对,但是还不知道内部存储方式,累了怎么用以后再说。

Interface

就是我们知道的interface。。。。但是使用的时候不是用implementation或者是继承,只要在interface里定义了会被继承的类元素就行。

继续抄例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package main

import (
"fmt"
)

type Phone interface {
call()
}

type NokiaPhone struct {
}

func (nokiaPhone NokiaPhone) call() {
fmt.Println("I am Nokia, I can call you!")
}

type IPhone struct {
}

func (iPhone IPhone) call() {
fmt.Println("I am iPhone, I can call you!")
}

func main() {
var phone Phone //!!!,在具体struct iphone和nokia里是万万不能这么写的,因为new返回的是一个地址,需要赋值给一个指针来指向这个地址。

phone = new(NokiaPhone)
phone.call()

phone = new(IPhone)
phone.call()

}

interface的具体实现详见https://www.jianshu.com/p/70003e0f49d1 (后面有空再研究)

1234
Nina

Nina

37 posts
19 categories
40 tags
© 2024 Nina
Powered by Hexo v3.9.0
|
Theme — NexT.Pisces v6.3.0