Golang 泛型提案学习

Go 1.18 支持泛型了。现在抽空就看了一下泛型提案。英文地址
简单翻译如下:

翻译名词对照

  • type => 类型
  • constrain => 结束
  • interface => interface

摘要

我们建议扩展Go语言,对类型和函数声明添加可选的类型参数。类型参数被 interface 类型约束。当 interface 类型被用作类型限制的时候,可以支持添加额外的元素,用来限制满足 interface 的类型的集合。参数化的类型和函数可能会使用参数的操作,但这种情况只有在所有满足参数约束的类型都允许才行。类型 interface 通过一个统一的算法进行类型推断,以允许在函数调用的时候去掉类型参数。这个设计对 Go1 完全后向兼容。

怎么读这个提案

这个文章非常长,这是如何阅读的指导:

  • 我们从一个高级的概述开始,非常简单的描述概念
  • 然后我们从零开始解释全部的设计,介绍详情,并有简单的例子
  • 在设计全部描述完之后,我们讨论实现,一些设计的问题,然后跟其它的泛型方法比较。
  • 然后我用几个完整的例子,以展示这个实现在实践中是如何使用的。
  • 例子之后,在附录中谈论了一些小的细节。

高级别的概述

这部分非常简要的描述了这个设计建议的修改。这部分是为那些已经熟悉一个语言中, 泛型如何工作的人准备的。这些概念在后面的章节中会有详细的解释。

  • 函数可以有一个使用方括号的额外的类型参数列表,但是其它方面像一个普通的参数列表。func F[T any](p T) {...}
  • 这些类型参数可以被普通参数使用,也可以在函数体内使用。
  • 类型也可以有类型参数列表:type M[T any] []T.
  • 每个类型参数都有一个类型约束,就像每个普通参数都有一个类型一样:func F[T Constraint](p T) {...}
  • 类型约束是 interface 类型。
  • 新的预定义的约束 any 是一个允许所有类型的类型约束。
  • 当 interface 类型作为类型约束的时候,可以另外嵌入元素以限制满足约束的类型集合:
    • 一个任意的类型 T 限制只能用那个类型。
    • 近似元素 ~T 限制只能使用那些底层类型是 T 的类型。
    • 联合元素 T1 | T2 | ... 限制只能使用列出来的类型。
  • 泛型函数使用的操作,必须被所有符合约束的类型都支持。
  • 使用泛型函数或类型,必须传一个类型参数。
  • 在常见情况下,类型推断允许去掉一个函数的类型参数。

在接下来的部分中,我们将会对这些语言的修改详细的过一遍,你可能更想跳过开头,到例子部分,看一下实践中的泛型代码是怎么写的。

背景

在 Go 中,已经有很多要求支持泛型的请求的了。在 issue traker 上也有大量的讨论。

这个设计建议,通过添加一种参数多态的形式,对 Go 语言进行扩展,这里的类型参数不是被声明的子类型关系限制的(就像别的面向对象语言一样),而是被明确定义的结构化约束限制。

这个版本的设计跟2019年7月31号的设计草稿有很多相似的地方,不过 contracts 已经去掉了,替换成了 interface 类型,并且语法也有改变。

针对增加类型参数,已经有几个提案了,可以通过以前的链接找到。这里呈现的很多想法之前也出现过。这里描述的主要新特性是语法和 interface 类型作为约束的仔细检查。

这个设计不支持模板元编程或其它任何形式的编译时编程。

因为术语 generic 在 Go 社区广泛使用,我们下面将使用它来代表一个带着类型参数的函数或类型。不要将本设计中使用的术语 generic 跟其它语言像 C++, C#, Java 或 Rust 中使用的同样的术语搞混淆。它们有相似的地方但是不一样。

设计

我们将分阶段基于例子来描述完整的设计。

类型参数

泛型代码是使用抽象的数据类型来写的,这种抽象数据类型,我们称作类型参数。当运行泛型代码的时候,类型参数将被实际参数替换。

这是一个函数,它输出 slice 中的每个元素,这里 slice 中元素的类型 T 是未知的。这是为了支持泛型编程我们想要允许的函数中,一个微不足道的例子。(稍后我们将讨论泛型类型)。

1
2
3
4
5
6
7
// Print 输出 slice 中的元素。
// 它可能被传入任何类型的 slice.
func Print(s []T) { // 只是一个例子,不是建议的语法
for _, v := range s {
fmt.Println(v)
}
}

在这个方法中,第一个需要做的决定是:类型参数 T 怎么被声明?在像 Go 这样的语言中,我们希望每一个标识符都以某种方式被声明。

这里我做一个设计决定:类型参数跟普通的非类型函数参数相似,并且跟其它参数一起列出来。然而,类型参数跟非类型参数不一样,所以虽然它们都出现在参数列表中,但是我们想要区分它们。这会导致我们下一个设计决定:我们定义一个另外的可选的参数列表来描述类型参数。

类型参数列表出现在普通参数前面。为了区分类型参数列表和普通参数列表,类型参数列表使用方括号而不是小圆括号。就像普通参数拥有类型,类型参数也有元类型,就是约束。我们稍后将讨论约束的细节。现在我们只需要知道 any 是一个有效的约束,意思是任意类型都可以。

1
2
3
4
5
// Print 输出任意 slice 的元素。
// Print 有一个类型参数 T 和一个单个的普通参数 s, 它是一个 slice, slice的元素类型是 T
func Print[T any](s []T) {
// same as above
}

这就是说在 Print 函数中,标识符 T 是一个类型参数,这个类型现在还不知道,但是当函数被调用时就知道了。any的意思是T可以是任何类型。就像上面看到的,当描述普通的非类型参数时,类型参数可以被当作类型使用。在函数体中,它也可以当作类型使用。

跟普通参数列表不一样的是,在类型参数列表中名字是必须的。这可以避免语法歧义,并且没有任何理由去省略类型参数的名字。

由于 Print 有一个类型参数,所有对 Print 的调用必须提供一个类型参数。稍后我们将看到这个类型参数怎么通过非类型参数推断出来。现在我们将明确的传入类型参数。类型参数被传入,就相当于类型参数被声明了:作为一个分享的参数列表。当有类型参数列表时,使用方括号。

1
2
3
4
5
6
7
8
9
10
// 使用 []int 调用 Print.
// Print 有一个类型参数 T,并且我们想传入 []int,
// 所以我们传一个 int 类型参数,这么写 Print[int].
// Print[int] 函数期望参数是 []int
Print[int]([]int{1,2,3})

// 这将会输出
// 1
// 2
// 3

约束

让我们的例子稍微复杂点。比如有一个函数,它将为了把一个任意类型的 slice 转换成 []string, 将通过调用每个元素的 String 方法来实现。

1
2
3
4
5
6
7
// 这个方法是非法的。只是演示
func Stringify[T any](s []T) (ret []string) {
for _, v := range s {
ret = append(ret, v.String()) // 非法
}
return ret
}

第一眼看上去好像可以,不过这个例子中 v 的类型是 T, 而 T 可以是任何类型。这意味着 T 不需要有 String 方法。所以 v.String() 的调用是非法的。

当然了,同样的问题在其它支持泛型的语言中也存在。例如在 C++ 中,一个泛型函数(用C++的术语,一个函数模板)可以调用一个泛型类型的值的任意方法。就是在 C++ 中,调用 v.String() 是可以的。如果函数调用使用了一个类型参数,它没有 String 方法,编译的时候会报错。这些报错可能很长,比如在报错发生前有好几层的泛型函数调用时,为了明白哪里出错了,所有这些都需要报出来。

C++ 的方案对 Go 来说是个糟糕的选择。一个原因是语言的风格。在 Go 中我们不引用名字,比如,在这个例子中,String, 并且希望它们存在。当它们被看见的时候,Go 解析所有的名字到它们声明的地方。

另一个原因是 Go 被设计用来支持大规模编程的。我们必须考虑这个例子中泛型函数的定义(上面的 Stringify)以及对泛型函数的调用(没有显示,不过可能在其它包中)相距甚远的情况。一般来讲,所有的泛型代码期望类型参数符合某种确定的要求。我们当这种要求称作约束(其它语言有类似的概念,比如类型限制或trait限制或概念)。在这个情况,约束非常明显:类型必须有 String() string 方法。在别的情况中,可能没有那么明显。

我们不想从 Stringify 发生的地方衍生约束(在这个情况中,调用 String 方法的地方)。如果我们做了,对 Stringify 的一个小的改动可能会改变约束。那就意味着一个的改动可能影响很远的代码,调用这个函数的代码意外退出。对 Stringify 故意改变它的约束,并强制调用方改变是没有问题的。我们想要避免的是 Stringify 意外的改变了它的约束。

这意味着约束必须同时在调用者传入的类型参数和泛型函数中的代码中设置限制。调用者只能传满足约束的类型参数。泛型函数只能以约束允许的方式使用这些值。我们相信任何尝试在 Go 中定义泛型编程,这都是一条重要的规则:泛型代码只能使用它的类型参数知道实现了的操作。

任意类型允许的操作

在我们讨论约束之前,我们简单的记住 any 约束是什么。如果一个泛型函数使用 any 约束,就像上面的 Print 函数一样,任意类型都允许。泛型函数中类型参数的值可以使用的操作就是那些被任意类型都允许的操作。在上面的例子中,Print 函数声明了一个变量 v 它的类型是 T,并且它把这个变量传给函数。

任意类型允许的操作是:

  • 声明这些类型的变量
  • 把同类型的其它值你分配给这些值
  • 把这些变量传给函数或者在函数返回它们
  • 取这些变量的地址
  • 转换或者分配这些类型的值给 interface{}
  • 转换 T 类型的值到类型 T (允许但是没有用)
  • 使用类型断言把一个 interace 类型的值转到这些类型
  • 用在 type switch 中的 case
  • 定义和使用这些类型的组合类型,比如这些类型的 slice
  • 把这些类型传一些预定义的函数,比如 new

可能随着未来语言变化,可能增加其它的操作,但这不是现在可以预料的。

定义约束

Go 中已经有一种合约非常接近我们约束的需要:interface 类型。一个 interface 类型是一组方法。只有那些类型实现了同样的方法,它们的值才能分配给 interface 类型的值。interface 类型的值可以的事情,不是类型允许的操作,只是调用这些方法。

使用类型参数调用一个泛型函数跟赋值给 interface 类型非常相似:传入的类型参数必须实现类型参数的约束。写一个泛型函数就像使用 interface 类型的值:泛型代码只能使用被约束允许的操作(或者被any 类型允许的操作)。

所以,在这个设计中,约束就是简单的 interface 类型。满足约束就意味着实现 interface 类型。(为了定义非方法的操作约束,比如二进制操作,后面我们再重述这个)。

对于 Stringify 例子,我们需要一个 interface 类型:它有一个 String 方法,没有入参,返回一个 String 的值。

1
2
3
4
5
// Stringer 是一个类型约束,它要求类型参数有一个 String 方法并允许泛型函数调用 String.
// String 方法应该返回一个 string 代表的值。
type Stringer interface {
String() string
}

(跟这个讨论来没关系,但是这里定义了跟标准库 fmt.Stringer 一样的 interface 类型,真实的代码应该直接使用 fmt.Stringer)。

any 约束

现在我们知道约束就是简单的 interface 类型,我们可以解释 any 约束是什么意思。就像上面显示的那样,any 约束允许任何类型作为类型参数,并且只允许函数使用任意类型允许的操作。它的 interface 类型就是 interface{} (空接口)。所以我们像这样写 Print 的例子

1
2
3
func Print[T interface{}](s []T) {
// 跟上面一样
}

然而,每次当你要写一个泛型函数,它不强制类型参数的约束的时候,必须每次都写 interface{},这太无聊了。所以在这个设计中,我们建议约束 any 等价于 interface{}。这将是一个预定义的名字,在 universe block 中隐式声明。使用 any 用作类型参数约束以外的地方是无效的。

(注意:显然我们可以将 any 作为 interface{} 的别名,或者作为定义为 interface{} 的新类型。但是我们不希望这种关于泛型的设计导致非泛型代码的重大修改。添加 any 作为 interface{} 的通用名称可以而且应该单独讨论)

使用约束

对于泛型函数,约束中可以认为是类型参数的类型:元类型。就像上面显示的那样,约束出现在类型参数列表中,作为类型参数的元类型。

1
2
3
4
5
6
// Stringify 调用 s 中每个元素的 String 方法, 并且返回结果
func Stringify[T Stringer](s []T) (ret []string) {
for _, v := range s {
ret = append(ret, v.String())
}
}

在这个情形中,类型参数 T 后面跟着对它的约束。

多个类型参数

虽然 Stringify 的例子只用了一个类型参数,函数也可以有多个类型参数。

1
2
// Print2 有两个类型参数,和两个非类型参数
func Print2[T1, T2 any](s1 []T1, s2 []T2) {...}

跟这个相比
1
2
// Print2Same 有一个类型参数和两个非类型参数
func Print2Same[T any](s1 []T, s2 []T) {...}

Print2 中,s1 和 s2 可能是不同类型的 slice。在 Print2Same 中,s1 和 s2 必须是同一个类型的 slice。

就像每个普通参数都有自己的类型,每个类型参数也有自己的约束。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// Stringer 是一个需要 String 方法的约束。
// String 方法需要返回一个 string。
type Stringer interface {
String() string
}

// Plusser 是一个需要 Plus 方法的约束。
// Plus 方法期望对内部的字符串增加一个参数,然后返回结果。
type Plusser interface {
Plus(string) string
}

// ConcatTo 接收一个slice, 其中每个元素都有一个 String 方法,和一个slice, 其中每个元素都有一个 Plus 方法。
// 这些 slice 的元素数量比須相同。这将把 s 中的每个元素转成一个字符串,把它传给 p 中相应元素的 Plus 方法,
// 并且返回一个 slice, 其中包含结果 string.
func ConcatTo[S Stringer, P Plusser](s []S, p []P) []string {
r := make([]string, len(s))
for i, v := range s {
r[i] = p[i].Plus(v.String())
}

return r
}

一个约束可以用到多个类型参数,就像一个类型参数可以用到多个非类型参数上。约束单独应用到每个类型参数上。

1
2
3
4
5
6
7
8
9
10
11
// Stringify2 将两个不类型的 slice 转成 string, 并且返回所有 string 连接的结果。
func Stringify2[T1, T2 Stringer](s1 []T1, s2 []T2) string {
r := ""
for _, v1 := range s1 {
r += v1.String()
}
for _, v2 := range s2 {
r += v2.String()
}
return r
}

泛型类型

除了泛型函数,我们还想要泛型类型。我们建议类型可以被扩展到接受类型参数。

1
2
// Vector 是一个元素可以为任意类型的 slice.
type Vector[T any] []T

类型的类型参数就像函数的类型参数一样。

在类型定义中,类型参数可以像别的类型一样使用。

为了使用泛型类型,你必须提供类型参数。这叫做实例化。类型参数就像以前一样出现在方括号中。当我们通过为类型参数提供类型参数来实例化一个类型时,我们会产生一个类型,其中在类型定义中的类型参数的每次使用,都会被相应类型实参替换。

1
2
3
4
5
6
7
8
9
// v 是一个整形值的 Vector
//
// 这就相当于假装 "Vector[int]" 是一个有效的标识符,
// 并且写做
// type "Vector[int]" []int
// var v "Vector[int]"
// 所有 Vector[int] 使用的地方都指向同一个 "Vector[int]" type.
//
var v Vector[int]

泛型类型也可以有方法。方法的接收者类型必须声明同样数量的类型参数, 跟声明在接收者类型定义一样。它们的声明没有约束。

1
2
// Push 增加一个值到一个 Vector 的末尾
func (v *Vector[T]) Push(x T) { *v = append(*v, x) }

在方法声明中列出的类型参数,不需要拥有跟在类型定义中的类型参数一样的名字。特别地,如果它们没有被方法使用,它们可以是 _

在一个类型通常可以引用自身的情况下,泛型类型可以引用自身。但是当它这样做时,类型参数必须是类型参数,以相同的顺序列出。此限制可以防止类型参数实例化的无限递归。

1
2
3
4
5
6
7
8
9
10
// List 是一个链表,其值是类型 T
type List[T any] struct {
next *List[T] // 这个引用到 List[T] 是允许的
val T
}

// 这个类型是无效的
type P[T1, T2 any] struct {
F *P[T2, T1] // 无效的; 必须是 [T1, T2]
}

这个限制对直接引用和间接引用都有效。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// ListHead 是一个链表的头
type ListHead[T any] struct {
head *ListElement[T]
}

// ListElement 是有头链表的一个元素。
// 每个元素指向头
type ListElement[T any] struct {
next *ListElement[T]
val T
// 这里使用 ListHead[T] 是可以的。
// ListHead[T] 引用 ListElement[T] 引用 ListHead[T]。
// 使用 ListHead[int] 就不可以,因为 ListHead[T] 可能有一个间接引用到 ListHead[int].
head *ListHead[T]
}

(注意:随着对人首怎么写代码越来越了解,可能会放松这个规则以允许一些使用不同类型参数的case)

泛型类型的类型参数可能拥有不是any 的约束。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// StringableVector 是一个一些类型的 slice,这里的类型必须有 String 方法。
type StringableVector[T Stringer] []T

func (s StringableVector[T]) String() string {
var sb strings.Builder
for i, v := range s {
if i > 0 {
sb.WriteString(", ")
}
// 这里调用 v.String() 是可以的,因为 v 的类型是 T,T有一个约束是 Stringer
sb.WriteString(v.String())
}

return sb.String()
}

方法可能不带另外的类型参数

尽管泛型类型的方法可能使用类型参数,方法可能没有另外的类型参数。这里给方法增加类型参数可能是有用的,人们不得不写一个适当参数化的顶级函数。

这里有更多的讨论

(未完,待续。。。)

HTTP 状态码 502 与 504 的区别

线上偶尔会有502或者504的报错. 我们访问网页的时候也经常会有. 那么它们到底有什么区别呢?
今天就查了一些资料, 来学习一下.

先来看释义:

  • 502: Bad Gateway. 表示web server 做为了一个gateway 或者 proxy 的时候, 从上游接受到了无效的 response.
  • 504: Gateway Timeout. 表示web server 做为一个gateway 或者 proxy 的时候, 无法即时的从上游得到一个response, 来完成请求.

看起来好像差不多. 但在实际开发中, 凭经验, 好像502, 504都是因为超时.

LNMP 下来看一下502, 504

下面结合 LNMP 的情形下来来看一下.

Nginx 产生 502 的原因

  • PHP-FPM 没有运行
  • Nginx 无法连接 PHP-FPM

PHP-FPM 没有启动

如果因为这些原因, Nginx 无法连上 PHP-FPM, 那么将会导致 502. access.log 中:

1
127.0.0.1 - - [22/May/2021:17:36:19 +0800] "GET / HTTP/1.1" 502 158 "-" "curl/7.76.1" "-"

这时候 error.log 中为:
1
connect() to unix:/run/php/php7.2-fpm.sock failed (2: No such file or directory) while connecting to upstream

这可能是因为没有启动 PHP-FPM, 启动就好了.

PHP-FPM 处理超时

如果你的应用反应时间太长, 将会产生一个超时的错误. 如果 PHP-FPM 的超时设置比 Nginx 的超时设置小. Nginx 将会返回 502.
这是因为 PHP-FPM 关闭了连接.

error.log 将会显示:

1
recv() failed (104: Connection reset by peer) while reading response header from upstream

这个时候, 如果有 php-fpm 日志, 将会显示:

1
ARNING: [pool mypool] child 2120, script '/var/www/html/index.php' (request: "GET /index.php") execution timed out (25.755070 sec), terminating

PHP-FPM 没有超时, Nginx 超时

如果这个时候你调高了 php-fpm 的超时时间, 这将引发另一个问题, nginx 可能会没有接收到 PHP-FPM 的响应而超时, 这个时候 Nginx 会返回 504.

参考

  1. (502)[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/502]
  2. (504)[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/504]

一个Go服务占用CPU太高的优化过程

最近上线一个Go服务, 在高峰期总是会报CPU超70%的报警.
然后就打开 pprof 开始追踪, 发现近 50% 的CPU被 runtime.gcDrain 及相关的 gc 函数占用了.

这说明 gc 很活跃.

这时候也不知道从哪里入手. 大概只有两个方向, 看一下 heap 的情况:

  1. 看看哪里占的内存总多
  2. 看看哪里分配的对象最多

最后定位到解码的函数, json 和 mapstructure.
仔细看了之后, 发现这里有很多直接传值的地方, 而不是传指针. 修改后, 再压测: 有改善, 但不明显.

这个时候就没有什么思路了.

经同事提醒. 再优化 elasticsearch 取数据的地方, 只取需要的 fields. 再压测, 发现有小幅提升. 但仍不明显.
跟同事讨论的时候, 我发现问题并不是CPU占用太高了, 而是CPU占用很高, 内存占用很少, 很不均衡. CPU占用70%, 内存不超过5%.

那搜索一下, 发现 Go 提供了一个 GOGC 的变量, 可以设置触发 GC 的值. 运行时也可以通过 runtime/debug 包的 func SetGCPercent(percent int) int 来修改.
默认是100, 那先修改成 500 看一下. 发现效果明显. CPU 降低了 50%.

问题解决了!!! 优化了一个星期, 改了很多代码. 最后是一行代码解决了!!!

不过目前看来 500 也有点低, 下次再调一下找一个更合理的值.

所以这个问题的原因是: 我们这个服务不是个CPU密集型的应用, 但是CPU占用过高, 而内存占用很低. (嗯, 只能说 Go 的 GC 很给力).
那这种情况下, 应该先看看, 是否可以降低触发 Gc 的阈值看一下, 也许就是你要的.

Go 优化Tips

今天看了一篇博客,介绍Go性能压测与pprof. 地址: profiling-and-optimizing-go-web-applications

其中总结的优化Tips很不错:

  1. 避免不必要的 heap 申请
  2. 对于不大的结构体,使用值比指针更好
  3. 对于maps和slice, 如果提前知道大小,最好预分配大小
  4. 如果不是必要,就不打LOG
  5. 如果做很多顺序读写,使用 buffered I/O
  6. 如果你的应用重度使用JSON,考虑使用生成器的分析序列化工具。If your application extensively uses JSON, consider utilizing parser/serializer generators (I personally prefer easyjson).
  7. 热点path 的每个操作都到头重要。 Every operation matters in a hot path.

原文:

  1. Avoid unnecessary heap allocations.
  2. Prefer values over pointers for not big structures.
  3. Preallocate maps and slices if you know the size beforehand.
  4. Don’t log if you don’t have to.
  5. Use buffered I/O if you do many sequential reads or writes.
  6. If your application extensively uses JSON, consider utilizing parser/serializer generators (I personally prefer easyjson).
  7. Every operation matters in a hot path.

PHP 中多个 Subpattern 匹配问题

今天无意中看到多个小括号嵌套的正则表达式. 突然就想, 那么匹配出来后的排序是怎么样的? 于是决定看一下文档.
文档地址: https://www.php.net/manual/en/regexp.reference.subpatterns.php

什么是子模式 (Subpattern)

子模式通过小括号来界定. 它有两个作用:

  1. 局部化一组可替代方案. 例如: (sun|mon)day, 既匹配 sunday, 也匹配 monday. 如果不用小括号的写法是 sunday|monday.
  2. 它建立一组可捕捉的子模式. 当整个模式匹配时, 匹配子模式的字符串通过参数传回给调用者(像 preg_match 中的 match参数). match 是个数组. 它从左到右按开括号的顺序数, (从1开始) 去获取捕捉字符串的数量.

例如:

1
2
字符串 "the red king" 匹配模式 ((red|white)(king|queen)). 
捕捉子字符串就是 "red king", "red", "king". 下标分别是 1, 2, 3

再来看一个例子:

1
2
3
模式是 /((Sat)ur|(Sun))day/
匹配字符串 Saturday 的结果是, 捕捉子字符串分别是 1 => Satur, 2 => Sat
匹配字符串 Sunday 的结果是, 捕捉子字符串分别是 1 => Sun, 2 => '', 3 => Sun

仔细想想为什么?

数量限制

捕捉子字符串最多 65535 个. 不过一般不会到这个上限. 应该能满足大多数的需要.

如何关闭捕捉子模式?

有的时候, 我们可能只想使用它的多组替代方案的功能(上面第1点), 而不想使用它的捕捉功能(上面第2点). 那该怎么做呢?
如果在左小括号后, 紧跟 “?:” 字符, 那么子模式不再时行捕捉, 并且在计算子模式的捕捉子序列时也不再计数.

例如:

1
2
((?:red|white)(king|queen)) 匹配字符串 white queen 时,
子模式匹配的子序列分别是: white queen, queen, 下标分别为1,2

可互相替代的组号

有时候需要有多个匹配的子模式, 但是他们的子组号可互相替代, 就是子组号要一样.
正常情况下, 每个子模式都会有一个后向引用的组号, 即使它们当中只有一个子模式会被匹配到.
解决这个问题, 需要 “?|”, 它允许产生重复的组号

比如:

1
(?:(Sat)ur|(Sun))day 匹配 Sunday, 子模式的匹配序列为: 1 => '', 2 => Sun

这里 Sun 的序号是2, 即使1是空的.
使用 ?| 后:
1
2
(?|(Sat)ur|(Sun))day 匹配 Sunday, 子模式的匹配序列为: 1 => Sun
(?|(Sat)ur|(Sun))day 匹配 Saturday, 子模式的匹配序列为: 1 => Sat

使用 ?| , Sat 和 Sun 的后向引用都是 1.

Linux下如何设置 TCP KeepAlive

1. 什么是 TCP KeepAlive

TCP KeepAlive 是一种机制, 检测 TCP 连接的另一端是否已经停止响应了

2. 怎么检测

TCP 在空闲一段时间之后, 将来发送包含null数据的检测包到另一端. 如果另一端没有响应, socket 就会自动关闭.

3. 如何设置

TCP 的 KeepAlive 可以提高带宽的使用率. 那么在 Linux 下怎么设置呢?
Linux 系统可以通过 /ect/sysctl.conf 来进行设置.

如果想查看当前系统使用的设置是什么, 可以通过以下命令:

1
2
3
4
5
6
7
8
9
ls -l /proc/sys/net/ipv4/tcp_keepalive*
-rw-r--r-- 1 root root 0 Jul 24 13:56 /proc/sys/net/ipv4/tcp_keepalive_intvl
-rw-r--r-- 1 root root 0 Jul 24 13:56 /proc/sys/net/ipv4/tcp_keepalive_probes
-rw-r--r-- 1 root root 0 Jul 24 13:56 /proc/sys/net/ipv4/tcp_keepalive_time

cat /proc/sys/net/ipv4/tcp_keepalive*
75
9
7200

他们都代表什么意思呢?

1
2
3
tcp_keepalive_time = 7200 (seconds)
tcp_keepalive_intvl = 75 (seconds)
tcp_keepalive_probes = 9 (number of probes)

    1. tcp keepalive 将会 socket 的活动后, 等待 7200 秒, 才会发送第一个 keepalive 探测.
    1. 然后它每隔 75 秒发一次探测. 只要 TCP/IP socket 正常交流并且活跃, 就不需要 keepalive 包.
    1. 只到9次检测都失败, 将会设为失败

4. 如何设置

    1. 编辑 /ect/sysclt.conf 文件
      1
      vi /etc/sysctl.conf
    1. 编辑或添加配置
      1
      2
      3
      net.ipv4.tcp_keepalive_time = 60
      net.ipv4.tcp_keepalive_intvl = 10
      net.ipv4.tcp_keepalive_probes = 6
    1. 加载配置使之生效
      1
      sysctl -p

Git rebase 与 merge 的区别

今天看到一篇文章, 讲到了 git rebase 与 git merge 的区别. 我觉得讲的非常好.

功能区别

背景

比如一个git项目有两个分支, master 和 feature. 当前在feature 开发. 那 feature 是从 master fork 出来的.
这个时候, 别人也在向 master 合并代码. 那么现在 master 和 feature 已经分叉了. 这时, 如果想让master的新提交在feature也出现.
有两个方法, 一是git rebase, 二是git merge.

  1. git merge. 会把 master 的内容和feature的内容合并, 并产生一个新的 commit.
  2. git rebase. 会把当前feautre 分支到master的根commit到最新的commit都修改一遍. 使feature与master的分叉commit 建立在 master 的最新commit 之后. 这样产生的历史是线性的.

git rebase 的注意事项

git rebase 应该永远使用在私有分支上. 因为它会修改 commit, 如果是公开的分支, 会影响别人.

上面没有图, 只有文字说明, 比较抽象. 具体详细的说明参见: https://www.atlassian.com/git/tutorials/merging-vs-rebasing

怎样在Ubuntu下设置交换内存

在云主机上更新PHP的composer的时候, 一直报错, 内存不够. htop看了一下, 一共才900多M的内存, 跑了很多软件, 已经占用了700M了.
没办法, 不能为了软件升个级就再买CPU吧. 只好使用交换内存了.

记录如下:

  1. 检查系统是否打开了交换内存. 一般没有.

    1
    sudo swapon -s
  2. 创建一个交换文件, 用于交换, 大小自定. 我设的是物理内存的2倍. 文件地址随意.

    1
    2
    sudo fallocate -l 2G /swapfile
    chmod 600 /swapfile
  3. 现在让这个文件可以用作交换内存.

    1
    sudo mkswap /swapfile
  4. 打开交换内存

    1
    sudo swapon /swapfile
  5. 现在再检查一下交换内存是否打开(参见步骤1)

  6. 如果想要永久生效, 就是重启也有效, 需要设置 /etc/fstab 文件

    1
    echo '/swapfile none swap sw 0 0' >> /etc/fstab
  7. 设置swappiness参数, 它表示在什么情况下使用交换内存. 取值在0-100之间, 表示在启用交换内存前, 物理内存空间的占比.
    那么:

  • 0: 表示关闭交换内存
  • 1: 最小数量的交换内存, 但并不完全关闭
  • 10: 比较推荐的值, 这样保证系统有足够的内存, 并且能保证性能.
  • 100: 积极的使用交换内存. (没有使用过这个值)

设置方法:

1
2
3
4
sudo vi /etc/sysctl.conf

add content
vm.swappiness=10

exit vim, 现在使它生效.

1
sudo sysctl -p

到这一步, 已经完成了交换的设置.

Linux下的进程和线程有什么区别?

概念

进程和线程有什么不同? 或者说有什么区别?
我们都知道: 进程是操作系统管理资源的最小单位, 线程是系统调度的基本单位.

那么具体到Linux系统, 进程和线程有什么区别呢?

首先, 在Linux Kernel看来, 其实是没有线程的. 所有的用户线程在在内核看来都是进程, 不过是轻量级的进程(Light Weight Process). 跟普通的进程有点区别. 区别在于:
轻量级进程之间共享同样的地址空间以及打开的文件等资源. 相比普通进程更轻量一些.

So, effectively we can say that threads and light weight processes are same.
It’s just that thread is a term that is used at user level while light weight process is a term used at kernel level.

实际上, 我们可以说线程和轻量级进程是一样的. 只是线程是一个用户级的术语, 而轻量级进程是一个内核级的术语.

实现

从实现的角度来看, 线程使用 pthread 库来创建. 它内部, 使用了 clone() 的系统调用来创建轻量级进程, 像创建普通进程一样. 这意味着创建一个普通进程的 fork() 函数, 后面也会调用 clone(), 根据创建线程或轻量级进程, 使用不同的参数.

所以进程和线程的主要不同, 就是调用 clone() 的传参不同.

系统调用 clone() 克隆一个任务, 带有一个可配置的共享级别, 它们是:

  1. CLONE_FILES: 共享同样的文件描述符表(而不是创建一个新的)
  2. CLONE_PARENT: 不在新任务和旧任务之间创建父子关系. 否则的话, 子进程的 getppid() = 父进程的 getpid()
  3. CLONE_VM: 共享同样的内存空间, 而不是复制一份.

fork() 调用 clone(), 最少的共享.
pthread_create() 调用 clone(), 最多的共享.

参考文献

  1. https://www.thegeekstuff.com/2013/11/linux-process-and-threads/
  2. https://stackoverflow.com/a/809049/2550332

命令行中向一个文件插入多行

命令行中向一个文件插入多行

1
2
3
4
5
6
7
8
9
10
11
12
13
# possibility 1:
echo "line 1" >> greetings.txt
echo "line 2" >> greetings.txt

# possibility 2:
echo "line 1
line 2" >> greetings.txt

# possibility 3:
cat <<EOT >> greetings.txt
line 1
line 2
EOT