go-gorountine node实例写法

Go 语言是 Google 于 2009 年 11 月公布的一个新语言项目,其目标是创造一门既简单又有效率的开源编程语言。由于有 C 语言创始人 Ken Thompson 的参与,Go 一面世,就被看成是 C 语言的继任者,受到很大关注。Go 一方面吸收了 C 简单清晰、执行效率高的优点,另一方面融合了动态语言的闭包、动态绑定等特性,更加适应目前多核与多机高并发的开发环境和快速敏捷的开发效率。此外,Go 并没有跟随主流的以“类和继承”为基础的面向对象实现方式,而是以接口和动态绑定的方式,将封装的粒度做得更细、更灵活,实现了另一种面向对象的代码组织形式。

本文带领大家用 Go 来实现一个简单的程序。程序本身是对 MapReduce 的一个模拟,将一组数字交给一组并发的 DoubleNode 节点做翻倍,然后再由一个 SumNode 将翻倍后的数累加并输出。节点间的关系类似如图 1 所示。

Node 的实现

SumNode
  |
  +--- DoubleNode
  |
  +--- DoubleNode
  |
  +--- DoubleNode

图 1 中的每个 Node 都是一个独立运行的节点,节点间是并行的。值得提醒的是,Go 提倡通过通信来共享数据,而不是通过共享数据来通信。因此每个节点既不访问别的节点的内部状态,也不访问全局变量,节点间通过 Go 语言的通道机制互相传递消息,通知工作完成并将数据传递给下一个工作节点。

确定 Node 的结构

首先是确定 Node 的结构。DoubleNode 和 SumNode 都作为 Node 的一种特化,不同之处在于在 Node 执行时执行的功能不一样。统一起见,为 Node 定义如下接口:

1
2
3
type NodeInterface interface {
  receive(i int)
}

蓝色的部分是 Go 的关键字,type 表示定义一个新的类型,语法上与 C 语言的 typedef 类似,只是将被定义的类型名字和类型的顺序颠倒了一下。Go 里所有涉及到类型名 / 变量名和类型的地方,使用顺序都和 C 是颠倒的。NodeInterface 是新类型的名字。interface 表示定义的是一个接口,这个接口可以类比 C++ 的纯虚类或者 Java 的接口,但是在使用的时候是动态绑定,不需要实现者必须继承自接口 (后文有更详细的说明)。接口内定义了两个函数 receive 和 run,其中 receive 接受一个参数 i,类型是整数类型 int,没有返回值,用处是处理从其他 Node 接收到的消息 ;run 没有参数,返回值为 int,用处是进行 Node 自身的运算。

定义 Node 内部的状态

另外定义 Node 内部的一些状态:

1
2
3
4
5
6
7
8
type Node struct {
  name string
  in_degree int
  in_ch chan int
  out_ch chan int

  inode NodeInterface
}

与前面不同,Node 的类型是 struct。和 C 语言的 struct 一样,Go 的 struct 里只含有变量,不能有函数。Node 类型里一共定义了以下变量:name,字符串类型,用来存储标示 Node 的名字 in_degree 表示一个节点的入度,也就是本节点需要从多少个其他节点接收数据,整数类型 in_ch 和 out_ch 是输入和输出管道,类型是传输整数的 chan,chan 的概念先按下不表,后面在使用的时候会讲到 ; 最后是 inode,类型是之前定义的 NodeInterface 接口,用来特化 Node 的行为。

这里值得注意的是,Node 使用了类似模版模式的概念,但和 C++/Java 不同,并没有从 NodeInterface 继承,而是将 NodeInterface 作为一个成员。由于 Go 无法让一个类的成员函数处于未定义的状态,因此无法像 C++/Java 一样藉由在子类特化父类里未定义的函数来实现模版模式。不过,这样虽然看似麻烦,但是好处在于将 Node 本身的状态和 NodeInterface 分离,两部分责任更清晰。

Node 相关的方法

之后是 Node 相关的方法。首先是 Node 的创建方法:

1
2
3
func NewNode(name string, inode NodeInterface) *Node {
  return &Node{name, 0, make(chan int), make(chan int), inode}
}

func 关键字表示接下来要定义一个函数。函数的名字是 NewNode,接受两个参数:字符串 name 和 NodeInterface 接口 inode。初始时,节点 Node 的入度 in_degree 为 0。系统函数 make 会创建一个 chan int 的实例,并返回其引用。关于 make 的更详细的用法和限制,可以参考 Go 语言的官方网站。Node{…}一句表示创建了一个 Node 实例,花括弧内按顺序给出 Node 结构内部变量的初值。然后用 & 运算符取新建实例的地址,作为返回值。是的,Go 里依然有指针的概念,而且这个指针和 C 的指针概念类似,相关的语法也类似。

Node 间需要互相连结,以下是连结的实现:

1
2
3
4
5
6
7
func (from *Node) ConnectTo(to *Node) {
  to.in_degree++
  go func() {
    i := <- from.out_ch
    to.in_ch <- i
  }()
}

函数名前的 (from *Node) 表示 ConnectTo 函数是类型 Node 的一个成员函数。与 C++/Java 不同,Go 里没有类似 this 的关键字,在声明函数时需要明确指定指向当前实例的变量名。每个链接,都会增加被指向 Node 的入度数。go 关键字启动一个 gorountine,等待前一个 Node 的输出,并将输出的内容传入后续的 Node。go 关键字之后是一个匿名函数并执行这个函数。可以参考下面这种定义:

1
2
f := func () {...}
f()

如果将中间变量 f 去掉,就是上面提到的定义一个函数并执行的写法:

1
func () {...} ()

Go 语言里的函数地位和变量是一样的,可以任意赋值给一个变量,有自己的生命周期,并且在其他函数间相互传递。而且 Go 的函数支持闭包,在一个定义域里定义的函数可以直接引用外层定义域的变量并在这个函数的生命周期里一直保存。不过要注意的是,如果闭包引用的是一个指针,需要小心操作这个变量,因为函数里和函数外的指针指向的是同一个地址,任何对这个指针指向的实例操作,都会对所有指针有影响。

关键字 <- 是对 chan 类型独有的操作。之前说过,chan 类型类似于通道,可以把一个数据放进去,并在之后取出来。在例子里,<- from.out_ch 是从 from 实例的 out_ch 通道里取出一个数,如果通道里没有数,则会阻塞等待。取出数后会把这个数赋值给 i。之后将取出的值 i 通过 to.in_ch <- i 传入到 to 实例的 in_ch 通道里。这样就完成了将 from 和 to 两个节点连结起来的功能。

gorountine

gorountine 是 Go 语言里很重要的新概念,有点类似线程,但消耗的资源比线程少很多,而且 gorountine 只是 Go 内部的概念,不会在操作系统层面有对应的实现。在 Go 里启动的各个 gorountine 之间是并行的,每个 gorountine 可能会映射到一个系统线程,也可能多个 gorountine 共用一个线程,如果是多核的机器,不同的 gorountine 会自动分配到不同的核心。gorountine 间的切换也由 Go 来控制,不需要程序员操心。gorountine 占用的内存远小于系统线程或进程,gorountine 间的切换成本也很低。程序里可以轻易创建数万个 gorountine 做并行,而不用担心会占用过多的系统资源。

Go 语言利用 gorountine 实现并发,用 chan 实现消息通信。通过这两个概念的配合,提供了对并发的支持。

值得注意的是 gorountine 里对 i 的赋值操作符:=,这个操作符是指声明并创建一个变量,并赋初值。变量的类型会自动设置成初值的类型。Go 继承了 C 语言的静态类型的特点,同时也在一定程度上借鉴了 C++ 类型推导的特性 (类似于 C++ 0x 的 auto 关键字,如果你知道 C++ 0x 的话)。另一种更传统的写法是:

1
var i int = nnn

传统的写法不仅多了不少字,而且还要自己注意类型是否匹配。所以 Go 更推荐使用:=(而且以后如果有 Go 语言混乱大赛,大概会用这东西来组成颜文字什么的 XD)。

Node 的核心函数 Run()

现在来看一下 Node 的核心函数 Run()。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
func (n *Node) Run() {
  go func() {
    defer func() {
      if x := recover(); x != nil {
        println(n.name, "panic with value ", x)
        panic(x)
      }
      println(n.name, "finished");
    }()
    // Run 函数的核心
    for n.in_degree > 0 {
      received := <- n.in_ch
      n.inode.receive(received)
      n.in_degree--
    }
    ret := n.inode.run()
    n.out_ch <- ret
  }()
}

进入 Run 后就启动了一个 gorountine,保证每个 Node 节点间都是并行的。在 gorountine 的内部,先略过 defer 不看,看 Run 函数的核心部分。首先等待所有前驱节点工作的完成。关键字 for 是 Go 语言里的循环语句,一般来说有四种用法:

1
2
3
4
for {}                     // 相当于 C 语言里的 while 1 {}
for i := 0; i < xx; i++ {} // 相当于 C 语言里 for (int i=0; i<xx; i++) {}
for i > 0 {}               // 相当于 C 语言里的 while (i>0) {}
for index, item := range array {} // 相当于 Python 里的 foreach,index 是循环序号

这里用的是第三种用法。由于每个节点都是通过 ConnectTo 来和前驱关联在一起,因此 in_degree 的数值就是前驱的个数,当有前驱完成,由 ConnectTo 启动的 gorountine 就会把前驱的输出放入 in_ch 里 (见前面对 ConnectTo 的分析)。for 循环等待所有前驱节点的输出,并把输出传入 inode 的 receive 接口做处理。

之后调用 inode 接口的 run 进行节点自身的处理,并将处理后的返回值赋给 ret。最后将 ret 的内容从 out_ch 里输出。

defer

defer 是 Go 另一个很有意思的特性,借鉴自 C++ 的析构函数和 Java 的 final。defer 指定的函数不会立刻执行,而是在当前函数退出时才执行。defer 主要是用来做一些清扫类的工作,比如常见的关闭文件、释放缓存。这里的 defer 用来处理 inode.run() 在执行时可能出现的异常。

Go 的异常机制也与其他语言不同。一般来讲,Go 的错误处理类似常见的 C 函数,推荐使用返回值做为控制手段。但是在一些情况下,可以通过内建的 panic 函数来触发一个异常。如果这个异常不被捕获,就会引起程序真正 panic。捕获异常使用内建的 recover 函数,如果这个函数执行前有 panic 发生,就会返回调用 panic 时传入的参数 ; 如果没有 panic 发生,就返回一个 nil──Go 里的空指针。

defer 里 if 的用法也很有意思。在 if 执行时,分号前的部分对变量 x 做初始化,分号后才是这个 if 的判断值。x 的作用域限制在 if 的语句块里。这里 Go 借鉴了 C++ 的思想:尽可能缩小变量的生命周期。当然,也可以使用传统的写法:

1
2
x := recover()
if x != nil { ... }

如果 recover 得到的值不为 nil,就简单输出异常并重新抛出。如果一切正常,就打印一句提示并退出。

应用 Node 来构造各种节点

上面就是 Node 的实现。接下来就要展示如何应用 Node 来构造各种节点了。

DoubleNode

首先是 DoubleNode,结构如下:

1
2
3
type DoubleNode struct {
  data int
}

对 DoubleNode 来说,只需要一个 data 存储需要处理的数值就可以,因此结构很简单。然后是 Node 的处理函数:

1
2
3
4
5
6
func (n *DoubleNode) receive(i int) {
}

func (n *DoubleNode) run() int {
  return n.data * 2
}

由于 DoubleNode 是初始节点,不会接收数据,所以 receive 没有做任何事情。run 里将 data 的值翻倍并返回。

值得注意的是 DoubleNode 并没有从 NodeInterface 做继承,除了实现了 NodeInterface 的两个接口,甚至没有任何提到 NodeInterface 的地方。这是 Go 的 interface 与 Java 和 C++ 侵入式的接口实现最大的不同。Go 的 interface 并不需要实现类与 interface 有任何直接的关联,在编译时,编译器会自动检查一个类是否符合 interface 的要求,并在运行时做动态绑定。由于并不要求强制的继承,因此在设计类的时候也不会受到继承体系的限制,想让一个类符合某个 interface,只要加入相应的函数实现就可以,不用改动整个继承体系。

之后是如何生成 DoubleNode:

1
2
3
func NewDoubleNode(name string, data int) *Node {
  return NewNode(name, &DoubleNode{data})
}

这里将新生成的 DoubleNode 实例的指针直接作为参数传入了 NewNode,Go 的编译器会帮你处理背后的工作。注意 interface 只能接收一个实例的指针,而不能直接接收实例作为参数。

SumNode

之后是 SumNode 的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
type SumNode struct {
  data int
}

func NewSumNode(name string) *Node {
  return NewNode(name, &SumNode{0})
}

func (n *SumNode) receive(i int) {
  n.data += i
}

func (n *SumNode) run() int {
  return n.data
}

SumNode 在接收到其他 Node 传入的数后,会将其累加到自己的 data 里,最后只用简单传回 data 的值就完成了全部工作。其他函数很简单就不细说了。

Node 组合

现在是将这些 Node 组合在一起完成工作的时候了:

1
2
3
4
5
6
7
8
9
10
func main() {
  sum := NewSumNode("sum")
  sum.Run()
  for _, num := range [5]int{1, 2, 3, 5, 6} {
    node := NewDoubleNode("double", num)
    node.ConnectTo(sum)
    node.Run()
  }
  println(<- sum.out_ch)
}

Go 在编译可执行文件时,会自动调用内部定义的 main 函数。main 函数里,[5]int{1, 2, 3, 5, 6}表示一个含有 5 个元素的 int 数组,“”和 Python 里的“”含义一样,是一个匿名变量,表示这里将接收一个值,但是程序后面会忽略这个值的具体内容,这里是忽略掉 range 返回的循环序数。其余的内容希望已经简单到读者能一眼看懂 (看不懂的话,大概是 Go 语言的失败吧)。值得注意的是,sum 的 Run 并没有阻塞主进程的运行,这正是 gorountine 的并发所达到的效果。

写到这里,本文的内容就全部结束了。要说的是,这篇文章仅仅展示了 Go 语言相比 C/C++/Java 最大的不同,并不是 Go 的全部内容。比如像字符串、数组、分片、包管理、闭包等内容完全没有涉及。有兴趣的读者可以到 Go 语言的官方网站http://golang.org 查阅相关文档。Go 的官方网站也提供了一个在线的 Go 编译环境,可以编译执行 Go 的代码,体验 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
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
83
84
85
86
87
88
89
90
91
package main

type NodeInterface interface {
  receive(i int)
  run() int
}

type Node struct {
  name string
  in_degree int
  in_ch chan int
  out_ch chan int

  inode NodeInterface
}

func NewNode(name string, inode NodeInterface) *Node {
  return &Node{name, 0, make(chan int), make(chan int), inode}
}

func (from *Node) ConnectTo(to *Node) {
  to.in_degree++
  go func() {
    i := <- from.out_ch
    to.in_ch <- i
  }()
}

func (n *Node) Run() {
  go func() {
    defer func() {
      if x := recover(); x != nil {
        println(n.name, "panic with value ", x)
        panic(x)
      }
      println(n.name, "finished");
    }()

    for n.in_degree > 0 {
      received := <- n.in_ch
      n.inode.receive(received)
      n.in_degree--
    }
    ret := n.inode.run()
    n.out_ch <- ret
  }()
}

type DoubleNode struct {
  data int
}

func NewDoubleNode(name string, data int) *Node {
  return NewNode(name, &DoubleNode{data})
}

func (n *DoubleNode) receive(i int) {
}

func (n *DoubleNode) run() int {
  return n.data * 2
}

type SumNode struct {
  data int
}

func NewSumNode(name string) *Node {
  return NewNode(name, &SumNode{0})
}

func (n *SumNode) receive(i int) {
  n.data += i
}

func (n *SumNode) run() int {
  return n.data
}

func main() {
  sum := NewSumNode("sum")
  sum.Run()

  for _, num := range [5]int{1, 2, 3, 5, 6} {
    node := NewDoubleNode("double", num)
    node.ConnectTo(sum)
    node.Run()
  }

  println(<- sum.out_ch)
}

原文地址:http://air.googol.im/2010/01/17/guide-to-golang.html

One thought on “go-gorountine node实例写法

发表评论

电子邮件地址不会被公开。 必填项已用*标注