Skip to content

入门

命令行参数

大多数的程序都是处理输入,产生输出;这也正是“计算”的定义。但是,程序如何获取要处理的输入数据呢?
一些程序生成自己的数据,但通常情况下,输入来自于程序外部: 文件、网络连接、其它程序的输出、敲键盘的用户、命令行参数或其它类似输入源。
下面几个例子会讨论其中几个输入源,首先是命令行参数。

os 包以跨平台的方式,提供了一些与操作系统交互的函数和变量。程序的命令行参数可从 os 包的 Args 变量获取;os 包外部使用 os.Args 访问该变量

os.Args 变量是一个字符串(string)的切片(slice)(注: slice 和 Python 语言中的切片类似,是一个简版的动态数组),切片是 Go 语言的基础概念。
现在先把切片 s 当作数组元素序列,序列的长度动态变化,用 s[i] 访问单个元素,用 s[m:n] 获取子序列(和 Python 里的语法差不多)。序列的元素数目为 len(s)
和大多数编程语言类似,区间索引时,Go 语言里采用左闭右开形式,因为这样可以简化逻辑。(比如 a = [1, 2, 3, 4, 5], a[0:] = [1, 2, 3],不包含最后一个元素。比如 s[m:n] 这个切片,0 <= m <= n <= len(s),包含 n - m 个元素)

os.Args 的第一个元素,os.Args[0],是命令本身的名字;其他的元素则是程序启动时传给它的参数。
下个例子用到的元素包含在 os.Args[1:len(os.Args)]切片中。如果省略切片表达式的 m 或 n,会默认传入 0 或 len(s),因此前面的切片可以简写成 os.Args[1:]

下面是 Unix 里 echo 命令的一份实现,echo 把它的命令行参数打印成一行。程序导入了两个包,用括号把它们括起来写成列表形式,而没有分开写成独立的import声明。
两种形式都合法,列表形式习惯上用的多。包导入顺序并不重要;gofmt 工具格式化时按照字母顺序对包名排序

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
// Echo1 prints its command-line arguments.
package main

import (
    "fmt"
    "os"
)

func main() {
    var s, sep string
    for i := 1; i < len(os.Args); i++ {
        s += sep + os.Args[i]
        sep = " "
    }
    fmt.Println(s)
}

注释语句以//开头。对于程序员来说,//之后到行末之间所有的内容都是注释,被编译器忽略。
按照惯例,我们在每个包的包声明前添加注释;对于 main package,注释包含一句或几句话,从整体角度对程序做个描述

var 声明定义了两个 string 类型的变量 s 和 sep。变量会在声明时直接初始化。如果变量没有显式初始化,则被隐式地赋予其类型的零值(zero value),数值类型是 0,字符串类型是空字符串 ""。这个例子里,声明把 s 和 sep 隐式地初始化成空字符串

对数值类型,Go 语言提供了常规的数值和逻辑运算符。而对 string 类型,+ 运算符连接字符串(和 C++ 或者 js 是一样的)。
所以表达式sep + os.Args[i] 表示连接字符串 sep 和 os.Args。
程序中使用的语句: s += sep + os.Args[i] 是一条赋值语句,将 s 的旧值跟 sep 与 os.Args[i] 连接后赋值回 s,等价于: s = s + sep + os.Args[i]

运算符 += 是赋值运算符(assignment operator),每种数值运算符或逻辑运算符,如 +*,都有对应的赋值运算符

自增语句 i++ 给 i 加 1; 这和 i += 1 以及 i = i + 1 都是等价的。对应的还有 i--i 减 1.
它们是语句,而不像 C 系的其它语言那样是表达式。所以 j = i++ 非法,而且 ++-- 都只能放在变量名后面,因此 --i 也非法

Go 语言只有 for 循环这一种循环语句。for 循环有多种形式,其中一种如下所示:

1
2
3
for initialization; condition; post {
    // zero or more statements
}

for 循环三个部分不许括号包围。大括号强制要求,左大括号必须和 post 语句在同一行。

initialization 语句是可选的,在循环开始前执行。initalization 如果存在,必须是一条简单语句(simple statement),即,短变量声明、自增语句、赋值语句或函数调用。
condition 是一个布尔表达式(boolean expression),其值在每次循环迭代开始时计算。如果为 true 则执行循环体语句。
post 语句在循环体执行结束后执行,之后再次对 condition 求值。condition 值为 false 时,循环结束

for 循环的这三个部分每个都可以省略,如果省略 initialization 和 post,分号也可以省略

1
2
3
4
// a traditional "while" loop
for condition {
    // ...
}

如果连 condition 也省略了,像下面这样:

1
2
3
4
// a traditional infinite loop
for {
    // ...
}

这就变成一个无限循环,尽管如此,还可以用其他方式种植循环,如一条 break 或 return 语句。

for 循环的另一种形式,在某种数据类型的区间(range)上遍历,如字符串或切片。echo 的第二版本展示了这种形式

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
// Echo2 prints its command-line arguments.
package main

import (
    "fmt"
    "os"
)

func main() {
    s, sep := "", ""
    for _, arg := range os.Args[1:] {
        s += sep + arg
        sep = " "
    }
    fmt.Println(s)
}

如果连接涉及的数据量很大,这种方式代价高昂。一种简单且高效的解决方案是使用 strings 包的 Join 函数

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// Echo3 prints its command-line arguments.
package main

import (
    "fmt"
    "os"
    "strings"
)

func main() {
    fmt.Println(strings.Join(os.Args[1:], " "))
}

打印os.Args[0]:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// echo.go
package main

import (
    "fmt"
    "os"
)

func main() {
    fmt.Println(os.Args[0])
}

执行go run echo.go输出结果:

1
/var/folders/3l/gn49pyf960s_sbmhpyyxs2g40000gn/T/go-build299032392/b001/exe/echo1

查找重复的行

对文件做拷贝、打印、搜索、排序、统计或类似事情的程序都有一个差不多的程序结构: 一个处理输入的循环,在每个元素上执行计算处理,在处理的同时货最后产生输出。
我们会展示一个名为 dup 的程序的三个版本;灵感来自于 Unix 的 uniq 命令,其寻找相邻的重复行。该程序使用的结构和包是个参考范例,可以方便地修改。

dup 的第一版本打印标准输入中多次出现的行,以重复次数开头。该程序将引入 if 语句,map 数据类型以及 bufio 包

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// Dup1 prints the text of each line that appears more than
// once in the standard input, preceded by its count.
package main

import (
    "bufio"
    "fmt"
    "os"
)

func main() {
    counts := make(map[string]int)
    input := bufio.NewScanner(os.Stdin)
    for input.Scan() {
        counts[input.Text()]++
    }
    // NOTE: ignoring potential errors from input.Err()
    for line, n := range counts {
        if n > 1 {
            fmt.Printf("%d\t%s\n", n, line)
        }
    }
}

使用:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
go run echo1.go
11
1
1
1
11
2
3
5
2
按下 Ctrl + d,表示输入结束(文件结束符EOF,Windows下为组合键Ctrl+Z,Unix/Linux下为组合键Ctrl+D)            
输出结果:    
2      11
3       1
2       2

来看bufio包,它使处理输入和输出方便又高效。Scanner类型是该包最有用的特性之一,它读取输入并将其拆成行或单词;通常是处理行形式的输入最简单的方法。

程序使用短变量声明创建 bufio.Scanner 类型的变量 input。

该变量从程序的标准输入中读取内容。每次调用 input.Scanner,即读入下一行,并移除行末的换行符;读取的内容可以调用input.Text()得到。
Scan函数在读到一行时返回true,在无输入时返回false。

类似于C或其它语言里的 printf函数,fmt.Printf 函数对一些表达式产生格式化输出。该函数的首个参数是个格式字符串,指定后续参数被如何格式化。
各个参数的格式取决于“转换字符”(conversion character),形式为百分号后跟一个字母。
举个例子,%d 表示以十进制形式打印一个整型操作数,而 %s 则表示把字符串型操作数的值展开。

Printf有一大堆这种转换,Go程序员称之为动词(verb)。下面的表格虽然远不是完整的规范,但展示了可用的很多特性:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
%d          十进制整数
%x, %o, %b  十六进制,八进制,二进制整数。
%f, %g, %e  浮点数: 3.141593 3.141592653589793 3.141593e+00
%t          布尔:true或false
%c          字符(rune) (Unicode码点)
%s          字符串
%q          带双引号的字符串"abc"或带单引号的字符'c'
%v          变量的自然形式(natural format)
%T          变量的类型
%%          字面上的百分号标志(无操作数)

dup1的格式字符串中还含有制表符 \t 和换行符 \n。字符串字面上可能含有这些代表不可见字符的转义字符(escap sequences)。
默认情况下,Printf 不会换行。按照惯例,以字母f结尾的格式化函数,如 log.Printffmt.Errorf,都采用 fmt.Printf 的格式化准则。
而以 ln 结尾的格式化函数,则遵循 Println 的方式,以跟 %v 差不多的方式格式化参数,并在最后添加一个换行符。(译注:后缀 f 指 fomart,ln 指 line。)

很多程序要么从标准输入中读取数据,如上面的例子所示,要么从一系列具名文件中读取数据。
dup程序的下个版本读取标准输入或是使用 os.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
31
32
33
34
35
36
37
38
39
40
// Dup2 prints the count and text of lines that appear more than once
// in the input.  It reads from stdin or from a list of named files.
package main

import (
    "bufio"
    "fmt"
    "os"
)

func main() {
    counts := make(map[string]int)
    files := os.Args[1:]
    if len(files) == 0 {
        countLines(os.Stdin, counts)
    } else {
        for _, arg := range files {
            f, err := os.Open(arg)
            if err != nil {
                fmt.Fprintf(os.Stderr, "dup2: %v\n", err)
                continue
            }
            countLines(f, counts)
            f.Close()
        }
    }
    for line, n := range counts {
        if n > 1 {
            fmt.Printf("%d\t%s\n", n, line)
        }
    }
}

func countLines(f *os.File, counts map[string]int) {
    input := bufio.NewScanner(f)
    for input.Scan() {
        counts[input.Text()]++
    }
    // NOTE: ignoring potential errors from input.Err()
}

os.Open 函数返回两个值。第一个值是被打开的文件(*os.File),其后被Scanner读取。

os.Open 返回的第二个值是内置 error 类型的值。如果err等于内置值nil(译注:相当于其它语言里的NULL),那么文件被成功打开。
读取文件,直到文件结束,然后调用Close关闭该文件,并释放占用的所有资源。相反的话,如果err的值不是nil,说明打开文件时出错了。这种情况下,错误值描述了所遇到的问题。
我们的错误处理非常简单,只是使用 Fprintf 与表示任意类型默认格式值的动词 %v,向标准错误流打印一条信息,然后dup继续处理下一个文件;

注意countLines函数在其声明前被调用。函数和包级别的变量(package-level entities)可以任意顺序声明,并不影响其被调用。(最好还是遵循一定的规范)

map是一个由make函数创建的数据结构的引用。
map作为为参数传递给某函数时,该函数接收这个引用的一份拷贝(copy,或译为副本),被调用函数对map底层数据结构的任何修改,调用者函数都可以通过持有的map引用看到。
在我们的例子中,countLines函数向counts插入的值,也会被main函数看到。(类似于C++里的引用传递,实际上指针是另一个指针了,但内部存的值指向同一块内存)

dup的前两个版本以"流”模式读取输入,并根据需要拆分成多个行。理论上,这些程序可以处理任意数量的输入数据。
还有另一个方法,就是一口气把全部输入数据读到内存中,一次分割为多行,然后处理它们。
下面这个版本,dup3,就是这么操作的。这个例子引入了 ReadFile 函数(来自于 io/ioutil 包),其读取指定文件的全部内容,strings.Split 函数把字符串分割成子串的切片。
(Split的作用与前文提到的strings.Join相反。)

我们略微简化了dup3。首先,由于ReadFile函数需要文件名作为参数,因此只读指定文件,不读标准输入。其次,由于行计数代码只在一处用到,故将其移回main函数。

 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
package main

import (
    "fmt"
    "io/ioutil"
    "os"
    "strings"
)

func main() {
    counts := make(map[string]int)
    for _, filename := range os.Args[1:] {
        data, err := ioutil.ReadFile(filename)
        if err != nil {
            fmt.Fprintf(os.Stderr, "dup3: %v\n", err)
            continue
        }
        for _, line := range strings.Split(string(data), "\n") {
            counts[line]++
        }
    }
    for line, n := range counts {
        if n > 1 {
            fmt.Printf("%d\t%s\n", n, line)
        }
    }
}

ReadFile函数返回一个字节切片(byte slice),必须把它转换为string,才能用 strings.Split 分割。

实现上,bufio.Scannerioutil.ReadFileioutil.WriteFile 都使用 *os.File的ReadWrite 方法,但是,大多数程序员很少需要直接调用那些低级(lower-level)函数。
高级(higher-level)函数,像 bufioio/ioutil 包中所提供的那些,用起来要容易点。

GIF动画

下面的程序会演示Go语言标准库里的image这个package的用法,我们会用这个包来生成一系列的bit-mapped图,然后将这些图片编码为一个GIF动画。
我们生成的图形名字叫利萨如图形(Lissajous figures),这种效果是在1960年代的老电影里出现的一种视觉特效。
它们是协振子在两个纬度上振动所产生的曲线,比如两个sin正弦波分别在x轴和y轴输入会产生的曲线。

图1.1是这样的一个例子:

图1_1.png

要看这个程序的结果,需要将标准输出重定向到一个GIF图像文件(使用 ./lissajous > output.gif 命令)。下面是GIF图像动画效果:

ch1-01

这段代码里我们用了一些新的结构,包括const声明,struct结构体类型,复合声明。
和我们举的其它的例子不太一样,这一个例子包含了浮点数运算。这些概念我们只在这里简单地说明一下

 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
// Lissajous generates GIF animations of random Lissajous figures.
package main

import (
    "image"
    "image/color"
    "image/gif"
    "io"
    "math"
    "math/rand"
    "os"
)

var palette = []color.Color{color.White, color.Black}

const (
    whiteIndex = 0 // first color in palette
    blackIndex = 1 // next color in palette
)

func main() {
    lissajous(os.Stdout)
}

func lissajous(out io.Writer) {
    const (
        cycles  = 5     // number of complete x oscillator revolutions
        res     = 0.001 // angular resolution
        size    = 100   // image canvas covers [-size..+size]
        nframes = 64    // number of animation frames
        delay   = 8     // delay between frames in 10ms units
    )

    freq := rand.Float64() * 3.0 // relative frequency of y oscillator
    anim := gif.GIF{LoopCount: nframes}
    phase := 0.0 // phase difference
    for i := 0; i < nframes; i++ {
        rect := image.Rect(0, 0, 2*size+1, 2*size+1)
        img := image.NewPaletted(rect, palette)
        for t := 0.0; t < cycles*2*math.Pi; t += res {
            x := math.Sin(t)
            y := math.Sin(t*freq + phase)
            img.SetColorIndex(size+int(x*size+0.5), size+int(y*size+0.5),
                blackIndex)
        }
        phase += 0.1
        anim.Delay = append(anim.Delay, delay)
        anim.Image = append(anim.Image, img)
    }
    gif.EncodeAll(out, &anim) // NOTE: ignoring encoding errors
}

当我们 import 了一个包路径包含有多个单词的 package 时,比如 image/color(image和color两个单词),通常我们只需要用最后那个单词表示这个包就可以。
所以当我们写 color.White 时,这个变量指向的是 image/color 包里的变量,同理 gif.GIF 是属于 image/gif 包里的变量。

这个程序里的常量声明给出了一系列的常量值,常量是指在程序编译后运行时始终都不会变化的值,比如圈数、帧数、延迟值。
常量声明和变量声明一般都会出现在包级别,所以这些常量在整个包中都是可以共享的,或者你也可以把常量声明定义在函数体内部,那么这种常量就只能在函数体内用。
目前常量声明的值必须是一个数字值、字符串或者一个固定的boolean值。

[]color.Color{...}gif.GIF{...} 这两个表达式就是我们说的复合声明。这是实例化Go语言里的复合类型的一种写法。
这里的前者生成的是一个slice切片,后者生成的是一个struct结构体。

gif.GIF是一个struct类型。struct是一组值或者叫字段的集合,不同的类型集合在一个struct可以让我们以一个统一的单元进行处理。
anim是一个 gif.GIF 类型的struct变量。这种写法会生成一个struct变量,并且其内部变量 LoopCount 字段会被设置为nframes;
而其它的字段会被设置为各自类型默认的零值。
struct内部的变量可以以一个点(.)来进行访问,就像在最后两个赋值语句中显式地更新了 anim 这个 struct 的 Delay 和 Image 字段。

lissajous函数内部有两层嵌套的for循环。外层循环会循环64次,每一次都会生成一个单独的动画帧。它生成了一个包含两种颜色的 201 * 201 大小的图片,白色和黑色。
所有像素点都会被默认设置为其零值(也就是调色板palette里的第0个值),这里我们设置的是白色。每次外层循环都会生成一张新图片,并将一些像素设置为黑色。
其结果会append到之前结果之后。这里我们用到了 append 内置函数,将结果append到anim中的帧列表末尾,并设置一个默认的80ms的延迟值。
循环结束后所有的延迟值被编码进了GIF图片中,并将结果写入到输出流。out这个变量是io.Writer类型,这个类型支持把输出结果写到很多目标,很快我们就可以看到例子。

内层循环设置两个偏振值。x 轴偏振使用 sin 函数。
y 轴偏振也是正弦波,但其相对 x 轴的偏振是一个 0-3 的随机值,初始偏振值是一个零值,随着动画的每一帧逐渐增加。
循环会一直跑到 x 轴完成五次完整的循环。每一步它都会调用 SetColorIndex 来为 (x, y) 点来染黑色。

main函数调用lissajous函数,用它来向标准输出流打印信息,所以下面这个命令会像图1.1中产生一个GIF动画。

1
2
$ go build gopl.io/ch1/lissajous
$ ./lissajous >out.gif

获取 URL

对于很多现代应用来说,访问互联网上的信息和访问本地文件系统一样重要。
Go语言在net这个强大package的帮助下提供了一系列的package来做这件事情,使用这些包可以更简单地用网络收发信息,还可以建立更底层的网络连接,编写服务器程序。
在这些情景下,Go语言原生的并发特性显得尤其好用。

为了最简单地展示基于HTTP获取信息的方式,下面给出一个示例程序fetch,这个程序将获取对应的url,并将其源文本打印出来;
这个例子的灵感来源于curl工具(unix下的一个用来发http请求的工具,具体可以 man curl)。
当然,curl提供的功能更为复杂丰富,这里只编写最简单的样例。这个样例之后还会多次被用到。

 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
// Fetch prints the content found at a URL.
package main

import (
    "fmt"
    "io/ioutil"
    "net/http"
    "os"
)

func main() {
    for _, url := range os.Args[1:] {
        resp, err := http.Get(url)
        if err != nil {
            fmt.Fprintf(os.Stderr, "fetch: %v\n", err)
            os.Exit(1)
        }
        b, err := ioutil.ReadAll(resp.Body)
        resp.Body.Close()
        if err != nil {
            fmt.Fprintf(os.Stderr, "fetch: reading %s: %v\n", url, err)
            os.Exit(1)
        }
        fmt.Printf("%s", b)
    }
}

这个程序从两个package中导入了函数,net/httpio/ioutil 包,http.Get 函数是创建HTTP请求的函数,如果获取过程没有出错,那么会在resp这个结构体中得到访问的请求结果。
resp的Body字段包括一个可读的服务器响应流。ioutil.ReadAll 函数从response中读取到全部内容;将其结果保存在变量b中。
resp.Body.Close 关闭resp的Body流,防止资源泄露,Printf函数会将结果b写出到标准输出流中。

无论哪种失败原因,我们的程序都用了 os.Exit 函数来终止进程,并且返回一个 status 错误码,其值为1。

并发获取多个URL

Go语言最有意思并且最新奇的特性就是对并发编程的支持。并发编程是一个大话题,这里我们只浅尝辄止地来体验一下Go语言里的 goroutine 和 channel。

下面的例子fetchall,和前面的fetch程序所要做的工作基本一致,fetchall 的特别之处在于它会同时去获取所有的URL,所以这个程序的总执行时间不会超过执行时间最长的那一个任务,前面的fetch程序执行时间则是所有任务执行时间之和。
fetchall 程序只会打印获取的内容大小和经过的时间,不会像之前那样打印获取的内容。

 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
// Fetchall fetches URLs in parallel and reports their times and sizes.
package main

import (
    "fmt"
    "io"
    "io/ioutil"
    "net/http"
    "os"
    "time"
)

func main() {
    start := time.Now()
    ch := make(chan string)
    for _, url := range os.Args[1:] {
        go fetch(url, ch) // start a goroutine
    }
    for range os.Args[1:] {
        fmt.Println(<-ch) // receive from channel ch
    }
    fmt.Printf("%.2fs elapsed\n", time.Since(start).Seconds())
}

func fetch(url string, ch chan<- string) {
    start := time.Now()
    resp, err := http.Get(url)
    if err != nil {
        ch <- fmt.Sprint(err) // send to channel ch
        return
    }
    nbytes, err := io.Copy(ioutil.Discard, resp.Body)
    resp.Body.Close() // don't leak resources
    if err != nil {
        ch <- fmt.Sprintf("while reading %s: %v", url, err)
        return
    }
    secs := time.Since(start).Seconds()
    ch <- fmt.Sprintf("%.2fs  %7d  %s", secs, nbytes, url)
}
1
2
3
4
5
$ go run test.go http://www.baidu.com http://www.sina.cn

0.14s   305387  http://www.baidu.com
0.60s   286410  http://www.sina.cn
0.60s elapsed

goroutine 是一种函数的并发执行方式,而channel是用来在goroutine之间进行参数传递。
main函数本身也运行在一个goroutine中,而go function则表示创建一个新的goroutine,并在这个新的goroutine中执行这个函数。

main函数中用make函数创建了一个传递string类型参数的channel,对每一个命令行参数,我们都用go这个关键字来创建一个goroutine,并且让函数在这个 goroutine 异步执行http.Get方法。
这个程序里的io.Copy会把响应的Body内容拷贝到 ioutil.Discard 输出流中(可以把这个变量看作一个垃圾桶,可以向里面写一些不需要的数据),因为我们需要这个方法返回的字节数,但是又不想要其内容。
每当请求返回内容时,fetch函数都会往ch这个channel里写入一个字符串,由main函数里的第二个for循环来处理并打印channel里的这个字符串。

当一个goroutine尝试在一个channel上做send或者receive操作时,这个goroutine会阻塞在调用处,直到另一个goroutine往这个channel里写入、或者接收值,这样两个goroutine才会继续执行channel操作之后的逻辑。
在这个例子中,每一个fetch函数在执行时都会往channel里发送一个值(ch <- expression),主函数负责接收这些值(<-ch)。
这个程序中我们用main函数来接收所有fetch函数传回的字符串,可以避免在goroutine异步执行还没有完成时main函数提前退出。

web 服务

Go语言的内置库使得写一个类似fetch的web服务器变得异常地简单。
我们会展示一个微型服务器,这个服务器的功能是返回当前用户正在访问的URL。
比如用户访问的是 http://localhost:8000/hello ,那么响应是 URL.Path = "hello"

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
// Server1 is a minimal "echo" server.
package main

import (
    "fmt"
    "log"
    "net/http"
)

func main() {
    http.HandleFunc("/", handler) // each request calls handler
    log.Fatal(http.ListenAndServe("localhost:8000", nil))
}

// handler echoes the Path component of the request URL r.
func handler(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "URL.Path = %q\n", r.URL.Path)
}

我们只用了八九行代码就实现了一个Web服务程序,这都是多亏了标准库里的方法已经帮我们完成了大量工作。
main函数将所有发送到 / 路径下的请求和handler函数关联起来,/ 开头的请求其实就是所有发送到当前站点上的请求,服务监听8000端口。
发送到这个服务的“请求”是一个http.Request类型的对象,这个对象中包含了请求中的一系列相关字段,其中就包括我们需要的URL。
当请求到达服务器时,这个请求会被传给handler函数来处理,这个函数会将 /hello 这个路径从请求的URL中解析出来,然后把其发送到响应中,这里我们用的是标准输出流的 fmt.Fprintf

让我们在后台运行这个服务程序。如果你的操作系统是 Mac OS X 或者 Linux,那么在运行命令的末尾加上一个 & 符号,即可让程序简单地跑在后台,windows下可以在另外一个命令行窗口去运行这个程序。

1
$ go run src/gopl.io/ch1/server1/main.go &

在这个服务的基础上叠加特性是很容易的。一种比较实用的修改是为访问的url添加某种状态。
比如,下面这个版本输出了同样的内容,但是会对请求的次数进行计算;
对URL的请求结果会包含各种URL被访问的总次数,直接对 /count 这个URL的访问要除外。

 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
// Server2 is a minimal "echo" and counter server.
package main

import (
    "fmt"
    "log"
    "net/http"
    "sync"
)

var mu sync.Mutex
var count int

func main() {
    http.HandleFunc("/", handler)
    http.HandleFunc("/count", counter)
    log.Fatal(http.ListenAndServe("localhost:8000", nil))
}

// handler echoes the Path component of the requested URL.
func handler(w http.ResponseWriter, r *http.Request) {
    mu.Lock()
    count++
    mu.Unlock()
    fmt.Fprintf(w, "URL.Path = %q\n", r.URL.Path)
}

// counter echoes the number of calls so far.
func counter(w http.ResponseWriter, r *http.Request) {
    mu.Lock()
    fmt.Fprintf(w, "Count %d\n", count)
    mu.Unlock()
}

这个服务器有两个请求处理函数,根据请求的url不同会调用不同的函数:对 /count 这个url的请求会调用到count这个函数,其它的url都会调用默认的处理函数。
如果你的请求pattern是以 / 结尾,那么所有以该url为前缀的url都会被这条规则匹配。
在这些代码的背后,服务器每一次接收请求处理时都会另起一个goroutine,这样服务器就可以同一时间处理多个请求。
然而在并发情况下,假如真的有两个请求同一时刻去更新count,那么这个值可能并不会被正确地增加;这个程序可能会引发一个严重的bug:竞态条件。

为了避免这个问题,我们必须保证每次修改变量的最多只能有一个goroutine,这也就是代码里的 mu.Lock()mu.Unlock() 调用将修改count的所有行为包在中间的目的。

下面是一个更为丰富的例子,handler函数会把请求的http头和请求的form数据都打印出来,这样可以使检查和调试这个服务更为方便:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// handler echoes the HTTP request.
func handler(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "%s %s %s\n", r.Method, r.URL, r.Proto)
    for k, v := range r.Header {
        fmt.Fprintf(w, "Header[%q] = %q\n", k, v)
    }
    fmt.Fprintf(w, "Host = %q\n", r.Host)
    fmt.Fprintf(w, "RemoteAddr = %q\n", r.RemoteAddr)
    if err := r.ParseForm(); err != nil {
        log.Print(err)
    }
    for k, v := range r.Form {
        fmt.Fprintf(w, "Form[%q] = %q\n", k, v)
    }
}

我们用 http.Request 这个struct里的字段来输出下面这样的内容:

1
2
3
4
5
6
GET /?q=query HTTP/1.1
Header["Accept-Encoding"] = ["gzip, deflate, sdch"] Header["Accept-Language"] = ["en-US,en;q=0.8"]
Header["Connection"] = ["keep-alive"]
Header["Accept"] = ["text/html,application/xhtml+xml,application/xml;..."] Header["User-Agent"] = ["Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_5)..."] Host = "localhost:8000"
RemoteAddr = "127.0.0.1:59911"
Form["q"] = ["query"]

可以看到这里的ParseForm被嵌套在了if语句中。
Go语言允许这样的一个简单的语句结果作为循环的变量声明出现在if语句的最前面,这一点对错误处理很有用处。
我们还可以像下面这样写(当然看起来就长了一些):

1
2
3
4
err := r.ParseForm()
if err != nil {
    log.Print(err)
}

用if和ParseForm结合可以让代码更加简单,并且可以限制err这个变量的作用域,这么做是很不错的。

在这些程序中,我们看到了很多不同的类型被输出到标准输出流中。比如前面的fetch程序,把HTTP的响应数据拷贝到了os.Stdout,lissajous程序里我们输出的是一个文件。
fetchall程序则完全忽略到了HTTP的响应Body,只是计算了一下响应Body的大小,这个程序中把响应Body拷贝到了ioutil.Discard。
这里的web服务器程序中则是用fmt.Fprintf直接写到了http.ResponseWriter中。

尽管三种具体的实现流程并不太一样,他们都实现一个共同的接口,即当它们被调用需要一个标准流输出时都可以满足。这个接口叫作io.Writer

为了在这里简单说明接口能做什么,让我们简单地将这里的web服务器和之前写的lissajous函数结合起来,这样GIF动画可以被写到HTTP的客户端,而不是之前的标准输出流。
只要在web服务器的代码里加入下面这几行。

1
2
3
4
handler := func(w http.ResponseWriter, r *http.Request) {
    lissajous(w)
}
http.HandleFunc("/", handler)

或者另一种等价形式:

1
2
3
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
    lissajous(w)
})

HandleFunc函数的第二个参数是一个函数的字面值,也就是一个在使用时定义的匿名函数。

总结

控制流:

我们只介绍了if控制和for,但是没有提到switch多路选择。这里是一个简单的switch的例子:

1
2
3
4
5
6
7
8
switch coinflip() {
case "heads":
    heads++
case "tails":
    tails++
default:
    fmt.Println("landed on edge!")
}

在翻转硬币的时候,例子里的coinflip函数返回几种不同的结果,每一个case都会对应一个返回结果,
这里需要注意,Go语言并不需要显式地在每一个case后写break,语言默认执行完case后的逻辑语句会自动退出。
当然了,如果你想要相邻的几个case都执行同一逻辑的话,需要自己显式地写上一个fallthrough语句来覆盖这种默认行为。
不过fallthrough语句在一般的程序中很少用到。

Go语言里的switch还可以不带操作对象(switch不带操作对象时默认用true值代替,然后将每个case的表达式和true值进行比较);
可以直接罗列多种条件,像其它语言里面的多个if else一样,下面是一个例子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
func Signum(x int) int {
    switch {
    case x > 0:
        return +1
    default:
        return 0
    case x < 0:
        return -1
    }
}

这种形式叫做无tag switch(tagless switch);这和switch true是等价的。

像for和if控制语句一样,switch也可以紧跟一个简短的变量声明,一个自增表达式、赋值语句,或者一个函数调用(比其它语言丰富)。

break和continue语句会改变控制流。和其它语言中的break和continue一样,break会中断当前的循环,并开始执行循环之后的内容,而continue会中跳过当前循环,并开始执行下一次循环。
这两个语句除了可以控制for循环,还可以用来控制switch和select语句,continue会跳过内层的循环,如果我们想跳过的是更外层的循环的话,我们可以在相应的位置加上label,这样break和continue就可以根据我们的想法来continue和break任意循环。
这看起来甚至有点像goto语句的作用了。当然,一般程序员也不会用到这种操作。这两种行为更多地被用到机器生成的代码中。

命名类型:
类型声明使得我们可以很方便地给一个特殊类型一个名字。因为struct类型声明通常非常地长,所以我们总要给这种struct取一个名字。
比如这样一个例子,二维点类型:

1
2
3
4
type Point struct {
    X, Y int
}
var p Point

指针:
Go语言提供了指针。指针是一种直接存储了变量的内存地址的数据类型。在其它语言中,比如C语言,指针操作是完全不受约束的。
在另外一些语言中,指针一般被处理为“引用”,除了到处传递这些指针之外,并不能对这些指针做太多事情。
Go语言在这两种范围中取了一种平衡。指针是可见的内存地址,&操作符可以返回一个变量的内存地址,并且 * 操作符可以获取指针指向的变量内容,但是在Go语言里没有指针运算,也就是不能像c语言里可以对指针进行加或减操作。

方法和接口:

方法是和命名类型关联的一类函数。Go语言里比较特殊的是方法可以被关联到任意一种命名类型。
接口是一种抽象类型,这种类型可以让我们以同样的方式来处理不同的固有类型,不用关心它们的具体实现,而只需要关注它们提供的方法。

包(packages):

Go语言提供了一些很好用的package,并且这些package是可以扩展的。Go语言社区已经创造并且分享了很多很多。
所以Go语言编程大多数情况下就是用已有的package来写我们自己的代码。

在你开始写一个新程序之前,最好先去检查一下是不是已经有了现成的库可以帮助你更高效地完成这件事情。
你可以在 https://golang.org/pkghttps://godoc.org 中找到标准库和社区写的package。
godoc这个工具可以让你直接在本地命令行阅读标准库的文档。比如下面这个例子。

1
2
3
4
5
6
$ go doc http.ListenAndServe
package http // import "net/http"
func ListenAndServe(addr string, handler Handler) error
    ListenAndServe listens on the TCP network address addr and then
    calls Serve with handler to handle requests on incoming connections.
...

注释:
我们之前已经提到过了在源文件的开头写的注释是这个源文件的文档。在每一个函数之前写一个说明函数行为的注释也是一个好习惯。
这些惯例很重要,因为这些内容会被像godoc这样的工具检测到,并且在执行命令时显示这些注释。

多行注释可以用 /* ... */ 来包裹,和其它大多数语言一样。
在文件一开头的注释一般都是这种形式,或者一大段的解释性的注释文字也会被这符号包住,来避免每一行都需要加 //
在注释中 ///* 是没什么意义的,所以不要在注释中再嵌入注释。