# 第一章 入門
本章介紹Go語言的基礎組件。本章提供了足夠的信息和示例程序,希望可以幫你盡快入門, 寫出有用的程序。本章和之后章節的示例程序都針對你可能遇到的現實案例。先了解幾個Go程序,涉及的主題從簡單的文件處理、圖像處理到互聯網客戶端和服務端并發。當然,第一章不會解釋細枝末節,但用這些程序來學習一門新語言還是很有效的。
學習一門新語言時,會有一種自然的傾向, 按照自己熟悉的語言的套路寫新語言程序。學習Go語言的過程中,請警惕這種想法,盡量別這么做。我們會演示怎么寫好Go語言程序,所以請使用本書的代碼作為你自己寫程序時的指南。
### 1.1. Hello, World
我們以現已成為傳統的“hello world”案例來開始吧, 這個例子首次出現于1978年出版的C語言圣經[《The C Programming Language》](http://s3-us-west-2.amazonaws.com/belllabs-microsite-dritchie/cbook/index.html)[1](#)。C語言是直接影響Go語言設計的語言之一。這個例子體現了Go語言一些核心理念。
*gopl.io/ch1/helloworld*
~~~
package main
import "fmt"
func main() {
fmt.Println("Hello, 世界")
}
~~~
Go是一門編譯型語言,Go語言的工具鏈將源代碼及其依賴轉換成計算機的機器指令[2](#)。Go語言提供的工具都通過一個單獨的命令`go`調用,`go`命令有一系列子命令。最簡單的一個子命令就是run。這個命令編譯一個或多個以.go結尾的源文件,鏈接庫文件,并運行最終生成的可執行文件。(本書使用$表示命令行提示符。)
~~~
$ go run helloworld.go
~~~
毫無意外,這個命令會輸出:
~~~
Hello, 世界
~~~
Go語言原生支持Unicode,它可以處理全世界任何語言的文本。
如果不只是一次性實驗,你肯定希望能夠編譯這個程序,保存編譯結果以備將來之用。可以用build子命令:
~~~
$ go build helloworld.go
~~~
這個命令生成一個名為helloworld的可執行的二進制文件[3](#),之后你可以隨時運行它[4](#),不需任何處理[5](#)。
~~~
$ ./helloworld
Hello, 世界
~~~
本書中, 所有的示例代碼上都有一行標記,利用這些標記, 可以從[gopl.io](http://gopl.io)網站上本書源碼倉庫里獲取代碼:
~~~
gopl.io/ch1/helloworld
~~~
執行 `go get gopl.io/ch1/helloworld` 命令,就會從網上獲取代碼,并放到對應目錄中[6](#)。2.6和10.7節有這方面更詳細的介紹。
來討論下程序本身。Go語言的代碼通過**包**(package)組織,包類似于其它語言里的庫(libraries)或者模塊(modules)。一個包由位于單個目錄下的一個或多個.go源代碼文件組成, 目錄定義包的作用。每個源文件都以一條`package`聲明語句開始,這個例子里就是`package main`, 表示該文件屬于哪個包,緊跟著一系列導入(import)的包,之后是存儲在這個文件里的程序語句。
Go的標準庫提供了100多個包,以支持常見功能,如輸入、輸出、排序以及文本處理。比如`fmt`包,就含有格式化輸出、接收輸入的函數。`Println`是其中一個基礎函數,可以打印以空格間隔的一個或多個值,并在最后添加一個換行符,從而輸出一整行。
`main`包比較特殊。它定義了一個獨立可執行的程序,而不是一個庫。在`main`里的`main`*函數* 也很特殊,它是整個程序執行時的入口[7](#)。`main`函數所做的事情就是程序做的。當然了,`main`函數一般調用其它包里的函數完成很多工作, 比如`fmt.Println`。
必須告訴編譯器源文件需要哪些包,這就是`import`聲明以及隨后的`package`聲明扮演的角色。hello world例子只用到了一個包,大多數程序需要導入多個包。
必須恰當導入需要的包,缺少了必要的包或者導入了不需要的包,程序都無法編譯通過。這項嚴格要求避免了程序開發過程中引入未使用的包[8](#)。
`import`聲明必須跟在文件的`package`聲明之后。隨后,則是組成程序的函數、變量、常量、類型的聲明語句(分別由關鍵字`func`, `var`, `const`, `type`定義)。這些內容的聲明順序并不重要[9](#)。這個例子的程序已經盡可能短了,只聲明了一個函數, 其中只調用了一個其他函數。為了節省篇幅,有些時候, 示例程序會省略`package`和`import`聲明,但是,這些聲明在源代碼里有,并且必須得有才能編譯。
一個函數的聲明由`func`關鍵字、函數名、參數列表、返回值列表(這個例子里的`main`函數參數列表和返回值都是空的)以及包含在大括號里的函數體組成。第五章進一步考察函數。
Go語言不需要在語句或者聲明的末尾添加分號,除非一行上有多條語句。實際上,編譯器會主動把特定符號后的換行符轉換為分號, 因此換行符添加的位置會影響Go代碼的正確解析[10](#)。。舉個例子, 函數的左括號`{`必須和`func`函數聲明在同一行上, 且位于末尾,不能獨占一行,而在表達式`x + y`中,可在`+`后換行,不能在`+`前換行。
Go語言在代碼格式上采取了很強硬的態度。`gofmt`工具把代碼格式化為標準格式[11](#),并且`go`工具中的`fmt`子命令會對指定包, 否則默認為當前目錄, 中所有.go源文件應用`gofmt`命令。本書中的所有代碼都被gofmt過。你也應該養成格式化自己的代碼的習慣。以法令方式規定標準的代碼格式可以避免無盡的無意義的瑣碎爭執[12](#)。更重要的是,這樣可以做多種自動源碼轉換,如果放任Go語言代碼格式,這些轉換就不大可能了。
很多文本編輯器都可以配置為保存文件時自動執行`gofmt`,這樣你的源代碼總會被恰當地格式化。還有個相關的工具,`goimports`,可以根據代碼需要, 自動地添加或刪除`import`聲明。這個工具并沒有包含在標準的分發包中,可以用下面的命令安裝:
~~~
$ go get golang.org/x/tools/cmd/goimports
~~~
對于大多數用戶來說,下載、編譯包、運行測試用例、察看Go語言的文檔等等常用功能都可以用go的工具完成。10.7節詳細介紹這些知識。
### 1.2. 命令行參數
大多數的程序都是處理輸入,產生輸出;這也正是“計算”的定義。但是, 程序如何獲取要處理的輸入數據呢?一些程序生成自己的數據,但通常情況下,輸入來自于程序外部:文件、網絡連接、其它程序的輸出、敲鍵盤的用戶、命令行參數或其它類似輸入源。下面幾個例子會討論其中幾個輸入源,首先是命令行參數。
`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:3] = [1, 2, 3],不包含最后一個元素)。比如s[m:n]這個切片,0 ≤ m ≤ n ≤ len(s),包含n-m個元素。
os.Args的第一個元素,os.Args[0], 是命令本身的名字;其它的元素則是程序啟動時傳給它的參數。s[m:n]形式的切片表達式,產生從第m個元素到第n-1個元素的切片,下個例子用到的元素包含在os.Args[1:len(os.Args)]切片中。如果省略切片表達式的m或n,會默認傳入0或len(s),因此前面的切片可以簡寫成os.Args[1:]。
下面是Unix里echo命令的一份實現,echo把它的命令行參數打印成一行。程序導入了兩個包,用括號把它們括起來寫成列表形式, 而沒有分開寫成獨立的`import`聲明。兩種形式都合法,列表形式習慣上用得多。包導入順序并不重要;gofmt工具格式化時按照字母順序對包名排序。(示例有多個版本時,我們會對示例編號, 這樣可以明確當前正在討論的是哪個。)
*gopl.io/ch1/echo1*
~~~
// 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隱式地初始化成空字符串。第2章再來詳細地講解變量和聲明。
對數值類型,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),每種數值運算符或邏輯運算符,如`+`或`*`,都有對應的賦值運算符。
echo程序可以每循環一次輸出一個參數,這個版本卻是不斷地把新文本追加到末尾來構造字符串。字符串s開始為空,即值為"",每次循環會添加一些文本;第一次迭代之后,還會再插入一個空格,因此循環結束時每個參數中間都有一個空格。這是一種二次加工(quadratic process),當參數數量龐大時,開銷很大,但是對于echo,這種情形不大可能出現。本章會介紹echo的若干改進版,下一章解決低效問題。
循環索引變量i在for循環的第一部分中定義。符號`:=`是*短變量聲明*(short variable declaration)的一部分, 這是定義一個或多個變量并根據它們的初始值為這些變量賦予適當類型的語句。下一章有這方面更多說明。
自增語句`i++`給`i`加1;這和`i += 1`以及`i = i + 1`都是等價的。對應的還有`i--`給`i`減1。它們是語句,而不像C系的其它語言那樣是表達式。所以`j = i++`非法,而且++和--都只能放在變量名后面,因此`--i`也非法。
Go語言只有for循環這一種循環語句。for循環有多種形式,其中一種如下所示:
~~~
for initialization; condition; post {
// zero or more statements
}
~~~
for循環三個部分不需括號包圍。大括號強制要求, 左大括號必須和*post*語句在同一行。
*initialization*語句是可選的,在循環開始前執行。*initalization*如果存在,必須是一條*簡單語句*(simple statement),即,短變量聲明、自增語句、賦值語句或函數調用。`condition`是一個布爾表達式(boolean expression),其值在每次循環迭代開始時計算。如果為`true`則執行循環體語句。`post`語句在循環體執行結束后執行,之后再次對`conditon`求值。`condition`值為`false`時,循環結束。
for循環的這三個部分每個都可以省略,如果省略`initialization`和`post`,分號也可以省略:
~~~
// a traditional "while" loop
for condition {
// ...
}
~~~
如果連`condition`也省略了,像下面這樣:
~~~
// a traditional infinite loop
for {
// ...
}
~~~
這就變成一個無限循環,盡管如此,還可以用其他方式終止循環, 如一條`break`或`return`語句。
`for`循環的另一種形式, 在某種數據類型的區間(range)上遍歷,如字符串或切片。`echo`的第二版本展示了這種形式:
*gopl.io/ch1/echo2*
~~~
// Echo2 prints its command-line arguments.
package main
import (
"fmt"
)
func main() {
s, sep := "", ""
for _, arg := range os.Args[1:] {
s += sep + arg
sep = " "
}
fmt.Println(s)
}
~~~
每次循環迭代,`range`產生一對值;索引以及在該索引處的元素值。這個例子不需要索引,但`range`的語法要求, 要處理元素, 必須處理索引。一種思路是把索引賦值給一個臨時變量, 如`temp`, 然后忽略它的值,但Go語言不允許使用無用的局部變量(local variables),因為這會導致編譯錯誤。
Go語言中這種情況的解決方法是用`空標識符`(blank identifier),即`_`(也就是下劃線)。空標識符可用于任何語法需要變量名但程序邏輯不需要的時候, 例如, 在循環里,丟棄不需要的循環索引, 保留元素值。大多數的Go程序員都會像上面這樣使用`range`和`_`寫`echo`程序,因為隱式地而非顯示地索引os.Args,容易寫對。
`echo`的這個版本使用一條短變量聲明來聲明并初始化`s`和`seps`,也可以將這兩個變量分開聲明,聲明一個變量有好幾種方式,下面這些都等價:
~~~
s := ""
var s string
var s = ""
var s string = ""
~~~
用哪種不用哪種,為什么呢?第一種形式,是一條短變量聲明,最簡潔,但只能用在函數內部,而不能用于包變量。第二種形式依賴于字符串的默認初始化零值機制,被初始化為""。第三種形式用得很少,除非同時聲明多個變量。第四種形式顯式地標明變量的類型,當變量類型與初值類型相同時,類型冗余,但如果兩者類型不同,變量類型就必須了。實踐中一般使用前兩種形式中的某個,初始值重要的話就顯式地指定變量的類型,否則使用隱式初始化。
如前文所述,每次循環迭代字符串s的內容都會更新。`+=`連接原字符串、空格和下個參數,產生新字符串, 并把它賦值給`s`。`s`原來的內容已經不再使用,將在適當時機對它進行垃圾回收。
如果連接涉及的數據量很大,這種方式代價高昂。一種簡單且高效的解決方案是使用`strings`包的`Join`函數:
*gopl.io/ch1/echo3*
~~~
func main() {
fmt.Println(strings.Join(os.Args[1:], " "))
}
~~~
最后,如果不關心輸出格式,只想看看輸出值,或許只是為了調試,可以用`Println`為我們格式化輸出。
~~~
fmt.Println(os.Args[1:])
~~~
這條語句的輸出結果跟`strings.Join`得到的結果很像,只是被放到了一對方括號里。切片都會被打印成這種格式。
**練習 1.1:** 修改`echo`程序,使其能夠打印`os.Args[0]`,即被執行命令本身的名字。
**練習 1.2:** 修改`echo`程序,使其打印每個參數的索引和值,每個一行。
**練習 1.3:** 做實驗測量潛在低效的版本和使用了`strings.Join`的版本的運行時間差異。(1.6節講解了部分`time`包,11.4節展示了如何寫標準測試程序,以得到系統性的性能評測。)
### 1.3. 查找重復的行
對文件做拷貝、打印、搜索、排序、統計或類似事情的程序都有一個差不多的程序結構:一個處理輸入的循環,在每個元素上執行計算處理,在處理的同時或最后產生輸出。我們會展示一個名為`dup`的程序的三個版本;靈感來自于Unix的`uniq`命令,其尋找相鄰的重復行。該程序使用的結構和包是個參考范例,可以方便地修改。
`dup`的第一個版本打印標準輸入中多次出現的行,以重復次數開頭。該程序將引入`if`語句,`map`數據類型以及`bufio`包。
*gopl.io/ch1/dup1*
~~~
// 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)
}
}
}
~~~
正如`for`循環一樣,`if`語句條件兩邊也不加括號,但是主體部分需要加。`if`語句的`else`部分是可選的,在`if`的條件為`false`時執行。
**map**存儲了鍵/值(key/value)的集合,對集合元素,提供常數時間的存、取或測試操作。鍵可以是任意類型,只要其值能用`==`運算符比較,最常見的例子是字符串;值則可以是任意類型。這個例子中的鍵是字符串,值是整數。內置函數`make`創建空`map`,此外,它還有別的作用。4.3節討論`map`。
(譯注:從功能和實現上說,`Go`的`map`類似于`Java`語言中的`HashMap`,Python語言中的`dict`,`Lua`語言中的`table`,通常使用`hash`實現。遺憾的是,對于該詞的翻譯并不統一,數學界術語為`映射`,而計算機界眾說紛紜莫衷一是。為了防止對讀者造成誤解,保留不譯。)
每次`dup`讀取一行輸入,該行被當做`map`,其對應的值遞增。`counts[input.Text()]++`語句等價下面兩句:
~~~
line := input.Text()
counts[line] = counts[line] + 1
~~~
`map`中不含某個鍵時不用擔心,首次讀到新行時,等號右邊的表達式`counts[line]`的值將被計算為其類型的零值,對于int`即0。
為了打印結果,我們使用了基于`range`的循環,并在`counts`這個`map`上迭代。跟之前類似,每次迭代得到兩個結果,鍵和其在`map`中對應的值。`map`的迭代順序并不確定,從實踐來看,該順序隨機,每次運行都會變化。這種設計是有意為之的,因為能防止程序依賴特定遍歷順序,而這是無法保證的。
繼續來看`bufio`包,它使處理輸入和輸出方便又高效。`Scanner`類型是該包最有用的特性之一,它讀取輸入并將其拆成行或單詞;通常是處理行形式的輸入最簡單的方法。
程序使用短變量聲明創建`bufio.Scanner`類型的變量`input`。
~~~
input := bufio.NewScanner(os.Stdin)
~~~
該變量從程序的標準輸入中讀取內容。每次調用`input.Scanner`,即讀入下一行,并移除行末的換行符;讀取的內容可以調用`input.Text()`得到。`Scan`函數在讀到一行時返回`true`,在無輸入時返回`false`。
類似于C或其它語言里的`printf`函數,`fmt.Printf`函數對一些表達式產生格式化輸出。該函數的首個參數是個格式字符串,指定后續參數被如何格式化。各個參數的格式取決于“轉換字符”(conversion character),形式為百分號后跟一個字母。舉個例子,`%d`表示以十進制形式打印一個整型操作數,而`%s`則表示把字符串型操作數的值展開。
`Printf`有一大堆這種轉換,Go程序員稱之為*動詞(verb)*。下面的表格雖然遠不是完整的規范,但展示了可用的很多特性:
~~~
%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.Printf`和`fmt.Errorf`,都采用`fmt.Printf`的格式化準則。而以`ln`結尾的格式化函數,則遵循`Println`的方式,以跟`%v`差不多的方式格式化參數,并在最后添加一個換行符。(譯注:后綴`f`指`fomart`,`ln`指`line`。)
很多程序要么從標準輸入中讀取數據,如上面的例子所示,要么從一系列具名文件中讀取數據。`dup`程序的下個版本讀取標準輸入或是使用`os.Open`打開各個具名文件,并操作它們。
*gopl.io/ch1/dup2*
~~~
// 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`繼續處理下一個文件;`continue`語句直接跳到`for`循環的下個迭代開始執行。
為了使示例代碼保持合理的大小,本書開始的一些示例有意簡化了錯誤處理,顯而易見的是,應該檢查`os.Open`返回的錯誤值,然而,使用`input.Scan`讀取文件過程中,不大可能出現錯誤,因此我們忽略了錯誤處理。我們會在跳過錯誤檢查的地方做說明。5.4節中深入介紹錯誤處理。
注意`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`函數。
*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`函數返回一個字節切片(byte slice),必須把它轉換為`string`,才能用`strings.Split`分割。我們會在3.5.4節詳細講解字符串和字節切片。
實現上,`bufio.Scanner`、`ioutil.ReadFile`和`ioutil.WriteFile`都使用`*os.File`的`Read`和`Write`方法,但是,大多數程序員很少需要直接調用那些低級(lower-level)函數。高級(higher-level)函數,像`bufio`和`io/ioutil`包中所提供的那些,用起來要容易點。
**練習 1.4:** 修改`dup2`,出現重復的行時打印文件名稱。
### 1.4. GIF動畫
下面的程序會演示Go語言標準庫里的image這個package的用法,我們會用這個包來生成一系列的bit-mapped圖,然后將這些圖片編碼為一個GIF動畫。我們生成的圖形名字叫利薩如圖形(Lissajous figures),這種效果是在1960年代的老電影里出現的一種視覺特效。它們是協振子在兩個緯度上振動所產生的曲線,比如兩個sin正弦波分別在x軸和y軸輸入會產生的曲線。圖1.1是這樣的一個例子:

譯注:要看這個程序的結果,需要將標準輸出重定向到一個GIF圖像文件(使用 `./lissajous > output.gif` 命令)。下面是GIF圖像動畫效果:

這段代碼里我們用了一些新的結構,包括const聲明,struct結構體類型,復合聲明。和我們舉的其它的例子不太一樣,這一個例子包含了浮點數運算。這些概念我們只在這里簡單地說明一下,之后的章節會更詳細地講解。
*gopl.io/ch1/lissajous*
~~~
// 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{...}這兩個表達式就是我們說的復合聲明(4.2和4.4.1節有說明)。這是實例化Go語言里的復合類型的一種寫法。這里的前者生成的是一個slice切片,后者生成的是一個struct結構體。
gif.GIF是一個struct類型(參考4.4節)。struct是一組值或者叫字段的集合,不同的類型集合在一個struct可以讓我們以一個統一的單元進行處理。anim是一個gif.GIF類型的struct變量。這種寫法會生成一個struct變量,并且其內部變量LoopCount字段會被設置為nframes;而其它的字段會被設置為各自類型默認的零值。struct內部的變量可以以一個點(.)來進行訪問,就像在最后兩個賦值語句中顯式地更新了anim這個struct的Delay和Image字段。
lissajous函數內部有兩層嵌套的for循環。外層循環會循環64次,每一次都會生成一個單獨的動畫幀。它生成了一個包含兩種顏色的201&201大小的圖片,白色和黑色。所有像素點都會被默認設置為其零值(也就是調色板palette里的第0個值),這里我們設置的是白色。每次外層循環都會生成一張新圖片,并將一些像素設置為黑色。其結果會append到之前結果之后。這里我們用到了append(參考4.2.1)內置函數,將結果append到anim中的幀列表末尾,并設置一個默認的80ms的延遲值。循環結束后所有的延遲值被編碼進了GIF圖片中,并將結果寫入到輸出流。out這個變量是io.Writer類型,這個類型支持把輸出結果寫到很多目標,很快我們就可以看到例子。
內層循環設置兩個偏振值。x軸偏振使用sin函數。y軸偏振也是正弦波,但其相對x軸的偏振是一個0-3的隨機值,初始偏振值是一個零值,隨著動畫的每一幀逐漸增加。循環會一直跑到x軸完成五次完整的循環。每一步它都會調用SetColorIndex來為(x, y)點來染黑色。
main函數調用lissajous函數,用它來向標準輸出流打印信息,所以下面這個命令會像圖1.1中產生一個GIF動畫。
~~~
$ go build gopl.io/ch1/lissajous
$ ./lissajous >out.gif
~~~
**練習 1.5:** 修改前面的Lissajous程序里的調色板,由黑色改為綠色。我們可以用`color.RGBA{0xRR, 0xGG, 0xBB, 0xff}`來得到`#RRGGBB`這個色值,三個十六進制的字符串分別代表紅、綠、藍像素。
**練習 1.6:** 修改Lissajous程序,修改其調色板來生成更豐富的顏色,然后修改SetColorIndex的第三個參數,看看顯示結果吧。
### 1.5. 獲取URL
對于很多現代應用來說,訪問互聯網上的信息和訪問本地文件系統一樣重要。Go語言在net這個強大package的幫助下提供了一系列的package來做這件事情,使用這些包可以更簡單地用網絡收發信息,還可以建立更底層的網絡連接,編寫服務器程序。在這些情景下,Go語言原生的并發特性(在第八章中會介紹)顯得尤其好用。
為了最簡單地展示基于HTTP獲取信息的方式,下面給出一個示例程序fetch,這個程序將獲取對應的url,并將其源文本打印出來;這個例子的靈感來源于curl工具(譯注:unix下的一個用來發http請求的工具,具體可以man curl)。當然,curl提供的功能更為復雜豐富,這里只編寫最簡單的樣例。這個樣例之后還會多次被用到。
*gopl.io/ch1/fetch*
~~~
// 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/http和io/ioutil包,http.Get函數是創建HTTP請求的函數,如果獲取過程沒有出錯,那么會在resp這個結構體中得到訪問的請求結果。resp的Body字段包括一個可讀的服務器響應流。ioutil.ReadAll函數從response中讀取到全部內容;將其結果保存在變量b中。resp.Body.Close關閉resp的Body流,防止資源泄露,Printf函數會將結果b寫出到標準輸出流中。
~~~
$ go build gopl.io/ch1/fetch
$ ./fetch http://gopl.io
<html>
<head>
<title>The Go Programming Language</title>title>
...
~~~
HTTP請求如果失敗了的話,會得到下面這樣的結果:
~~~
$ ./fetch http://bad.gopl.io
fetch: Get http://bad.gopl.io: dial tcp: lookup bad.gopl.io: no such host
~~~
譯注:在大天朝的網絡環境下很容易重現這種錯誤,下面是Windows下運行得到的錯誤信息:
~~~
$ go run main.go http://gopl.io
fetch: Get http://gopl.io: dial tcp: lookup gopl.io: getaddrinfow: No such host is known.
~~~
無論哪種失敗原因,我們的程序都用了os.Exit函數來終止進程,并且返回一個status錯誤碼,其值為1。
**練習 1.7:** 函數調用io.Copy(dst, src)會從src中讀取內容,并將讀到的結果寫入到dst中,使用這個函數替代掉例子中的ioutil.ReadAll來拷貝響應結構體到os.Stdout,避免申請一個緩沖區(例子中的b)來存儲。記得處理io.Copy返回結果中的錯誤。
**練習 1.8:** 修改fetch這個范例,如果輸入的url參數沒有 `http://` 前綴的話,為這個url加上該前綴。你可能會用到strings.HasPrefix這個函數。
**練習 1.9:** 修改fetch打印出HTTP協議的狀態碼,可以從resp.Status變量得到該狀態碼。
### 1.6. 并發獲取多個URL
Go語言最有意思并且最新奇的特性就是對并發編程的支持。并發編程是一個大話題,在第八章和第九章中會專門講到。這里我們只淺嘗輒止地來體驗一下Go語言里的goroutine和channel。
下面的例子fetchall,和前面小節的fetch程序所要做的工作基本一致,fetchall的特別之處在于它會同時去獲取所有的URL,所以這個程序的總執行時間不會超過執行時間最長的那一個任務,前面的fetch程序執行時間則是所有任務執行時間之和。fetchall程序只會打印獲取的內容大小和經過的時間,不會像之前那樣打印獲取的內容。
*gopl.io/ch1/fetchall*
~~~
// 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)
}
~~~
下面使用fetchall來請求幾個地址:
~~~
$ go build gopl.io/ch1/fetchall
$ ./fetchall https://golang.org http://gopl.io https://godoc.org
0.14s 6852 https://godoc.org
0.16s 7261 https://golang.org
0.48s 2475 http://gopl.io
0.48s 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函數提前退出。
**練習 1.10:** 找一個數據量比較大的網站,用本小節中的程序調研網站的緩存策略,對每個URL執行兩遍請求,查看兩次時間是否有較大的差別,并且每次獲取到的響應內容是否一致,修改本節中的程序,將響應結果輸出,以便于進行對比。
**練習 1.11:** 在fatchall中嘗試使用長一些的參數列表,比如使用在alexa.com的上百萬網站里排名靠前的。如果一個網站沒有回應,程序將采取怎樣的行為?(Section8.9 描述了在這種情況下的應對機制)。
### 1.7. Web服務
Go語言的內置庫使得寫一個類似fetch的web服務器變得異常地簡單。在本節中,我們會展示一個微型服務器,這個服務器的功能是返回當前用戶正在訪問的URL。比如用戶訪問的是 http://localhost:8000/hello ,那么響應是URL.Path = "hello"。
*gopl.io/ch1/server1*
~~~
// 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。Web服務會在第7.7節中做更詳細的闡述。
讓我們在后臺運行這個服務程序。如果你的操作系統是Mac OS X或者Linux,那么在運行命令的末尾加上一個&符號,即可讓程序簡單地跑在后臺,windows下可以在另外一個命令行窗口去運行這個程序。
~~~
$ go run src/gopl.io/ch1/server1/main.go &
~~~
現在可以通過命令行來發送客戶端請求了:
~~~
$ go build gopl.io/ch1/fetch
$ ./fetch http://localhost:8000
URL.Path = "/"
$ ./fetch http://localhost:8000/help
URL.Path = "/help"
~~~
還可以直接在瀏覽器里訪問這個URL,然后得到返回結果,如圖1.2:

在這個服務的基礎上疊加特性是很容易的。一種比較實用的修改是為訪問的url添加某種狀態。比如,下面這個版本輸出了同樣的內容,但是會對請求的次數進行計算;對URL的請求結果會包含各種URL被訪問的總次數,直接對/count這個URL的訪問要除外。
*gopl.io/ch1/server2*
~~~
// 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:競態條件(參見9.1)。為了避免這個問題,我們必須保證每次修改變量的最多只能有一個goroutine,這也就是代碼里的mu.Lock()和mu.Unlock()調用將修改count的所有行為包在中間的目的。第九章中我們會進一步講解共享變量。
下面是一個更為豐富的例子,handler函數會把請求的http頭和請求的form數據都打印出來,這樣可以使檢查和調試這個服務更為方便:
*gopl.io/ch1/server3*
~~~
// 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里的字段來輸出下面這樣的內容:
~~~
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語句的最前面,這一點對錯誤處理很有用處。我們還可以像下面這樣寫(當然看起來就長了一些):
~~~
err := r.ParseForm()
if err != nil {
log.Print(err)
}
~~~
用if和ParseForm結合可以讓代碼更加簡單,并且可以限制err這個變量的作用域,這么做是很不錯的。我們會在2.7節中講解作用域。
在這些程序中,我們看到了很多不同的類型被輸出到標準輸出流中。比如前面的fetch程序,把HTTP的響應數據拷貝到了os.Stdout,lissajous程序里我們輸出的是一個文件。fetchall程序則完全忽略到了HTTP的響應Body,只是計算了一下響應Body的大小,這個程序中把響應Body拷貝到了ioutil.Discard。在本節的web服務器程序中則是用fmt.Fprintf直接寫到了http.ResponseWriter中。
盡管三種具體的實現流程并不太一樣,他們都實現一個共同的接口,即當它們被調用需要一個標準流輸出時都可以滿足。這個接口叫作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:** 修改Lissajour服務,從URL讀取變量,比如你可以訪問 http://localhost:8000/?cycles=20 這個URL,這樣訪問可以將程序里的cycles默認的5修改為20。字符串轉換為數字可以調用strconv.Atoi函數。你可以在godoc里查看strconv.Atoi的詳細說明。
### 1.8. 本章要點
本章對Go語言做了一些介紹,Go語言很多方面在有限的篇幅中無法覆蓋到。本節會把沒有講到的內容也做一些簡單的介紹,這樣讀者在讀到完整的內容之前,可以有個簡單的印象。
**控制流:** 在本章我們只介紹了if控制和for,但是沒有提到switch多路選擇。這里是一個簡單的switch的例子:
~~~
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一樣,下面是一個例子:
~~~
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語句(之后會講到),在1.3節中我們看到,continue會跳過內層的循環,如果我們想跳過的是更外層的循環的話,我們可以在相應的位置加上label,這樣break和continue就可以根據我們的想法來continue和break任意循環。這看起來甚至有點像goto語句的作用了。當然,一般程序員也不會用到這種操作。這兩種行為更多地被用到機器生成的代碼中。
**命名類型:** 類型聲明使得我們可以很方便地給一個特殊類型一個名字。因為struct類型聲明通常非常地長,所以我們總要給這種struct取一個名字。本章中就有這樣一個例子,二維點類型:
~~~
type Point struct {
X, Y int
}
var p Point
~~~
類型聲明和命名類型會在第二章中介紹。
**指針:** Go語言提供了指針。指針是一種直接存儲了變量的內存地址的數據類型。在其它語言中,比如C語言,指針操作是完全不受約束的。在另外一些語言中,指針一般被處理為“引用”,除了到處傳遞這些指針之外,并不能對這些指針做太多事情。Go語言在這兩種范圍中取了一種平衡。指針是可見的內存地址,&操作符可以返回一個變量的內存地址,并且*操作符可以獲取指針指向的變量內容,但是在Go語言里沒有指針運算,也就是不能像c語言里可以對指針進行加或減操作。我們會在2.3.2中進行詳細介紹。
**方法和接口:** 方法是和命名類型關聯的一類函數。Go語言里比較特殊的是方法可以被關聯到任意一種命名類型。在第六章我們會詳細地講方法。接口是一種抽象類型,這種類型可以讓我們以同樣的方式來處理不同的固有類型,不用關心它們的具體實現,而只需要關注它們提供的方法。第七章中會詳細說明這些內容。
**包(packages):** Go語言提供了一些很好用的package,并且這些package是可以擴展的。Go語言社區已經創造并且分享了很多很多。所以Go語言編程大多數情況下就是用已有的package來寫我們自己的代碼。通過這本書,我們會講解一些重要的標準庫內的package,但是還是有很多限于篇幅沒有去說明,因為我們沒法在這樣的厚度的書里去做一部代碼大全。
在你開始寫一個新程序之前,最好先去檢查一下是不是已經有了現成的庫可以幫助你更高效地完成這件事情。你可以在 https://golang.org/pkg 和 https://godoc.org 中找到標準庫和社區寫的package。godoc這個工具可以讓你直接在本地命令行閱讀標準庫的文檔。比如下面這個例子。
~~~
$ 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這樣的工具檢測到,并且在執行命令時顯示這些注釋。具體可以參考10.7.4。
多行注釋可以用 `/* ... */` 來包裹,和其它大多數語言一樣。在文件一開頭的注釋一般都是這種形式,或者一大段的解釋性的注釋文字也會被這符號包住,來避免每一行都需要加//。在注釋中//和/*是沒什么意義的,所以不要在注釋中再嵌入注釋。
1.
本書作者之一Brian W. Kernighan也是《The C Programming Language》一書的作者。[?](#)
1.
靜態編譯。[?](#)
1.
Windows系統下生成的可執行文件是helloworld.exe,增加了.exe后綴名。[?](#)
1.
在Windows系統下在命令行直接輸入helloworld.exe命令運行。[?](#)
1.
因為靜態編譯,所以不用擔心在系統庫更新的時候沖突,幸福感滿滿。[?](#)
1.
需要先安裝Git或Hg之類的版本管理工具,并將對應的命令添加到PATH環境變量中。序言已經提及,需要先設置好GOPATH環境變量,下載的代碼會放在`$GOPATH/src/gopl.io/ch1/helloworld`目錄。[?](#)
1.
C系語言差不多都這樣。[?](#)
1.
Go語言編譯過程沒有警告信息,爭議特性之一。[?](#)
1.
最好還是定一下規范。[?](#)
1.
比如行末是標識符、整數、浮點數、虛數、字符或字符串文字、關鍵字`break`、`continue`、`fallthrough`或`return`中的一個、運算符和分隔符`++`、`--`、`)`、`]`或`}`中的一個。[?](#)
1.
這個格式化工具沒有任何可以調整代碼格式的參數,Go語言就是這么任性。[?](#)
1.
也導致了Go語言的TIOBE排名較低,因為缺少撕逼的話題。[?](#)
- Go語言程序設計
- Go語言圣經(中文版)
- 譯者序
- 前言
- 第一章 入門
- 第二章 程序結構
- 第三章 基礎數據類型
- 第四章 復合數據類型
- 第五章 函數
- 第六章 方法
- 第七章 接口
- 7.4. flag.Value接口
- 7.6. sort.Interface接口
- 7.7. http.Handler接口
- 第八章 Goroutines和Channels
- 第九章 基于共享變量的并發
- 9.2. sync.Mutex互斥鎖
- 第十章 包和工具
- 第十一章 測試
- 第十二章 反射
- 12.2. reflect.Type和reflect.Value
- 12.5. 通過reflect.Value修改值
- 第13章 底層編程
- 13.1. unsafe.Sizeof, Alignof 和 Offsetof
- 附錄