Blog

动静兼备

动静兼备 #

接口的静态特性体现在接口类型变量具有静态类型,比如var err error中变量 err 的静态类型为 error。拥有静态类型,那就意味着编译器会在编译阶段对所有接口类型变量的赋值操作进行类型检查,编译器会检查右值的类型是否实现了该接口方法集合中的所有方法。如果不满足,就会报错:

var err error = 1
// cannot use 1 (type int) as type error in assignment:
//int does not implement error (missing Error method)

接口的动态特性,就体现在接口类型变量在运行时还存储了右值的真实类型信息,这个右值的真实类型被称为接口类型变量的动态类型。你看一下下面示例代码:

var err error
err = errors.New("error1")
fmt.Printf("<2022-08-07 日 10:53>\n", err)  // *errors.errorString

这个示例通过 errros.New 构造了一个错误值,赋值给了 error 接口类型变量 err,并通过 fmt.Printf 函数输出接口类型变量 err 的动态类型为 *errors.errorString。

“动静皆备”好处:

  1. 接口类型变量在程序运行时可以被赋值为不同的动态类型变量,每次赋值后,接口类型变量中存储的动态类型信息都会发生变化,这让 Go 语言可以像动态语言(比如 Python)那样拥有使用Duck Typing(鸭子类型)的灵活性。

比如下面的例子:

type QuackableAnimal interface {
    Quack()
}
type Duck struct{}
func (Duck) Quack() {
    println("duck quack!")
}
type Dog struct{}
func (Dog) Quack() {
    println("dog quack!")
}
type Bird struct{}
func (Bird) Quack() {
    println("bird quack!")
}
func AnimalQuackInForest(a QuackableAnimal) {
    a.Quack()
}
func main() {
    animals := []QuackableAnimal{new(Duck), new(Dog), new(Bird)}
    for _, animal := range animals {
        AnimalQuackInForest(animal)
    }
}

我们用接口类型 QuackableAnimal 来代表具有“会叫”这一特征的动物,而 Duck、Bird 和 Dog 类型各自都具有这样的特征,于是我们可以将这三个类型的变量赋值给 QuackableAnimal 接口类型变量 a。每次赋值,变量 a 中存储的动态类型信息都不同,Quack 方法的执行结果将根据变量 a 中存储的动态类型信息而定。

...

空接口类型

空接口类型 #

如果一个变量的类型是空接口类型,由于空接口类型的方法集合为空,这就意味着任何类型都实现了空接口的方法集合,所以我们可以将任何类型的值作为右值,赋值给空接口类型的变量,比如下面例子:

var i interface{} = 15 // ok
i = "hello, golang" // ok
type T struct{}
var t T
i = t  // ok
i = &t // ok

空接口类型的这一可接受任意类型变量值作为右值的特性,让他成为 Go 加入泛型语法之前唯一一种具有“泛型”能力的语法元素,包括 Go 标准库在内的一些通用数据结构与算法的实现,都使用了空类型interface{}作为数据元素的类型,这样我们就无需为每种支持的元素类型单独做一份代码拷贝了。

Go 语言还支持接口类型变量赋值的“逆操作”,也就是通过接口类型变量“还原”它的右值的类型与值信息,这个过程被称为“类型断言(Type Assertion)”。类型断言通常使用下面的语法形式:

v, ok := i.(T)

其中 i 是某一个接口类型变量,如果 T 是一个非接口类型且 T 是想要还原的类型,那么这句代码的含义就是断言存储在接口类型变量 i 中的值的类型为 T。

Viewpoint #

From #

28|接口:接口即契约

括号化定理

Content #

括号化定理(Parenthesis theorem)。在对有向或无向图G=(V,E)进行的任意深度优先搜索中,对于任意两个结点u和v来说,三种情况中只有一种成立。

  1. 区间[u.d, u.f]与区间[v.d, v.f]完全分离,在深度优先森林中,彼此不是对方的后代。
  2. 区间[u.d, u.f]完全包含在区间[v.d, v.d]内,在深度优先树中,结点u是结点v的后代。
  3. 区间[v.d, v.f]完全包含在区间[u.d, u.d]内,在深度优先树中,结点v是结点u的后代。

Viewpoint #

From #

获得undefined值

Content #

JavaScript获得undefined值有哪两种手段?

  1. 用全局变量 undefined(就是名为 undefined 的这个变量)来表达这个值。
  2. 用 void 运算来把任意一个表达式变成 undefined 值。

Viewpoint #

From #

用嵌入汇编的形式使用open系统调用

用嵌入汇编的形式使用open系统调用 #

在下面这段代码中,我们直接使用机器指令调用了 open 系统调用函数。

 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
#include <unistd.h>
#include <fcntl.h>
int main(void) {
  const char str[] = "Enter some characters:\n";
  write(STDOUT_FILENO, str, sizeof(str));
  const char* fileName = "./temp.txt";
  // Call to `open` starts:
  // const int fd = open("./temp.txt", O_RDWR | O_CREAT);
  volatile int fd;
  asm("mov $2, %%rax\n\t"                                      (asm)
      "mov %0, %%rdi\n\t"
      "mov $66, %rsi\n\t"  // 2 | 64 -> 66;
      "syscall\n\t"
      "mov %rax, %1\n\t"                                       (write_to_fd)
       : "=m" (fileName)
       : "m" (fd));
  // Call ended.
  if (fd > 0) {
    char ch;
    while (read(STDIN_FILENO, &ch, 1)) {
      if (ch == 'z') break;
      write(fd, &ch, sizeof(ch));
    }
  } else {
    const char errMsg[] = "File open failed.";
    write(STDERR_FILENO, errMsg, sizeof(errMsg));
  }
  close(fd);
  return 0;
}

asm,我们以内联汇编的形式,在程序的执行流中插入了 5 条机器指令。第 1 条指令,我们将系统调用 open 对应的整型 ID 值 2 放入到了寄存器 rax 中;第 2 条指令,我们将存放有目标文件名称的字节数组 fileName 的首地址放到了寄存器 rdi 中,该参数也对应着低级 IO 接口 open 的第一个参数。接下来的一条指令,我们将配置参数对应表达式 O_RDWR | O_CREAT 的计算结果值 66 放入到了寄存器 rsi 中。最后,通过指令 syscall,我们得以调用对应的系统调用函数。而当系统调用执行完毕后,其对应的返回值将会被放置在寄存器 rax 中。在 write_to_fd,我们将该寄存器中的值传送到了变量 fd 在栈内存中的位置。

...

SysV调用约定

SysV调用约定 #

参数传递 #

在调用函数时,对于整型和指针类型的实参,需要分别使用寄存器 rdi、rsi、 rdx、rcx、r8、r9,按函数定义时参数从左到右的顺序进行传值。而若一个函数接收的参数超过了 6 个,则余下参数将通过栈内存进行传送。此时,多出来的参数将按照从右往左(RTL)的顺序被逐个压入栈中。

对于浮点参数,编译器将会使用另外的 xmm0 到 xmm7,共 8 个寄存器进行存储。对于更宽的值,也可能会使用 ymm 与 zmm 寄存器来替代 xmm 寄存器。而 xmm、 ymm、zmm 寄存器,都是由 x86 指令集架构中名为 AVX(Advanced Vector Extensions)的扩展指令集使用的。这些指令集一般专门用于浮点数计算以及 SIMD 相关的处理过程。

返回值传递 #

当函数调用产生整数类型的返回值,且小于等于 64 位时,通过寄存器 rax 进行传递;当大于 64 位,小于等于 128 位时,则使用寄存器 rax 与 rdx 分别存储返回值的低 64 位与高 64 位。对于复合类型(比如结构体)的返回值,编译器可能会直接使用栈内存进行“中转”。对于浮点数类型的返回值,同参数传递类似,编译器会默认使用 xmm0 与 xmm1 寄存器进行存储。而当返回值过大时,则会选择性使用 ymm 与 zmm 来替代 xmm 寄存器。

寄存器使用 #

对于寄存器 rbx、rbp、rsp,以及 r12 到 r15,若被调用函数需要使用它们,则需要该函数在使用之前将这些寄存器中的值进行暂存,并在函数退出之前恢复它们的值(callee-saved)。而对于其他寄存器,则根据调用方的需要,自行保存和恢复它们的值(caller-saved)。

...

ProCode与LowCode的主要区别

ProCode与LowCode的主要区别 #

Pro Code 与 Low Code 的关键差异不在于代码量的多少,而在于:

  1. 代码在这两者创造业务价值的过程中所扮演的角色,对 Pro Code 来说,代码是关键输入,而对 Low Code 来说,代码仅仅是中间产物、是副产品;
  2. 使用 Low Code 开发虽然也需要少量编码,但是基本上都是在填写表达式,直接实现业务价值,与业务价值无关的代码则几乎全都被自动生成;
  3. 使用 Low Code 开发业务的人几乎时时刻刻都在描述和细化业务最终的样子,使用 Pro Code 的开发人员不仅需要思考业务最终的样子,还要将其翻译成一条条计算机指令。

这些差异正是 Low Code 的比较优势,这也是为啥 Low Code 可以帮助业务提效和赋能的原因:无需长篇累牍地编码可以大幅降低使用的门槛,实际上就是对无编码技能者进行赋能;几乎所有框架性、非功能代码全部自动生成,以及无需将业务翻译成一条条计算机指令,可以大幅压缩开发周期,实际上就是在提效。

但即使如此,当下的 Low Code 也不是银弹,因为我们在分析毒瘤论的过程中意识到 Low Code 除了注重开发能力之外,还应该具有诸多其他能力以覆盖业务研发全生命周期,包括一键导出和部署、较高的可测试性甚至自动化测试能力、多人协作、兜底能力等等,若低代码平台不具备这些能力,则很难发挥低代码技术的优势。

当然,我们也发现了 Low Code 带来了许多增值功能,包括自动对齐 UX 设计规范、UX 设计稿转代码(D2C)能力、App 的埋点 & 数据采集、开源合规治理、安全漏洞治理等等,这些功能是拉开 Pro Code 差距的重要抓手。

Viewpoint #

From #

02|低代码到底是银弹,还是行业毒瘤?

spanClass中的noscan属性

spanClass中的noscan属性 #

spanClass 的 ID 中还会通过最后一位来存放 noscan 的属性。这个标志位是用来告诉 Collector 该 span 中是否需要扫描。如果当前 span 中并不存放任何堆上的指针,就意味着 Collector 不需要扫描这段 span 区间。

type spanClass uint8
func makeSpanClass(sizeclass uint8, noscan bool) spanClass {
    return spanClass(sizeclass<<1) | spanClass(bool2int(noscan))
}
func (sc spanClass) sizeclass() int8 {
    return int8(sc >> 1)
}
func (sc spanClass) noscan() bool {
    return sc&1 != 0
}

spanClass其实是uint8,最低位用来表示noscan。

Viewpoint #

From #

24 | GC实例:Python和Go的内存管理机制是怎样的?

68个spanClass

68个spanClass #

在 Go 的三级内存管理器中,维护的对象都是小于 32KB 的小对象。对于这些小对象,Go 又将其按照大小分成了 67 个类别,称为 spanClass。每一个 spanClass 都用来存储固定大小的对象。这 67 个 spanClass 的信息在 runtime.sizeclasses.go 中可以看到详细的说明。

// class  bytes/obj  bytes/span  objects  tail waste  max waste  min align
//     1          8        8192     1024           0     87.50%          8
//     2         16        8192      512           0     43.75%         16
//     3         24        8192      341           8     29.24%          8
//     4         32        8192      256           0     21.88%         32
//    ...
//    67      32768       32768        1           0     12.50%       8192

class 3 是说在 spanClass 为 3 的 span 结构中,存储的对象的大小是 24 字节,整个 span 的大小是 8192 字节,也就是一个内存页的大小,可以存放的对象数目最多是 341。

...

Go虚拟内存布局

Go虚拟内存布局 #

Go 的虚拟内存布局了。Go 整体的虚拟内存布局是存放在 mheap 中的一个 heapArena 的二维数组。定义如下:

arenas [1 << arenaL1Bits]*[1 << arenaL2Bits]*heapArena

这里二维数组的大小在不同架构跟操作系统上有所不同,对于 x86-64 架构下的 Linux 系统,第一维数组长度是 1,而第二维数组长度是 4194304。这样每个 heapArena 管理的内存大小是 64MB,由此可以算出 Go 的整个堆空间最多可以管理 256TB 的大小。

Go 通过 heapArena 来对虚拟内存进行管理的方式其实跟操作系统通过页表来管理物理内存是一样的。

Viewpoint #

From #

24 | GC实例:Python和Go的内存管理机制是怎样的?