mirror of
https://github.com/gopl-zh/gopl-zh.github.com.git
synced 2025-12-18 03:34:19 +08:00
make loop
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
## 1.1. Hello, World
|
||||
|
||||
我們以1978年齣版的C語言聖經《The C Programming Language》中經典的“hello world”案例來開始吧(譯註:本書作者之一Brian W. Kernighan也是C語言聖經一書的作者)。C語言對Go語言的設計産生了很多影響。用這個例子,我們來講解一些Go語言的覈心特性:
|
||||
我們以1978年出版的C語言聖經《The C Programming Language》中經典的“hello world”案例來開始吧(譯註:本書作者之一Brian W. Kernighan也是C語言聖經一書的作者)。C語言對Go語言的設計産生了很多影響。用這個例子,我們來講解一些Go語言的覈心特性:
|
||||
|
||||
```go
|
||||
gopl.io/ch1/helloworld
|
||||
@@ -19,7 +19,7 @@ Go是一門編譯型語言,Go語言的工具鏈將源代碼和其依賴一起
|
||||
$ go run helloworld.go
|
||||
```
|
||||
|
||||
毫無意外,這個命令會輸齣:
|
||||
毫無意外,這個命令會輸出:
|
||||
|
||||
```
|
||||
Hello, 世界
|
||||
@@ -52,7 +52,7 @@ gopl.io/ch1/helloworld
|
||||
|
||||
我們來討論一下程序本身。Go語言的代碼是通過package來組織的,package的概念和你知道的其它語言里的libraries或者modules概念比較類似。一個package會包含一個或多個.go結束的源代碼文件。每一個源文件都是以一個package xxx的聲明語句開頭的,比如我們的例子里就是package main。這行聲明語句表示該文件是屬於哪一個package,緊跟着是一繫列import的package名,表示這個文件中引入的package。再之後是本文件本身的代碼。
|
||||
|
||||
Go的標準庫已經提供了100多個package,用來完成一門程序語言的一些常見的基本任務,比如輸入、輸齣、排序或者字符串/文本處理。比如fmt這個package,就包括接收輸入、格式化輸齣的各種函數。Println是其中的一個常用的函數,可以用這個函數來打印一個或多個值,該函數會將這些參數用空格隔開進行輸齣,併在輸齣完畢之後在行末加上一個換行符。
|
||||
Go的標準庫已經提供了100多個package,用來完成一門程序語言的一些常見的基本任務,比如輸入、輸出、排序或者字符串/文本處理。比如fmt這個package,就包括接收輸入、格式化輸出的各種函數。Println是其中的一個常用的函數,可以用這個函數來打印一個或多個值,該函數會將這些參數用空格隔開進行輸出,併在輸出完畢之後在行末加上一個換行符。
|
||||
|
||||
package main是一個比較特殊的package。這個package里會定義一個獨立的程序,這個程序是可以運行的,而不是像其它package一樣對應一個library。在main這個package里,main函數也是一個特殊的函數,這是我們整個程序的入口(譯註:其實C繫語言差不多都是這樣)。main函數所做的事情就是我們程序做的事情。當然了,main函數一般是通過是調用其它packge里的函數來完成自己的工作,比如fmt.Println。
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
## 1.2. 命令行參數
|
||||
|
||||
大多數的程序都是處理輸入,産生輸齣;這也正是“計算”的定義。但是一個程序要如何穫取輸入呢?一些程序會生成自己的數據,但通常情況下,輸入都來自於程序外部:比如文件、網絡連接、其它程序的輸齣、用戶的鍵盤、命令行的參數或其它類似輸入源。下面幾個例子會討論其中的一些輸入類型,首先是命令行參數。
|
||||
大多數的程序都是處理輸入,産生輸出;這也正是“計算”的定義。但是一個程序要如何穫取輸入呢?一些程序會生成自己的數據,但通常情況下,輸入都來自於程序外部:比如文件、網絡連接、其它程序的輸出、用戶的鍵盤、命令行的參數或其它類似輸入源。下面幾個例子會討論其中的一些輸入類型,首先是命令行參數。
|
||||
|
||||
os這個package提供了操作繫統無關(跨平颱)的,與繫統交互的一些函數和相關的變量,運行時程序的命令行參數可以通過os包中一個叫Args的這個變量來穫取;當在os包外部使用該變量時,需要用os.Args來訪問。
|
||||
|
||||
@@ -8,7 +8,7 @@ os.Args這個變量是一個字符串(string)的slice(譯註:slice和Pyt
|
||||
|
||||
os.Args的第一個元素,卽os.Args[0]是命令行執行時的命令本身;其它的元素則是執行該命令時傳給這個程序的參數。前面提到的切片表達式,s[m:n]會返迴第m到第n-1個元素,所以下一個例子里需要用到的os.Args[1:len(os.Args)]卽是除了命令本身外的所有傳入參數。如果我們省略s[m:n]里的m和n,那麽默認這個表達式會填入0:len(s),所以這里我們還可以省略掉n,寫成os.Args[1:]。
|
||||
|
||||
下面是一個Unix里echo命令的實現,這個命令會在單行內打印齣命令行參數。這個程序import了兩個package,併且用括號把這兩個package包了起來,這是分别import各個package聲明的簡化寫法。當然了你分開來寫import也沒有什麽問題,隻是一般爲了方便我們都會像下面這樣來導入多個package。我們自己寫的導入順序併不重要,因爲gofmt工具會幫助我們按照字母順序來排列好這些導入包名。(本書中如果一個例子有多種版本時,我們會用編號標記齣來)
|
||||
下面是一個Unix里echo命令的實現,這個命令會在單行內打印出命令行參數。這個程序import了兩個package,併且用括號把這兩個package包了起來,這是分别import各個package聲明的簡化寫法。當然了你分開來寫import也沒有什麽問題,隻是一般爲了方便我們都會像下面這樣來導入多個package。我們自己寫的導入順序併不重要,因爲gofmt工具會幫助我們按照字母順序來排列好這些導入包名。(本書中如果一個例子有多種版本時,我們會用編號標記出來)
|
||||
|
||||
```go
|
||||
gopl.io/ch1/echo1
|
||||
@@ -56,7 +56,7 @@ s = s + sep + os.Args[i]
|
||||
|
||||
運算符+=是一個賦值運算符(assignment operator),每一種數值和邏輯運算符,例如*或者+都有其對應的賦值運算符。
|
||||
|
||||
echo程序可以每循環一次輸齣一個參數,不過我們這里的版本是不斷地將其結果連接到一個字符串的末尾。s這個字符串在聲明的時候是一個空字符串,而之後循環每次都會被在末尾添加一段字符串;第一次迭代之後,一個空格會被插入到字符串末尾,所以每插入一個新值,都會和前一個中間有一個空格隔開。這是一種非線性的操作,當我們的參數數量變得龐大的時候(當然不是説這里的echo,一般echo也不會有太多參數)其運行開銷也會變得龐大。下面我們會介紹一繫列的echo改進版,來應對這里説到的運行效率低下。
|
||||
echo程序可以每循環一次輸出一個參數,不過我們這里的版本是不斷地將其結果連接到一個字符串的末尾。s這個字符串在聲明的時候是一個空字符串,而之後循環每次都會被在末尾添加一段字符串;第一次迭代之後,一個空格會被插入到字符串末尾,所以每插入一個新值,都會和前一個中間有一個空格隔開。這是一種非線性的操作,當我們的參數數量變得龐大的時候(當然不是説這里的echo,一般echo也不會有太多參數)其運行開銷也會變得龐大。下面我們會介紹一繫列的echo改進版,來應對這里説到的運行效率低下。
|
||||
|
||||
在for循環中,我們用到了i來做下標索引,可以看到我們用了:=符號來給i進行初始化和賦值,這是var xxx=yyy的一種簡寫形式,Go語言會根據等號右邊的值的類型自動判斷左邊的值類型,下一章會對這一點進行詳細説明。
|
||||
|
||||
@@ -141,17 +141,17 @@ func main() {
|
||||
}
|
||||
```
|
||||
|
||||
最後,如果我們對輸齣的格式也不是很關心,隻是想簡單地輸齣值得的話,還可以像下面這麽寫,Println函數會爲我們自動格式化輸齣。
|
||||
最後,如果我們對輸出的格式也不是很關心,隻是想簡單地輸出值得的話,還可以像下面這麽寫,Println函數會爲我們自動格式化輸出。
|
||||
|
||||
```go
|
||||
fmt.Println(os.Args[1:])
|
||||
```
|
||||
|
||||
這個輸齣結果和前面的string.Join得到的結果很相似,隻是被自動地放到了一個方括號里,對slice調用Println函數都會被打印成這樣形式的結果。
|
||||
這個輸出結果和前面的string.Join得到的結果很相似,隻是被自動地放到了一個方括號里,對slice調用Println函數都會被打印成這樣形式的結果。
|
||||
|
||||
**練習 1.1:** 脩改echo程序,使其能夠打印os.Args[0]。
|
||||
|
||||
**練習 1.2:** 脩改echo程序,使其打印value和index,每個value和index顯示一行。
|
||||
|
||||
**練習 1.3:** 上手實踐前面提到的strings.Join和直接Println,併觀察輸齣結果的區别。
|
||||
**練習 1.3:** 上手實踐前面提到的strings.Join和直接Println,併觀察輸出結果的區别。
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
## 1.3. 査找重複的行
|
||||
|
||||
文件拷貝、文件打印、文件蒐索、文件排序、文件統計類的程序一般都會有比較相似的程序結構:一個處理輸入的循環,在每一個輸入元素上執行計算處理,在處理的同時或者處理完成之後進行結果輸齣。我們會展示一個叫dup程序的三個版本;這個程序的靈感來自於linux的uniq命令,我們的程序將會找到相鄰的重複的行。這個程序提供的模式可以很方便地被脩改來完成不同的需求。
|
||||
文件拷貝、文件打印、文件蒐索、文件排序、文件統計類的程序一般都會有比較相似的程序結構:一個處理輸入的循環,在每一個輸入元素上執行計算處理,在處理的同時或者處理完成之後進行結果輸出。我們會展示一個叫dup程序的三個版本;這個程序的靈感來自於linux的uniq命令,我們的程序將會找到相鄰的重複的行。這個程序提供的模式可以很方便地被脩改來完成不同的需求。
|
||||
|
||||
第一個版本的dup會輸齣標準輸入流中的齣現多次的行,在行內容前會有其齣現次數的計數。這個程序將引入if表達式,map內置數據結構和bufio的package。
|
||||
第一個版本的dup會輸出標準輸入流中的出現多次的行,在行內容前會有其出現次數的計數。這個程序將引入if表達式,map內置數據結構和bufio的package。
|
||||
|
||||
```go
|
||||
gopl.io/ch1/dup1
|
||||
@@ -47,7 +47,7 @@ counts[line] = counts[line] + 1
|
||||
|
||||
在這里我們又用了一個range的循環來打印結果,這次range是被用在map這個數據結構之上。這一次的情況和上次比較類似,range會返迴兩個值,一個key和在map對應這個key的value。對map進行range循環時,其迭代順序是不確定的,從實踐來看,很可能每次運行都會有不一樣的結果(譯註:這是Go語言的設計者有意爲之的,因爲其底層實現不保證插入順序和遍歷順序一致,也希望程序員不要依賴遍歷時的順序,所以榦脆直接在遍歷的時候做了隨機化處理,醉了。補充:好像説隨機序可以防止某種類型的攻擊,雖然不太明白,但是感覺還蠻厲害的),來避免程序員在業務中依賴遍歷時的順序。
|
||||
|
||||
然後輪到我們例子中的bufio這個package了,這個package主要的目的是幫助我們更方便有效地處理程序的輸入和輸齣。而這個包最有用的一個特性就是其中的一個Scanner類型,用它可以簡單地接收輸入,或者把輸入打散成行或者單詞;這個類型通常是處理行形式的輸入最簡單的方法了。
|
||||
然後輪到我們例子中的bufio這個package了,這個package主要的目的是幫助我們更方便有效地處理程序的輸入和輸出。而這個包最有用的一個特性就是其中的一個Scanner類型,用它可以簡單地接收輸入,或者把輸入打散成行或者單詞;這個類型通常是處理行形式的輸入最簡單的方法了。
|
||||
|
||||
本程序中用了一個短變量聲明,來創建一個buffio.Scanner對象:
|
||||
|
||||
@@ -57,9 +57,9 @@ input := bufio.NewScanner(os.Stdin)
|
||||
|
||||
scanner對象可以從程序的標準輸入中讀取內容。對input.Scanner的每一次調用都會調入一個新行,併且會自動將其行末的換行符去掉;其結果可以用input.Text()得到。Scan方法在讀到了新行的時候會返迴true,而在沒有新行被讀入時,會返迴false。
|
||||
|
||||
例子中還有一個fmt.Printf,這個函數和C繫的其它語言里的那個printf函數差不多,都是格式化輸齣的方法。fmt.Printf的第一個參數卽是輸齣內容的格式規約,每一個參數如何格式化是取決於在格式化字符串里齣現的“轉換字符”,這個字符串是跟着%號後的一個字母。比如%d表示以一個整數的形式來打印一個變量,而%s,則表示以string形式來打印一個變量。
|
||||
例子中還有一個fmt.Printf,這個函數和C繫的其它語言里的那個printf函數差不多,都是格式化輸出的方法。fmt.Printf的第一個參數卽是輸出內容的格式規約,每一個參數如何格式化是取決於在格式化字符串里出現的“轉換字符”,這個字符串是跟着%號後的一個字母。比如%d表示以一個整數的形式來打印一個變量,而%s,則表示以string形式來打印一個變量。
|
||||
|
||||
Printf有一大堆這種轉換,Go語言程序員把這些叫做verb(動詞)。下面的表格列齣了常用的動詞,當然了不是全部,但基本也夠用了。
|
||||
Printf有一大堆這種轉換,Go語言程序員把這些叫做verb(動詞)。下面的表格列出了常用的動詞,當然了不是全部,但基本也夠用了。
|
||||
|
||||
```
|
||||
%d int變量
|
||||
@@ -69,12 +69,12 @@ Printf有一大堆這種轉換,Go語言程序員把這些叫做verb(動詞
|
||||
%c rune (Unicode碼點),Go語言里特有的Unicode字符類型
|
||||
%s string
|
||||
%q 帶雙引號的字符串 "abc" 或 帶單引號的 rune 'c'
|
||||
%v 會將任意變量以易讀的形式打印齣來
|
||||
%v 會將任意變量以易讀的形式打印出來
|
||||
%T 打印變量的類型
|
||||
%% 字符型百分比標誌(%符號本身,沒有其他操作)
|
||||
```
|
||||
|
||||
dup1中的程序還包含了一個\t和\n的格式化字符串。在字符串中會以這些特殊的轉義字符來表示不可見字符。Printf默認不會在輸齣內容後加上換行符。按照慣例,用來格式化的函數都會在末尾以f字母結尾(譯註:f後綴對應format或fmt縮寫),比如log.Printf,fmt.Errorf,同時還有一繫列對應以ln結尾的函數(譯註:ln後綴對應line縮寫),這些函數默認以%v來格式化他們的參數,併且會在輸齣結束後在最後自動加上一個換行符。
|
||||
dup1中的程序還包含了一個\t和\n的格式化字符串。在字符串中會以這些特殊的轉義字符來表示不可見字符。Printf默認不會在輸出內容後加上換行符。按照慣例,用來格式化的函數都會在末尾以f字母結尾(譯註:f後綴對應format或fmt縮寫),比如log.Printf,fmt.Errorf,同時還有一繫列對應以ln結尾的函數(譯註:ln後綴對應line縮寫),這些函數默認以%v來格式化他們的參數,併且會在輸出結束後在最後自動加上一個換行符。
|
||||
|
||||
許多程序從標準輸入中讀取數據,像上面的例子那樣。除此之外,還可能從一繫列的文件中讀取。下一個dup程序就是從標準輸入中讀到一些文件名,用os.Open函數來打開每一個文件穫取內容的。
|
||||
|
||||
@@ -124,7 +124,7 @@ func countLines(f *os.File, counts map[string]int) {
|
||||
|
||||
os.Open函數會返迴兩個值。第一個值是一個打開的文件類型(*os.File),這個對象在下面的程序中被Scanner讀取。
|
||||
|
||||
os.Open返迴的第二個值是一個Go語言內置的error類型。如果這個error和內置值的nil(譯註:相當於其它語言里的NULL)相等的話,説明文件被成功的打開了。之後文件被讀取,一直到文件的最後,文件的Close方法關閉該文件,併釋放相應的占用一切資源。另一方面,如果err的值不是nil的話,那説明在打開文件的時候齣了某種錯誤。這種情況下,error類型的值會描述具體的問題。我們例子里的簡單錯誤處理會在標準錯誤流中用Fprintf和%v來格式化該錯誤字符串。然後繼續處理下一個文件;continue語句會直接跳過之後的語句,直接開始執行下一個循環迭代。
|
||||
os.Open返迴的第二個值是一個Go語言內置的error類型。如果這個error和內置值的nil(譯註:相當於其它語言里的NULL)相等的話,説明文件被成功的打開了。之後文件被讀取,一直到文件的最後,文件的Close方法關閉該文件,併釋放相應的占用一切資源。另一方面,如果err的值不是nil的話,那説明在打開文件的時候出了某種錯誤。這種情況下,error類型的值會描述具體的問題。我們例子里的簡單錯誤處理會在標準錯誤流中用Fprintf和%v來格式化該錯誤字符串。然後繼續處理下一個文件;continue語句會直接跳過之後的語句,直接開始執行下一個循環迭代。
|
||||
|
||||
我們在本書中早期的例子中做了比較詳盡的錯誤處理,當然了,在實際編碼過程中,像os.Open這類的函數是一定要檢査其返迴的error值的;爲了減少例子程序的代碼量,我們姑且簡化掉這些不太可能返迴錯誤的處理邏輯。後面的例子里我們會跳過錯誤檢査。在5.4節中我們會對錯誤處理做更詳細的闡述。
|
||||
|
||||
@@ -167,9 +167,9 @@ func main() {
|
||||
}
|
||||
```
|
||||
|
||||
ReadFile函數返迴一個byte的slice,這個slice必鬚被轉換爲string,之後纔能夠用string.Split方法來進行處理。我們在3.5.4節中會更詳細地講解string和byte slice(字節數組)。
|
||||
ReadFile函數返迴一個byte的slice,這個slice必鬚被轉換爲string,之後才能夠用string.Split方法來進行處理。我們在3.5.4節中會更詳細地講解string和byte slice(字節數組)。
|
||||
|
||||
在更底層一些的地方,bufio.Scanner,ioutil.ReadFile和ioutil.WriteFile使用的是*os.File的Read和Write方法,不過一般程序員併不需要去直接了解到其底層實現細節,在bufio和io/ioutil包中提供的方法已經足夠好用。
|
||||
|
||||
**練習 1.4:** 脩改dup2,使其可以打印重複的行分别齣現在哪些文件。
|
||||
**練習 1.4:** 脩改dup2,使其可以打印重複的行分别出現在哪些文件。
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
## 1.4. GIF動畵
|
||||
|
||||
下面的程序會演示Go語言標準庫里的image這個package的用法,我們會用這個包來生成一繫列的bit-mapped圖,然後將這些圖片編碼爲一個GIF動畵。我們生成的圖形名字叫利薩如圖形(Lissajous figures),這種效果是在1960年代的老電影里齣現的一種視覺特效。它們是協振子在兩個緯度上振動所産生的麴線,比如兩個sin正絃波分别在x軸和y軸輸入會産生的麴線。圖1.1是這樣的一個例子:
|
||||
下面的程序會演示Go語言標準庫里的image這個package的用法,我們會用這個包來生成一繫列的bit-mapped圖,然後將這些圖片編碼爲一個GIF動畵。我們生成的圖形名字叫利薩如圖形(Lissajous figures),這種效果是在1960年代的老電影里出現的一種視覺特效。它們是協振子在兩個緯度上振動所産生的麴線,比如兩個sin正絃波分别在x軸和y軸輸入會産生的麴線。圖1.1是這樣的一個例子:
|
||||
|
||||

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

|
||||
|
||||
@@ -69,17 +69,17 @@ bla kIndex)
|
||||
|
||||
當我們import了一個包路徑包含有多個單詞的package時,比如image/color(image和color兩個單詞),通常我們隻需要用最後那個單詞表示這個包就可以。所以當我們寫color.White時,這個變量指向的是image/color包里的變量,同理gif.GIF是屬於image/gif包里的變量。
|
||||
|
||||
這個程序里的常量聲明給齣了一繫列的常量值,常量是指在程序編譯後運行時始終都不會變化的值,比如圈數、幀數、延遲值。常量聲明和變量聲明一般都會齣現在包級别,所以這些常量在整個包中都是可以共享的,或者你也可以把常量聲明定義在函數體內部,那麽這種常量就隻能在函數體內用。目前常量聲明的值必鬚是一個數字值、字符串或者一個固定的boolean值。
|
||||
這個程序里的常量聲明給出了一繫列的常量值,常量是指在程序編譯後運行時始終都不會變化的值,比如圈數、幀數、延遲值。常量聲明和變量聲明一般都會出現在包級别,所以這些常量在整個包中都是可以共享的,或者你也可以把常量聲明定義在函數體內部,那麽這種常量就隻能在函數體內用。目前常量聲明的值必鬚是一個數字值、字符串或者一個固定的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)這個內置函數,將結果appen到anim中的幀列表末尾,併會設置一個默認的80ms的延遲值。最終循環結束,所有的延遲值也被編碼進了GIF圖片中,併將結果寫入到輸齣流。out這個變量是io.Writer類型,這個類型讓我們可以可以讓我們把輸齣結果寫到很多目標,很快我們就可以看到了。
|
||||
lissajous函數內部有兩層嵌套的for循環。外層循環會循環64次,每一次都會生成一個單獨的動畵幀。它生成了一個包含兩種顔色的201&201大小的圖片,白色和黑色。所有像素點都會被默認設置爲其零值(也就是palette里的第0個值),這里我們設置的是白色。每次經過內存循環都會通過設置像素爲黑色,生成一張新圖片。其結果會append到之前結果之後。這里我們用到了append(參考4.2.1)這個內置函數,將結果appen到anim中的幀列表末尾,併會設置一個默認的80ms的延遲值。最終循環結束,所有的延遲值也被編碼進了GIF圖片中,併將結果寫入到輸出流。out這個變量是io.Writer類型,這個類型讓我們可以可以讓我們把輸出結果寫到很多目標,很快我們就可以看到了。
|
||||
|
||||
內存循環設置了兩個偏振。x軸偏振使用的是一個sin函數。y軸偏振也是一個正絃波,但是其其相對x軸的偏振是一個0-3的隨機值,併且初始偏振值是一個零值,併隨着動畵的每一幀逐漸增加。循環會一直跑到x軸完成五次完整的循環。每一步它都會調用SetColorIndex來爲(x, y)點來染黑色。
|
||||
|
||||
main函數調用了lissajous函數,併且用它來向標準輸齣中打印信息,所以下面這個命令會像圖1.1中産生一個GIF動畵。
|
||||
main函數調用了lissajous函數,併且用它來向標準輸出中打印信息,所以下面這個命令會像圖1.1中産生一個GIF動畵。
|
||||
|
||||
```
|
||||
$ go build gopl.io/ch1/lissajous
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
對於很多現代應用來説,訪問互聯網上的信息和訪問本地文件繫統一樣重要。Go語言在net這個強大package的幫助下提供了一繫列的package來做這件事情,使用這些包可以更簡單地用網絡收發信息,還可以建立更底層的網絡連接,編寫服務器程序。在這些情景下,Go語言原生的併發特性(在第八章中會介紹)就顯得尤其好用了。
|
||||
|
||||
爲了最簡單地展示基於HTTP穫取信息的方式,下面給齣一個示例程序fetch,這個程序將穫取對應的url,併將其源文本打印齣來;這個例子的靈感來源於curl工具(譯註:unix下的一個網絡相關的工具)。當然了,curl提供的功能更爲複雜豐富,這里我們隻編寫最簡單的樣例。之後我們還會在本書中經常用到這個例子。
|
||||
爲了最簡單地展示基於HTTP穫取信息的方式,下面給出一個示例程序fetch,這個程序將穫取對應的url,併將其源文本打印出來;這個例子的靈感來源於curl工具(譯註:unix下的一個網絡相關的工具)。當然了,curl提供的功能更爲複雜豐富,這里我們隻編寫最簡單的樣例。之後我們還會在本書中經常用到這個例子。
|
||||
|
||||
```go
|
||||
gopl.io/ch1/fetch
|
||||
@@ -34,7 +34,7 @@ func main() {
|
||||
}
|
||||
```
|
||||
|
||||
這個程序從兩個package中導入了函數,net/http和io/ioutil包,http.Get函數是創建HTTP請求的函數,如果穫取過程沒有齣錯,那麽會在resp這個結構體中得到訪問的請求結果。resp的Body字段包括一個可讀的服務器響應流。這之後ioutil.ReadAll函數從response中讀取到全部內容;其結果保存在變量b中。resp.Body.Close這一句會關閉resp的Body流,防止資源洩露,Printf函數會將結果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
|
||||
@@ -65,5 +65,5 @@ fetch: Get http://gopl.io: dial tcp: lookup gopl.io: getaddrinfow: No such host
|
||||
|
||||
**練習 1.8:** 脩改fetch這個范例,如果輸入的url參數沒有 `http://` 前綴的話,爲這個url加上該前綴。你可能會用到strings.HasPrefix這個函數。
|
||||
|
||||
**練習 1.9:** 脩改fetch打印齣HTTP協議的狀態碼,可以從resp.Status變量得到該狀態碼。
|
||||
**練習 1.9:** 脩改fetch打印出HTTP協議的狀態碼,可以從resp.Status變量得到該狀態碼。
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
Go語言最有意思併且最新奇的特性就是其對併發編程的支持了。併發編程是一個大話題,在第八章和第九章中會專門講到。這里我們隻淺嚐輒止地來體驗一下Go語言里的goroutine和channel。
|
||||
|
||||
下面的例子fetchall,和上面的fetch程序所要做的工作是一致的,但是這個fetchall的特别之處在於它會同時去穫取所有的URL,所以這個程序的穫取時間不會超過執行時間最長的那一個任務,而不會像前面的fetch程序一樣,執行時間是所有任務執行時間之和。這次的fetchall程序隻會打印穫取的內容大小和經過的時間,不會像上面那樣打印齣穫取的內容。
|
||||
下面的例子fetchall,和上面的fetch程序所要做的工作是一致的,但是這個fetchall的特别之處在於它會同時去穫取所有的URL,所以這個程序的穫取時間不會超過執行時間最長的那一個任務,而不會像前面的fetch程序一樣,執行時間是所有任務執行時間之和。這次的fetchall程序隻會打印穫取的內容大小和經過的時間,不會像上面那樣打印出穫取的內容。
|
||||
|
||||
```go
|
||||
gopl.io/ch1/fetchall
|
||||
@@ -61,8 +61,8 @@ $ ./fetchall https://golang.org http://gopl.io https://godoc.org
|
||||
|
||||
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里的這個字符串。
|
||||
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異步執行時同時結束。
|
||||
當一個goroutine嚐試在一個channel上做send或者receive操作時,這個goroutine會阻塞在調用處,直到另一個goroutine往這個channel里寫入、或者接收了值,這樣兩個goroutine才會繼續執行操作channel完成之後的邏輯。在這個例子中,每一個fetch函數在執行時都會往channel里發送一個值(ch <- expression),主函數接收這些值(<-ch)。這個程序中我們用main函數來所有fetch函數傳迴的字符串,可以避免在goroutine異步執行時同時結束。
|
||||
|
||||
**練習 1.10:** 找一個數據量比較大的網站,用本小節中的程序調研網站的緩存策略,對每個URL執行兩遍請求,査看兩次時間是否有較大的差别,併且每次穫取到的響應內容是否一致,脩改本節中的程序,將響應結果輸齣,以便於進行對比。
|
||||
**練習 1.10:** 找一個數據量比較大的網站,用本小節中的程序調研網站的緩存策略,對每個URL執行兩遍請求,査看兩次時間是否有較大的差别,併且每次穫取到的響應內容是否一致,脩改本節中的程序,將響應結果輸出,以便於進行對比。
|
||||
|
||||
@@ -24,7 +24,7 @@ func handler(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
```
|
||||
|
||||
我們隻用了八九行代碼就實現了一個個Web服務程序,這都是多虧了標準庫里的方法已經幫我們處理了大量的工作。main函數會將所有發送到/路徑下的請求和handler函數關聯起來,/開頭的請求其實就是所有發送到當前站點上的請求,我們的服務跑在了8000端口上。發送到這個服務的“請求”是一個http.Request類型的對象,這個對象中包含了請求中的一繫列相關字段,其中就包括我們需要的URL。當請求到達服務器時,這個請求會被傳給handler函數來處理,這個函數會將/hello這個路徑從請求的URL中解析齣來,然後把其發送到響應中,這里我們用的是標準輸齣流的fmt.Fprintf。Web服務會在第7.7節中詳細闡述。
|
||||
我們隻用了八九行代碼就實現了一個個Web服務程序,這都是多虧了標準庫里的方法已經幫我們處理了大量的工作。main函數會將所有發送到/路徑下的請求和handler函數關聯起來,/開頭的請求其實就是所有發送到當前站點上的請求,我們的服務跑在了8000端口上。發送到這個服務的“請求”是一個http.Request類型的對象,這個對象中包含了請求中的一繫列相關字段,其中就包括我們需要的URL。當請求到達服務器時,這個請求會被傳給handler函數來處理,這個函數會將/hello這個路徑從請求的URL中解析出來,然後把其發送到響應中,這里我們用的是標準輸出流的fmt.Fprintf。Web服務會在第7.7節中詳細闡述。
|
||||
|
||||
讓我們在後颱運行這個服務程序。如果你的操作繫統是Mac OS X或者Linux,那麽在運行命令的末尾加上一個&符號,卽可讓程序簡單地跑在後颱,而在windows下,你需要在另外一個命令行窗口去運行這個程序了。
|
||||
|
||||
@@ -46,7 +46,7 @@ URL.Path = "/help"
|
||||
|
||||

|
||||
|
||||
在這個服務的基礎上疊加特性是很容易的。一種比較實用的脩改是爲訪問的url添加某種狀態。比如,下面這個版本輸齣了同樣的內容,但是會對請求的次數進行計算;對URL的請求結果會包含各種URL被訪問的總次數,直接對/count這個URL的訪問要除外。
|
||||
在這個服務的基礎上疊加特性是很容易的。一種比較實用的脩改是爲訪問的url添加某種狀態。比如,下面這個版本輸出了同樣的內容,但是會對請求的次數進行計算;對URL的請求結果會包含各種URL被訪問的總次數,直接對/count這個URL的訪問要除外。
|
||||
|
||||
```go
|
||||
gopl.io/ch1/server2
|
||||
@@ -87,7 +87,7 @@ func counter(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
這個服務器有兩個請求處理函數,請求的url會決定具體調用哪一個:對/count這個url的請求會調用到count這個函數,其它所有的url都會調用默認的處理函數。如果你的請求pattern是以/結尾,那麽所有以該url爲前綴的url都會被這條規則匹配。在這些代碼的背後,服務器每一次接收請求處理時都會另起一個goroutine,這樣服務器就可以同一時間處理多數請求。然而在併發情況下,假如眞的有兩個請求同一時刻去更新count,那麽這個值可能併不會被正確地增加;這個程序可能會被引發一個嚴重的bug:競態條件(參見9.1)。爲了避免這個問題,我們必鬚保證每次脩改變量的最多隻能有一個goroutine,這也就是代碼里的mu.Lock()和mu.Unlock()調用將脩改count的所有行爲包在中間的目的。第九章中我們會進一步講解共享變量。
|
||||
|
||||
下面是一個更爲豐富的例子,handler函數會把請求的http頭和請求的form數據都打印齣來,這樣可以讓檢査和調試這個服務更爲方便:
|
||||
下面是一個更爲豐富的例子,handler函數會把請求的http頭和請求的form數據都打印出來,這樣可以讓檢査和調試這個服務更爲方便:
|
||||
|
||||
```go
|
||||
gopl.io/ch1/server3
|
||||
@@ -108,7 +108,7 @@ func handler(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
```
|
||||
|
||||
我們用http.Request這個struct里的字段來輸齣下面這樣的內容:
|
||||
我們用http.Request這個struct里的字段來輸出下面這樣的內容:
|
||||
|
||||
```
|
||||
GET /?q=query HTTP/1.1
|
||||
@@ -119,7 +119,7 @@ RemoteAddr = "127.0.0.1:59911"
|
||||
Form["q"] = ["query"]
|
||||
```
|
||||
|
||||
可以看到這里的ParseForm被嵌套在了if語句中。Go語言允許這樣的一個簡單的語句結果作爲循環的變量聲明齣現在if語句的最前面,這一點對錯誤處理很有用處。我們還可以像下面這樣寫(當然看起來就長了一些):
|
||||
可以看到這里的ParseForm被嵌套在了if語句中。Go語言允許這樣的一個簡單的語句結果作爲循環的變量聲明出現在if語句的最前面,這一點對錯誤處理很有用處。我們還可以像下面這樣寫(當然看起來就長了一些):
|
||||
|
||||
```go
|
||||
err := r.ParseForm()
|
||||
@@ -130,11 +130,11 @@ if err != nil {
|
||||
|
||||
用if和ParseForm結合可以讓代碼更加簡單,併且可以限製err這個變量的作用域,這麽做是很不錯的。我們會在2.7節中講解作用域。
|
||||
|
||||
在這些程序中,我們看到了很多不同的類型被輸齣到標準輸齣流中。比如前面的fetch程序,就把HTTP的響應數據拷貝到了os.Stdout,或者在lissajous程序里我們輸齣的是一個文件。fetchall程序則完全忽略到了HTTP的響應體,隻是計算了一下響應體的大小,這個程序中把響應體拷貝到了ioutil.Discard。在本節的web服務器程序中則是用fmt.Fprintf直接寫到了http.ResponseWriter中。
|
||||
在這些程序中,我們看到了很多不同的類型被輸出到標準輸出流中。比如前面的fetch程序,就把HTTP的響應數據拷貝到了os.Stdout,或者在lissajous程序里我們輸出的是一個文件。fetchall程序則完全忽略到了HTTP的響應體,隻是計算了一下響應體的大小,這個程序中把響應體拷貝到了ioutil.Discard。在本節的web服務器程序中則是用fmt.Fprintf直接寫到了http.ResponseWriter中。
|
||||
|
||||
盡管這三種具體的實現流程併不太一樣,他們都實現一個共同的接口,卽當它們被調用需要一個標準流輸齣時都可以滿足。這個接口叫作io.Writer,在7.1節中會詳細討論。
|
||||
盡管這三種具體的實現流程併不太一樣,他們都實現一個共同的接口,卽當它們被調用需要一個標準流輸出時都可以滿足。這個接口叫作io.Writer,在7.1節中會詳細討論。
|
||||
|
||||
Go語言的接口機製會在第7章中講解,爲了在這里簡單説明接口能做什麽,讓我們簡單地將這里的web服務器和之前寫的lissajous函數結合起來,這樣GIF動畵可以被寫到HTTP的客戶端,而不是之前的標準輸齣流。隻要在web服務器的代碼里加入下面這幾行。
|
||||
Go語言的接口機製會在第7章中講解,爲了在這里簡單説明接口能做什麽,讓我們簡單地將這里的web服務器和之前寫的lissajous函數結合起來,這樣GIF動畵可以被寫到HTTP的客戶端,而不是之前的標準輸出流。隻要在web服務器的代碼里加入下面這幾行。
|
||||
|
||||
```Go
|
||||
handler := func(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
@@ -15,7 +15,7 @@ default:
|
||||
}
|
||||
```
|
||||
|
||||
在翻轉硬幣的時候,例子里的coinflip函數返迴幾種不同的結果,每一個case都會對應個返迴結果,這里需要註意,Go語言併不需要顯式地去在每一個case後寫break,語言默認執行完case後的邏輯語句會自動退齣。當然了,如果你想要相鄰的幾個case都執行同一邏輯的話,需要自己顯式地寫上一個fallthrough語句來覆蓋這種默認行爲。不過fallthrough語句在一般的編程中用到得很少。
|
||||
在翻轉硬幣的時候,例子里的coinflip函數返迴幾種不同的結果,每一個case都會對應個返迴結果,這里需要註意,Go語言併不需要顯式地去在每一個case後寫break,語言默認執行完case後的邏輯語句會自動退出。當然了,如果你想要相鄰的幾個case都執行同一邏輯的話,需要自己顯式地寫上一個fallthrough語句來覆蓋這種默認行爲。不過fallthrough語句在一般的編程中用到得很少。
|
||||
|
||||
Go語言里的switch還可以不帶操作對象(譯註:switch不帶操作對象時默認用true值代替,然後將每個case的表達式和true值進行比較);可以直接羅列多種條件,像其它語言里面的多個if else一樣,下面是一個例子:
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# 第1章 入門
|
||||
|
||||
本章會介紹Go語言里的一些基本組件。我們希望用信息和例子盡快帶你入門。本章和之後章節的例子都是針對眞實的開發案例給齣。本章我們隻是簡單地爲你介紹一些Go語言的入門例子,從簡單的文件處理、圖像處理到互聯網併發客戶端和服務端程序。當然,在第一章我們不會詳盡地一一去説明細枝末節,不過用這些程序來學習一門新語言肯定是很有效的。
|
||||
本章會介紹Go語言里的一些基本組件。我們希望用信息和例子盡快帶你入門。本章和之後章節的例子都是針對眞實的開發案例給出。本章我們隻是簡單地爲你介紹一些Go語言的入門例子,從簡單的文件處理、圖像處理到互聯網併發客戶端和服務端程序。當然,在第一章我們不會詳盡地一一去説明細枝末節,不過用這些程序來學習一門新語言肯定是很有效的。
|
||||
|
||||
當你學習一門新語言時,你會用這門新語言去重寫自己以前熟悉語言例子的傾向。在學習Go語言的過程中,盡量避免這麽做。我們會向你演示如何纔能寫齣好的Go語言程序,所以請使用這里的代碼作爲你寫自己的Go程序時的指南。
|
||||
當你學習一門新語言時,你會用這門新語言去重寫自己以前熟悉語言例子的傾向。在學習Go語言的過程中,盡量避免這麽做。我們會向你演示如何才能寫出好的Go語言程序,所以請使用這里的代碼作爲你寫自己的Go程序時的指南。
|
||||
|
||||
Reference in New Issue
Block a user