HelloWorld
main
包比较特殊。它定义了一个独立可执行的程序,而不是一个库。在main
里的main
函数 也很特殊,它是整个程序执行时的入口(译注:C系语言差不多都这样)。main
函数所做的事情就是程序做的。当然了,main
函数一般调用其它包里的函数完成很多工作(如:fmt.Println
)。
必须告诉编译器源文件需要哪些包,这就是跟随在package
声明后面的import
声明扮演的角色。hello world例子只用到了一个包,大多数程序需要导入多个包。
必须恰当导入需要的包,缺少了必要的包或者导入了不需要的包,程序都无法编译通过。这项严格要求避免了程序开发过程中引入未使用的包(译注:Go语言编译过程没有警告信息,争议特性之一)。
Go语言不需要在语句或者声明的末尾添加分号,除非一行上有多条语句。实际上,编译器会主动把特定符号后的换行符转换为分号,因此换行符添加的位置会影响Go代码的正确解析
命令行参数
切片s当作数组元素序列,序列的长度动态变化,用s[i]
访问单个元素,用s[m:n]
获取子序列。
循环索引变量i在for循环的第一部分中定义。符号:=
是短变量声明(short variable declaration)的一部分,这是定义一个或多个变量并根据它们的初始值为这些变量赋予适当类型的语句。
Go语言只有for循环这一种循环语句。for循环有多种形式,其中一种如下所示:
for initialization; condition; post {
// zero or more statements
}
每次循环迭代,range
产生一对值;索引以及在该索引处的元素值。这个例子不需要索引,但range
的语法要求,要处理元素,必须处理索引。一种思路是把索引赋值给一个临时变量(如temp
)然后忽略它的值,但Go语言不允许使用无用的局部变量(local variables),因为这会导致编译错误。
Go语言中这种情况的解决方法是用空标识符
(blank identifier),即_
(也就是下划线)。空标识符可用于在任何语法需要变量名但程序逻辑不需要的时候(如:在循环里)丢弃不需要的循环索引,并保留元素值。
查找重复行
正如for
循环一样,if
语句条件两边也不加括号,但是主体部分需要加。
map存储了键/值(key/value)的集合,对集合元素,提供常数时间的存、取或测试操作。键可以是任意类型,只要其值能用==
运算符比较,最常见的例子是字符串;值则可以是任意类型。这个例子中的键是字符串,值是整数。内置函数make
创建空map
。
使用了基于range
的循环,并在counts
这个map
上迭代。
程序使用短变量声明创建bufio.Scanner
类型的变量input
。该变量从程序的标准输入中读取内容。每次调用input.Scan()
,即读入下一行,并移除行末的换行符;读取的内容可以调用input.Text()
得到。Scan
函数在读到一行时返回true
,不再有输入时返回false
。
os.Open
函数返回两个值。第一个值是被打开的文件(*os.File
),其后被Scanner
读取。
os.Open
返回的第二个值是内置error
类型的值。如果err
等于内置值nil
(译注:相当于其它语言里的NULL),那么文件被成功打开。读取文件,直到文件结束,然后调用Close
关闭该文件,并释放占用的所有资源。相反的话,如果err
的值不是nil
,说明打开文件时出错了。这种情况下,错误值描述了所遇到的问题。
map
是一个由make
函数创建的数据结构的引用。map
作为参数传递给某函数时,该函数接收这个引用的一份拷贝(copy,或译为副本),被调用函数对map
底层数据结构的任何修改,调用者函数都可以通过持有的map
引用看到。
strings.Split
函数把字符串分割成子串的切片。(Split
的作用与前文提到的strings.Join
相反。)
ReadFile
函数返回一个字节切片(byte slice),必须把它转换为string
,才能用strings.Split
分割。
GIF动画
当我们import了一个包路径包含有多个单词的package时,比如image/color(image和color两个单词),通常我们只需要用最后那个单词表示这个包就可以。所以当我们写color.White时,这个变量指向的是image/color包里的变量,同理gif.GIF是属于image/gif包里的变量。
[]color.Color{…}和gif.GIF{…}这两个表达式就是我们说的复合声明。这是实例化Go语言里的复合类型的一种写法。这里的前者生成的是一个slice切片,后者生成的是一个struct结构体。
gif.GIF是一个struct类型。struct是一组值或者叫字段的集合,不同的类型集合在一个struct可以让我们以一个统一的单元进行处理。anim是一个gif.GIF类型的struct变量。这种写法会生成一个struct变量,并且其内部变量LoopCount字段会被设置为nframes;而其它的字段会被设置为各自类型默认的零值。struct内部的变量可以以一个点(.)来进行访问,就像在最后两个赋值语句中显式地更新了anim这个struct的Delay和Image字段。
用到了append内置函数,将结果append到anim中的帧列表末尾
获取URL
这个程序从两个package中导入了函数,net/http和io/ioutil包,http.Get函数是创建HTTP请求的函数,如果获取过程没有出错,那么会在resp这个结构体中得到访问的请求结果。resp的Body字段包括一个可读的服务器响应流。ioutil.ReadAll函数从response中读取到全部内容;将其结果保存在变量b中。resp.Body.Close关闭resp的Body流,防止资源泄露,Printf函数会将结果b写出到标准输出流中。
并发获取多个URL
goroutine是一种函数的并发执行方式,而channel是用来在goroutine之间进行参数传递。main函数本身也运行在一个goroutine中,而go function则表示创建一个新的goroutine,并在这个新的goroutine中执行这个函数。
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服务
这个服务器有两个请求处理函数,根据请求的url不同会调用不同的函数:对/count这个url的请求会调用到counter这个函数,其它的url都会调用默认的处理函数。如果你的请求pattern是以/结尾,那么所有以该url为前缀的url都会被这条规则匹配。在这些代码的背后,服务器每一次接收请求处理时都会另起一个goroutine,这样服务器就可以同一时间处理多个请求。然而在并发情况下,假如真的有两个请求同一时刻去更新count,那么这个值可能并不会被正确地增加;这个程序可能会引发一个严重的bug:竞态条件。为了避免这个问题,我们必须保证每次修改变量的最多只能有一个goroutine,这也就是代码里的mu.Lock()和mu.Unlock()调用将修改count的所有行为包在中间的目的。
Go语言允许这样的一个简单的语句结果作为局部的变量声明出现在if语句的最前面,这一点对错误处理很有用处。
尽管三种具体的实现流程并不太一样,他们都实现一个共同的接口,即当它们被调用需要一个标准流输出时都可以满足。这个接口叫作io.Writer。
本章要点
控制流: 在本章我们只介绍了if控制和for,但是没有提到switch多路选择。
Go语言并不需要显式地在每一个case后写break,语言默认执行完case后的逻辑语句会自动退出。
如果你想要相邻的几个case都执行同一逻辑的话,需要自己显式地写上一个fallthrough语句来覆盖这种默认行为。不过fallthrough语句在一般的程序中很少用到。
Go语言里的switch还可以不带操作对象(译注:switch不带操作对象时默认用true值代替,然后将每个case的表达式和true值进行比较);可以直接罗列多种条件,像其它语言里面的多个if else一样。
像for和if控制语句一样,switch也可以紧跟一个简短的变量声明,一个自增表达式、赋值语句,或者一个函数调用。
continue会跳过内层的循环,如果我们想跳过的是更外层的循环的话,我们可以在相应的位置加上label,这样break和continue就可以根据我们的想法来continue和break任意循环。这看起来甚至有点像goto语句的作用了。当然,一般程序员也不会用到这种操作。这两种行为更多地被用到机器生成的代码中。
指针: Go语言提供了指针。指针是一种直接存储了变量的内存地址的数据类型。在其它语言中,比如C语言,指针操作是完全不受约束的。在另外一些语言中,指针一般被处理为“引用”,除了到处传递这些指针之外,并不能对这些指针做太多事情。Go语言在这两种范围中取了一种平衡。指针是可见的内存地址,&操作符可以返回一个变量的内存地址,并且*操作符可以获取指针指向的变量内容,但是在Go语言里没有指针运算,也就是不能像c语言里可以对指针进行加或减操作。