Go编程语言简介

简介

本文是关于Go编程语言的基础教程,主要面向有C/C++基础的读者。它并不是一个语言的完整指南,关于 Go的具体细节请参考 语言规范 一文。在读完这个入门教程后, 深入的华可以继续看 Effective Go ,这个文档 将涉及到Go语言的更多特性。此外,还有一个《Go语言三日教程》系列讲座: 第一日第二日第三日

下面将通过一些小程序来演示go语言的一些关键特性。所有的演示程序都是可以运行的,程序的代码在安装目录的 "/doc/progs/"子目录中。

文中的代码都会标出在源代码文件中对应的行号。同时为了清晰起见,我们忽略了源代码文件空白行的行号。

Hello,世界

让我们从经典的"Hello, World"程序开始:

05    package main

07    import fmt "fmt"  // Package implementing formatted I/O.


09    func main() {
10        fmt.Printf("Hello, world; or Καλημέρα κόσμε; or こんにちは 世界/n")
11    }

每个Go源文件开头都有一个"package"声明语句,指明源文件所在的包。同时,我们也可以根据具体的需要 来选择导入(import语句)特定功能的包。在这个例子中,我们通过导入“fmt”包来使用我们熟悉的printf函数。 不过在Go语言中,Printf函数的是大写字母开头,并且以fmt包名作为前缀:fmt.Printf。

关键字“func“用于定义函数。在所有初始化完成后,程序从main包中的main函数开始执行。

常量字符串可以包含Unicode字符,采用UTF-8编码。实际上,所有的Go语言源文件都采用UTF-8编码。

代码注释的方式和C++类似:

        /* ... */
       
// ...

稍后,我们还有很多的关于打印的话题。

分号(Semicolons)

比较细心的读者可能发现前面的代码中基本没有出现分号“;”。其实在go语言中,只有在分隔 for循环的初始化语句时才经常用到;但是代码段末尾的分号一般都是省略的。

当然,你也可以像C或JAVA中那样使用分号。不过在大多数情况下,一个完整语句末尾的分号 都是有go编译器自动添加的——用户不需要输入每个分号。

关于分号的详细描述,可以查看Go语言说明文档。不过在实际写代码时,只需要记得一行末尾的分号 可以省略就可以了(对于一行写多个语句的,可以用分号隔开)。还有一个额外的好处是:在退出 大括号包围的子区域时,分号也是可以省略的。

在一些特殊情况下,甚至可以写出没有任何分号的代码。不过有一个重要的地方:对于"if"等 后面有大括弧的语句,需要将左大括弧放在"if"语句的同一行,如果不这样的话可能出现编译错误。 Go语言强制使用将开始大括弧放在同一行末尾的编码风格。

编译

Go是一个编译型的语言。目前有两种编译器,其中"Gccgo"采用GCC作为编译后端。另外还有 根据处理器架构命名的编译器:针对64位x86结构为"6g",针对32位x86结构的为"8g"等等。 这些go专用的编译器编译很快,但是产生的目标代码效率比gccgo稍差一点。目前(2009年底), go专用的编译器的运行时系统比"gccgo"要相对健壮一点。

下面看看如何编译并运行程序。先是用针对64位x86结构处理器的“6g”:

        $ 6g helloworld.go  # 编译; 输出 helloworld.6
        $
6l helloworld.6   # 链接; 输出 6.out
        $
6.out
       
Hello, world; or Καλημέρα κόσμε; or こんにちは 世界
        $

如果是用gccgo编译,方法和传统的gcc编译方法类似:

        $ gccgo helloworld.go
        $ a
.out
       
Hello, world; or Καλημέρα κόσμε; or こんにちは 世界
        $

Echo

下面的例子是Unix系统中"echo"命令的简单实现:

05    package main

07    import (
08        "os"
09        "flag"  // command line option parser
10    )


12    var omitNewline = flag.Bool("n", false, "don't print final newline")


14    const (
15        Space = " "
16        Newline = "/n"
17    )


19    func main() {
20        flag.Parse()   // Scans the arg list and sets up flags
21        var s string = ""
22        for i := 0; i < flag.NArg(); i++ {
23            if i > 0 {
24                s += Space
25            }
26            s += flag.Arg(i)
27        }
28        if !*omitNewline {
29            s += Newline
30        }
31        os.Stdout.WriteString(s)
32    }

程序虽然很小,但是包含了go语言的更多特性。在上一个的例子中,我们演示了如何用"func"关键字定义函数。 类似的关键字还有:"var"、"const"和"type"等,它们可以用于定义变量、常量和类型等,用法和"import"一致。 我们可以小括弧声明一组类型相同的变量(如7-10和14-17行所示)。当然,也可以分开独立定义:

        const Space = " "
       
const Newline = "/n"

程序首先导入 os 包,因为后面要用到包中的一个 *os.File 类型的 Stdout 变量。 这里的 import 语句实际上是一个声明,和我们在 hello world 程序中所使用方法一样,包的名字标识符(fmt) 为前缀用于定位包中定位包中的成员,包可以是在当前目录或标准包目录。在导入包的时候一般会默认选用包本身的 名字(在必要的时候可以将导入的包重新命名)。在“hello world”程序中,我们只是简单的 import "fmt" 。

如果需要,你可以自己重新命名被import的包。但那不是必须的,只在处理包名字冲突的时候会用到。

通过"os.Stdout",我们可以用包中的"WriteString"方法来输出字符串。

现在已经导入"flag"包,并且在12行创建了一个全局变量,用于保存echo的"-n"命令行选项。变量 "omitNewline"为一个只想bool变量的bool型指针。

在"main.main"中,我们首先解析命令行参数(20行),然后创建了一个局部字符串变量用于保存要输出的内容。

变量声明语法如下:

        var s string = "";

这里有一个"var"关键字,后面跟着变量名字和变量的数据类型,再后面可以用“=”符号来进行赋初值。

简洁是go的一个目标,变量的定义也有更简略的语法。go可以根据初始值来判断变量的类型, 没有必要显式写出数据类型。也可以这样定义变量:

        var s = "";

还有更短的写法:

        s := "";

操作符":="将在Go中声明同时进行初始化一个变量时会经常使用。下面的代码是在"for"中声明并 初始化变量:

 
22        for i := 0; i < flag.NArg(); i++ {

"flag"包会解析命令行参数,并将不是flag选项的参数保存到一个列表中。可以通过flag的参数列表 访问普通的命令行参数。

Go语言的"for"语句和C语言中有几个不同的地方:第一,for是Go中唯一的循环语句,Go中没有while或 do语句;第二,for的条件语句并不需要用小括号包起来,但是循环体却必须要花括弧,这个规则同样适用于 if和switch。后面我们会看到for的一些例子。

在循环体中,通过"+="操作符向字符串"s"添加要命令行参数和空白。在循环结束后,根据命令行是否有"-n"选项, 判断末尾是否要添加换行符。最后输出结果。

值得注意的地方是"main.main"函数并没有返回值(函数被定义为没有返回值的类型)。如果"main.main" 运行到了末尾,就表示“成功”。如果想返回一个出错信息,可用系统调用强制退出:

        os.Exit(1)

"os"包还包含了其它的许多启动相关的功能,例如"os.Args"是"flag"包的一部分(用来获取命令行输入)。

类型简介

Go语言中有一些通用的类型,例如"int"和"float",它们对应的内存大小和处理器类型相关。同时, 也包含了许多固定大小的类型,例如"int8"和"float64",还有无符号类型"uint"和"uint32"等。 需要注意的是,即使"int"和"int32"占有同样的内存大小,但并不是同一种数据类型。不过 "byte"和"uint8"对应是相同的数据类型,它们是字符串中字符类型。

go中的字符串是一个内建数据类型。字符串虽然是字符序列,但并不是一个字符数组。可以创建新的 字符串,但是不能改变字符串。不过我们可以通过新的字符串来达到想改变字符串的目的。 下面列举"strings.go"例子说明字符串的常见用法:

 
11        s := "hello"
12        if s[1] != 'e' { os.Exit(1) }
13        s = "good bye"
14        var p *string = &s
15        *p = "ciao"

不管如何,试图修改字符串的做法都是被禁止的:

        s[0] = 'x';
       
(*p)[1] = 'y';

Go中的字符串和C++中的"const strings"概念类似,字符串指针则相当于C++中的"const strings" 引用。

是的,它们都是指针,但是Go中用法更简单一些。

数组的声明如下:

        var arrayOfInt [10]int;

数组和字符串一样也是一个值对象,不过数组的元素是可以修改的。不同于C语言的是:"int"类型数组 "arrayOfInt"并不能转化为"int"指针。因为,在Go语言中数组是一个值对象,它在内部保存"int"指针。

数组的大小是数组类型的一部分。我们还可以通过slice(切片)类型的变量来访问数组。 首先,数据元素的类型要和slice(切片)类型相同,然后通过"a: high"类似的 语法来关联数组的low到heigh-1的子区间元素。Slices和数组的声明语法类似,但是不像数组那样 要指定元素的个数(""和"10"的区别);它在内部引用特定的空间,或者其它数组的空间。 如果多个Slices引用同一个数组,则可以共享数组的空间。但是不同数组之间是无法共享内存空间的。

在Go语言中Slices比数组使用的更为普遍,因为它更有弹性,引用的语法也使得它效率很高。 但是,Slices缺少对内存的绝对控制比数组要差一些。例如你只是想要一个可以存放100个元素 的空间,那么你就可以选择数组了。创建数组:

 [3]int{1,2,3}

上面的语句创建一个含有3个元素的int数组。

当需要传递一个数组给函数时,你应该将函数的参数定义为一个Slice。这样,在调用函数的时候, 数组将被自动转换为slice传入。

比如以下函数以slices类型为参数(来自"sum.go"):

 
09    func sum(a []int) int { // returns an int
10        s := 0
11        for i := 0; i < len(a); i++ {
12            s += a[i]
13        }
14        return s
15    }

函数的返回值类型(int)在sum()函数的参数列表后面定义。

为了调用sum函数,我们需要一个slice作为参数。我们先创建一个数组,然后将数组转为slice类型:

    s := sum([3]int{1,2,3}[:])

如果你创建一个初始化的数组,你可以让编译器自动计算数组的元素数目,只要在数组大小中填写"..."就 可以了:

        s := sum([...]int{1,2,3}[:])

是实际编码中,如果不关心内存的具体细节,可以用slice类型(省略数组的大小)来代替数组地址为函数参数:

        s := sum([]int{1,2,3});

还有map类型,可以用以下代码初始化:

        m := map[string]int{"one":1 , "two":2}

用内建的"len()"函数,可以获取map中元素的数目,该函数在前面的"sum"中用到过。"len()"函数 还可以用在strings, arrays, slices, maps, 和 channels 中。

还有另外的"range"语法可以用到strings, arrays, slices, maps, 和 channels 中, 它可以用于"for"循环的迭代。例如以下代码

        for i := 0; i &lt; len(a); i++ { ... }

用"range"语法可以写成:

        for i, v := range a { ... }

这里的"i"对应元素的索引,"v"对应元素的值。关于更多的细节可以参考 Effective Go

申请内存

在Go语言中,大部分的类型都是值变量。例如int或struct(结构体)或array(数组)类型变量, 赋值的时候都是复制整个元素。如果需要为一个值类型的变量分配空间,可以用new():

        type T struct { a, b int }
       
var t *T = new(T);

或者更简洁的写法:

        t := new(T);

还有另外一些类型,如:maps, slices 和 channels(见下面)是引用语意(reference semantics)。 如果你一个slice 或 map内的元素,那么其他引用了相同slice 或 map的变量也能看到这个改变。 对于这三类引用类型的变量,需要用另一个内建的make()分配并初始化空间:

        m := make(map[string]int);

上目的代码定义一个新的map并分配了存储空间。如果只是定一个map而不想分配空间的话,可以这样:

        var m map[string]int;

它创建了一个nil(空的)引用并且没有分配存储空间。如果你想用这个map, 你必须使用make来 分配并初始化内存空间或者指向一个已经有存储空间的map。

注意: new(T) 返回的类型是 *T , 而 make(T) 返回的是引用语意的 T 。如果你(错误的)使用 new()` 分配了一个引用对象,你将会得到一个指向 nil引用的指针。这个相当于声明了一个未初始化引用变量并取得 它的地址。

常量

虽然在Go中整数(integer)占用了大量的空间,但是常量类型的整数并没有占用很多空间。 这里没有像0LL 或 0x0UL的常量,取而代之的是使用整数常量作为大型高精度的值。常量只有 在最终被赋值给一个变量的时候才可以会出现溢出的情况:

        const hardEight = (1 &lt;&lt; 100) &gt;&gt; 97  // legal,合法

具体的语法细节比较琐屑,下面是一些简单的例子:

        var a uint64 = 0  // a has type uint64, value 0
        a
:= uint64(0)    // equivalent; uses a "conversion"
        i
:= 0x1234       // i gets default type: int
       
var j int = 1e6   // legal - 1000000 is representable in an int
        x
:= 1.5          // a float
        i3div2
:= 3/2     // integer division - result is 1
        f3div2
:= 3./2.   // floating point division - result is 1.5

(强制?)转换只适用于几种简单的情况:转换整数(int)到去其他的精度和大小,整数(int)与 浮点数(float)的转换, 还有其他一些简单情形。在Go语言中,系统不会对两种不同类型变量作 任何隐式的类型转换。此外,由常数初始化的变量需要指定确定的类型和大小。

I/O包

接下来我们使用open/close/read/write等基本的系统调用实现一个用于文件IO的包。 让我们从文件file.go开始:

 
05    package file

07    import (
08        "os"
09        "syscall"
10    )


12    type File struct {
13        fd   int    // file descriptor number
14        name string // file name at Open time
15    }

文件的第一行声明当前代码对应&mdash;"file"&mdash;包,然后导入os和syscall两个包。 包os封装了不同操作系统底层的实现,例如将文件抽象成相同的类型。我们将在系统接口基础 上封装一个基本的文件IO接口。

另外还有其他一些比较底层的syscall包,它提供一些底层的系统调用(system's calls)。

接下来是一个类型(type)定义:用"type"这个关键字来声明一个类。在这个例子里数据结构(data structure) 名为"File"。为了让这事变的有趣些,我们的File包含了一个这个文件的名字(name)用来描述这个文件。

因为结构体名字"File"的首字母是大写,所以这个类型包(package)可以被外部访问。在GO中访问规则的处理 是非常简单的:如果顶极类型名字首字母(包括:function, method, constant or variable, or of a structure field or method)是大写,那么引用了这个包(package)的使用者就可以访问到它。不然 名称和被命名的东西将只能有package内部看到。这是一个要严格遵循的规则,因为这个访问规则是由 编译器(compiler)强制规范的。在GO中,一组公开可见的名称是"exported"。

在这个File例子中,所有的字段(fields)都是小写所以从包外部是不能访问的,不过我们在下面将会一个 一个对外访问的出口(exported) —— 一个以大写字母开头的方法。

首先是一个创建File结构体的函数:

 
17    func newFile(fd int, name string) *File {
18        if fd < 0 {
19            return nil
20        }
21        return &File{fd, name}
22    }

这将返回一个指向新File结构体的指针,结构体存有文件描述符和文件名。这段代码使用了GO的''复合变量''(composite literal)的概念,和创建内建的maps和arrays类型变量一样。要创建在堆(heap-allocated)中创建一个新的 对象,我们可以这样写:

        n := new(File);
        n
.fd = fd;
        n
.name = name;
       
return n

如果结构比较简单的话,我们可以直接在返回结构体变量地址的时候初始化成员字段,如前面例子的 21行代码所示。

我们可以用前面的函数(newFile)构造一些File类型的变量,返回File:

 
24    var (
25        Stdin  = newFile(0, "/dev/stdin")
26        Stdout = newFile(1, "/dev/stdout")
27        Stderr = newFile(2, "/dev/stderr")
28    )

这里的newFile是内部函数,真正包外部可以访问的函数是Open:

 
30    func Open(name string, mode int, perm uint32) (file *File, err os.Error) {
31        r, e := syscall.Open(name, mode, perm)
32        if e != 0 {
33            err = os.Errno(e)
34        }
35        return newFile(r, name), err
36    }

在这几行里出现了一些新的东西。首先,函数Open返回多个值(multi-value):一个File指针和一个error( 等一下会介绍errors)》我们用括号来表来声明返回多个变量值(multi-value),语法上它看 起来像第二个参数列表。syscall.Open系统调用同样也是返回多个值multi-value。接着我们能在31行 创建了r和e两个变量用于保存syscall.Open的返回值。函数最终也是返回2个值,分别为File指针和一个error。 如果syscall.Open打开失败,文件描述r将会是个负值,newFile将会返回nil。

关于错误:os包包含了一些常见的错误类型。在用户自己的代码中也尽量使用这些通用的错误。 在Open函数中,我们用os.Error函数将Unix的整数错误代码转换为go语言的错误类型。

现在我们可以创建Files,我们为它定义了一些常用的方法(methods)。要给一个类型定义一个方法(method), 需要在函数名前增加一个用于访问当前类型的变量。这些是为*File类型创建的一些方法:

38    func (file *File) Close() os.Error {
39        if file == nil {
40            return os.EINVAL
41        }
42        e := syscall.Close(file.fd)
43        file.fd = -1 // so it can't be closed again
44        if e != 0 {
45            return os.Errno(e)
46        }
47        return nil
48    }

50    func (file *File) Read(b []byte) (ret int, err os.Error) {
51        if file == nil {
52            return -1, os.EINVAL
53        }
54        r, e := syscall.Read(file.fd, b)
55        if e != 0 {
56            err = os.Errno(e)
57        }
58        return int(r), err
59    }


61    func (file *File) Write(b []byte) (ret int, err os.Error) {
62        if file == nil {
63            return -1, os.EINVAL
64        }
65        r, e := syscall.Write(file.fd, b)
66        if e != 0 {
67            err = os.Errno(e)
68        }
69        return int(r), err
70    }


72    func (file *File) String() string {
73        return file.name
74    }

这些并没有隐含的this指针(参考C++类),而且类型的方法(methods)也不是定义在struct内部——struct结构 只声明数据成员(data members)。事实上,我们可以给任意数据类型定义方法,例如:整数(integer),数组(array) 等。后面我们会有一个给数组定义方法的例子。

String这个方法之所以会被调用是为了更好的打印信息,我们稍后会详细说明。

方法(methods)使用os.EINVAL来表示(os.Error的版本)Unix错误代码EINVAL。 在os包中针对标准的error变量定义各种错误常量。

现在我们可以使用我们自己创建的包(package)了:

 
05    package main

07    import (
08        "./file"
09        "fmt"
10        "os"
11    )


13    func main() {
14        hello := []byte("hello, world/n")
15        file.Stdout.Write(hello)
16        file, err := file.Open("/does/not/exist",  0,  0)
17        if file == nil {
18            fmt.Printf("can't open file; err=%s/n",  err.String())
19            os.Exit(1)
20        }
21    }

--PROG progs/helloworld3.go /package/ END

这个''"./"''在导入(import)''"./file"''时告诉编译器(compiler)使用我们自己的package,而不是在 默认的package路径中找。

最后,我们来执行这个程序:

    $ 6g file.go                       # compile file package
    $
6g helloworld3.go                # compile main package
    $
6l -o helloworld3 helloworld3.6  # link - no need to mention "file"
    $ helloworld3
    hello
, world
    can
't open file; err=No such file or directory
     $

Rotting cats

在我们上面创建的file包(package)基础之上,实现一个简单的Unix工具 "cat(1)", "progs/cat.go":

05    package main

07    import (
08        "./file"
09        "flag"
10        "fmt"
11        "os"
12    )


14    func cat(f *file.File) {
15        const NBUF = 512
16        var buf [NBUF]byte
17        for {
18            switch nr, er := f.Read(buf[:]); true {
19            case nr < 0:
20                fmt.Fprintf(os.Stderr, "cat: error reading from %s: %s/n", f.String(), er.String())
21                os.Exit(1)
22            case nr == 0:  // EOF
23                return
24            case nr > 0:
25                if nw, ew := file.Stdout.Write(buf[0:nr]); nw != nr {
26                    fmt.Fprintf(os.Stderr, "cat: error writing from %s: %s/n", f.String(), ew.String())
27                }
28            }
29        }
30    }


32    func main() {
33        flag.Parse()   // Scans the arg list and sets up flags
34        if flag.NArg() == 0 {
35            cat(file.Stdin)
36        }
37        for i := 0; i < flag.NArg(); i++ {
38            f, err := file.Open(flag.Arg(i), 0, 0)
39            if f == nil {
40                fmt.Fprintf(os.Stderr, "cat: can't open %s: error %s/n", flag.Arg(i), err)
41                os.Exit(1)
42            }
43            cat(f)
44            f.Close()
45        }
46    }

现在应该很容易被理解,但是还有些新的语法"switch". 比如: 包括了"for"循环, "if"和 "switch"初始化的语句。在"switch"语句的18行用了"f.Read()"函数的返回值"nr"和"er"做为 变量(25行中的"if"也采用同样的方法)。这里的"switch"语法和其他语言语法基本相同,每个分支(cases) 从上到下查找是否与相关的表达式相同,分支(case)的表达式不仅仅是常量(constants)或整数(integers), 它可以是你想到的任意类型。

这个"switch"的值永远是"真(true)", 我们会一直执行它, 就像"for"语句,不写值默认是"真"(true). 事实上,"switch"是从"if-else"由来的。在这里我们要说明, "switch"语句中的每个"分支"(case)都 默认隐藏了"break".

在25行中调用"Write()"采用了slicing来取得buffer数据. 在标准的GO中提供了Slices对I/O buffers的操作。

现在让我们做一个"cat"的升级版让"rot13"来处理输入, 就是个简单的字符处理,但是要 采用GO的新特性"接口(interface)"来实现。

这个"cat()"使用了2个子程序"f":"Read()"和"String", 让我们定义这2个接口, 源码参考 "progs/cat_rot13.go"

 
26    type reader interface {
27        Read(b []byte) (ret int, err os.Error)
28        String() string
29    }

任何类型的方法都有 reader 这两个方法 —— 也就是说实现了这两个方法, 任何类型的方法都能使用。由于 file.File 实现了 reader 接口,我们就可以让 cat 的子程序访问 reader 从而取代了 *file.File 并且能正常工作,让我们来些第二个类型实现 reader , 一个关注现有的 reader ,另一个 rot13 只关注数据。我们只是定义了这个类型和 实现了这个方法并没有做其他的内部处理, 我们实现了第二个 reader 接口.

31    type rotate13 struct {
32        source    reader
33    }

35    func newRotate13(source reader) *rotate13 {
36        return &rotate13{source}
37    }


39    func (r13 *rotate13) Read(b []byte) (ret int, err os.Error) {
40        r, e := r13.source.Read(b)
41        for i := 0; i < r; i++ {
42            b[i] = rot13(b[i])
43        }
44        return r, e
45    }


47    func (r13 *rotate13) String() string {
48        return r13.source.String()
49    }
50    // end of rotate13 implementation

(42行的"rot13"函数非常简单,没有必要在这里进行讨论)

为了使用新的特性,我们定义了一个标记(flag):

 
14    var rot13Flag = flag.Bool("rot13", false, "rot13 the input")

用它基本上不需要修改"cat()"这个函数:

 
52    func cat(r reader) {
53        const NBUF = 512
54        var buf [NBUF]byte

56        if *rot13Flag {
57            r = newRotate13(r)
58        }
59        for {
60            switch nr, er := r.Read(buf[:]); {
61            case nr < 0:
62                fmt.Fprintf(os.Stderr, "cat: error reading from %s: %s/n", r.String(), er.String())
63                os.Exit(1)
64            case nr == 0:  // EOF
65                return
66            case nr > 0:
67                nw, ew := file.Stdout.Write(buf[0:nr])
68                if nw != nr {
69                    fmt.Fprintf(os.Stderr, "cat: error writing from %s: %s/n", r.String(), ew.String())
70                }
71            }
72        }
73    }

(我们应该对 main 和 cat 单独做些封装,不仅仅是对类型参数的修改,就当是练习)从56行到 58行: 如果 rot13 标记是真,封装的 reader 就会接受数据并传给 rotate13 并处理. 注意: 这个接口的值是变量,不是指针,这个参数是 reader 类型,不是 *reader , 尽管后面转换为 指向结构体的指针。

这里是执行结果:

        % echo abcdefghijklmnopqrstuvwxyz | ./cat
        abcdefghijklmnopqrstuvwxyz
       
% echo abcdefghijklmnopqrstuvwxyz | ./cat --rot13
        nopqrstuvwxyzabcdefghijklm
       
%

也许你会说使用注入依赖(dependency injection)能轻松的让接口以一个文件描述符执行。

接口(interfaces)是Go的一个特性,一个接口是由类型实现的,接口就是声明该类型的所有方法。 也就是说一个类型可以实现多个不同的接口, 没有任何类型的限制,就像我们的例子"rot13". "file.File"这个类型实现了"reader", 它也能实现"writer", 或通过其他的方法来实现这个接口。 参考空接口(empty interface)

        type Empty interface {}

任何类型都默认实现了空接口,我们可以用空接口来保存任意类型。

Sorting

接口(interfaces)提供了一个简单形式的多态(polymorphism). 他们把对象的定义和 如何实现的分开处理,允许相同的接口可以有不能的实现方法。

参考这个简单的排序算法(sort algorithm)"progs/sort.go"

 
13    func Sort(data Interface) {
14        for i := 1; i < data.Len(); i++ {
15            for j := i; j > 0 && data.Less(j, j-1); j-- {
16                data.Swap(j, j-1)
17            }
18        }
19    }

--PROG progs/sort.go /func.Sort/ /^}/

我们要封装这个排序(sort)的接口(interface)仅需要三个方法。

 
07    type Interface interface {
08        Len() int
09        Less(i, j int) bool
10        Swap(i, j int)
11    }

我们可以用任何类型的"Sort"去实现"Len", "Less" 和 "Swap". 这个"sort"包里面 包含一些方法(methods). 下面是整型数组的代码:

 
33    type IntArray []int

35    func (p IntArray) Len() int            { return len(p) }
36    func (p IntArray) Less(i, j int) bool  { return p[i] < p[j] }
37    func (p IntArray) Swap(i, j int)       { p[i], p[j] = p[j], p[i] }

你看到的是一个没有任何类型的"结构体"(non-struct type). 在你的包里面你可以定义 任何你想定义的类型.

现在用"progs/sortmain.go"程序进行测试,用"sort"包里面的排序函数进行排序。

 
12    func ints() {
13        data := []int{74, 59, 238, -784, 9845, 959, 905, 0, 0, 42, 7586, -5467984, 7586}
14        a := sort.IntArray(data)
15        sort.Sort(a)
16        if !sort.IsSorted(a) {
17            panic("fail")
18        }
19    }

如果我们为sort提供一个新类型,我们就需要为这个类型实现三个方法,如下:

 
30    type day struct {
31        num        int
32        shortName  string
33        longName   string
34    }

36    type dayArray struct {
37        data []*day
38    }


40    func (p *dayArray) Len() int            { return len(p.data) }
41    func (p *dayArray) Less(i, j int) bool  { return p.data[i].num < p.data[j].num }
42    func (p *dayArray) Swap(i, j int)       { p.data[i], p.data[j] = p.data[j], p.data[i] }

打印输出

前面例子中涉及到的打印都比较简单。在这一节中,我们将要讨论Go语言格式化输出的功能。

我们已经用过"fmt"包中的"Printf"和"Fprintf"等输出函数。"fmt"包中的"Printf"函数的 完整说明如下:

        Printf(format string, v ...) (n int, errno os.Error)

其中"..."表示数目可变参数,和C语言中"stdarg.h"中的宏类似。不过Go中,可变参数是通道 一个空接口("interface {}")和反射(reflection)库实现的。反射特性可以帮助"Printf" 函数很好的获取参数的详细特征。

在C语言中,printf函数的要格式化的参数类型必须和格式化字符串中的标志一致。不过在Go语言中, 这些细节都被简化了。我们不再需要"%llud"之类的标志,只用"%d"表示要输出一个整数。至于对应 参数的实际类型,"Printf"可以通过反射获取。例如:

 
10        var u64 uint64 = 1<<64-1
11        fmt.Printf("%d %d/n", u64, int64(u64))

输出

        18446744073709551615 -1

最简单的方法是用"%v"标志,它可以以适当的格式输出任意的类型(包括数组和结构)。下面的程序,

 
14        type T struct {
15            a int
16            b string
17        }
18        t := T{77, "Sunset Strip"}
19        a := []int{1, 2, 3, 4}
20        fmt.Printf("%v %v %v/n", u64, t, a)

将输出:

        18446744073709551615 {77 Sunset Strip} [1 2 3 4]

如果是使用"Print"或"Println"函数的话,甚至不需要格式化字符串。这些函数会针对数据类型 自动作转换。"Print"函数默认将每个参数以"%v"格式输出,"Println"函数则是在"Print"函数 的输出基础上增加一个换行。一下两种输出方式和前面的输出结果是一致的。

 
21        fmt.Print(u64, " ", t, " ", a, "/n")
22        fmt.Println(u64, t, a)

如果要用"Printf"或"Print"函数输出似有的结构类型,之需要为该结构实现一个"String()"方法, 返回相应的字符串就可以了。打印函数会先检测该类型是否实现了"String()"方法,如果实现了则以 该方法返回字符串作为输出。下面是一个简单的例子。

 
09    type testType struct {
10        a int
11        b string
12    }

14    func (t *testType) String() string {
15        return fmt.Sprint(t.a) + " " + t.b
16    }


18    func main() {
19        t := &testType{77, "Sunset Strip"}
20        fmt.Println(t)
21    }

因为 *testType 类型有 String() 方法,因此格式化函数用它作为输出结果:

        77 Sunset Strip

前面的例子中,"String()"方法用到了"Sprint"(从字面意思可以猜测函数将返回一个字符串) 作为格式化的基础函数。在Go中,我们可以递归使用"fmt"库中的函数来为格式化服务。

"Printf"函数的另一种输出是"%T"格式,它输出的内容更加详细,可以作为调试信息用。

自己实现一个功能完备,可以输出各种格式和精度的函数是可能的。不过这不是该教程的重点,大家 可以把它当作一个课后练习。

读者可能有疑问,"Printf"函数是如何知道变量是否有"String()"函数实现的。实际上,我们 需要先将变量转换为Stringer接口类型,如果转换成功则表示有"String()"方法。下面是一个 演示的例子:

        type Stringer interface {
               
String() string
       
}

        s
, ok := v.(Stringer);  // Test whether v implements "String()"
       
if ok {
                result
= s.String()
       
} else {
                result
= defaultOutput(v)
       
}

这里用到了类型断言("v.(Stringer)"),用来判断变量"v"是否可以满足"Stringer"接口。 如果满足,"s"将对应转换后的Stringer接口类型并且"ok"被设置为"true"。然后我们通过"s", 以Stringer接口的方式调用String()函数。如果不满足该接口特征,"ok"将被设置为false。

"Stringer"接口的命名通常是在接口方法的名字后面加''er''后缀,这里是"String+er"。

Go中的打印函数,除了"Printf"和"Sprintf"等之外,还有一个"Fprintf"函数。不过"Fprintf"函数和 的第一个参数并不是一个文件,而是一个在"io"库中定义的接口类型:

        type Writer interface {
               
Write(p []byte) (n int, err os.Error);
       
}

这里的接口也是采用类似的命名习惯,类型的接口还有"io.Reader"和"io.ReadWriter"等。 在调用"Fprintf"函数时,可以用实现了"Write"方法的任意类型变量作为参数,例如文件、网络、管道等等。

生成素数

这里我们要给出一个并行处理程序及之间的通信。这是一个非常大的课题,我们这里只是给出一些要点。

素数筛选是一个比较经典的问题(这里侧重于Eratosthenes素数筛选算法的并行特征)。它以全部的 自然后为筛选对象。首选从第一个素数2开始,后续数列中是已经素数倍数的数去掉。每次筛选可以得到 一个新的素数,然后将新的素数加入筛选器,继续筛选后面的自然数列(这里要参考算法的描述调整)。

这里是算法工作的原理图。每个框对应一个素数筛选器,并且将剩下的数列传给下一个素数筛进行筛选。

为了产生整数序列,我们使用管道。管道可以用于连接两个并行的处理单。在Go语言中, 管道由运行时库管理,可以用"make"来创建新的管道。

这是"progs/sieve.go"程序的第一个函数:

09    // Send the sequence 2, 3, 4, ... to channel 'ch'.
10    func generate(ch chan int) {
11        for i := 2; ; i++ {
12            ch <- i  // Send 'i' to channel 'ch'.
13        }
14    }

函数"generate"用于生成2, 3, 4, 5, ...自然数序列,然后依次发送到管道。 这里用到了二元操作符"&lt;-", 它用于向管道发送数据。当管道没有接受者的时候 会阻塞,直到有接收者从管道接受数据为止。

过滤器函数有三个参数:输入输出管道和用于过滤的素数。当输入管道读出来的数不能被 过滤素数整除时,则将当前整数发送到输出管道。这里用到了"&lt;-"操作符,它用于从 管道读取数据。

16    // Copy the values from channel 'in' to channel 'out',
17    // removing those divisible by 'prime'.
18    func filter(in, out chan int, prime int) {
19        for {
20            i := <-in  // Receive value of new variable 'i' from 'in'.
21            if i % prime != 0 {
22                out <- i  // Send 'i' to channel 'out'.
23            }
24        }
25    }

整数生成器generator函数和过滤器filters是并行执行的。Go语言有自己的并发 程序设计模型,这个和传统的进程/线程/轻量线程类似。为了区别,我们把Go语言 中的并行程序称为goroutines。如果一个函数要以goroutines方式并行执行, 只要用"go"关键字作为函数调用的前缀即可。goroutines和它的启动线程并行执行, 但是共享一个地址空间。例如,以goroutines方式执行前面的sum函数:

        go sum(hugeArray); // calculate sum in the background

如果想知道计算什么时候结束,可以让sum用管道把结果返回:

        ch := make(chan int);
        go sum
(hugeArray, ch);
       
// ... do something else for a while
        result
:= &lt;-ch;  // wait for, and retrieve, result

再回到我们的素数筛选程序。下面程序演示如何将不同的素数筛链接在一起:

28    func main() {
29        ch := make(chan int)  // Create a new channel.
30        go generate(ch)  // Start generate() as a goroutine.
31        for {
32            prime := <-ch
33            fmt.Println(prime)
34            ch1 := make(chan int)
35            go filter(ch, ch1, prime)
36            ch = ch1
37        }
38    }

29行先调用"generate"函数,用于产生最原始的自然数序列(从2开始)。然后 从输出管道读取的第一个数为新的素数,并以这个新的素数生成一个新的过滤器。 然后将新创建的过滤器添加到前一个过滤器后面,新过滤器的输出作为新的输出 管道。

sieve程序还可以写的更简洁一点。这里是"generate"的改进,代码在 "progs/sieve1.go"中:

10    func generate() chan int {
11        ch := make(chan int)
12        go func(){
13            for i := 2; ; i++ {
14                ch <- i
15            }
16        }()
17        return ch
18    }

新完善的generate函数在内部进行必须的初始化操作。它创建输出管道,然后 启动goroutine用于产生整数序列,最后返回输出管道。它类似于一个并发程序 的工厂函数,完成后返回一个用于链接的管道。

第12-16行用go关键字启动一个匿名函数。需要注意的是,generate函数的"ch" 变量对于匿名函数是可见,并且"ch"变量在generate函数返回后依然存在(因为 匿名的goroutine还在运行)。

这里我们采用过滤器"filter"来筛选后面的素数:

21    func filter(in chan int, prime int) chan int {
22        out := make(chan int)
23        go func() {
24            for {
25                if i := <-in; i % prime != 0 {
26                    out <- i
27                }
28            }
29        }()
30        return out
31    }

函数"sieve"对应处理的一个主循环,它只是依次将数列交给后面的素数筛选器进行筛选。 如果遇到新的素数,再输出素数后以该素数创建信的筛选器。

33    func sieve() chan int {
34        out := make(chan int)
35        go func() {
36            ch := generate()
37            for {
38                prime := <-ch
39                out <- prime
40                ch = filter(ch, prime)
41            }
42        }()
43        return out
44    }

主函数入口启动素数生成服务器,然后打印从管道输出的素数:

46    func main() {
47        primes := sieve()
48        for {
49            fmt.Println(<-primes)
50        }
51    }

Multiplexing

基于管道,我们可以很容易实现一个支持多路客户端的服务器程序。采用的技巧是将每个客户端私有的通信管道 作为消息的一部分发送给服务器,然后服务器通过这些管道和客户端独立通信。现实中的服务器实现都很复杂, 我们这里只给出一个服务器的简单实现来展现前面描述的技巧。首先定义一个"request"类型,里面包含一个 客户端的通信管道。

 
09    type request struct {
10        a, b    int
11        replyc  chan int
12    }

服务器对客户端发送过来的两个整数进行运算。下面是具体的函数,函数在运算完之后将结构通过结构中的 管道返回给客户端。

 
14    type binOp func(a, b int) int

16    func run(op binOp, req *request) {
17        reply := op(req.a, req.b)
18        req.replyc <- reply
19    }

第14行现定义一个"binOp"函数类型,用于对两个整数进行运算。

服务器routine线程是一个无限循环,它接受客户端请求。然后为每个客户端启动一个独立的routine线程, 用于处理客户数据(不会被某个客户端阻塞)。

 
21    func server(op binOp, service chan *request) {
22        for {
23            req := <-service
24            go run(op, req)  // don't wait for it
25        }
26    }

启动服务器的方法也是一个类似的routine线程,然后返回服务器的请求管道。

 
28    func startServer(op binOp) chan *request {
29        req := make(chan *request)
30        go server(op, req)
31        return req
32    }

这里是一个简单的测试。首先启动服务器,处理函数为计算两个整数的和。接着向服务器发送"N"个请求(无阻塞)。 当所有请求都发送完了之后,再进行验证返回结果。

 
34    func main() {
35        adder := startServer(func(a, b int) int { return a + b })
36        const N = 100
37        var reqs [N]request
38        for i := 0; i < N; i++ {
39            req := &reqs[i]
40            req.a = i
41            req.b = i + N
42            req.replyc = make(chan int)
43            adder <- req
44        }
45        for i := N-1; i >= 0; i-- {   // doesn't matter what order
46            if <-reqs[i].replyc != N + 2*i {
47                fmt.Println("fail at", i)
48            }
49        }
50        fmt.Println("done")
51    }

前面的服务器程序有个小问题:当main函数退出之后,服务器没有关闭,而且可能有一些客户端被阻塞在 管道通信中。为了处理这个问题,我们可给服务器增加一个控制管道,用于退出服务器。

 
32    func startServer(op binOp) (service chan *request, quit chan bool) {
33        service = make(chan *request)
34        quit = make(chan bool)
35        go server(op, service, quit)
36        return service, quit
37    }

首先给"server"函数增加一个控制管道参数,然后这样使用:

 
21    func server(op binOp, service chan *request, quit chan bool) {
22        for {
23            select {
24            case req := <-service:
25                go run(op, req)  // don't wait for it
26            case <-quit:
27                return
28            }
29        }
30    }

在服务器函数中,"select"操作服用于从多个通讯管道中选择一个就绪的管道。如果所有的管道都没有数据, 那么将等待知道有任意一个管道有数据。如果有多个管道就绪,则随即选择一个。服务器处理客户端请求,如果 有退出消息则退出。

最后是在main函数中保存"quit"管道,然后在退出的时候向服务线程发送停止命令。

40        adder, quit := startServer(func(a, b int) int { return a + b })

...

55        quit <- true

当然,Go语言及并行编程要讨论的问题很多。这个入门只是给出一些简单的例子。

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值