赞
踩
本章是对于Go语言基本组件的一些说明。本书所有例子都是针对现实世界的任务的。本章将带您尝试体验用Go语言来编写各种程序:从简单的文件、图片处理到并发的客户端和服务器的互联网应用开发。虽然在一章里不能把所有东西讲清楚,但是以这类应用作为学习一门语言的开始是一种高效地方式。
学习新语言比较自然的方式,是使用新语言写一些你已经可以用其他语言实现的程序。
1.1 hello, world
我们依然以永恒的“hello, world”例子开始,它出现在1978年出版的《The C Programming Language》这本书的开头。C对Go的影响非常直接,我们用“hello, world”来说明一些主要的思路:
// gopl.io/ch1/helloworld
package main
import "fmt"
func main() {
fmt.Println("Hello, 世界")
}
Go是编译型语言。Go的工具链将程序的源文件转变成机器相关的原生二进制指令。这些工具可以通过单一的go命令配合其子命令进行使用。最简单的子命令是run,它将一个或多个以.go为后缀的源文件进行编译、链接,然后运行生成的可执行文件(本书中我们使用$符号作为命令提示符):
$ go run helloworld.go
不出意料地,这将输出:
Go原生地支持Unicode,所以它可以处理所有国家的语言。
如果这个程序不是一次性的实验,那么编译输出成一个可复用的程序比较好。这通过go build来实现:
$ go build helloworld.go
这条命令生成了一个叫作helloworld的二进制程序,它可以不用进行任何其他处理,随时执行:
我们给每个重要的例子都加了一个标签,提示你可以从本书在gopl.io的源码库获取代码:
如果执行go get gopl.io/ch1/helloworld,它会把源代码取到相应的目录。这将在2.6和10.7进行更多的讨论。
现在我们来说说该程序本身。Go代码是使用包来组织的,包类似于其他语言中的库和模块。一个包由一个或多个.go源文件组成,放在一个文件夹中,该文件夹的名字描述了包的作用。每一个源文件的开始都用package声明,上例是package main,指明了这个文件属于哪个包。后面跟着它导入的其他包的列表,然后是存储在文件中的程序声明。
Go的标准库中有100多个包用来完成输入、输出、排序、文本处理等常规任务。例如,fmt包中的函数用来格式化输出和扫描输入。Println是fmt中一个基本的输出函数,它输出一个或多个用空格分隔的值,结尾使用一个换行符,这样看起来这些值是单行输出。
名为main的包比较特殊,它用来定义一个独立的可执行程序,而不是库。在main包中,函数main也是特殊的,不管在什么程序中,main做什么事情,它总是程序开始执行的地方。当然,main通常调用其他包中的函数来做更多的事情,比如fmt.Println。
我们需要告诉编译器原文件需要哪些包,用package声明后面的import来导入这些包。“hello, world”程序仅使用了一个来自于其他包的函数,而大多数程序可能导入更多的包。
你必须精确地导入需要的包。在缺失导入或存在不需要的包的情况下,编译会失败,这种严格的要求可以防止程序演化中引用不需要的包。
import声明必须跟在package声明之后。import导入声明后面,是组成程序的函数、变量、常量、类型(以func、var、const、type开头)声明。大部分情况下,声明的顺序是没有关系的。示例中的程序足够短,因为它只声明了一个函数,这个函数又仅仅调用了一个其他的函数。为了节省空间,在处理示例的时候,我们有时不展示package和import声明,但是它们存在于源文件中,并且编译时必不可少。
一个函数的声明由func关键字、函数名、参数列表(main函数为空)、返回值列表(可以为空)、放在大括号内的函数体组成,函数体定义函数是用来做什么的,这将在第5章详细介绍。
Go不需要在语句或声明后面使用分号结尾,除非有多个语句或声明出现在同一行。事实上,跟在特定符号后面的换行符被转换为分号,在什么地方进行换行会影响对Go代码的解析。例如,“{”符号必须和关键字func在同一行,不能独自成行,并且在x+y这个表达式中,换行符可以在+操作符的后面,但是不能在+操作符的前面。
Go对于代码的格式化要求非常严格。gofmt工具将代码以标准格式重写,go工具的fmt子命令使用gofmt工具来格式化指定包里的所有文件或者当前文件夹中的文件(默认情况下)。本书中包含的所有Go源代码文件都使用gofmt运行过,你应该养成对自己的代码使用gofmt工具的习惯。定制一个标准的格式,可以省去大量无关紧要的辩论,更重要的是,如果允许随心所欲的格式,各种自动化的源代码转换工具将不可用。
许多文本编辑器可以配置为每次在保存文件时自动运行gofmt,因此源文件总可以保持正确的形式。此外,一个相关的工具goimports可以按需管理导入声明的插入和移除。它不是标准发布版的一部分,可通过执行下面的命令获取到:
$ go get golang.org/x/tools/cmd/goimports
对大多数用户来说,按照常规方式下载、编译包,执行自带的测试,查看文档等操作,使用go命令都可以实现,这将在10.7详细介绍。
1.2 命令行参数
大部分程序处理输入然后产生输出,这就是关于计算的大致定义。但是程序怎样获取数据的输入呢?一些程序自己生成数据,更多的时候,输入来自一个外部源:文件、网络连接、其他程序的输出、键盘、命令行参数等。随后的一些示例将从命令行参数开始讨论这些输入。
os包提供一些函数和变量,以与平台无关的方式和操作系统打交道。命令行参数以os包中Args名字的变量供程序访问,在os包外面,使用os.Args这个名字。
变量os.Args是一个字符串slice。slice是Go中的基础概念,很快我们将讨论到它。现在只需理解它是一个动态容量的顺序数组s,可以通过s[i]来访问单个元素,通过s[m:n]来访问一段连续子区间,数组长度用len(s)表示。与大部分编程语言一样,在Go中,所有的索引使用半开区间,即包含第一个索引,不包含最后一个索引,因为这样逻辑比较简单。例如,slice s[m:n],其中,0<=m<=n<=len(s),包含n-m个元素。
os.Args的第一个元素是os.Args[0],它是命令本身的名字;另外的元素是程序开始执行时的参数。表达式s[m:n]表示一个从第m个到第n-1个元素的slice,所以下一个示例中slice需要的元素是os.Args[1:len(os.Args)]。如果m或n缺失,默认分别是0或len(s),所以我们可以将期望的slice简写为os.Args[1:]。
这里有一个UNIX echo命令的实现,它将命令行参数输出到一行。该实现导入两个包,使用由圆括号括起来的列表,而不是独立的import声明。两者都是合法的,但为了方便起见,我们使用列表的方式。导入的顺序是没有关系的,gofmt工具会将其按照字母顺序表进行排序(当一个示例有几个版本时,通常给它们编号以区分出当前讨论的版本)。
// gopl.io/ch1/echo1 // echo1输出其命令行参数 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包,注释是一个或多个完整的句子,用来对这个程序进行整体概括。
var关键字声明了两个string类型的变量s和sep。变量可以在声明的时候初始化。如果变量没有明确地初始化,它将隐式地初始化为这个类型的空值。例如,对于数字初始化结果是0,对于字符串是空字符串""。在这个示例中,s和sep隐式初始化为空字符串。第2章将讨论变量和声明。
对于数字,Go提供常规的算术和逻辑操作符。当应用于字符串时,+操作符对字符串的值进行追加操作,所以表达式:
sep + os.Args[i]
表示将sep和os.Args[i]追加到一起。程序中使用的语句:
s += sep + os.Args[i]
是一个赋值语句,将sep和os.Args[i]追加到旧的s上面,并重新赋给s,它等价于下面的语句:
s = s + sep + os.Args[i]
操作符+=是一个赋值操作符。每一个算术和逻辑操作符(例如+或者*)都有一个对应的赋值操作符。
echo程序会循环每次输出,但是这个版本中我们通过反复追加来构建一个字符串。字符串s一开始为空字符串"",每一次循环追加一些文本。在第一次迭代后,一个空格被插入,这样当循环结束时,每个参数之间都有一个空格。这是一个二次过程,如果参数数量很大成本会比较高,不过对于echo程序还好。本章和下一章会展示几个改进版本,它们会逐步处理掉低效的地方。
循环的索引变量i在for循环开始处声明。:=符号用于短变量声明,这种语句声明一个或多个变量,并且根据初始化的值给予合适的类型,下一章会详细讨论它。
递增语句i++对i进行加1,它等价于i+=1,又等价于i=i+1。对应的递减语句i–对i进行减1。这些是语句,而不像其他C族语言一样是表达式,所以j=i++是不合法的,并且仅支持后缀,所以–i不合法。
for是Go里面的唯一循环语句。它有几种形式,这里展示其中一种:
for initialization; condition; post {
// 零个或多个语句
}
for循环的三个组成部分两边不用小括号。大括号是必需的,但左大括号必须和post(后置)语句在同一行。
可选的initialization(初始化)语句在循环开始之前执行。如果存在,它必须是一个简单的语句,比如一个简短的变量声明,一个递增或赋值语句,或者一个函数调用。condition(条件)是一个布尔表达式,在循环的每一次迭代开始前推演,如果推演的结果是真,循环则继续执行。post语句在循环体之后被执行,然后条件被再次推演。条件变成假之后循环结束。
三部分都是可以省略的。如果没有initialization和post语句,分号可以省略:
// 传统的“while”循环
for condition {
// ...
}
如果条件部分都不存在,例子如下:
// 传统的无限循环
for {
// ...
}
循环是无限的,尽管这种形式的循环可以通过如break或return等语句进行终止。
另一种形式的for循环在字符串或slice数据上迭代。为了说明,这里给出第2版的echo:
// echo2输出其命令行参数 package main import ( "fmt" "os" ) func main() { s, sep := "", "" for _, arg := range os.Args[1:] { s += sep + arg sep = " " } fmt.Println(s) }
每一次迭代,range产生一对值:索引和这个索引处元素的值。这个例子里,我们不需要索引,但是语法上range循环需要处理,因此也必须处理索引。一个主意是我们将索引赋予一个临时变量(如temp)然后忽略它,但是Go不允许存在无用的临时变量,不然会出现编译错误。
解决方案是使用空标识符,它的名字是_
(下划线)。空标识符可以用在任何语法需要变量名但是程序逻辑不需要的地方,例如丢弃每次迭代产生的无用的索引。大多数Go程序员喜欢搭配使用range和_
来写上面的echo程序,因为索引在os.Args上面是隐式的,所以更不容易犯错。
这个版本的程序使用短的变量声明来声明和初始化s和sep,但是我们可以等价地分开声明变量。以下几种声明字符串变量的方式是等价的:
s := ""
var s string
var s = ""
var s string = ""
为什么我们更喜欢某一个?第一种形式的短变量声明更加简洁,但是通常在一个函数内部使用,不适合包级别的变量。第二种形式依赖默认初始化为空字符串的""。第三种形式很少用,除非我们声明多个变量。第四种形式是显式的变量类型,在类型一致的情况下是冗余的信息,在类型不一致的情况下是必需的。实践中,我们应当使用前两种形式,使用显式的初始化来说明初始化变量的重要性(第一种),使用隐式的初始化来表明初始化变量不重要(第二种)。
如上所述,每次循环,字符串s有了新的内容。+=语句通过追加旧的字符串、空格字符串和下一个参数,生成一个新的字符串,然后把新字符串赋值给s。旧的内容不再需要使用,会被例行垃圾回收。
如果有大量的数据需要处理,这样的代价会比较大。一个简单和高效的方式是使用strings包中的Join函数:
package main
import (
"fmt"
"os"
"strings"
)
func main() {
fmt.Println(strings.Join(os.Args[1:], " "))
}
最后,如果我们不关心格式,只是想看值,或许只是调试,那么用Println格式化结果就可以了:
fmt.Println(os.Args[1:])
这个输出语句和我们从strings.Join得到的输出很像,不过两边有括号。任何slice都能够以这样的方式输出。
练习1.1:修改echo程序输出os.Args[0],即命令的名字:
package main
import (
"fmt"
"os"
)
func main() {
fmt.Println(os.Args[0])
}
练习1.2:修改echo程序,输出参数的索引和值,每行一个:
package main
import (
"fmt"
"os"
)
func main() {
for index, arg := range os.Args[1:] {
// fmt.Print不会输出最后的换行
fmt.Print(index, " ")
fmt.Println(arg)
}
}
运行以上代码:
练习1.3:尝试测量可能低效的程序和使用strings.Join的程序在执行时间上的差异(1.6有time包,11.4展示如何撰写系统性的性能评估测试):
// 可能低效的程序 package main import ( "fmt" "os" "time" ) func main() { start := time.Now() var s, sep string for i := 1; i < len(os.Args); i++ { s += sep + os.Args[i] sep = " " } fmt.Println(s) fmt.Printf("%dns elapsed", time.Since(start).Nanoseconds()) }
执行它:
换用strings.Join:
package main
import (
"fmt"
"os"
"strings"
"time"
)
func main() {
start := time.Now()
fmt.Println(strings.Join(os.Args[1:], " "))
fmt.Printf("%dns elapsed", time.Since(start).Nanoseconds())
}
执行它:
应该是参数太少了,看不出差异。
1.3 找出重复行
用于文件复制、打印、检索、排序、统计的程序,通常有一个相似的结构:在输入接口上循环读取,然后对一个元素进行一些计算,在运行时或在最后输出结果。我们展示三个版本的dup程序,它受UNIX的uniq命令启发来找到相邻的重复行。这个程序使用容易适配的结构和包。
第一个版本的dup程序输出标准输入中出现次数大于1的行,前面是次数。这个程序引入if语句、map类型和bufio包。
package main import ( "bufio" "fmt" "os" ) func main() { counts := make(map[string]int) input := bufio.NewScanner(os.Stdin) for input.Scan() { counts[input.Text()]++ } // 注意:忽略input.Err()中可能的错误 for line, n := range counts { if n > 1 { fmt.Printf("%d\t%s\n", n, line) } } }
像for一样,if语句中的条件部分也从不放在圆括号里边,但是程序体中需要用到大括号。这里还可以有一个可选的else部分,当条件为false时执行。
map存储一个键/值对集合,并且提供常量时间的操作来存储、获取或测试集合中的某个元素。键可以是其值能够进行相等(==)比较的任意类型,字符串是最常见的例子;值可以是任意类型。这个例子中,键的类型是字符串,值是int。内置的函数make可以用来新建map,它还可以有其他用途。map将在4.3中进行更多讨论。
每次dup从输入读取一行内容,这一行就作为map中的键,对应的值递增1。语句counts[input.Text()]++等价于下面两个语句:
line := input.Text()
counts[line] = counts[line] + 1
键在map中不存在时也是没有问题的。当一个新的行第一次出现时,右边的表达式counts[line]根据值类型被推演为零值,int的零值是0。
为了输出结果,我们使用基于range的for循环,这次在map类型的counts变量上遍历。像以前一样,每次迭代输出两个结果,map里边一个元素对应的键和值。map里面的键的迭代顺序不是固定的,通常是随机的,每次运行都不一致。这是有意设计的,以防止程序依赖某种特定的序列,此处不对排序做任何保证。
下面讨论bufio包,使用它可以简便和高效地处理输入和输出。其中一个最有用的特性是称为扫描器(Scanner)的类型,它可以读取输入,以行或者单词为单位断开,这是处理以行为单位的输入内容的最简单方式。
程序使用短变量的声明方式,新建一个bufio.Scanner类型input变量:
input := bufio.NewScanner(os.Stdin)
扫描器从程序的标准输入进行读取。每一次调用input.Scan()读取下一行,并且将结尾的换行符去掉;通过调用input.Text()来获取读到的内容。Scan函数在读到新行的时候返回true,在没有更多内容的时候返回false。
像C语言或其他语言中的printf一样,函数fmt.Printf从一个表达式列表生成格式化的输出。它的第一个参数是格式化指示字符串,由它指定其他参数如何格式化。例如:%d讲一个整数格式化为十进制的形式,%s把参数展开为字符串变量的值。
Printf函数有超过10个这样的转义字符,Go程序员称为verb。下表远不完整,但是它说明有很多可以用的功能:
程序dup1中的格式化字符串还包含一个制表符\t和一个换行符\n。字符串字面量可以包含类似转义序列(escape sequence)来表示不可见字符。Printf默认不输出换行符。按照约定,诸如log.Printf和fmt.Errorf之类的格式化函数以f结尾,使用和fmt.Printf相同的格式化规则;而那些以ln结尾的函数(如Println)则使用%v的方式来格式化参数(直接输出,不用%d这些,见上例),并在最后追加换行符。
许多程序既可以像dup一样从标准输入进行读取,也可以从具体的文件读取。下一个版本的dup程序可以从标准输入或一个文件列表进行读取,使用os.Open函数来逐个打开:
// gopl.io/ch1/dup2 // dup2打印输出中多次出现的行的个数和文本 // 它从stdin或指定的文件列表读取 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()]++ } // 注意:忽略input.Err()中可能的错误 }
函数os.Open返回两个值。第一个是打开的文件(*os.File),该文件随后被Scanner读取。
第二个返回值是一个内置的error类型的值。如果err等于特殊的内置nil值,标准文件成功打开。文件在被读到结尾的时候,Close函数关闭文件,然后释放相应的资源(内存等)。另一方面,如果err不是nil,说明出错了。这时,error的值描述错误原因。简单的错误处理是使用Fprintf和%v在标准错误流上输出一条消息,%v可以使用默认格式显示任意类型的值;错误处理后,dup开始处理下一个文件;continue语句让循环进入下一个迭代。
为了保持示例代码简短,这里对错误处理有意进行了一定程度的忽略。很明显,必须检查os.Open返回的错误;但是,我们忽略了使用input.Scan读取文件的过程中出现概率很小的错误。我们将标记所跳过的错误检查,5.4将更详细地讨论错误处理。
值得注意的是,对countLines的调用出现在其声明之前。函数和其他包级别的实体可以以任意次序声明。
map是一个使用make创建的数据结构的引用。当一个map被传递给一个函数时,函数接收到这个引用的副本,所以被调用函数中对于map数据结构中的改变对函数调用者使用的map引用也是可见的。在示例中,countLines函数在counts map中插入的值,在main函数中也是可见的。
这个版本的dup使用“流式”模式读取输入,然后按需拆分为行,这样原理上这些程序可以处理海量的输入。一个可选的方式是一次读取整个输入到大块内存,一次性地分割所有行,然后处理这些行。dup3将以这种方式处理。这里引入一个ReadFile函数(从io/ioutil包),它读取整个命名文件的内容,还引入一个strings.Split函数,它将一个字符串分割为一个由子串组成的slice(Split是前面介绍过的strings.Join的反操作)。
我们在某种程度上简化了dup3:第一,它仅读取指定的文件,而非标准输入,因为ReadFile需要一个文件名作为参数;第二,我们将统计行数的工作放回main函数中,因为它当前仅在一处用到。
// gopl.io/ch1/dup3 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函数返回一个可以转换成字符串的字节slice,这样它就可以被strings.Split分割。3.5.4将详细讨论字符串和字节slice。
实际上,bufio.Scanner、ioutil.ReadFile以及ioutil.WriteFile使用*os.File中的Read和Write方法,但是大多数程序员很少需要直接访问底层的例程。像bufio和io/ioutil包中的上层的方法更易使用。
练习1.4:修改dup2程序,输出出现重复行的文件的名称:
package main import ( "bufio" "fmt" "os" ) func main() { counts := make(map[string]int) files := os.Args[1:] dupFiles := make(map[string]int) for _, arg := range files { f, err := os.Open(arg) if err != nil { fmt.Fprintf(os.Stderr, "dup2: %v\n", err) continue } countLines(f, counts, dupFiles, arg) f.Close() } for line, n := range counts { if n > 1 { fmt.Printf("%d\t%s\n", n, line) } } fmt.Print(dupFiles) } func countLines(f *os.File, counts map[string]int, dupFiles map[string]int, file string) { input := bufio.NewScanner(f) for input.Scan() { counts[input.Text()]++ // 如果当前文件中某行已经出现过,就将其加入dupFiles if counts[input.Text()] > 1 && dupFiles[file] == 0 { dupFiles[file] = 1 } } // 注意:忽略input.Err()中可能的错误 }
1.4 GIF动画
下一个程序展示Go标准的图像包的使用,用来创建一系列的位图图像(使用像素阵列来表示的图像),然后将位图序列编码为GIF动画。下面的图像叫做利萨茹图形,是20世纪60年代科幻片中的纤维状视觉效果。利萨茹图形是参数化的二维谐振曲线,如示波器x轴和y轴馈电(供电)输入的两个正弦波。图1-1是几个示例:
这段代码里有几个新的组成,包括const声明、结构体以及复合字面量。不像大多数例子,本例还引入了浮点运算。这个示例的主要目的是提供一些思路,表明Go语言看起来是怎样的,以及利用Go语言和它的库可以轻易完成哪些事情,这里只简短地讨论这几个主题,更多细节将放在后面章节。
// gopl.io/ch1/lissajous // lissajous产生随机利萨茹图形的GIF动画 package main import ( "image" "image/color" "image/gif" "io" "log" "math" "math/rand" "net/http" "os" "time" ) var palette = []color.Color{color.White, color.Black} const ( whiteIndex = 0 // 画板中的第一种颜色 blackIndex = 1 // 画板中的下一种颜色 ) func main() { rand.Seed(time.Now().UTC().UnixNano()) if len(os.Args) > 1 && os.Args[1] == "web" { handler := func(w http.ResponseWriter, r *http.Request) { lissajous(w) } http.HandleFunc("/", handler) log.Fatal((http.ListenAndServe("localhost:8000", nil))) return } lissajous(os.Stdout) } func lissajous(out io.Writer) { const ( cycles = 5 // 完整的x振荡器变化的个数 res = 0.001 // 角度分辨率 size = 100 // 图像画布包含[-size..+size] nframes = 64 // 动画中的帧数 delay = 8 // 以8ms为单位的帧间延迟 ) freq := rand.Float64() * 3.0 // y振荡器的相对频率 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 // 将delay追加到anim.Delay字段中,该字段是一个slice,Image同理 anim.Delay = append(anim.Delay, delay) anim.Image = append(anim.Image, img) } gif.EncodeAll(out, &anim) // 注意:忽略编码错误 }
在导入那些由多段路径如image/color组成的包之后,使用路径最后的一段来引用这个包。所以变量color.White属于image/color包,gif.GIF属于image/gif包。
const声明(参考3.6节)用来给常量命名,常量是其值在编译期间固定的量,例如周期、帧数和延迟等数值参数。与var声明类似,const声明可以出现在包级别(所以这些常量名字在包生命周期内都是可见的)或在一个函数内(所以名字仅在函数体内可见)。常量必须是数字、字符串或布尔值。
表达式[]color.Color{…}和gif.GIF{…}是复合字面量(参考4.2,4.4.1),即用一系列元素的值初始化Go的复合类型的紧凑表达方式。这里,第一个是slice,第二个是结构体。
gif.GIF是一个结构体类型(4.4)。结构体由一组称为字段的值组成,字段通常有不同的数据类型,它们一起组成单个对象,作为一个单位被对待。anim变量是gif.GIF结构体类型。anim的LoopCount字段值设置为nframes;其他字段的值是对应类型的零值。结构体的每个字段可以通过点记法来访问,在最后两个赋值语句中,显式更新anim结构体的Delay和Image字段。
lissajous函数有两个嵌套的循环。外层有64个迭代,每个迭代产生一个动画帧。它创建一个201×201大小的画板,使用黑和白两种颜色。所有的像素值默认设置为0(画板中的初始颜色),这里设置为白色。每一个内层循环通过设置一些像素为黑色产生一个新的图像。结果使用内置的append参数将其追加到anim的帧列表中,并且指定8ms的延迟。最后帧和延迟的序列被编码成GIF各式,然后写入输出流out。out的类型是io.Writer,它可以帮我们输出到很多地方,稍后即可看到。
内层循环运行两个振荡器。x方向的振荡器是正弦函数,y方向也是正弦化的,但是它的频率相对x的振动周期是0~3之间的一个随机数,它的相位相对于x的初始值为0,然后随着每个动画帧增加。该循环在x振荡器完成5个完整周期后停止。每一步它都调用SetColorIndex将对应画板上面的(x, y)涂成黑色,在画板上的值为1。
main函数调用lissajous函数,直接写到标准输出,所以这个命令产生一个像图1-1那样的GIF动画:
$ go build gopl.io/ch1/lissajous
$ ./lissajous >out.gif
以上命令不要在powershell里重定向,因为powershell输出的是文本而非二进制结构,会有差异,在cmd下执行即可。执行结果动图截图:
也可以用web参数来创建一个web服务器,然后通过网页访问localhost:8000。
练习1.5:改变利萨茹程序的画板颜色为绿色黑底来增加真实性。使用color.RGBA{0xRR, 0xGG, 0xBB, 0xff}创建一种Web颜色#RRGGBB,每一对十六进制数字表示组成一个像素红、绿、蓝分量的亮度:
package main import ( "image" "image/color" "image/gif" "io" "log" "math" "math/rand" "net/http" "os" "time" ) // RGBA中的第4个参数是不透明度,0xff是完全不透明 var palette = []color.Color{color.Black, color.RGBA{0x00, 0xff, 0x00, 0xff}} const ( whiteIndex = 0 // 画板中的第一种颜色 blackIndex = 1 // 画板中的下一种颜色 ) func main() { rand.Seed(time.Now().UTC().UnixNano()) if len(os.Args) > 1 && os.Args[1] == "web" { handler := func(w http.ResponseWriter, r *http.Request) { lissajous(w) } http.HandleFunc("/", handler) log.Fatal((http.ListenAndServe("localhost:8000", nil))) return } lissajous(os.Stdout) } func lissajous(out io.Writer) { const ( cycles = 5 // 完整的x振荡器变化的个数 res = 0.001 // 角度分辨率 size = 100 // 图像画布包含[-size..+size] nframes = 64 // 动画中的帧数 delay = 8 // 以10ms为单位的帧间延迟 ) freq := rand.Float64() * 3.0 // y振荡器的相对频率 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) // 使用palette中的第一个颜色作为初始所有像素的颜色 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) // 注意:忽略编码错误 }
运行它:
练习1.6:通过在画板中添加更多颜色,然后通过有趣的方式改变SetColorIndex的第三个参数,修改利萨茹程序来产生多种色彩的图片:
package main import ( "image" "image/color" "image/gif" "io" "log" "math" "math/rand" "net/http" "os" "time" ) var palette = []color.Color{color.Black, color.RGBA{0x00, 0xff, 0x00, 0xff}, color.RGBA{0xff, 0x00, 0x00, 0xff}, color.RGBA{0x00, 0x00, 0xff, 0xff}} const ( whiteIndex = 0 // 画板中的第一种颜色 blackIndex = 1 // 画板中的下一种颜色 ) func main() { rand.Seed(time.Now().UTC().UnixNano()) if len(os.Args) > 1 && os.Args[1] == "web" { handler := func(w http.ResponseWriter, r *http.Request) { lissajous(w) } http.HandleFunc("/", handler) log.Fatal((http.ListenAndServe("localhost:8000", nil))) return } lissajous(os.Stdout) } func lissajous(out io.Writer) { const ( cycles = 5 // 完整的x振荡器变化的个数 res = 0.001 // 角度分辨率 size = 100 // 图像画布包含[-size..+size] nframes = 64 // 动画中的帧数 delay = 8 // 以10ms为单位的帧间延迟 ) freq := rand.Float64() * 3.0 // y振荡器的相对频率 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), uint8(i%(len(palette)-1)+1)) } phase += 0.1 anim.Delay = append(anim.Delay, delay) anim.Image = append(anim.Image, img) } gif.EncodeAll(out, &anim) // 注意:忽略编码错误 }
以上代码每一帧的图形颜色都不同。
1.5 获取一个URL
对许多应用而言,访问互联网上的信息和访问本地文件系统一样重要。Go提供了一系列包,在net包下面分组管理,使用它们可以方便地通过互联网发送和接收信息,使用底层的网络连接,创建服务器,此时Go的并发特性(第8章)特别有用。
程序fetch展示从互联网获取信息的最小需求,它获取每个指定URL的内容,然后不加解析地输出。fetch来自curl这个非常重要的工具。显然可以使用这些数据做更多的事情,但这里只讲基本的思路,本书将会频繁用到这个程序:
// gopl.io/ch1/fetch // fetch输出从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) } }
这个程序使用的函数来自两个包:net/http和io/ioutil。http.Get函数产生一个HTTP请求,如果没有出错,返回结果存在响应结构resp里面。其中resp的Body域包含服务器端响应的一个可读取数据流。随后ioutil.ReadAll读取整个响应结果并存入b。关闭Body数据流来避免资源泄露,使用Printf将响应输出到标准输出。
如果HTTP请求失败,fetch报告失败:
无论哪种错误情况,os.Exit(1)会在进程退出时返回状态码1。
练习1.7:函数io.Copy(dst, src)从src读,并且写入dst。使用它代替ioutil.ReadAll来复制响应内容到os.Stdout,这样不需要装下整个响应数据流的缓冲区。确保检查io.Copy返回的错误结果:
package main import ( "fmt" "io" "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) } copyNum, err := io.Copy(os.Stdout, resp.Body) fmt.Println("copy num is ", copyNum) if err != nil { fmt.Fprintf(os.Stderr, "fetch: reading %s: %v\n", url, err) os.Exit(1) } resp.Body.Close() } }
练习1.8:修改fetch程序添加一个http://前缀(假如该URL参数缺失协议前缀)。可能会用到strings.HasPrefix:
package main import ( "fmt" "io" "net/http" "os" "strings" ) func main() { for _, url := range os.Args[1:] { if !strings.HasPrefix(url, "http://") { url = "http://" + url } resp, err := http.Get(url) if err != nil { fmt.Fprintf(os.Stderr, "fetch: %v\n", err) os.Exit(1) } copyNum, err := io.Copy(os.Stdout, resp.Body) fmt.Println("copy num is ", copyNum) if err != nil { fmt.Fprintf(os.Stderr, "fetch: reading %s: %v\n", url, err) os.Exit(1) } resp.Body.Close() } }
练习1.9:修改fetch来输出HTTP的状态码,可以在resp.Status中找到它:
package main import ( "fmt" "io" "net/http" "os" "strings" ) func main() { for _, url := range os.Args[1:] { if !strings.HasPrefix(url, "http://") { url = "http://" + url } resp, err := http.Get(url) if err != nil { fmt.Fprintf(os.Stderr, "fetch: %v\n", err) os.Exit(1) } status := resp.Status fmt.Println("http status is ", status) copyNum, err := io.Copy(os.Stdout, resp.Body) fmt.Println("copy num is ", copyNum) if err != nil { fmt.Fprintf(os.Stderr, "fetch: reading %s: %v\n", url, err) os.Exit(1) } resp.Body.Close() } }
1.6 并发获取多个URL
Go最令人感兴趣和新颖的特点是支持并发编程。这是一个大话题,第8章和第9章将专门讨论,所以此处只是简单了解一下Go主要的并发机制、goroutine和通道(channel)。
下一个程序fetchall和前一个一样获取URL的内容,但是它并发获取很多URL内容,于是这个进程使用的时间不超过耗时最长时间的获取任务,而不是所有获取任务总的时间。这个版本的fetchall丢弃响应的内容,但是报告每一个响应的大小和花费的时间:
// gopl.io/ch1/fetchall // fetchall并发获取URL并报告它们的时间和大小 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) // 启动一个goroutine } for range os.Args[1:] { fmt.Println(<-ch) // 从通道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) // 发送到通道ch return } nbytes, err := io.Copy(ioutil.Discard, resp.Body) resp.Body.Close() // 不要泄露资源 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) }
这有一个例子:
goroutine是一个并发执行的函数。通道是一种允许某一例程向另一个例程传递指定类型的值的通信机制。main函数在一个goroutine中执行,然后go语句创建额外的goroutine。
main函数使用make创建一个字符串通道。对于每个命令行参数,go语句在第一轮循环中启动一个新的goroutine,它异步调用fetch来使用http.Get获取URL内容。io.Copy函数读取响应的内容,然后通过写入ioutil.Discard输出流进行丢弃。Copy返回字节数以及出现的任何错误。每一个结果返回时,fetch发送一行汇总信息到通道ch。main中的第二轮循环接收并且输出那些汇总行。
当一个goroutine试图在一个通道上进行发送或接收操作时,它会阻塞,直到另一个goroutine试图进行接收或发送操作才传递值,并开始处理两个goroutine。本例中,每一个fetch在通道ch上发送一个值(ch <- expression),main函数接收它们(<-ch)。由main来处理所有的输出确保每个goroutine作为一个整体单元处理,这样就避免了两个goroutine同时完成造成输出交织所带来的风险。
练习1.10:找一个产生大量数据的网站。连续两次运行fetchall,看报告的时间是否会有大的变化,调查缓存情况。每一次获取的内容一样吗?修改fetchall将内容输出到文件,这样可以检查它是否一致:
package main import ( "fmt" "io" "net/http" "os" "time" ) func main() { start := time.Now() ch := make(chan string) for _, url := range os.Args[1:] { go fetch(url, ch) // 启动一个goroutine } for range os.Args[1:] { fmt.Println(<-ch) // 从通道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) // 发送到通道ch return } file, err := os.Create("./" + url[len(url)-5:] + ".htm") if err != nil { ch <- fmt.Sprint(err) return } // defer关键字表示延迟执行,直到所在的函数结束前才执行 defer file.Close() nbytes, err := io.Copy(file, resp.Body) resp.Body.Close() // 不要泄露资源 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) }
报告的时间有大的变化,每次获取的内容不同。差距来源于DNS缓存,首先cmd执行ipconfig /flushdns
清理dns缓存,然后运行以上程序两次:
对于获取的网页内容,http.Get是不会缓存的。
练习1.11:使用更长的参数列表来尝试fetchall,例如使用alexa.com排名前100万的网站。如果一个网站没有响应,程序的行为是怎样的(8.9会通过复制这个例子来描述响应的机制)?
如果有一个网站没有响应,程序会多次重连,直到放弃,然后程序结束。
1.7 一个Web服务器
使用Go的库非常容易实现一个Web服务器,用来响应像fetch那样的客户端请求。本节将展示一个迷你服务器,返回访问服务器的URL的路径部分。例如,如果请求的URL是http://localhost:8000/hello,响应将是URL.Path = “/hello”。
// gopl.io/ch1/server1 // server1是一个迷你回声服务器 package main import ( "fmt" "log" "net/http" ) func main() { http.HandleFunc("/", handler) // 回声请求调用处理程序 log.Fatal(http.ListenAndServe("localhost:8000", nil)) } // 处理程序回显请求URL r的路径部分 func handler(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, "URL.Path = %q\n", r.URL.Path) }
这个程序只有寥寥几行代码,因为库函数做了大部分工作。main函数将一个处理函数和以/开头的URL链接在一起,代表所有的URL使用这个函数处理,然后启动服务器监听进入8000端口处的请求。一个请求由一个http.Request类型的结构体表示,它包含很多关联的域,其中一个是所请求的URL。当一个请求到达时,它被转交给处理函数,并从请求的URL中提取路径部分(/hello),使用fmp.FPrintf格式化,然后作为响应发送回去。Web服务器将在7.7节进行详细讨论。
让我们在后台启动服务器。在Mac OS X或者Linux上,在命令行后添加一个&符号;在微软Windows上,不需要&符号,而需要单独开启一个独立的命令行窗口。
$ go run src/gopl.io/ch1/server1/main.go &
可以从命令行发起客户请求:
另外,还可以通过浏览器进行访问,如图1-1所示。
为服务器添加功能很容易。一个有用的扩展是一个特定的URL,它返回某种排序的状态。例如,这个版本的程序完成和回声服务器一样的事情,但同时返回请求的数量;URL /count请求返回到现在为止的个数,去掉/count请求本身:
// gopl.io/ch1/server2 // server2是一个迷你的回声和计数器服务器 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)) } // 处理程序回显请求的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回显目前为止调用的次数 func counter(w http.ResponseWriter, r *http.Request) { mu.Lock() fmt.Fprintf(w, "Count %d\n", count) mu.Unlock() }
这个服务器有两个处理函数,通过请求的URL来决定哪一个被调用:请求/count调用counter,其他的调用handler。在后台,对于每个传入的请求,服务器在不同的goroutine中运行该处理函数,这样它可以同时处理多个请求。然而,如果两个并发的请求试图同时更新计数值count,它可能会不一致地增加,程序会产生一个严重的竞态bug(9.1)。为避免该问题,必须确保最多只有一个goroutine在同一时间访问变量,这正是mu.Lock()和mu.Unlock()语句的作用。第9章将更细致地讨论共享变量的并发访问。
作为一个更完整的例子,处理函数可以报告它接收到的消息头和表单数据,这样可以方便服务器审查和调试请求:
// 处理程序回显HTTP请求
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", k, v)
}
}
这里使用http.Request结构体的成员来产生类似下面的输出:
注意这里是如何在if语句中嵌套调用ParseForm的。Go允许一个简单的语句(如一个局部变量声明)跟在if条件的前面,这在错误处理的时候特别有用。也可以这样写:
err := r.ParseForm()
if err != nil {
log.Print(err)
}
但是合并的语句更短而且可以缩小err变量的作用域,这是一个好的实践。2.7节将介绍作用域。
这些程序中,我们看到了作为输出流的三种非常不同的类型。fetch程序复制HTTP响应到文件os.Stdout,像lissajous一样;fetchall程序通过将响应复制到ioutil.Discard中进行丢弃(在统计其长度时);Web服务器使用fmt.Fprintf通过写入http.ResponseWriter来让浏览器显示。
尽管三种类型细节不同,但都满足一个通用的接口(interface),该接口允许它们按需使用任何一种输出流。该接口(称为io.Writer)将在7.1进行讨论。
Go的接口机制是第7章的内容,但是为了说明它可以做什么,我们来看一下整合Web服务器和lissajous函数是一件多么容易的事情,这样GIF动画将不再输出到标准输出而是HTTP客户端。简单添加这些行到Web服务器:
handler := func(w http.ResponseWriter, r *http.Request) {
lissajous(w)
}
http.HandleFunc("/", handler);
或者也可以:
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
lissajous(w)
})
上面的HandleFunc函数中立即调用的第二个参数是函数字面量,这是一个在该场景中使用它时才定义的匿名函数,这将在5.6节进一步解释。
一旦你完成这个改变,就可以通过浏览器访问http://localhost:8000。每次加载页面,你将看到一个类似图1-3的动画。
练习1.12:修改利萨茹服务器以通过URL参数读取参数值。例如,你可以通过调整它,使得像http://localhost:8000/?cycles=20这样的网址将其周期设置为20,以替代默认的5。使用strconv.Atoi函数来将字符串参数转换为整型。可以通过go doc strconv.Atoi来查看文档:
package main import ( "image" "image/color" "image/gif" "io" "log" "math" "math/rand" "net/http" "strconv" "time" ) var palette = []color.Color{color.Black, color.RGBA{0x00, 0xff, 0x00, 0xff}, color.RGBA{0xff, 0x00, 0x00, 0xff}, color.RGBA{0x00, 0x00, 0xff, 0xff}} const ( whiteIndex = 0 // 画板中的第一种颜色 blackIndex = 1 // 画板中的下一种颜色 ) func main() { rand.Seed(time.Now().UTC().UnixNano()) handler := func(w http.ResponseWriter, r *http.Request) { lissajous(w, r) } http.HandleFunc("/", handler) log.Fatal((http.ListenAndServe("localhost:8000", nil))) } func lissajous(out io.Writer, r *http.Request) { if err := r.ParseForm(); err != nil { log.Print(err) } form := make(map[string]string) for k, v := range r.Form { form[k] = v[0] } cyclesStr, ok := form["cycles"] // 完整的x振荡器变化的个数 cycles := 5 if ok { if c, err := strconv.Atoi(cyclesStr); err == nil { cycles = c } else { log.Print(err) } } const ( res = 0.001 // 角度分辨率 size = 100 // 图像画布包含[-size..+size] nframes = 64 // 动画中的帧数 delay = 8 // 以10ms为单位的帧间延迟 ) freq := rand.Float64() * 3.0 // y振荡器的相对频率 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 < float64(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), uint8(i%(len(palette)-1)+1)) } phase += 0.1 anim.Delay = append(anim.Delay, delay) anim.Image = append(anim.Image, img) } gif.EncodeAll(out, &anim) // 注意:忽略编码错误 }
这样就可通过http://localhost:8000/?cycles=999999指定cycles了。
1.8 其他内容
Go里面的东西远比这个快速入门中介绍的多。这里是一些很少提及或者完全忽略掉的主题,下面简单地介绍一下这些主题,以便读者在用到时能够熟悉这些内容。
1.控制流:我们前面介绍了两个基础的控制语句if和for,但没有介绍switch语句,它是多路分支控制。这里有一个例子:
switch coinflip() {
case "heads":
heads++
case "tails":
tails++
default:
fmt.Println("landed on edge!")
}
coinflip的调用结果会和每一个条件的值进行比较。case语句从上到下进行推演,所以第一个匹配的case语句会被执行。如果没有其他的case语句符合条件,那么可选的默认case语句将被执行。默认case语句可以放在任何地方。case语句不像C语言那样从上到下贯穿执行(尽管有一个很少使用的fallthrough语句可以改写这个行为)。
switch语句不需要操作数,它就像一个case语句列表,每条case语句都是一个布尔表达式:
func Signum(x int) int {
switch {
case x > 0:
return +1
default:
return 0
case x < 0:
return -1
}
}
这种形式称为无标签(tagless)选择,它等价于switch true。
与for和if语句类似,switch可以包含一个可选的简单语句:一个短变量声明,一个递增或赋值语句,或者一个函数调用,用来在判断条件前设置一个值。
break和continue语句可以改变控制流。break可以打断for、switch或select的最内层调用,开始执行下面的语句。正如我们在1.3节中看到的,continue可以让for的内层循环开始新的迭代。语句可以标签化,这样方便break和continue引用它们来跳出多层嵌套的循环,或者执行最外层循环的迭代。这里还有一个goto语句,通常在机器生成的代码中使用,程序员一般不用它。
2.命名类型:type声明给已有类型命名。因为结构体类型通常很长,所以它们基本上都独立命名。一个熟悉的例子是定义一个2D图形系统的Point类型:
type Point struct {
X, Y int
}
var p Point
类型声明和命名将在第2章讲述。
3.指针:Go提供了指针,它的值是变量的地址。在一些语言(比如C)中,指针基本是没有约束的。其他语言中,指针称为“引用”,并且除了到处传递之外,它不能做其他事。Go做了一个折中,指针显式可见。使用&操作符可以获取一个变量的地址,使用*操作符可以获取指针引用的变量的值,但是指针不支持算术运算。这将在2.3.2节进行介绍。
4.方法和接口:一个关联了命名类型的函数称为方法。Go里面的方法可以关联到几乎所有的命名类型。方法在第6章讲述。接口是可以用相同的方式处理不同的具体类型的抽象类型,它基于这些类型所包含的方法,而不是类型的描述或实现。接口是第7章的主题。
5.包:Go自带一个可扩展并且实用的标准库,Go社区创建和共享了更多的库。编程时,更多使用现有的包,而不是自己写所有的源码。本书将指出一些比较重要的标准库包,但是这些包太多了,本书无法一一展示,并且也无法提供诸如包的完整参考手册之类的恭喜。
在着手新程序前,看看是否已经有现成的包。可以在https://golang.org/pkg找到标准库包的索引,社区贡献的包可以在https://godoc.org找到。使用go doc工具可以方便地通过命令行访问这些文档:
6.注释:我们已经在程序或包的开始提到文档注释。在声明任何函数前,写一段注释来说明它的行为是一个好的风格。这个约定很重要,因为它们可以被go doc和godoc工具定位和作为文档显示(10.7.4)。
对于跨越多行的注释,可以使用/* ... */
,这样可以避免在文件的开始有一大块说明文本时每一行都有//。在注释内部,//和/*没有特殊含义,所以注释不能嵌套。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。