mirror of
https://github.com/gopl-zh/gopl-zh.github.com.git
synced 2025-12-18 11:44:20 +08:00
回到简体
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
## 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語言一些核心理念。
|
||||
我们以现已成为传统的“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语言一些核心理念。
|
||||
|
||||
<u><i>gopl.io/ch1/helloworld</i></u>
|
||||
```go
|
||||
@@ -13,76 +13,76 @@ func main() {
|
||||
}
|
||||
```
|
||||
|
||||
Go是一門編譯型語言,Go語言的工具鏈將源代碼及其依賴轉換成計算機的機器指令[^2]。Go語言提供的工具都通過一個單獨的命令`go`調用,`go`命令有一繫列子命令。最簡單的一個子命令就是run。這個命令編譯一個或多個以.go結尾的源文件,鏈接庫文件,併運行最終生成的可執行文件。(本書使用$表示命令行提示符。)
|
||||
Go是一门编译型语言,Go语言的工具链将源代码及其依赖转换成计算机的机器指令[^2]。Go语言提供的工具都通过一个单独的命令`go`调用,`go`命令有一系列子命令。最简单的一个子命令就是run。这个命令编译一个或多个以.go结尾的源文件,链接库文件,并运行最终生成的可执行文件。(本书使用$表示命令行提示符。)
|
||||
|
||||
```
|
||||
$ go run helloworld.go
|
||||
```
|
||||
|
||||
毫無意外,這個命令會輸出:
|
||||
毫无意外,这个命令会输出:
|
||||
|
||||
```
|
||||
Hello, 世界
|
||||
```
|
||||
|
||||
Go語言原生支持Unicode,它可以處理全世界任何語言的文本。
|
||||
Go语言原生支持Unicode,它可以处理全世界任何语言的文本。
|
||||
|
||||
如果不隻是一次性實驗,你肯定希望能夠編譯這個程序,保存編譯結果以備將來之用。可以用build子命令:
|
||||
如果不只是一次性实验,你肯定希望能够编译这个程序,保存编译结果以备将来之用。可以用build子命令:
|
||||
|
||||
```
|
||||
$ go build helloworld.go
|
||||
```
|
||||
|
||||
這個命令生成一個名爲helloworld的可執行的二進製文件[^3],之後你可以隨時運行它[^4],不需任何處理[^5]。
|
||||
这个命令生成一个名为helloworld的可执行的二进制文件[^3],之后你可以随时运行它[^4],不需任何处理[^5]。
|
||||
|
||||
```
|
||||
$ ./helloworld
|
||||
Hello, 世界
|
||||
```
|
||||
|
||||
本書中, 所有的示例代碼上都有一行標記,利用這些標記, 可以從[gopl.io](http://gopl.io)網站上本書源碼倉庫里獲取代碼:
|
||||
本书中, 所有的示例代码上都有一行标记,利用这些标记, 可以从[gopl.io](http://gopl.io)网站上本书源码仓库里获取代码:
|
||||
|
||||
```
|
||||
gopl.io/ch1/helloworld
|
||||
```
|
||||
|
||||
執行 `go get gopl.io/ch1/helloworld` 命令,就會從網上獲取代碼,併放到對應目録中[^6]。2.6和10.7節有這方面更詳細的介紹。
|
||||
执行 `go get gopl.io/ch1/helloworld` 命令,就会从网上获取代码,并放到对应目录中[^6]。2.6和10.7节有这方面更详细的介绍。
|
||||
|
||||
來討論下程序本身。Go語言的代碼通過**包**(package)組織,包類似於其它語言里的庫(libraries)或者模塊(modules)。一個包由位於單個目録下的一個或多個.go源代碼文件組成, 目録定義包的作用。每個源文件都以一條`package`聲明語句開始,這個例子里就是`package main`, 表示該文件屬於哪個包,緊跟着一繫列導入(import)的包,之後是存儲在這個文件里的程序語句。
|
||||
来讨论下程序本身。Go语言的代码通过**包**(package)组织,包类似于其它语言里的库(libraries)或者模块(modules)。一个包由位于单个目录下的一个或多个.go源代码文件组成, 目录定义包的作用。每个源文件都以一条`package`声明语句开始,这个例子里就是`package main`, 表示该文件属于哪个包,紧跟着一系列导入(import)的包,之后是存储在这个文件里的程序语句。
|
||||
|
||||
Go的標準庫提供了100多個包,以支持常見功能,如輸入、輸出、排序以及文本處理。比如`fmt`包,就含有格式化輸出、接收輸入的函數。`Println`是其中一個基礎函數,可以打印以空格間隔的一個或多個值,併在最後添加一個換行符,從而輸出一整行。
|
||||
Go的标准库提供了100多个包,以支持常见功能,如输入、输出、排序以及文本处理。比如`fmt`包,就含有格式化输出、接收输入的函数。`Println`是其中一个基础函数,可以打印以空格间隔的一个或多个值,并在最后添加一个换行符,从而输出一整行。
|
||||
|
||||
`main`包比較特殊。它定義了一個獨立可執行的程序,而不是一個庫。在`main`里的`main` *函數* 也很特殊,它是整個程序執行時的入口[^7]。`main`函數所做的事情就是程序做的。當然了,`main`函數一般調用其它包里的函數完成很多工作, 比如`fmt.Println`。
|
||||
`main`包比较特殊。它定义了一个独立可执行的程序,而不是一个库。在`main`里的`main` *函数* 也很特殊,它是整个程序执行时的入口[^7]。`main`函数所做的事情就是程序做的。当然了,`main`函数一般调用其它包里的函数完成很多工作, 比如`fmt.Println`。
|
||||
|
||||
必須告訴編譯器源文件需要哪些包,這就是`import`聲明以及隨後的`package`聲明扮演的角色。hello world例子隻用到了一個包,大多數程序需要導入多個包。
|
||||
必须告诉编译器源文件需要哪些包,这就是`import`声明以及随后的`package`声明扮演的角色。hello world例子只用到了一个包,大多数程序需要导入多个包。
|
||||
|
||||
必須恰當導入需要的包,缺少了必要的包或者導入了不需要的包,程序都無法編譯通過。這項嚴格要求避免了程序開發過程中引入未使用的包[^8]。
|
||||
必须恰当导入需要的包,缺少了必要的包或者导入了不需要的包,程序都无法编译通过。这项严格要求避免了程序开发过程中引入未使用的包[^8]。
|
||||
|
||||
`import`聲明必須跟在文件的`package`聲明之後。隨後,則是組成程序的函數、變量、常量、類型的聲明語句(分别由關鍵字`func`, `var`, `const`, `type`定義)。這些內容的聲明順序併不重要[^9]。這個例子的程序已經盡可能短了,隻聲明了一個函數, 其中隻調用了一個其他函數。爲了節省篇幅,有些時候, 示例程序會省略`package`和`import`聲明,但是,這些聲明在源代碼里有,併且必須得有才能編譯。
|
||||
`import`声明必须跟在文件的`package`声明之后。随后,则是组成程序的函数、变量、常量、类型的声明语句(分别由关键字`func`, `var`, `const`, `type`定义)。这些内容的声明顺序并不重要[^9]。这个例子的程序已经尽可能短了,只声明了一个函数, 其中只调用了一个其他函数。为了节省篇幅,有些时候, 示例程序会省略`package`和`import`声明,但是,这些声明在源代码里有,并且必须得有才能编译。
|
||||
|
||||
一個函數的聲明由`func`關鍵字、函數名、參數列表、返迴值列表(這個例子里的`main`函數參數列表和返迴值都是空的)以及包含在大括號里的函數體組成。第五章進一步考察函數。
|
||||
一个函数的声明由`func`关键字、函数名、参数列表、返回值列表(这个例子里的`main`函数参数列表和返回值都是空的)以及包含在大括号里的函数体组成。第五章进一步考察函数。
|
||||
|
||||
Go語言不需要在語句或者聲明的末尾添加分號,除非一行上有多條語句。實際上,編譯器會主動把特定符號後的換行符轉換爲分號, 因此換行符添加的位置會影響Go代碼的正確解析[^10]。。舉個例子, 函數的左括號`{`必須和`func`函數聲明在同一行上, 且位於末尾,不能獨占一行,而在表達式`x + y`中,可在`+`後換行,不能在`+`前換行。
|
||||
Go语言不需要在语句或者声明的末尾添加分号,除非一行上有多条语句。实际上,编译器会主动把特定符号后的换行符转换为分号, 因此换行符添加的位置会影响Go代码的正确解析[^10]。。举个例子, 函数的左括号`{`必须和`func`函数声明在同一行上, 且位于末尾,不能独占一行,而在表达式`x + y`中,可在`+`后换行,不能在`+`前换行。
|
||||
|
||||
Go語言在代碼格式上采取了很強硬的態度。`gofmt`工具把代碼格式化爲標準格式[^12],併且`go`工具中的`fmt`子命令會對指定包, 否則默認爲當前目録, 中所有.go源文件應用`gofmt`命令。本書中的所有代碼都被gofmt過。你也應該養成格式化自己的代碼的習慣。以法令方式規定標準的代碼格式可以避免無盡的無意義的瑣碎爭執[^13]。更重要的是,這樣可以做多種自動源碼轉換,如果放任Go語言代碼格式,這些轉換就不大可能了。
|
||||
Go语言在代码格式上采取了很强硬的态度。`gofmt`工具把代码格式化为标准格式[^12],并且`go`工具中的`fmt`子命令会对指定包, 否则默认为当前目录, 中所有.go源文件应用`gofmt`命令。本书中的所有代码都被gofmt过。你也应该养成格式化自己的代码的习惯。以法令方式规定标准的代码格式可以避免无尽的无意义的琐碎争执[^13]。更重要的是,这样可以做多种自动源码转换,如果放任Go语言代码格式,这些转换就不大可能了。
|
||||
|
||||
很多文本編輯器都可以配置爲保存文件時自動執行`gofmt`,這樣你的源代碼總會被恰當地格式化。還有個相關的工具,`goimports`,可以根據代碼需要, 自動地添加或刪除`import`聲明。這個工具併沒有包含在標準的分發包中,可以用下面的命令安裝:
|
||||
很多文本编辑器都可以配置为保存文件时自动执行`gofmt`,这样你的源代码总会被恰当地格式化。还有个相关的工具,`goimports`,可以根据代码需要, 自动地添加或删除`import`声明。这个工具并没有包含在标准的分发包中,可以用下面的命令安装:
|
||||
```
|
||||
$ go get golang.org/x/tools/cmd/goimports
|
||||
```
|
||||
|
||||
對於大多數用戶來説,下載、編譯包、運行測試用例、察看Go語言的文檔等等常用功能都可以用go的工具完成。10.7節詳細介紹這些知識。
|
||||
对于大多数用户来说,下载、编译包、运行测试用例、察看Go语言的文档等等常用功能都可以用go的工具完成。10.7节详细介绍这些知识。
|
||||
|
||||
[^1]: 本書作者之一Brian W. Kernighan也是《The C Programming Language》一書的作者。
|
||||
[^2]: 靜態編譯。
|
||||
[^3]: Windows繫統下生成的可執行文件是helloworld.exe,增加了.exe後綴名。
|
||||
[^4]: 在Windows繫統下在命令行直接輸入helloworld.exe命令運行。
|
||||
[^5]: 因爲靜態編譯,所以不用擔心在繫統庫更新的時候衝突,幸福感滿滿。
|
||||
[^6]: 需要先安裝Git或Hg之類的版本管理工具,併將對應的命令添加到PATH環境變量中。序言已經提及,需要先設置好GOPATH環境變量,下載的代碼會放在`$GOPATH/src/gopl.io/ch1/helloworld`目録。
|
||||
[^7]: C繫語言差不多都這樣。
|
||||
[^8]: Go語言編譯過程沒有警告信息,爭議特性之一。
|
||||
[^9]: 最好還是定一下規范。
|
||||
[^10]: 比如行末是標識符、整數、浮點數、虛數、字符或字符串文字、關鍵字`break`、`continue`、`fallthrough`或`return`中的一個、運算符和分隔符`++`、`--`、`)`、`]`或`}`中的一個。
|
||||
[^11]: 以+結尾的話不會被插入分號分隔符,但是以x結尾的話則會被分號分隔符,從而導致編譯錯誤。
|
||||
[^12]: 這個格式化工具沒有任何可以調整代碼格式的參數,Go語言就是這麽任性。
|
||||
[^13]: 也導致了Go語言的TIOBE排名較低,因爲缺少撕逼的話題。
|
||||
[^1]: 本书作者之一Brian W. Kernighan也是《The C Programming Language》一书的作者。
|
||||
[^2]: 静态编译。
|
||||
[^3]: Windows系统下生成的可执行文件是helloworld.exe,增加了.exe后缀名。
|
||||
[^4]: 在Windows系统下在命令行直接输入helloworld.exe命令运行。
|
||||
[^5]: 因为静态编译,所以不用担心在系统库更新的时候冲突,幸福感满满。
|
||||
[^6]: 需要先安装Git或Hg之类的版本管理工具,并将对应的命令添加到PATH环境变量中。序言已经提及,需要先设置好GOPATH环境变量,下载的代码会放在`$GOPATH/src/gopl.io/ch1/helloworld`目录。
|
||||
[^7]: C系语言差不多都这样。
|
||||
[^8]: Go语言编译过程没有警告信息,争议特性之一。
|
||||
[^9]: 最好还是定一下规范。
|
||||
[^10]: 比如行末是标识符、整数、浮点数、虚数、字符或字符串文字、关键字`break`、`continue`、`fallthrough`或`return`中的一个、运算符和分隔符`++`、`--`、`)`、`]`或`}`中的一个。
|
||||
[^11]: 以+结尾的话不会被插入分号分隔符,但是以x结尾的话则会被分号分隔符,从而导致编译错误。
|
||||
[^12]: 这个格式化工具没有任何可以调整代码格式的参数,Go语言就是这么任性。
|
||||
[^13]: 也导致了Go语言的TIOBE排名较低,因为缺少撕逼的话题。
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
## 1.2. 命令行參數
|
||||
## 1.2. 命令行参数
|
||||
|
||||
大多數的程序都是處理輸入,産生輸出;這也正是“計算”的定義。但是, 程序如何獲取要處理的輸入數據呢?一些程序生成自己的數據,但通常情況下,輸入來自於程序外部:文件、網絡連接、其它程序的輸出、敲鍵盤的用戶、命令行參數或其它類似輸入源。下面幾個例子會討論其中幾個輸入源,首先是命令行參數。
|
||||
大多数的程序都是处理输入,产生输出;这也正是“计算”的定义。但是, 程序如何获取要处理的输入数据呢?一些程序生成自己的数据,但通常情况下,输入来自于程序外部:文件、网络连接、其它程序的输出、敲键盘的用户、命令行参数或其它类似输入源。下面几个例子会讨论其中几个输入源,首先是命令行参数。
|
||||
|
||||
`os`包以跨平台的方式,提供了一些與操作繫統交互的函數和變量。程序的命令行參數可從os包的Args變量獲取;os包外部使用os.Args訪問該變量。
|
||||
`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变量是一个字符串(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:]。
|
||||
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工具格式化時按照字母順序對包名排序。(示例有多個版本時,我們會對示例編號, 這樣可以明確當前正在討論的是哪個。)
|
||||
下面是Unix里echo命令的一份实现,echo把它的命令行参数打印成一行。程序导入了两个包,用括号把它们括起来写成列表形式, 而没有分开写成独立的`import`声明。两种形式都合法,列表形式习惯上用得多。包导入顺序并不重要;gofmt工具格式化时按照字母顺序对包名排序。(示例有多个版本时,我们会对示例编号, 这样可以明确当前正在讨论的是哪个。)
|
||||
|
||||
<u><i>gopl.io/ch1/echo1</i></u>
|
||||
```go
|
||||
@@ -30,37 +30,37 @@ func main() {
|
||||
}
|
||||
```
|
||||
|
||||
註釋語句以`//`開頭。對於程序員來説,//之後到行末之間所有的內容都是註釋,被編譯器忽略。按照慣例,我們在每個包的包聲明前添加註釋;對於`main package`,註釋包含一句或幾句話,從整體角度對程序做個描述。
|
||||
注释语句以`//`开头。对于程序员来说,//之后到行末之间所有的内容都是注释,被编译器忽略。按照惯例,我们在每个包的包声明前添加注释;对于`main package`,注释包含一句或几句话,从整体角度对程序做个描述。
|
||||
|
||||
var聲明定義了兩個string類型的變量s和sep。變量會在聲明時直接初始化。如果變量沒有顯式初始化,則被隱式地賦予其類型的*零值*(zero value),數值類型是0,字符串類型是空字符串""。這個例子里,聲明把s和sep隱式地初始化成空字符串。第2章再來詳細地講解變量和聲明。
|
||||
var声明定义了两个string类型的变量s和sep。变量会在声明时直接初始化。如果变量没有显式初始化,则被隐式地赋予其类型的*零值*(zero value),数值类型是0,字符串类型是空字符串""。这个例子里,声明把s和sep隐式地初始化成空字符串。第2章再来详细地讲解变量和声明。
|
||||
|
||||
對數值類型,Go語言提供了常規的數值和邏輯運算符。而對string類型,`+`運算符連接字符串(譯註:和C++或者js是一樣的)。所以表達式:
|
||||
对数值类型,Go语言提供了常规的数值和逻辑运算符。而对string类型,`+`运算符连接字符串(译注:和C++或者js是一样的)。所以表达式:
|
||||
|
||||
```go
|
||||
sep + os.Args[i]
|
||||
```
|
||||
|
||||
表示連接字符串sep和os.Args。程序中使用的語句:
|
||||
表示连接字符串sep和os.Args。程序中使用的语句:
|
||||
|
||||
```go
|
||||
s += sep + os.Args[i]
|
||||
```
|
||||
|
||||
是一條*賦值語句*, 將s的舊值跟sep與os.Args[i]連接後賦值迴s,等價於:
|
||||
是一条*赋值语句*, 将s的旧值跟sep与os.Args[i]连接后赋值回s,等价于:
|
||||
|
||||
```go
|
||||
s = s + sep + os.Args[i]
|
||||
```
|
||||
|
||||
運算符`+=`是賦值運算符(assignment operator),每種數值運算符或邏輯運算符,如`+`或`*`,都有對應的賦值運算符。
|
||||
运算符`+=`是赋值运算符(assignment operator),每种数值运算符或逻辑运算符,如`+`或`*`,都有对应的赋值运算符。
|
||||
|
||||
echo程序可以每循環一次輸出一個參數,這個版本卻是不斷地把新文本追加到末尾來構造字符串。字符串s開始爲空,卽值爲"",每次循環會添加一些文本;第一次迭代之後,還會再插入一個空格,因此循環結束時每個參數中間都有一個空格。這是一種二次加工(quadratic process),當參數數量龐大時,開銷很大,但是對於echo,這種情形不大可能出現。本章會介紹echo的若榦改進版,下一章解決低效問題。
|
||||
echo程序可以每循环一次输出一个参数,这个版本却是不断地把新文本追加到末尾来构造字符串。字符串s开始为空,即值为"",每次循环会添加一些文本;第一次迭代之后,还会再插入一个空格,因此循环结束时每个参数中间都有一个空格。这是一种二次加工(quadratic process),当参数数量庞大时,开销很大,但是对于echo,这种情形不大可能出现。本章会介绍echo的若干改进版,下一章解决低效问题。
|
||||
|
||||
循環索引變量i在for循環的第一部分中定義。符號`:=`是*短變量聲明*(short variable declaration)的一部分, 這是定義一個或多個變量併根據它們的初始值爲這些變量賦予適當類型的語句。下一章有這方面更多説明。
|
||||
循环索引变量i在for循环的第一部分中定义。符号`:=`是*短变量声明*(short variable declaration)的一部分, 这是定义一个或多个变量并根据它们的初始值为这些变量赋予适当类型的语句。下一章有这方面更多说明。
|
||||
|
||||
自增語句`i++`給`i`加1;這和`i += 1`以及`i = i + 1`都是等價的。對應的還有`i--`給`i`減1。它們是語句,而不像C繫的其它語言那樣是表達式。所以`j = i++`非法,而且++和--都隻能放在變量名後面,因此`--i`也非法。
|
||||
自增语句`i++`给`i`加1;这和`i += 1`以及`i = i + 1`都是等价的。对应的还有`i--`给`i`减1。它们是语句,而不像C系的其它语言那样是表达式。所以`j = i++`非法,而且++和--都只能放在变量名后面,因此`--i`也非法。
|
||||
|
||||
Go語言隻有for循環這一種循環語句。for循環有多種形式,其中一種如下所示:
|
||||
Go语言只有for循环这一种循环语句。for循环有多种形式,其中一种如下所示:
|
||||
|
||||
```go
|
||||
for initialization; condition; post {
|
||||
@@ -68,11 +68,11 @@ for initialization; condition; post {
|
||||
}
|
||||
```
|
||||
|
||||
for循環三個部分不需括號包圍。大括號強製要求, 左大括號必須和*post*語句在同一行。
|
||||
for循环三个部分不需括号包围。大括号强制要求, 左大括号必须和*post*语句在同一行。
|
||||
|
||||
*initialization*語句是可選的,在循環開始前執行。*initalization*如果存在,必須是一條*簡單語句*(simple statement),卽,短變量聲明、自增語句、賦值語句或函數調用。`condition`是一個布爾表達式(boolean expression),其值在每次循環迭代開始時計算。如果爲`true`則執行循環體語句。`post`語句在循環體執行結束後執行,之後再次對`conditon`求值。`condition`值爲`false`時,循環結束。
|
||||
*initialization*语句是可选的,在循环开始前执行。*initalization*如果存在,必须是一条*简单语句*(simple statement),即,短变量声明、自增语句、赋值语句或函数调用。`condition`是一个布尔表达式(boolean expression),其值在每次循环迭代开始时计算。如果为`true`则执行循环体语句。`post`语句在循环体执行结束后执行,之后再次对`conditon`求值。`condition`值为`false`时,循环结束。
|
||||
|
||||
for循環的這三個部分每個都可以省略,如果省略`initialization`和`post`,分號也可以省略:
|
||||
for循环的这三个部分每个都可以省略,如果省略`initialization`和`post`,分号也可以省略:
|
||||
|
||||
```go
|
||||
// a traditional "while" loop
|
||||
@@ -81,7 +81,7 @@ for condition {
|
||||
}
|
||||
```
|
||||
|
||||
如果連`condition`也省略了,像下面這樣:
|
||||
如果连`condition`也省略了,像下面这样:
|
||||
|
||||
```go
|
||||
// a traditional infinite loop
|
||||
@@ -90,9 +90,9 @@ for {
|
||||
}
|
||||
```
|
||||
|
||||
這就變成一個無限循環,盡管如此,還可以用其他方式終止循環, 如一條`break`或`return`語句。
|
||||
这就变成一个无限循环,尽管如此,还可以用其他方式终止循环, 如一条`break`或`return`语句。
|
||||
|
||||
`for`循環的另一種形式, 在某種數據類型的區間(range)上遍歷,如字符串或切片。`echo`的第二版本展示了這種形式:
|
||||
`for`循环的另一种形式, 在某种数据类型的区间(range)上遍历,如字符串或切片。`echo`的第二版本展示了这种形式:
|
||||
|
||||
<u><i>gopl.io/ch1/echo2</i></u>
|
||||
```go
|
||||
@@ -113,11 +113,11 @@ func main() {
|
||||
}
|
||||
```
|
||||
|
||||
每次循環迭代,`range`産生一對值;索引以及在該索引處的元素值。這個例子不需要索引,但`range`的語法要求, 要處理元素, 必須處理索引。一種思路是把索引賦值給一個臨時變量, 如`temp`, 然後忽略它的值,但Go語言不允許使用無用的局部變量(local variables),因爲這會導致編譯錯誤。
|
||||
每次循环迭代,`range`产生一对值;索引以及在该索引处的元素值。这个例子不需要索引,但`range`的语法要求, 要处理元素, 必须处理索引。一种思路是把索引赋值给一个临时变量, 如`temp`, 然后忽略它的值,但Go语言不允许使用无用的局部变量(local variables),因为这会导致编译错误。
|
||||
|
||||
Go語言中這種情況的解決方法是用`空標識符`(blank identifier),卽`_`(也就是下劃線)。空標識符可用於任何語法需要變量名但程序邏輯不需要的時候, 例如, 在循環里,丟棄不需要的循環索引, 保留元素值。大多數的Go程序員都會像上面這樣使用`range`和`_`寫`echo`程序,因爲隱式地而非顯示地索引os.Args,容易寫對。
|
||||
Go语言中这种情况的解决方法是用`空标识符`(blank identifier),即`_`(也就是下划线)。空标识符可用于任何语法需要变量名但程序逻辑不需要的时候, 例如, 在循环里,丢弃不需要的循环索引, 保留元素值。大多数的Go程序员都会像上面这样使用`range`和`_`写`echo`程序,因为隐式地而非显示地索引os.Args,容易写对。
|
||||
|
||||
`echo`的這個版本使用一條短變量聲明來聲明併初始化`s`和`seps`,也可以將這兩個變量分開聲明,聲明一個變量有好幾種方式,下面這些都等價:
|
||||
`echo`的这个版本使用一条短变量声明来声明并初始化`s`和`seps`,也可以将这两个变量分开声明,声明一个变量有好几种方式,下面这些都等价:
|
||||
|
||||
```go
|
||||
s := ""
|
||||
@@ -126,11 +126,11 @@ var s = ""
|
||||
var s string = ""
|
||||
```
|
||||
|
||||
用哪種不用哪種,爲什麽呢?第一種形式,是一條短變量聲明,最簡潔,但隻能用在函數內部,而不能用於包變量。第二種形式依賴於字符串的默認初始化零值機製,被初始化爲""。第三種形式用得很少,除非同時聲明多個變量。第四種形式顯式地標明變量的類型,當變量類型與初值類型相同時,類型冗餘,但如果兩者類型不同,變量類型就必須了。實踐中一般使用前兩種形式中的某個,初始值重要的話就顯式地指定變量的類型,否則使用隱式初始化。
|
||||
用哪种不用哪种,为什么呢?第一种形式,是一条短变量声明,最简洁,但只能用在函数内部,而不能用于包变量。第二种形式依赖于字符串的默认初始化零值机制,被初始化为""。第三种形式用得很少,除非同时声明多个变量。第四种形式显式地标明变量的类型,当变量类型与初值类型相同时,类型冗余,但如果两者类型不同,变量类型就必须了。实践中一般使用前两种形式中的某个,初始值重要的话就显式地指定变量的类型,否则使用隐式初始化。
|
||||
|
||||
如前文所述,每次循環迭代字符串s的內容都會更新。`+=`連接原字符串、空格和下個參數,産生新字符串, 併把它賦值給`s`。`s`原來的內容已經不再使用,將在適當時機對它進行垃圾迴收。
|
||||
如前文所述,每次循环迭代字符串s的内容都会更新。`+=`连接原字符串、空格和下个参数,产生新字符串, 并把它赋值给`s`。`s`原来的内容已经不再使用,将在适当时机对它进行垃圾回收。
|
||||
|
||||
如果連接涉及的數據量很大,這種方式代價高昂。一種簡單且高效的解決方案是使用`strings`包的`Join`函數:
|
||||
如果连接涉及的数据量很大,这种方式代价高昂。一种简单且高效的解决方案是使用`strings`包的`Join`函数:
|
||||
|
||||
<u><i>gopl.io/ch1/echo3</i></u>
|
||||
```go
|
||||
@@ -139,16 +139,16 @@ func main() {
|
||||
}
|
||||
```
|
||||
|
||||
最後,如果不關心輸出格式,隻想看看輸出值,或許隻是爲了調試,可以用`Println`爲我們格式化輸出。
|
||||
最后,如果不关心输出格式,只想看看输出值,或许只是为了调试,可以用`Println`为我们格式化输出。
|
||||
|
||||
```go
|
||||
fmt.Println(os.Args[1:])
|
||||
```
|
||||
|
||||
這條語句的輸出結果跟`strings.Join`得到的結果很像,隻是被放到了一對方括號里。切片都會被打印成這種格式。
|
||||
这条语句的输出结果跟`strings.Join`得到的结果很像,只是被放到了一对方括号里。切片都会被打印成这种格式。
|
||||
|
||||
**練習 1.1:** 脩改`echo`程序,使其能夠打印`os.Args[0]`,卽被執行命令本身的名字。
|
||||
**练习 1.1:** 修改`echo`程序,使其能够打印`os.Args[0]`,即被执行命令本身的名字。
|
||||
|
||||
**練習 1.2:** 脩改`echo`程序,使其打印每個參數的索引和值,每個一行。
|
||||
**练习 1.2:** 修改`echo`程序,使其打印每个参数的索引和值,每个一行。
|
||||
|
||||
**練習 1.3:** 做實驗測量潛在低效的版本和使用了`strings.Join`的版本的運行時間差異。(1.6節講解了部分`time`包,11.4節展示了如何寫標準測試程序,以得到繫統性的性能評測。)
|
||||
**练习 1.3:** 做实验测量潜在低效的版本和使用了`strings.Join`的版本的运行时间差异。(1.6节讲解了部分`time`包,11.4节展示了如何写标准测试程序,以得到系统性的性能评测。)
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
## 1.3. 査找重複的行
|
||||
## 1.3. 查找重复的行
|
||||
|
||||
對文件做拷貝、打印、蒐索、排序、統計或類似事情的程序都有一個差不多的程序結構:一個處理輸入的循環,在每個元素上執行計算處理,在處理的同時或最後産生輸出。我們會展示一個名爲`dup`的程序的三個版本;靈感來自於Unix的`uniq`命令,其尋找相鄰的重複行。該程序使用的結構和包是個參考范例,可以方便地脩改。
|
||||
对文件做拷贝、打印、搜索、排序、统计或类似事情的程序都有一个差不多的程序结构:一个处理输入的循环,在每个元素上执行计算处理,在处理的同时或最后产生输出。我们会展示一个名为`dup`的程序的三个版本;灵感来自于Unix的`uniq`命令,其寻找相邻的重复行。该程序使用的结构和包是个参考范例,可以方便地修改。
|
||||
|
||||
`dup`的第一個版本打印標準輸入中多次出現的行,以重複次數開頭。該程序將引入`if`語句,`map`數據類型以及`bufio`包。
|
||||
`dup`的第一个版本打印标准输入中多次出现的行,以重复次数开头。该程序将引入`if`语句,`map`数据类型以及`bufio`包。
|
||||
|
||||
<u><i>gopl.io/ch1/dup1</i></u>
|
||||
```go
|
||||
@@ -31,53 +31,53 @@ func main() {
|
||||
}
|
||||
```
|
||||
|
||||
正如`for`循環一樣,`if`語句條件兩邊也不加括號,但是主體部分需要加。`if`語句的`else`部分是可選的,在`if`的條件爲`false`時執行。
|
||||
正如`for`循环一样,`if`语句条件两边也不加括号,但是主体部分需要加。`if`语句的`else`部分是可选的,在`if`的条件为`false`时执行。
|
||||
|
||||
**map**存儲了鍵/值(key/value)的集合,對集合元素,提供常數時間的存、取或測試操作。鍵可以是任意類型,隻要其值能用`==`運算符比較,最常見的例子是字符串;值則可以是任意類型。這個例子中的鍵是字符串,值是整數。內置函數`make`創建空`map`,此外,它還有别的作用。4.3節討論`map`。
|
||||
**map**存储了键/值(key/value)的集合,对集合元素,提供常数时间的存、取或测试操作。键可以是任意类型,只要其值能用`==`运算符比较,最常见的例子是字符串;值则可以是任意类型。这个例子中的键是字符串,值是整数。内置函数`make`创建空`map`,此外,它还有别的作用。4.3节讨论`map`。
|
||||
|
||||
(譯註:從功能和實現上説,`Go`的`map`類似於`Java`語言中的`HashMap`,Python語言中的`dict`,`Lua`語言中的`table`,通常使用`hash`實現。遺憾的是,對於該詞的翻譯併不統一,數學界術語爲`映射`,而計算機界衆説紛紜莫衷一是。爲了防止對讀者造成誤解,保留不譯。)
|
||||
(译注:从功能和实现上说,`Go`的`map`类似于`Java`语言中的`HashMap`,Python语言中的`dict`,`Lua`语言中的`table`,通常使用`hash`实现。遗憾的是,对于该词的翻译并不统一,数学界术语为`映射`,而计算机界众说纷纭莫衷一是。为了防止对读者造成误解,保留不译。)
|
||||
|
||||
每次`dup`讀取一行輸入,該行被當做`map`,其對應的值遞增。`counts[input.Text()]++`語句等價下面兩句:
|
||||
每次`dup`读取一行输入,该行被当做`map`,其对应的值递增。`counts[input.Text()]++`语句等价下面两句:
|
||||
|
||||
```go
|
||||
line := input.Text()
|
||||
counts[line] = counts[line] + 1
|
||||
```
|
||||
|
||||
`map`中不含某個鍵時不用擔心,首次讀到新行時,等號右邊的表達式`counts[line]`的值將被計算爲其類型的零值,對於int`卽0。
|
||||
`map`中不含某个键时不用担心,首次读到新行时,等号右边的表达式`counts[line]`的值将被计算为其类型的零值,对于int`即0。
|
||||
|
||||
爲了打印結果,我們使用了基於`range`的循環,併在`counts`這個`map`上迭代。跟之前類似,每次迭代得到兩個結果,鍵和其在`map`中對應的值。`map`的迭代順序併不確定,從實踐來看,該順序隨機,每次運行都會變化。這種設計是有意爲之的,因爲能防止程序依賴特定遍歷順序,而這是無法保證的。
|
||||
为了打印结果,我们使用了基于`range`的循环,并在`counts`这个`map`上迭代。跟之前类似,每次迭代得到两个结果,键和其在`map`中对应的值。`map`的迭代顺序并不确定,从实践来看,该顺序随机,每次运行都会变化。这种设计是有意为之的,因为能防止程序依赖特定遍历顺序,而这是无法保证的。
|
||||
|
||||
繼續來看`bufio`包,它使處理輸入和輸出方便又高效。`Scanner`類型是該包最有用的特性之一,它讀取輸入併將其拆成行或單詞;通常是處理行形式的輸入最簡單的方法。
|
||||
继续来看`bufio`包,它使处理输入和输出方便又高效。`Scanner`类型是该包最有用的特性之一,它读取输入并将其拆成行或单词;通常是处理行形式的输入最简单的方法。
|
||||
|
||||
程序使用短變量聲明創建`bufio.Scanner`類型的變量`input`。
|
||||
程序使用短变量声明创建`bufio.Scanner`类型的变量`input`。
|
||||
|
||||
```
|
||||
input := bufio.NewScanner(os.Stdin)
|
||||
```
|
||||
|
||||
該變量從程序的標準輸入中讀取內容。每次調用`input.Scanner`,卽讀入下一行,併移除行末的換行符;讀取的內容可以調用`input.Text()`得到。`Scan`函數在讀到一行時返迴`true`,在無輸入時返迴`false`。
|
||||
该变量从程序的标准输入中读取内容。每次调用`input.Scanner`,即读入下一行,并移除行末的换行符;读取的内容可以调用`input.Text()`得到。`Scan`函数在读到一行时返回`true`,在无输入时返回`false`。
|
||||
|
||||
類似於C或其它語言里的`printf`函數,`fmt.Printf`函數對一些表達式産生格式化輸出。該函數的首個參數是個格式字符串,指定後續參數被如何格式化。各個參數的格式取決於“轉換字符”(conversion character),形式爲百分號後跟一個字母。舉個例子,`%d`表示以十進製形式打印一個整型操作數,而`%s`則表示把字符串型操作數的值展開。
|
||||
类似于C或其它语言里的`printf`函数,`fmt.Printf`函数对一些表达式产生格式化输出。该函数的首个参数是个格式字符串,指定后续参数被如何格式化。各个参数的格式取决于“转换字符”(conversion character),形式为百分号后跟一个字母。举个例子,`%d`表示以十进制形式打印一个整型操作数,而`%s`则表示把字符串型操作数的值展开。
|
||||
|
||||
`Printf`有一大堆這種轉換,Go程序員稱之爲*動詞(verb)*。下面的表格雖然遠不是完整的規范,但展示了可用的很多特性:
|
||||
`Printf`有一大堆这种转换,Go程序员称之为*动词(verb)*。下面的表格虽然远不是完整的规范,但展示了可用的很多特性:
|
||||
|
||||
```
|
||||
%d 十進製整數
|
||||
%x, %o, %b 十六進製,八進製,二進製整數。
|
||||
%f, %g, %e 浮點數: 3.141593 3.141592653589793 3.141593e+00
|
||||
%t 布爾:true或false
|
||||
%c 字符(rune) (Unicode碼點)
|
||||
%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 變量的類型
|
||||
%% 字面上的百分號標誌(無操作數)
|
||||
%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`。)
|
||||
`dup1`的格式字符串中还含有制表符`\t`和换行符`\n`。字符串字面上可能含有这些代表不可见字符的**转义字符(escap sequences)**。默认情况下,`Printf`不会换行。按照惯例,以字母`f`结尾的格式化函数,如`log.Printf`和`fmt.Errorf`,都采用`fmt.Printf`的格式化准则。而以`ln`结尾的格式化函数,则遵循`Println`的方式,以跟`%v`差不多的方式格式化参数,并在最后添加一个换行符。(译注:后缀`f`指`fomart`,`ln`指`line`。)
|
||||
|
||||
很多程序要麽從標準輸入中讀取數據,如上面的例子所示,要麽從一繫列具名文件中讀取數據。`dup`程序的下個版本讀取標準輸入或是使用`os.Open`打開各個具名文件,併操作它們。
|
||||
很多程序要么从标准输入中读取数据,如上面的例子所示,要么从一系列具名文件中读取数据。`dup`程序的下个版本读取标准输入或是使用`os.Open`打开各个具名文件,并操作它们。
|
||||
|
||||
<u><i>gopl.io/ch1/dup2</i></u>
|
||||
```go
|
||||
@@ -123,19 +123,19 @@ func countLines(f *os.File, counts map[string]int) {
|
||||
}
|
||||
```
|
||||
|
||||
`os.Open`函數返迴兩個值。第一個值是被打開的文件(`*os.File`),其後被`Scanner`讀取。
|
||||
`os.Open`函数返回两个值。第一个值是被打开的文件(`*os.File`),其后被`Scanner`读取。
|
||||
|
||||
`os.Open`返迴的第二個值是內置`error`類型的值。如果`err`等於內置值`nil`(譯註:相當於其它語言里的NULL),那麽文件被成功打開。讀取文件,直到文件結束,然後調用`Close`關閉該文件,併釋放占用的所有資源。相反的話,如果`err`的值不是`nil`,説明打開文件時出錯了。這種情況下,錯誤值描述了所遇到的問題。我們的錯誤處理非常簡單,隻是使用`Fprintf`與表示任意類型默認格式值的動詞`%v`,向標準錯誤流打印一條信息,然後`dup`繼續處理下一個文件;`continue`語句直接跳到`for`循環的下個迭代開始執行。
|
||||
`os.Open`返回的第二个值是内置`error`类型的值。如果`err`等于内置值`nil`(译注:相当于其它语言里的NULL),那么文件被成功打开。读取文件,直到文件结束,然后调用`Close`关闭该文件,并释放占用的所有资源。相反的话,如果`err`的值不是`nil`,说明打开文件时出错了。这种情况下,错误值描述了所遇到的问题。我们的错误处理非常简单,只是使用`Fprintf`与表示任意类型默认格式值的动词`%v`,向标准错误流打印一条信息,然后`dup`继续处理下一个文件;`continue`语句直接跳到`for`循环的下个迭代开始执行。
|
||||
|
||||
爲了使示例代碼保持合理的大小,本書開始的一些示例有意簡化了錯誤處理,顯而易見的是,應該檢査`os.Open`返迴的錯誤值,然而,使用`input.Scan`讀取文件過程中,不大可能出現錯誤,因此我們忽略了錯誤處理。我們會在跳過錯誤檢査的地方做説明。5.4節中深入介紹錯誤處理。
|
||||
为了使示例代码保持合理的大小,本书开始的一些示例有意简化了错误处理,显而易见的是,应该检查`os.Open`返回的错误值,然而,使用`input.Scan`读取文件过程中,不大可能出现错误,因此我们忽略了错误处理。我们会在跳过错误检查的地方做说明。5.4节中深入介绍错误处理。
|
||||
|
||||
註意`countLines`函數在其聲明前被調用。函數和包級别的變量(package-level entities)可以任意順序聲明,併不影響其被調用。(譯註:最好還是遵循一定的規范)
|
||||
注意`countLines`函数在其声明前被调用。函数和包级别的变量(package-level entities)可以任意顺序声明,并不影响其被调用。(译注:最好还是遵循一定的规范)
|
||||
|
||||
`map`是一個由`make`函數創建的數據結構的引用。`map`作爲爲參數傳遞給某函數時,該函數接收這個引用的一份拷貝(copy,或譯爲副本),被調用函數對`map`底層數據結構的任何脩改,調用者函數都可以通過持有的`map`引用看到。在我們的例子中,`countLines`函數向`counts`插入的值,也會被`main`函數看到。(譯註:類似於C++里的引用傳遞,實際上指針是另一個指針了,但內部存的值指向同一塊內存)
|
||||
`map`是一个由`make`函数创建的数据结构的引用。`map`作为为参数传递给某函数时,该函数接收这个引用的一份拷贝(copy,或译为副本),被调用函数对`map`底层数据结构的任何修改,调用者函数都可以通过持有的`map`引用看到。在我们的例子中,`countLines`函数向`counts`插入的值,也会被`main`函数看到。(译注:类似于C++里的引用传递,实际上指针是另一个指针了,但内部存的值指向同一块内存)
|
||||
|
||||
`dup`的前兩個版本以"流”模式讀取輸入,併根據需要拆分成多個行。理論上,這些程序可以處理任意數量的輸入數據。還有另一個方法,就是一口氣把全部輸入數據讀到內存中,一次分割爲多行,然後處理它們。下面這個版本,`dup3`,就是這麽操作的。這個例子引入了`ReadFile`函數(來自於`io/ioutil`包),其讀取指定文件的全部內容,`strings.Split`函數把字符串分割成子串的切片。(`Split`的作用與前文提到的`strings.Join`相反。)
|
||||
`dup`的前两个版本以"流”模式读取输入,并根据需要拆分成多个行。理论上,这些程序可以处理任意数量的输入数据。还有另一个方法,就是一口气把全部输入数据读到内存中,一次分割为多行,然后处理它们。下面这个版本,`dup3`,就是这么操作的。这个例子引入了`ReadFile`函数(来自于`io/ioutil`包),其读取指定文件的全部内容,`strings.Split`函数把字符串分割成子串的切片。(`Split`的作用与前文提到的`strings.Join`相反。)
|
||||
|
||||
我們略微簡化了`dup3`。首先,由於`ReadFile`函數需要文件名作爲參數,因此隻讀指定文件,不讀標準輸入。其次,由於行計數代碼隻在一處用到,故將其移迴`main`函數。
|
||||
我们略微简化了`dup3`。首先,由于`ReadFile`函数需要文件名作为参数,因此只读指定文件,不读标准输入。其次,由于行计数代码只在一处用到,故将其移回`main`函数。
|
||||
|
||||
<u><i>gopl.io/ch1/dup3</i></u>
|
||||
```go
|
||||
@@ -168,8 +168,8 @@ func main() {
|
||||
}
|
||||
```
|
||||
|
||||
`ReadFile`函數返迴一個字節切片(byte slice),必須把它轉換爲`string`,才能用`strings.Split`分割。我們會在3.5.4節詳細講解字符串和字節切片。
|
||||
`ReadFile`函数返回一个字节切片(byte slice),必须把它转换为`string`,才能用`strings.Split`分割。我们会在3.5.4节详细讲解字符串和字节切片。
|
||||
|
||||
實現上,`bufio.Scanner`、`outil.ReadFile`和`ioutil.WriteFile`都使用`*os.File`的`Read`和`Write`方法,但是,大多數程序員很少需要直接調用那些低級(lower-level)函數。高級(higher-level)函數,像`bufio`和`io/ioutil`包中所提供的那些,用起來要容易點。
|
||||
实现上,`bufio.Scanner`、`outil.ReadFile`和`ioutil.WriteFile`都使用`*os.File`的`Read`和`Write`方法,但是,大多数程序员很少需要直接调用那些低级(lower-level)函数。高级(higher-level)函数,像`bufio`和`io/ioutil`包中所提供的那些,用起来要容易点。
|
||||
|
||||
**練習 1.4:** 脩改`dup2`,出現重複的行時打印文件名稱。
|
||||
**练习 1.4:** 修改`dup2`,出现重复的行时打印文件名称。
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
## 1.4. GIF動畵
|
||||
## 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图像动画效果:
|
||||
|
||||

|
||||
|
||||
這段代碼里我們用了一些新的結構,包括const聲明,struct結構體類型,複合聲明。和我們舉的其它的例子不太一樣,這一個例子包含了浮點數運算。這些概念我們隻在這里簡單地説明一下,之後的章節會更詳細地講解。
|
||||
这段代码里我们用了一些新的结构,包括const声明,struct结构体类型,复合声明。和我们举的其它的例子不太一样,这一个例子包含了浮点数运算。这些概念我们只在这里简单地说明一下,之后的章节会更详细地讲解。
|
||||
|
||||
<u><i>gopl.io/ch1/lissajous</i></u>
|
||||
```go
|
||||
@@ -66,25 +66,25 @@ func lissajous(out io.Writer) {
|
||||
|
||||
```
|
||||
|
||||
當我們import了一個包路徑包含有多個單詞的package時,比如image/color(image和color兩個單詞),通常我們隻需要用最後那個單詞表示這個包就可以。所以當我們寫color.White時,這個變量指向的是image/color包里的變量,同理gif.GIF是屬於image/gif包里的變量。
|
||||
当我们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結構體。
|
||||
[]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字段。
|
||||
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類型,這個類型支持把輸出結果寫到很多目標,很快我們就可以看到例子。
|
||||
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)點來染黑色。
|
||||
内层循环设置两个偏振值。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
|
||||
$ ./lissajous >out.gif
|
||||
```
|
||||
|
||||
**練習 1.5:** 脩改前面的Lissajous程序里的調色闆,由黑色改爲緑色。我們可以用`color.RGBA{0xRR, 0xGG, 0xBB, 0xff}`來得到`#RRGGBB`這個色值,三個十六進製的字符串分别代表紅、緑、藍像素。
|
||||
**练习 1.5:** 修改前面的Lissajous程序里的调色板,由黑色改为绿色。我们可以用`color.RGBA{0xRR, 0xGG, 0xBB, 0xff}`来得到`#RRGGBB`这个色值,三个十六进制的字符串分别代表红、绿、蓝像素。
|
||||
|
||||
**練習 1.6:** 脩改Lissajous程序,脩改其調色闆來生成更豐富的顔色,然後脩改SetColorIndex的第三個參數,看看顯示結果吧。
|
||||
**练习 1.6:** 修改Lissajous程序,修改其调色板来生成更丰富的颜色,然后修改SetColorIndex的第三个参数,看看显示结果吧。
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
## 1.5. 獲取URL
|
||||
## 1.5. 获取URL
|
||||
|
||||
對於很多現代應用來説,訪問互聯網上的信息和訪問本地文件繫統一樣重要。Go語言在net這個強大package的幫助下提供了一繫列的package來做這件事情,使用這些包可以更簡單地用網絡收發信息,還可以建立更底層的網絡連接,編寫服務器程序。在這些情景下,Go語言原生的併發特性(在第八章中會介紹)顯得尤其好用。
|
||||
对于很多现代应用来说,访问互联网上的信息和访问本地文件系统一样重要。Go语言在net这个强大package的帮助下提供了一系列的package来做这件事情,使用这些包可以更简单地用网络收发信息,还可以建立更底层的网络连接,编写服务器程序。在这些情景下,Go语言原生的并发特性(在第八章中会介绍)显得尤其好用。
|
||||
|
||||
爲了最簡單地展示基於HTTP獲取信息的方式,下面給出一個示例程序fetch,這個程序將獲取對應的url,併將其源文本打印出來;這個例子的靈感來源於curl工具(譯註:unix下的一個用來發http請求的工具,具體可以man curl)。當然,curl提供的功能更爲複雜豐富,這里隻編寫最簡單的樣例。這個樣例之後還會多次被用到。
|
||||
为了最简单地展示基于HTTP获取信息的方式,下面给出一个示例程序fetch,这个程序将获取对应的url,并将其源文本打印出来;这个例子的灵感来源于curl工具(译注:unix下的一个用来发http请求的工具,具体可以man curl)。当然,curl提供的功能更为复杂丰富,这里只编写最简单的样例。这个样例之后还会多次被用到。
|
||||
|
||||
<u><i>gopl.io/ch1/fetch</i></u>
|
||||
```go
|
||||
@@ -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
|
||||
@@ -45,24 +45,24 @@ $ ./fetch http://gopl.io
|
||||
...
|
||||
```
|
||||
|
||||
HTTP請求如果失敗了的話,會得到下面這樣的結果:
|
||||
HTTP请求如果失败了的话,会得到下面这样的结果:
|
||||
|
||||
```
|
||||
$ ./fetch http://bad.gopl.io
|
||||
fetch: Get http://bad.gopl.io: dial tcp: lookup bad.gopl.io: no such host
|
||||
```
|
||||
|
||||
譯註:在大天朝的網絡環境下很容易重現這種錯誤,下面是Windows下運行得到的錯誤信息:
|
||||
译注:在大天朝的网络环境下很容易重现这种错误,下面是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。
|
||||
无论哪种失败原因,我们的程序都用了os.Exit函数来终止进程,并且返回一个status错误码,其值为1。
|
||||
|
||||
**練習 1.7:** 函數調用io.Copy(dst, src)會從src中讀取內容,併將讀到的結果寫入到dst中,使用這個函數替代掉例子中的ioutil.ReadAll來拷貝響應結構體到os.Stdout,避免申請一個緩衝區(例子中的b)來存儲。記得處理io.Copy返迴結果中的錯誤。
|
||||
**练习 1.7:** 函数调用io.Copy(dst, src)会从src中读取内容,并将读到的结果写入到dst中,使用这个函数替代掉例子中的ioutil.ReadAll来拷贝响应结构体到os.Stdout,避免申请一个缓冲区(例子中的b)来存储。记得处理io.Copy返回结果中的错误。
|
||||
|
||||
**練習 1.8:** 脩改fetch這個范例,如果輸入的url參數沒有 `http://` 前綴的話,爲這個url加上該前綴。你可能會用到strings.HasPrefix這個函數。
|
||||
**练习 1.8:** 修改fetch这个范例,如果输入的url参数没有 `http://` 前缀的话,为这个url加上该前缀。你可能会用到strings.HasPrefix这个函数。
|
||||
|
||||
**練習 1.9:** 脩改fetch打印出HTTP協議的狀態碼,可以從resp.Status變量得到該狀態碼。
|
||||
**练习 1.9:** 修改fetch打印出HTTP协议的状态码,可以从resp.Status变量得到该状态码。
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
## 1.6. 併發獲取多個URL
|
||||
## 1.6. 并发获取多个URL
|
||||
|
||||
Go語言最有意思併且最新奇的特性就是對併發編程的支持。併發編程是一個大話題,在第八章和第九章中會專門講到。這里我們隻淺嚐輒止地來體驗一下Go語言里的goroutine和channel。
|
||||
Go语言最有意思并且最新奇的特性就是对并发编程的支持。并发编程是一个大话题,在第八章和第九章中会专门讲到。这里我们只浅尝辄止地来体验一下Go语言里的goroutine和channel。
|
||||
|
||||
下面的例子fetchall,和前面小節的fetch程序所要做的工作基本一致,fetchall的特别之處在於它會同時去獲取所有的URL,所以這個程序的總執行時間不會超過執行時間最長的那一個任務,前面的fetch程序執行時間則是所有任務執行時間之和。fetchall程序隻會打印獲取的內容大小和經過的時間,不會像之前那樣打印獲取的內容。
|
||||
下面的例子fetchall,和前面小节的fetch程序所要做的工作基本一致,fetchall的特别之处在于它会同时去获取所有的URL,所以这个程序的总执行时间不会超过执行时间最长的那一个任务,前面的fetch程序执行时间则是所有任务执行时间之和。fetchall程序只会打印获取的内容大小和经过的时间,不会像之前那样打印获取的内容。
|
||||
|
||||
<u><i>gopl.io/ch1/fetchall</i></u>
|
||||
```go
|
||||
@@ -48,7 +48,7 @@ func fetch(url string, ch chan<- string) {
|
||||
}
|
||||
```
|
||||
|
||||
下面使用fetchall來請求幾個地址:
|
||||
下面使用fetchall来请求几个地址:
|
||||
|
||||
```
|
||||
$ go build gopl.io/ch1/fetchall
|
||||
@@ -59,10 +59,10 @@ $ ./fetchall https://golang.org http://gopl.io https://godoc.org
|
||||
0.48s elapsed
|
||||
```
|
||||
|
||||
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里的這個字符串。
|
||||
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函數提前退出。
|
||||
当一个goroutine尝试在一个channel上做send或者receive操作时,这个goroutine会阻塞在调用处,直到另一个goroutine往这个channel里写入、或者接收值,这样两个goroutine才会继续执行channel操作之后的逻辑。在这个例子中,每一个fetch函数在执行时都会往channel里发送一个值(ch <- expression),主函数负责接收这些值(<-ch)。这个程序中我们用main函数来接收所有fetch函数传回的字符串,可以避免在goroutine异步执行还没有完成时main函数提前退出。
|
||||
|
||||
**練習 1.10:** 找一個數據量比較大的網站,用本小節中的程序調研網站的緩存策略,對每個URL執行兩遍請求,査看兩次時間是否有較大的差别,併且每次獲取到的響應內容是否一致,脩改本節中的程序,將響應結果輸出,以便於進行對比。
|
||||
**练习 1.10:** 找一个数据量比较大的网站,用本小节中的程序调研网站的缓存策略,对每个URL执行两遍请求,查看两次时间是否有较大的差别,并且每次获取到的响应内容是否一致,修改本节中的程序,将响应结果输出,以便于进行对比。
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
## 1.7. Web服務
|
||||
## 1.7. Web服务
|
||||
|
||||
Go語言的內置庫使得寫一個類似fetch的web服務器變得異常地簡單。在本節中,我們會展示一個微型服務器,這個服務器的功能是返迴當前用戶正在訪問的URL。比如用戶訪問的是 http://localhost:8000/hello ,那麽響應是URL.Path = "hello"。
|
||||
Go语言的内置库使得写一个类似fetch的web服务器变得异常地简单。在本节中,我们会展示一个微型服务器,这个服务器的功能是返回当前用户正在访问的URL。比如用户访问的是 http://localhost:8000/hello ,那么响应是URL.Path = "hello"。
|
||||
|
||||
<u><i>gopl.io/ch1/server1</i></u>
|
||||
```go
|
||||
@@ -24,15 +24,15 @@ 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下可以在另外一個命令行窗口去運行這個程序。
|
||||
让我们在后台运行这个服务程序。如果你的操作系统是Mac OS X或者Linux,那么在运行命令的末尾加上一个&符号,即可让程序简单地跑在后台,windows下可以在另外一个命令行窗口去运行这个程序。
|
||||
|
||||
```
|
||||
$ go run src/gopl.io/ch1/server1/main.go &
|
||||
```
|
||||
|
||||
現在可以通過命令行來發送客戶端請求了:
|
||||
现在可以通过命令行来发送客户端请求了:
|
||||
|
||||
```
|
||||
$ go build gopl.io/ch1/fetch
|
||||
@@ -42,11 +42,11 @@ $ ./fetch http://localhost:8000/help
|
||||
URL.Path = "/help"
|
||||
```
|
||||
|
||||
還可以直接在瀏覽器里訪問這個URL,然後得到返迴結果,如圖1.2:
|
||||
还可以直接在浏览器里访问这个URL,然后得到返回结果,如图1.2:
|
||||
|
||||

|
||||
|
||||
在這個服務的基礎上疊加特性是很容易的。一種比較實用的脩改是爲訪問的url添加某種狀態。比如,下面這個版本輸出了同樣的內容,但是會對請求的次數進行計算;對URL的請求結果會包含各種URL被訪問的總次數,直接對/count這個URL的訪問要除外。
|
||||
在这个服务的基础上叠加特性是很容易的。一种比较实用的修改是为访问的url添加某种状态。比如,下面这个版本输出了同样的内容,但是会对请求的次数进行计算;对URL的请求结果会包含各种URL被访问的总次数,直接对/count这个URL的访问要除外。
|
||||
|
||||
<u><i>gopl.io/ch1/server2</i></u>
|
||||
```go
|
||||
@@ -85,9 +85,9 @@ 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的所有行爲包在中間的目的。第九章中我們會進一步講解共享變量。
|
||||
这个服务器有两个请求处理函数,根据请求的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数据都打印出来,这样可以使检查和调试这个服务更为方便:
|
||||
|
||||
<u><i>gopl.io/ch1/server3</i></u>
|
||||
```go
|
||||
@@ -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()
|
||||
@@ -128,13 +128,13 @@ if err != nil {
|
||||
}
|
||||
```
|
||||
|
||||
用if和ParseForm結合可以讓代碼更加簡單,併且可以限製err這個變量的作用域,這麽做是很不錯的。我們會在2.7節中講解作用域。
|
||||
用if和ParseForm结合可以让代码更加简单,并且可以限制err这个变量的作用域,这么做是很不错的。我们会在2.7节中讲解作用域。
|
||||
|
||||
在這些程序中,我們看到了很多不同的類型被輸出到標準輸出流中。比如前面的fetch程序,把HTTP的響應數據拷貝到了os.Stdout,lissajous程序里我們輸出的是一個文件。fetchall程序則完全忽略到了HTTP的響應Body,隻是計算了一下響應Body的大小,這個程序中把響應Body拷貝到了ioutil.Discard。在本節的web服務器程序中則是用fmt.Fprintf直接寫到了http.ResponseWriter中。
|
||||
在这些程序中,我们看到了很多不同的类型被输出到标准输出流中。比如前面的fetch程序,把HTTP的响应数据拷贝到了os.Stdout,lissajous程序里我们输出的是一个文件。fetchall程序则完全忽略到了HTTP的响应Body,只是计算了一下响应Body的大小,这个程序中把响应Body拷贝到了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) {
|
||||
@@ -143,7 +143,7 @@ handler := func(w http.ResponseWriter, r *http.Request) {
|
||||
http.HandleFunc("/", handler)
|
||||
```
|
||||
|
||||
或者另一種等價形式:
|
||||
或者另一种等价形式:
|
||||
|
||||
```Go
|
||||
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -151,11 +151,11 @@ http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
})
|
||||
```
|
||||
|
||||
HandleFunc函數的第二個參數是一個函數的字面值,也就是一個在使用時定義的匿名函數。這些內容我們會在5.6節中講解。
|
||||
HandleFunc函数的第二个参数是一个函数的字面值,也就是一个在使用时定义的匿名函数。这些内容我们会在5.6节中讲解。
|
||||
|
||||
|
||||
做完這些脩改之後,在瀏覽器里訪問 http://localhost:8000 。每次你載入這個頁面都可以看到一個像圖1.3那樣的動畵。
|
||||
做完这些修改之后,在浏览器里访问 http://localhost:8000 。每次你载入这个页面都可以看到一个像图1.3那样的动画。
|
||||
|
||||

|
||||
|
||||
**練習 1.12:** 脩改Lissajour服務,從URL讀取變量,比如你可以訪問 http://localhost:8000/?cycles=20 這個URL,這樣訪問可以將程序里的cycles默認的5脩改爲20。字符串轉換爲數字可以調用strconv.Atoi函數。你可以在godoc里査看strconv.Atoi的詳細説明。
|
||||
**练习 1.12:** 修改Lissajour服务,从URL读取变量,比如你可以访问 http://localhost:8000/?cycles=20 这个URL,这样访问可以将程序里的cycles默认的5修改为20。字符串转换为数字可以调用strconv.Atoi函数。你可以在godoc里查看strconv.Atoi的详细说明。
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
## 1.8. 本章要點
|
||||
## 1.8. 本章要点
|
||||
|
||||
本章對Go語言做了一些介紹,Go語言很多方面在有限的篇幅中無法覆蓋到。本節會把沒有講到的內容也做一些簡單的介紹,這樣讀者在讀到完整的內容之前,可以有個簡單的印象。
|
||||
本章对Go语言做了一些介绍,Go语言很多方面在有限的篇幅中无法覆盖到。本节会把没有讲到的内容也做一些简单的介绍,这样读者在读到完整的内容之前,可以有个简单的印象。
|
||||
|
||||
**控製流:** 在本章我們隻介紹了if控製和for,但是沒有提到switch多路選擇。這里是一個簡單的switch的例子:
|
||||
**控制流:** 在本章我们只介绍了if控制和for,但是没有提到switch多路选择。这里是一个简单的switch的例子:
|
||||
|
||||
```go
|
||||
switch coinflip() {
|
||||
@@ -15,9 +15,9 @@ 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一樣,下面是一個例子:
|
||||
Go语言里的switch还可以不带操作对象(译注:switch不带操作对象时默认用true值代替,然后将每个case的表达式和true值进行比较);可以直接罗列多种条件,像其它语言里面的多个if else一样,下面是一个例子:
|
||||
|
||||
```go
|
||||
func Signum(x int) int {
|
||||
@@ -32,13 +32,13 @@ func Signum(x int) int {
|
||||
}
|
||||
```
|
||||
|
||||
這種形式叫做無tag switch(tagless switch);這和switch true是等價的。
|
||||
这种形式叫做无tag switch(tagless switch);这和switch true是等价的。
|
||||
|
||||
像for和if控製語句一樣,switch也可以緊跟一個簡短的變量聲明,一個自增表達式、賦值語句,或者一個函數調用(譯註:比其它語言豐富)。
|
||||
像for和if控制语句一样,switch也可以紧跟一个简短的变量声明,一个自增表达式、赋值语句,或者一个函数调用(译注:比其它语言丰富)。
|
||||
|
||||
break和continue語句會改變控製流。和其它語言中的break和continue一樣,break會中斷當前的循環,併開始執行循環之後的內容,而continue會中跳過當前循環,併開始執行下一次循環。這兩個語句除了可以控製for循環,還可以用來控製switch和select語句(之後會講到),在1.3節中我們看到,continue會跳過內層的循環,如果我們想跳過的是更外層的循環的話,我們可以在相應的位置加上label,這樣break和continue就可以根據我們的想法來continue和break任意循環。這看起來甚至有點像goto語句的作用了。當然,一般程序員也不會用到這種操作。這兩種行爲更多地被用到機器生成的代碼中。
|
||||
break和continue语句会改变控制流。和其它语言中的break和continue一样,break会中断当前的循环,并开始执行循环之后的内容,而continue会中跳过当前循环,并开始执行下一次循环。这两个语句除了可以控制for循环,还可以用来控制switch和select语句(之后会讲到),在1.3节中我们看到,continue会跳过内层的循环,如果我们想跳过的是更外层的循环的话,我们可以在相应的位置加上label,这样break和continue就可以根据我们的想法来continue和break任意循环。这看起来甚至有点像goto语句的作用了。当然,一般程序员也不会用到这种操作。这两种行为更多地被用到机器生成的代码中。
|
||||
|
||||
**命名類型:** 類型聲明使得我們可以很方便地給一個特殊類型一個名字。因爲struct類型聲明通常非常地長,所以我們總要給這種struct取一個名字。本章中就有這樣一個例子,二維點類型:
|
||||
**命名类型:** 类型声明使得我们可以很方便地给一个特殊类型一个名字。因为struct类型声明通常非常地长,所以我们总要给这种struct取一个名字。本章中就有这样一个例子,二维点类型:
|
||||
|
||||
```go
|
||||
type Point struct {
|
||||
@@ -47,15 +47,15 @@ type Point struct {
|
||||
var p Point
|
||||
```
|
||||
|
||||
類型聲明和命名類型會在第二章中介紹。
|
||||
类型声明和命名类型会在第二章中介绍。
|
||||
|
||||
**指針:** Go語言提供了指針。指針是一種直接存儲了變量的內存地址的數據類型。在其它語言中,比如C語言,指針操作是完全不受約束的。在另外一些語言中,指針一般被處理爲“引用”,除了到處傳遞這些指針之外,併不能對這些指針做太多事情。Go語言在這兩種范圍中取了一種平衡。指針是可見的內存地址,&操作符可以返迴一個變量的內存地址,併且*操作符可以獲取指針指向的變量內容,但是在Go語言里沒有指針運算,也就是不能像c語言里可以對指針進行加或減操作。我們會在2.3.2中進行詳細介紹。
|
||||
**指针:** Go语言提供了指针。指针是一种直接存储了变量的内存地址的数据类型。在其它语言中,比如C语言,指针操作是完全不受约束的。在另外一些语言中,指针一般被处理为“引用”,除了到处传递这些指针之外,并不能对这些指针做太多事情。Go语言在这两种范围中取了一种平衡。指针是可见的内存地址,&操作符可以返回一个变量的内存地址,并且*操作符可以获取指针指向的变量内容,但是在Go语言里没有指针运算,也就是不能像c语言里可以对指针进行加或减操作。我们会在2.3.2中进行详细介绍。
|
||||
|
||||
**方法和接口:** 方法是和命名類型關聯的一類函數。Go語言里比較特殊的是方法可以被關聯到任意一種命名類型。在第六章我們會詳細地講方法。接口是一種抽象類型,這種類型可以讓我們以同樣的方式來處理不同的固有類型,不用關心它們的具體實現,而隻需要關註它們提供的方法。第七章中會詳細説明這些內容。
|
||||
**方法和接口:** 方法是和命名类型关联的一类函数。Go语言里比较特殊的是方法可以被关联到任意一种命名类型。在第六章我们会详细地讲方法。接口是一种抽象类型,这种类型可以让我们以同样的方式来处理不同的固有类型,不用关心它们的具体实现,而只需要关注它们提供的方法。第七章中会详细说明这些内容。
|
||||
|
||||
**包(packages):** Go語言提供了一些很好用的package,併且這些package是可以擴展的。Go語言社區已經創造併且分享了很多很多。所以Go語言編程大多數情況下就是用已有的package來寫我們自己的代碼。通過這本書,我們會講解一些重要的標準庫內的package,但是還是有很多限於篇幅沒有去説明,因爲我們沒法在這樣的厚度的書里去做一部代碼大全。
|
||||
**包(packages):** Go语言提供了一些很好用的package,并且这些package是可以扩展的。Go语言社区已经创造并且分享了很多很多。所以Go语言编程大多数情况下就是用已有的package来写我们自己的代码。通过这本书,我们会讲解一些重要的标准库内的package,但是还是有很多限于篇幅没有去说明,因为我们没法在这样的厚度的书里去做一部代码大全。
|
||||
|
||||
在你開始寫一個新程序之前,最好先去檢査一下是不是已經有了現成的庫可以幫助你更高效地完成這件事情。你可以在 https://golang.org/pkg 和 https://godoc.org 中找到標準庫和社區寫的package。godoc這個工具可以讓你直接在本地命令行閲讀標準庫的文檔。比如下面這個例子。
|
||||
在你开始写一个新程序之前,最好先去检查一下是不是已经有了现成的库可以帮助你更高效地完成这件事情。你可以在 https://golang.org/pkg 和 https://godoc.org 中找到标准库和社区写的package。godoc这个工具可以让你直接在本地命令行阅读标准库的文档。比如下面这个例子。
|
||||
|
||||
```
|
||||
$ go doc http.ListenAndServe
|
||||
@@ -66,7 +66,7 @@ func ListenAndServe(addr string, handler Handler) error
|
||||
...
|
||||
```
|
||||
|
||||
**註釋:** 我們之前已經提到過了在源文件的開頭寫的註釋是這個源文件的文檔。在每一個函數之前寫一個説明函數行爲的註釋也是一個好習慣。這些慣例很重要,因爲這些內容會被像godoc這樣的工具檢測到,併且在執行命令時顯示這些註釋。具體可以參考10.7.4。
|
||||
**注释:** 我们之前已经提到过了在源文件的开头写的注释是这个源文件的文档。在每一个函数之前写一个说明函数行为的注释也是一个好习惯。这些惯例很重要,因为这些内容会被像godoc这样的工具检测到,并且在执行命令时显示这些注释。具体可以参考10.7.4。
|
||||
|
||||
多行註釋可以用 `/* ... */` 來包裹,和其它大多數語言一樣。在文件一開頭的註釋一般都是這種形式,或者一大段的解釋性的註釋文字也會被這符號包住,來避免每一行都需要加//。在註釋中//和/*是沒什麽意義的,所以不要在註釋中再嵌入註釋。
|
||||
多行注释可以用 `/* ... */` 来包裹,和其它大多数语言一样。在文件一开头的注释一般都是这种形式,或者一大段的解释性的注释文字也会被这符号包住,来避免每一行都需要加//。在注释中//和/*是没什么意义的,所以不要在注释中再嵌入注释。
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# 第1章 入門
|
||||
# 第1章 入门
|
||||
|
||||
本章介紹Go語言的基礎組件。本章提供了足夠的信息和示例程序,希望可以幫你盡快入門, 寫出有用的程序。本章和之後章節的示例程序都針對你可能遇到的現實案例。先了解幾個Go程序,涉及的主題從簡單的文件處理、圖像處理到互聯網客戶端和服務端併發。當然,第一章不會解釋細枝末節,但用這些程序來學習一門新語言還是很有效的。
|
||||
本章介绍Go语言的基础组件。本章提供了足够的信息和示例程序,希望可以帮你尽快入门, 写出有用的程序。本章和之后章节的示例程序都针对你可能遇到的现实案例。先了解几个Go程序,涉及的主题从简单的文件处理、图像处理到互联网客户端和服务端并发。当然,第一章不会解释细枝末节,但用这些程序来学习一门新语言还是很有效的。
|
||||
|
||||
學習一門新語言時,會有一種自然的傾向, 按照自己熟悉的語言的套路寫新語言程序。學習Go語言的過程中,請警惕這種想法,盡量别這麽做。我們會演示怎麽寫好Go語言程序,所以請使用本書的代碼作爲你自己寫程序時的指南。
|
||||
学习一门新语言时,会有一种自然的倾向, 按照自己熟悉的语言的套路写新语言程序。学习Go语言的过程中,请警惕这种想法,尽量别这么做。我们会演示怎么写好Go语言程序,所以请使用本书的代码作为你自己写程序时的指南。
|
||||
|
||||
Reference in New Issue
Block a user