Blog

方法集合决定接口实现

方法集合决定接口实现 #

我们可以使用 dumpMethodSet 工具函数,输出下面例子中 pt 与 t 各自所属类型的方法集合:

type Interface interface {
    M1()
    M2()
}
type T struct{}
func (t T) M1()  {}
func (t *T) M2() {}
func main() {
    var t T
    var pt *T
    var i Interface
    i = pt //正确
    i = t //报错,显示T未实现M2方法
    dumpMethodSet(t)
    dumpMethodSet(pt)
}

运行上述代码,我们得到如下结果:

main.T's method set:
- M1
*main.T's method set:
- M1
- M2

T 类型的方法集合中只包含 M1,没有 Interface 类型方法集合中的 M2 方法,这就是 Go 编译器认为变量 t 不能赋值给 Interface 类型变量的原因。

在输出的结果中,我们还看到 *T 类型的方法集合除了包含它自身定义的 M2 方法外,还包含了 T 类型定义的 M1 方法,*T 的方法集合与 Interface 接口类型的方法集合是一样的,因此 pt 可以被赋值给 Interface 接口类型的变量 i。

...

dumpMethodSet

Content #

函数 dumpMethodSet 用于输出一个非接口类型的方法集合:

func dumpMethodSet(i interface{}) {
    dynTyp := reflect.TypeOf(i)
    if dynTyp == nil {
        fmt.Printf("there is no dynamic type\n")
        return
    }
    n := dynTyp.NumMethod()
    if n == 0 {
        fmt.Printf("%s's method set is empty!\n", dynTyp)
        return
    }
    fmt.Printf("%s's method set:\n", dynTyp)
    for j := 0; j < n; j++ {
        fmt.Println("-", dynTyp.Method(j).Name)
    }
    fmt.Printf("\n")
}

Viewpoint #

From #

toc:Go:Cookbook

Content #

dumpMethodSet #

用于输出一个非接口类型的方法集合。

用结构体嵌入接口简化单元测试的编写 #

数据库查询的测试案例。

功能选项(functional option) #

Go 语言之父 Rob Pike 在 2014 年在博文《自引用函数与选项设计》中论述的一种,这种方案也被后人称为“功能选项(functional option)”方案。

获取 goroutine id #

Concurrent #

用select实现超时机制 #

用nil channel解决关闭的channel输出0的问题 #

用select实现心跳机制 #

条件轮询(Mutex and Cond) #

实现一个可重入的锁 #

Cond基本用法(百米赛跑开始) #

用Once初始化Cache资源 #

用Once提供只初始化一次的值 #

reflect #

reflect基本用法 #

判断某个类型是否实现了Lock接口 #

unsafe基本用法 #

T类型与*T类型方法集合的差别

T类型与*T类型方法集合的差别 #

下面我们利用 dumpMethodSet函数,输出 Go 原生类型以及自定义类型的方法集合,看下面代码:

type T struct{}
func (T) M1() {}
func (T) M2() {}
func (*T) M3() {}
func (*T) M4() {}
func main() {
    var n int
    dumpMethodSet(n)
    dumpMethodSet(&n)
    var t T
    dumpMethodSet(t)
    dumpMethodSet(&t)
}

运行这段代码,我们得到如下结果:

int's method set is empty!
*int's method set is empty!
main.T's method set:
- M1
- M2
*main.T's method set:
- M1
- M2
- M3
- M4

以 int、*int 为代表的 Go 原生类型由于没有定义方法,所以它们的方法集合都是空的。

自定义类型 T 定义了方法 M1 和 M2,因此它的方法集合包含了 M1 和 M2,也符合我们预期。但 *T 的方法集合中除了预期的 M3 和 M4 之外,居然还包含了类型 T 的方法 M1 和 M2!

...

为宏函数的参数添加括号

为宏函数的参数添加括号 #

下面这个例子:

#include <stdio.h>
#define FOO(x) (1 + x * x)
int main(void) {
  printf("%d", FOO(1 + 2));
  return 0;
}

我们直接将表达式 1 + 2 作为参数传递给FOO函数。由于编译器在处理宏函数时,仅会进行实参在各自位置上的文本替换,传入函数的表达式并不会在函数展开前进行求值。因此,经过编译器的预处理后,上述代码中第四行对 printf 语句的调用过程会被变更为如下形式:

printf("%d", (1 + 1 + 2 * 1 + 2));

由于乘法运算符 “*” 的存在,此时整个表达式的求值顺序发生了改变。本该被优先求值的子表达式 1 + 2 并没有被提前计算。

很明显,这并不是我们在设计 FOO 函数时所期望的。而通过为宏函数定义中的每个参数都加上括号,我们便可以解决这个问题。

Viewpoint #

From #

09|编译准备:预处理器是怎样处理程序代码的?

定义完备的多语句宏函数

定义完备的多语句宏函数 #

通常情况下,为了与 C 代码的风格保持一致,在调用宏函数时,我们也会习惯性地为每一个调用语句的末尾加上分号。但也正是因为这样,当含有多行语句的宏函数与某些控制语句一起配合使用时,可能会出现意想不到的结果。比如下面这个例子:

#include <stdio.h>
#define SAY() printf("Hello, "); printf("world!")
int main(void) {
  int input;
  scanf("%d", &input);
  if (input > 0)
    SAY();
  return 0;
}

无论用户输入何值,字符串 “world!” 都会被打印。

那么,应该怎样解决这个问题呢?迭代语句 do…while 便可以满足这个要求。如下面这段代码所示,我们使用该语句,改写了宏函数 SAY 的实现方式。

#include <stdio.h>
#define SAY() \
  do { printf("Hello, "); printf("world!"); } while(0)
int main(void) {
  int input;
  scanf("%d", &input);
  if (input > 0)
    SAY();
  return 0;
}

可以看到,通过将 while 关键字中的参数设置为 0,我们可以保证整个迭代语句仅会被执行一次。而 do…while 语句“天生”需要以分号结尾的性质,也正好满足了宏函数替换后的 C 语法格式要求。并且,对于 while(0) 这种特殊的迭代形式,大多数编译器也会通过相应的优化,去掉不必要的循环控制结构,以降低对程序运行时性能的影响。

Viewpoint #

From #

09|编译准备:预处理器是怎样处理程序代码的?

Python解决循环引用的方案

Python解决循环引用的方案 #

使用了引用计数的地方,就会存在循环引用。例如下图中的四个对象,A 是根对象,它与 B 之间有循环引用,那么它们都不是垃圾对象。C 和 D 之间也有循环引用,但因为没有外界的引用指向它们了,所以它们就是垃圾对象,但是循环引用导致他们都不能释放。 Python 为了解决这个问题,在虚拟机中引入了一个双向链表,把所有对象都放到这个链表里。Python 的每个对象头上都有一个名为 PyGC_Head 的结构:

/* GC information is stored BEFORE the object structure. */
typedef union _gc_head {
    struct {
        union _gc_head *gc_next;
        union _gc_head *gc_prev;
        Py_ssize_t gc_refs;
    } gc;
    long double dummy;  /* force worst-case alignment */
} PyGC_Head;

在这个结构里,gc_next 和 gc_prev 的作用就是把对象关联到链表里。而 gc_refs 则是用于消除循环引用的。当链表中的对象达到一定数目时,Python 的 GC 模块就会执行一次标记清除。

具体来讲,一共有四步。

  1. 将 ob_refcnt 的值复制到 gc_refs 中。对于上面的例子,它们的 gc_refs 的值就如下图所示:
  1. 遍历整个链表,对每个对象,将它直接引用的对象的 gc_refs 的值减一。比如遍历到 A 对象时,只把 B 对象的 gc_refs 值减一;遍历到 B 对象时,再把它直接引用的 A 对象的 gc_refs 值减一。经过这一步骤后,四个对象的 gc_refs 的值如下图所示:
  1. 将 gc_refs 值为 0 的对象,从对象链表中摘下来,放入一个名为“临时不可达”的链表中。之所以使用“临时”,是因为有循环引用的垃圾对象的 gc_refs 在此时一定为 0,比如 C 和 D。但 gc_refs 值为 0 的对象不一定是垃圾对象,比如 B 对象。此时,B、C 和 D 对象就被放入临时不可达链表中了,示意图如下所示:
  1. 以可达对象链表中的对象为根开始深度优先搜索,将所有访问到 gc_refs 为 0 的对象,再从临时不可达链表中移回可达链表中。最后留在临时不可达链表中的对象,就是真正的垃圾对象了。

接下来就可以使用 _Py_Dealloc 逐个释放链表中的对象了,对于上面的例子,就是把 B 对象重新加回到可达对象链表中,然后将 C 和 D 分别释放。

...

超长流水线的性能瓶颈

超长流水线的性能瓶颈 #

既然流水线可以增加的吞吐率,为什么不把流水线级数做得更深呢?为什么不做成 20 级,乃至 40 级呢?

一个最基本的原因,就是增加流水线深度,其实是有性能成本的。

用来同步时钟周期的,不再是指令级别的,而是流水线阶段级别的。每一级流水线对应的输出,都要放到流水线寄存器(Pipeline Register)里面,然后在下一个时钟周期,交给下一个流水线级去处理。所以,每增加一级的流水线,就要多一级写入到流水线寄存器的操作。虽然流水线寄存器非常快,比如只有 20 皮秒(ps,10^−12 秒)。 但是,如果不断加深流水线,这些操作占整个指令的执行时间的比例就会不断增加。

最后,性能瓶颈就会出现在这些 overhead 上。如果指令的执行有 3 纳秒,也就是 3000 皮秒。需要 20 级的流水线,那流水线寄存器的写入就需要花费 400 皮秒,占了超过 10%。如果需要 50 级流水线,就要多花费 1 纳秒在流水线寄存器上,占到 25%。这也就意味着,单纯地增加流水线级数,不仅不能提升性能,反而会有更多的 overhead 的开销。

所以,设计合理的流水线级数也是现代 CPU 中非常重要的一点。

Viewpoint #

From #

20 | 面向流水线的指令设计(上):一心多用的现代CPU

陈皓的技术学习模板

陈皓的技术学习模板 #

当然学习一门技术时,Go 语言也好,Docker 也好,我都有一个学习模板。只有把这个学习模板中的内容都填实了,我才罢休。这个模板如下。

  1. 这个技术出现的背景、初衷和要达到什么样的目标或是要解决什么样的问题。这个问题非常关键,也就是说,你在学习一个技术的时候,需要知道这个技术的成因和目标,也就是这个技术的灵魂。如果不知道这些的话,那么你会看不懂这个技术的一些设计理念。
  2. 这个技术的优势和劣势分别是什么,或者说,这个技术的 trade-off 是什么。任何技术都有其好坏,在解决一个问题的时候,也会带来新的问题。另外,一般来说,任何设计都有 trade-off(要什么和不要什么),所以,你要清楚这个技术的优势和劣势,以及带来的挑战。
  3. 这个技术适用的场景。任何技术都有其适用的场景,离开了这个场景,这个技术可能会有很多槽点,所以学习技术不但要知道这个技术是什么,还要知道其适用的场景。没有任何一个技术是普适的。注意,所谓场景一般分别两个,一个是业务场景,一个是技术场景。
  4. 技术的组成部分和关键点。这是技术的核心思想和核心组件了,也是这个技术的灵魂所在了。学习技术的核心部分是快速掌握的关键。
  5. 技术的底层原理和关键实现。任何一个技术都有其底层的关键基础技术,这些关键技术很有可能也是其它技术的关键基础技术。所以,学习这些关键的基础底层技术,可以让你未来很快地掌握其它技术。
  6. 已有的实现和它之间的对比。一般来说,任何一个技术都会有不同的实现,不同的实现都会有不同的侧重。学习不同的实现,可以让你得到不同的想法和思路,对于开阔思维,深入细节是非常重要的。

Viewpoint #

From #

98 | 高效学习:深度,归纳和坚持实践

不同receiver类型引发的问题

不同receiver类型引发的问题 #

代码是这样的:

package main
import (
    "fmt"
    "time"
)
type field struct {
    name string
}
func (p *field) print() {
    fmt.Println(p.name)
}
func main() {
    data1 := []*field{{"one"}, {"two"}, {"three"}}
    for _, v := range data1 {
        go v.print()
    }
    data2 := []field{{"four"}, {"five"}, {"six"}}
    for _, v := range data2 {
        go v.print()
    }
    time.Sleep(3 * time.Second)
}

运行结果是这样(由于 Goroutine 调度顺序不同,你自己的运行结果中的行序可能与下面的有差异):

one
two
three
six
six
six

为什么对 data2 迭代输出的结果是三个“six”,而不是 four、five、six?

...