Blog

语义导入版本机制

Go Module 的语义导入版本机制 #

我们看到 go.mod 的 require 段中依赖的版本号,都符合 vX.Y.Z 的格式。在 Go Module 构建模式下,一个符合 Go Module 要求的版本号,由前缀 v 和一个满足语义版本规范的版本号组成。

语义版本号分成 3 部分:主版本号 (major)、次版本号 (minor) 和补丁版本号 (patch)。

按照语义版本规范,主版本号不同的两个版本是相互不兼容的。而且,在主版本号相同的情况下,次版本号大都是向后兼容次版本号小的版本。补丁版本号也不影响兼容性。

而且,Go Module 规定:如果同一个包的新旧版本是兼容的,那么它们的包导入路径应该是相同的。怎么理解呢?我们来举个简单示例。我们就以 logrus 为例,它有很多发布版本,我们从中选出两个版本 v1.7.0 和 v1.8.1.。按照上面的语义版本规则,这两个版本的主版本号相同,新版本 v1.8.1 是兼容老版本 v1.7.0 的。那么,我们就可以知道,如果一个项目依赖 logrus,无论它使用的是 v1.7.0 版本还是 v1.8.1 版本,它都可以使用下面的包导入语句导入 logrus 包:

import "github.com/sirupsen/logrus"

那么问题又来了,假如在未来的某一天,logrus 的作者发布了 logrus v2.0.0 版本。那么根据语义版本规则,该版本的主版本号为 2,已经与 v1.7.0、 v1.8.1 的主版本号不同了,那么 v2.0.0 与 v1.7.0、v1.8.1 就是不兼容的包版本。然后我们再按照 Go Module 的规定,如果一个项目依赖 logrus v2.0.0 版本,那么它的包导入路径就不能再与上面的导入方式相同了。那我们应该使用什么方式导入 logrus v2.0.0 版本呢?

...

remove disabled snap packages

Content #

LANG=C snap list --all | awk '/disabled/{print $1, $3}' |
    while read snapname revision; do
        snap remove "$snapname" --revision="$revision"
    done

Viewpoint #

From #

Go语言的版本发布策略

Go语言的版本发布策略 #

Go 团队已经将版本发布节奏稳定在每年发布两次大版本上,一般是在二月份和八月份发布。Go 团队承诺对最新的两个 Go 稳定大版本提供支持,比如目前最新的大版本是 Go 1.17,那么 Go 团队就会为 Go 1.17 和 Go 1.16 版本提供支持。如果 Go 1.18 版本发布,那支持的版本将变成 Go 1.18 和 Go 1.17。支持的范围主要包括修复版本中存在的重大问题、文档变更以及安全问题更新等。

Viewpoint #

From #

垂直组合与水平组合

垂直组合 #

Go 语言为支撑组合的设计提供了类型嵌入(Type Embedding)。通过类型嵌入,我们可以将已经实现的功能嵌入到新类型中,以快速满足新类型的功能需求,这种方式有些类似经典面向对象语言中的“继承”机制,但在原理上却与面向对象中的继承完全不同,这是一种 Go 设计者们精心设计的“语法糖”。

被嵌入的类型和新类型两者之间没有任何关系,甚至相互完全不知道对方的存在,更没有经典面向对象语言中的那种父类、子类的关系,以及向上、向下转型(Type Casting)。通过新类型实例调用方法时,方法的匹配主要取决于方法名字,而不是类型。这种组合方式,我称之为垂直组合,即通过类型嵌入,快速让一个新类型“复用”其他类型已经实现的能力,实现功能的垂直扩展。

你可以看看下面这个 Go 标准库中的一段使用类型嵌入的组合方式的代码段:

// $GOROOT/src/sync/pool.go
type poolLocal struct {
    private interface{}
    shared  []interface{}
    Mutex
    pad     [128]byte
}

在代码段中,我们在 poolLocal 这个结构体类型中嵌入了类型 Mutex,这就使得 poolLocal 这个类型具有了互斥同步的能力,我们可以通过 poolLocal 类型的变量,直接调用 Mutex 类型的方法 Lock 或 Unlock。

另外,我们在标准库中还会经常看到类似如下定义接口类型的代码段:

// $GOROOT/src/io/io.go
type ReadWriter interface {
    Reader
    Writer
}

这里,标准库通过嵌入接口类型的方式来实现接口行为的聚合,组成大接口,这种方式在标准库中尤为常用,并且已经成为了 Go 语言的一种惯用法。

水平组合 #

垂直组合本质上是一种“能力继承”,采用嵌入方式定义的新类型继承了嵌入类型的能力。Go 还有一种常见的组合方式,叫水平组合。和垂直组合的能力继承不同,水平组合是一种能力委托(Delegate),我们通常使用接口类型来实现水平组合。

Go 语言中的接口是一个创新设计,它只是方法集合,并且它与实现者之间的关系无需通过显式关键字修饰,它让程序内部各部分之间的耦合降至最低,同时它也是连接程序各个部分之间“纽带”。

水平组合的模式有很多,比如一种常见方法就是,通过接受接口类型参数的普通函数进行组合,如以下代码段所示:

// $GOROOT/src/io/ioutil/ioutil.go
func ReadAll(r io.Reader)([]byte, error)
// $GOROOT/src/io/io.go
func Copy(dst Writer, src Reader)(written int64, err error)

也就是说,函数 ReadAll 通过 io.Reader 这个接口,将 io.Reader 的实现与 ReadAll 所在的包低耦合地水平组合在一起了,从而达到从任意实现 io.Reader 的数据源读取所有数据的目的。类似的水平组合“模式”还有点缀器、中间件等,这里我就不展开了,在后面讲到接口类型时再详细叙述。

...

Go语言的组合设计哲学

Go语言的组合设计哲学 #

这个设计哲学和我们各个程序之间的耦合有关,Go 语言不像 C++、Java 等主流面向对象语言,我们在 Go 中是找不到经典的面向对象语法元素、类型体系和继承机制的,Go 推崇的是组合的设计哲学。

在诠释组合之前,我们需要先来了解一下 Go 在语法元素设计时,是如何为“组合”哲学的应用奠定基础的。

在 Go 语言设计层面,Go 设计者为开发者们提供了正交的语法元素,以供后续组合使用,包括:

  1. Go 语言无类型层次体系,各类型之间是相互独立的,没有子类型的概念;
  2. 每个类型都可以有自己的方法集合,类型定义与方法实现是正交独立的;
  3. 实现某个接口时,无需像 Java 那样采用特定关键字修饰;
  4. 包之间是相对独立的,没有子包的概念。

我们可以看到,无论是包、接口还是一个个具体的类型定义,Go 语言其实是为我们呈现了这样的一幅图景:一座座没有关联的“孤岛”,但每个岛内又都很精彩。那么现在摆在面前的工作,就是在这些孤岛之间以最适当的方式建立关联,并形成一个整体。而 Go 选择采用的组合方式,也是最主要的方式。

Viewpoint #

From #

缓存一致性问题

缓存一致性问题 #

所谓缓存一致性,就是保证同一个数据在每个 CPU 的私有缓存(一般为 L1 Cache)中副本是相同的。考虑下面的例子:

global sum = 0
// Thread1:
sum += 3
// Thread2:
sum += 5

假设 Thread1 由 CPU 核 P1 执行,Thread2 由 P2 执行,那么 P1、P2 的私有缓存和主存的状态可能出现下表所示的情况:

在这个表里,脏是缓存块的一个标识位,用来表示缓存中的数据有没有被改写,如果该缓存块的内容被修改,并且还没有同步到主存,就称它为脏的;

sum 对于 Thread1 和 Thread2 是共享的。初始状态 sum 的值为 0,Thread1 将 sum 加 3,Thread2 将 sum 加 5。正常来说,我们期望内存中的 sum 值是 8。但实际两个线程执行结束后,内存中的 sum 的取值根据缓存状态的传播情况,就会有不同的取值。

上表中展示了一种内存中 sum 值为 5 的操作序列。但是,第 5 步和第 6 步的顺序有可能会对调,所以 sum 值还有可能是 3。如果第 3 步,P1 的缓存中的值能被正确地传播到 P2,那么 P2 的 sum 值就为 8,所以最终内存中的值还有可能是 8。

...

缓存写策略

缓存写策略 #

写回和写直达 #

当 CPU 修改了缓存中的数据后,这些修改什么时候能传播到主存?解决这个问题有两种策略: 写回(Write-Back)写直达(Write-Through)

  • 写回策略对缓存的修改不会立刻传播到主存,只有当缓存块被替换时,这些被修改的缓存块,才会写回并覆盖内存中过时的数据;
  • 写直达策略缓存中任何一个字节的修改,都会立刻传播到内存,这种做法就像穿透了缓存一样,所以用英文单词“Through”来命名。

写更新与写无效 #

当某个 CPU 的缓存中执行写操作,修改其中的某个值时,其他 CPU 的缓存所保有该数据副本的更新策略也有两种:写更新(Write Update)和写无效(Write Invalidate)。

  • 写更新策略每次它的缓存写入新的值,该 CPU 都必须发起一次总线请求,通知其他 CPU 将它们的缓存值更新为刚写入的值,所以写更新会很占用总线带宽。
  • 写无效如果在一个 CPU 修改缓存时,将其他 CPU 中的缓存全部设置为无效,这种策略叫做写无效。这意味着,当其他 CPU 再次访问该缓存副本时,会发现这一部分缓存已经失效,此时 CPU 就会从内存中重新载入最新的数据。

在具体的实现中,绝大多数 CPU 都会采用写无效策略。这是因为多次写操作只需要发起一次总线事件即可,第一次写已经将其他缓存的值置为无效,之后的写不必再更新状态,这样可以有效地节省 CPU 核间总线带宽。

写分配与写不分配 #

当前要写入的数据不在缓存中时,根据是否要先将数据加载到缓存中,写策略又分为两种:写分配(Write Allocate)和写不分配(Not Write Allocate)。

  • 写分配在写入数据前将数据读入缓存,这是写分配策略。当缓存块中的数据在未来读写概率较高,也就是程序空间局部性较好时,写分配的效率较好;
  • 写不分配在写入数据时,直接将要写入的数据传播内存,而并不将数据块读入缓存,这是写不分配策略。当数据块中的数据在未来使用的概率较低时,写不分配性能较好。

如果缓存块的大小比较大,该缓存块未来被多次访问的概率也会增加,这种情况下,写分配的策略性能要优于写不分配。

Viewpoint #

From #

Go语言的设计哲学

Go语言的设计哲学 #

Go 语言的设计哲学:简单、显式、组合、并发和面向工程。

  1. 简单是指 Go 语言特性始终保持在少且足够的水平,不走语言特性融合的道路,但又不乏生产力。简单是 Go 生产力的源泉,也是 Go 对开发者的最大吸引力;
  2. 显式是指任何代码行为都需开发者明确知晓,不存在因“暗箱操作”而导致可维护性降低和不安全的结果;
  3. 组合是构建 Go 程序骨架的主要方式,它可以大幅降低程序元素间的耦合,提高程序的可扩展性和灵活性;
  4. 并发是 Go 敏锐地把握了 CPU 向多核方向发展这一趋势的结果,可以让开发人员在多核时代更容易写出充分利用系统资源、支持性能随 CPU 核数增加而自然提升的应用程序;
  5. 面向工程是 Go 语言在语言设计上的一个重大创新,它将语言要解决的问题域扩展到那些原本并不是由编程语言去解决的领域,从而覆盖了更多开发者在开发过程遇到的“痛点”,为开发者提供了更好的使用体验。

Viewpoint #

From #

toc:Go

设计 #

Go语言的设计哲学 #

Go语言的组合设计哲学 #

垂直组合与水平组合 #

Go语言提供的正交性 #

接受接口返回结构体 #

包装器模式 #

适配器函数类型(http.HandlerFunc) #

中间件应用模式 #

什么是 type set #

版本 #

Go语言的版本发布策略 #

retract指示符 #

升级module的major版本号 #

变量 #

控制结构 #

类型 #

常量 #

数值类型 #

string #

数组和切片 #

切片的内部结构

数组的切片化 指根据已经存在的数组来创建切片。

切片与map与零值可用,切片的零值可用,而map结构的零值不可用。

Full Slice Expression 可用于强制切片的扩容。

map #

struct #

嵌入字段的用法 #

实现继承的原理 #

interface #

空接口类型 #

动静兼备 #

接口的内部结构 #

eface结构 用于表示没有方法的空接口。 iface结构 用于表示非空的拥有方法的接口。 iface与eface 比较了两个结构的差别。 iface与eface结构不同,但在比较时,go却会做特殊的处理。具体的例子参考: 空接口类型变量与非空接口类型变量的等值比较 Go语言中将某类型变量赋值给接口变量的过程称为“装箱” ,具体原理参考: 接口类型的装箱(boxing)原理 装箱后的数据与原变量的值再无瓜葛 nil error 值 != nil 来自 Go FAQ的问题。

...

X-Y Problem

X-Y Problem #

对于X-Y Problem的意思如下:

1)有人想解决问题X 2)他觉得Y可能是解决X问题的方法 3)但是他不知道Y应该怎么做 4)于是他去问别人Y应该怎么做?

简而言之,没有去问怎么解决问题X,而是去问解决方案Y应该怎么去实现和操作。于是乎:

1)热心的人们帮助并告诉这个人Y应该怎么搞,但是大家都觉得Y这个方案有点怪异。 2)在经过大量地讨论和浪费了大量的时间后,热心的人终于明白了原始的问题X是怎么一回事。 3)于是大家都发现,Y根本就不是用来解决X的合适的方案。

X-Y Problem最大的严重的问题就是:在一个根本错误的方向上浪费他人大量的时间和精力!

示例举个两个例子:

Q) 我怎么用Shell取得一个字符串的后3位字符? A1) 如果这个字符的变量是$foo,你可以这样来 echo ${foo:-3} A2) 为什么你要取后3位?你想干什么? Q) 其实我就想取文件的扩展名 A1) 我靠,原来你要干这事,那我的方法不对,文件的扩展名并不保证一定有3位啊。 A1) 如果你的文件必然有扩展名的话,你可以这来样来:echo ${foo##*.}

再来一个示例:

Q)问一下大家,我如何得到一个文件的大小 A1) size = ls -l $file | awk ‘{print $5}’ Q) 哦,要是这个文件名是个目录呢? A2) 用du吧 A3) 不好意思,你到底是要文件的大小还是目录的大小?你到底要干什么? Q) 我想把一个目录下的每个文件的每个块(第一个块有512个字节)拿出来做md5,并且计算他们的大小 …… A1) 哦,你可以使用dd吧。 A2) dd不行吧。 A3) 你用md5来计算这些块的目的是什么?你究竟想干什么啊? Q) 其实,我想写一个网盘,对于小文件就直接传输了,对于大文件我想分块做增量同步。 A2) 用rsync啊,你妹!

Viewpoint #

From #