《Go语言圣经》学习笔记 第一章 Go语言入门
Go語(yǔ)言圣經(jīng)學(xué)習(xí)筆記 第一章 Go語(yǔ)言入門(mén)
目錄
注:學(xué)習(xí)《Go語(yǔ)言圣經(jīng)》筆記,PDF點(diǎn)擊下載,建議看書(shū)。
Go語(yǔ)言小白學(xué)習(xí)筆記,書(shū)上的內(nèi)容照搬,大佬看了勿噴,以后熟悉了會(huì)總結(jié)成自己的讀書(shū)筆記。
1. Hello, World
gopl.io/ch1/helloworld
package mainimport "fmt"func main() {fmt.Println("Hello, 世界") }Go是一門(mén)編譯型語(yǔ)言, Go語(yǔ)言的工具鏈將源代碼及其依賴(lài)轉(zhuǎn)換成計(jì)算機(jī)的機(jī)器指令。
Go語(yǔ)言提供的工具都通過(guò)一個(gè)單獨(dú)的命令 go 調(diào)用, go 命令有一系列子命令。 最簡(jiǎn)單的一個(gè)子命令就是run。 這個(gè)命令編譯一個(gè)或多個(gè)以.go結(jié)尾的源文件, 鏈接庫(kù)文件, 并運(yùn)行最終生成的可執(zhí)行文件。 ( 本書(shū)使用$表示命令行提示符。 )
$ go run helloworld.go輸出:
Go語(yǔ)言原生支持Unicode, 它可以處理全世界任何語(yǔ)言的文本。
如果不只是一次性實(shí)驗(yàn), 你肯定希望能夠編譯這個(gè)程序, 保存編譯結(jié)果以備將來(lái)之用。 可以
用build子命令:
這個(gè)命令生成一個(gè)名為helloworld的可執(zhí)行的二進(jìn)制文件, 之后你可以隨時(shí)運(yùn)行它, 不需任
何處理(注:顯示的是win命令行,不是Linux)。
本書(shū)中, 所有的示例代碼上都有一行標(biāo)記, 利用這些標(biāo)記, 可以從gopl.io網(wǎng)站上本書(shū)源碼倉(cāng)庫(kù)
里獲取代碼:
執(zhí)行 go get gopl.io/ch1/helloworld 命令, 就會(huì)從網(wǎng)上獲取代碼, 并放到對(duì)應(yīng)目錄中。 2.6
和10.7節(jié)有這方面更詳細(xì)的介紹。
來(lái)討論下程序本身。 Go語(yǔ)言的代碼通過(guò)包( package) 組織, 包類(lèi)似于其它語(yǔ)言里的庫(kù)
( libraries) 或者模塊( modules) 。 一個(gè)包由位于單個(gè)目錄下的一個(gè)或多個(gè).go源代碼文件組成, 目錄定義包的作用。 每個(gè)源文件都以一條 package 聲明語(yǔ)句開(kāi)始, 這個(gè)例子里就是 package main , 表示該文件屬于哪個(gè)包, 緊跟著一系列導(dǎo)入( import) 的包, 之后是存儲(chǔ)在這個(gè)文件里的程序語(yǔ)句。
Go的標(biāo)準(zhǔn)庫(kù)提供了100多個(gè)包, 以支持常見(jiàn)功能, 如輸入、 輸出、 排序以及文本處理。 比如 fmt 包, 就含有格式化輸出、 接收輸入的函數(shù)。 Println 是其中一個(gè)基礎(chǔ)函數(shù), 可以打印以空格間隔的一個(gè)或多個(gè)值, 并在最后添加一個(gè)換行符, 從而輸出一整行。
main 包比較特殊。 它定義了一個(gè)獨(dú)立可執(zhí)行的程序, 而不是一個(gè)庫(kù)。 在 main 里的 main 函數(shù)也很特殊, 它是整個(gè)程序執(zhí)行時(shí)的入口。 main 函數(shù)所做的事情就是程序做的。 當(dāng)然了, main 函數(shù)一般調(diào)用其它包里的函數(shù)完成很多工作, 比如 fmt.Println 。
必須告訴編譯器源文件需要哪些包, 這就是 import 聲明以及隨后的 package 聲明扮演的角色。 hello world例子只用到了一個(gè)包, 大多數(shù)程序需要導(dǎo)入多個(gè)包。
必須恰當(dāng)導(dǎo)入需要的包, 缺少了必要的包或者導(dǎo)入了不需要的包, 程序都無(wú)法編譯通過(guò)。 這項(xiàng)嚴(yán)格要求避免了程序開(kāi)發(fā)過(guò)程中引入未使用的包。
import 聲明必須跟在文件的 package 聲明之后。 隨后, 則是組成程序的函數(shù)、 變量、 常量、類(lèi)型的聲明語(yǔ)句( 分別由關(guān)鍵字 func , var , const , type 定義) 。 這些內(nèi)容的聲明順序并不重要。 這個(gè)例子的程序已經(jīng)盡可能短了, 只聲明了一個(gè)函數(shù), 其中只調(diào)用了一個(gè)其他函數(shù)。 為了節(jié)省篇幅, 有些時(shí)候, 示例程序會(huì)省略 package 和 import 聲明, 但是, 這些聲明在源代碼里有, 并且必須得有才能編譯。
一個(gè)函數(shù)的聲明由 func 關(guān)鍵字、 函數(shù)名、 參數(shù)列表、 返回值列表( 這個(gè)例子里的 main 函數(shù)參數(shù)列表和返回值都是空的) 以及包含在大括號(hào)里的函數(shù)體組成。 第五章進(jìn)一步考察函數(shù)。
Go語(yǔ)言不需要在語(yǔ)句或者聲明的末尾添加分號(hào), 除非一行上有多條語(yǔ)句。 實(shí)際上, 編譯器會(huì)
主動(dòng)把特定符號(hào)后的換行符轉(zhuǎn)換為分號(hào), 因此換行符添加的位置會(huì)影響Go代碼的正確解析 。 舉個(gè)例子, 函數(shù)的左括號(hào) { 必須和 func 函數(shù)聲明在同一行上, 且位于末尾, 不能獨(dú)占一行, 而在表達(dá)式 x + y 中, 可在 + 后換行, 不能在 + 前換行。
Go語(yǔ)言在代碼格式上采取了很強(qiáng)硬的態(tài)度。 gofmt 工具把代碼格式化為標(biāo)準(zhǔn)格式, 并且 go 工具中的 fmt 子命令會(huì)對(duì)指定包, 否則默認(rèn)為當(dāng)前目錄,中所有**.go源文件**應(yīng)用 gofmt 命令。 本書(shū)中的所有代碼都被gofmt過(guò)。 你也應(yīng)該養(yǎng)成格式化自己的代碼的習(xí)慣。 以法令方式規(guī)定標(biāo)準(zhǔn)的代碼格式可以避免無(wú)盡的無(wú)意義的瑣碎爭(zhēng)執(zhí)。 更重要的是, 這樣可以做多種自動(dòng)源碼轉(zhuǎn)換, 如果放任Go語(yǔ)言代碼格式, 這些轉(zhuǎn)換就不大可能了。
很多文本編輯器都可以配置為保存文件時(shí)自動(dòng)執(zhí)行 gofmt , 這樣你的源代碼總會(huì)被恰當(dāng)?shù)馗袷交?還有個(gè)相關(guān)的工具, goimports , 可以根據(jù)代碼需要, 自動(dòng)地添加或刪除 import 聲明。 這個(gè)工具并沒(méi)有包含在標(biāo)準(zhǔn)的分發(fā)包中, 可以用下面的命令安裝:
$ go get golang.org/x/tools/cmd/goimports對(duì)于大多數(shù)用戶(hù)來(lái)說(shuō), 下載、 編譯包、 運(yùn)行測(cè)試用例、 查看Go語(yǔ)言的文檔等等常用功能都可以用go的工具完成。 10.7節(jié)詳細(xì)介紹這些知識(shí)。
本節(jié)注釋
者。
編譯錯(cuò)誤。
2. 命令行參數(shù)
大多數(shù)的程序都是處理輸入, 產(chǎn)生輸出; 這也正是“計(jì)算”的定義。 但是, 程序如何獲取要處理的輸入數(shù)據(jù)呢? 一些程序生成自己的數(shù)據(jù), 但通常情況下, 輸入來(lái)自于程序外部: 文件、 網(wǎng)絡(luò)連接、 其它程序的輸出、 敲鍵盤(pán)的用戶(hù)、 命令行參數(shù)或其它類(lèi)似輸入源。 下面幾個(gè)例子會(huì)討論其中幾個(gè)輸入源, 首先是命令行參數(shù)。
os 包以跨平臺(tái)的方式, 提供了一些與操作系統(tǒng)交互的函數(shù)和變量。 程序的命令行參數(shù)可從os
包的Args變量獲取; os包外部使用os.Args訪問(wèn)該變量。
os.Args變量是一個(gè)字符串( string) 的切片( slice) ( 譯注: slice和Python語(yǔ)言中的切片類(lèi)似, 是一個(gè)簡(jiǎn)版的動(dòng)態(tài)數(shù)組) , 切片是Go語(yǔ)言的基礎(chǔ)概念, 稍后詳細(xì)介紹。 現(xiàn)在先把切片s當(dāng)作數(shù)組元素序列, 序列的成長(zhǎng)度動(dòng)態(tài)變化, 用 s[i] 訪問(wèn)單個(gè)元素, 用 s[m:n] 獲取子序列(譯注: 和python里的語(yǔ)法差不多)。 序列的元素?cái)?shù)目為len(s)。 和大多數(shù)編程語(yǔ)言類(lèi)似, 區(qū)間索引時(shí), Go言里也采用左閉右開(kāi)形式, 即, 區(qū)間包括第一個(gè)索引元素, 不包括最后一個(gè), 因?yàn)檫@樣可以簡(jiǎn)化邏輯。 ( 譯注: 比如a = [1, 2, 3, 4, 5], a[0:3] = [1, 2, 3], 不包含最后一個(gè)元素) 。 比如s[m:n]這個(gè)切片, 0 ≤ m ≤ n ≤ len(s), 包含n-m個(gè)元素。
os.Args的第一個(gè)元素, os.Args[0], 是命令本身的名字; 其它的元素則是程序啟動(dòng)時(shí)傳給它的參數(shù)。 s[m:n]形式的切片表達(dá)式, 產(chǎn)生從第m個(gè)元素到第n-1個(gè)元素的切片, 下個(gè)例子用到的元素包含在os.Args[1:len(os.Args)]切片中。 如果省略切片表達(dá)式的m或n, 會(huì)默認(rèn)傳入0或len(s), 因此前面的切片可以簡(jiǎn)寫(xiě)成os.Args[1:]。
下面是Unix里echo命令的一份實(shí)現(xiàn), echo把它的命令行參數(shù)打印成一行。 程序?qū)肓藘蓚€(gè)包, 用括號(hào)把它們括起來(lái)寫(xiě)成列表形式, 而沒(méi)有分開(kāi)寫(xiě)成獨(dú)立的 import 聲明。 兩種形式都合法, 列表形式習(xí)慣上用得多。 包導(dǎo)入順序并不重要; gofmt工具格式化時(shí)按照字母順序?qū)Π判颉?( 示例有多個(gè)版本時(shí), 我們會(huì)對(duì)示例編號(hào), 這樣可以明確當(dāng)前正在討論的是哪個(gè)。 )
gopl.io/ch1/echo1
// Echo1 prints its command-line arguments. package mainimport ("fmt""os" )func main() {var s, sep stringfor i := 1; i < len(os.Args); i++ {s += sep + os.Args[i]sep = " "}fmt.Println(s) }注釋語(yǔ)句以 // 開(kāi)頭。 對(duì)于程序員來(lái)說(shuō), //之后到行末之間所有的內(nèi)容都是注釋, 被編譯器忽略。 按照慣例, 我們?cè)诿總€(gè)包的包聲明前添加注釋; 對(duì)于 main package , 注釋包含一句或幾句話(huà), 從整體角度對(duì)程序做個(gè)描述。
var聲明定義了兩個(gè)string類(lèi)型的變量s和sep。 變量會(huì)在聲明時(shí)直接初始化。 如果變量沒(méi)有顯
式初始化, 則被隱式地賦予其類(lèi)型的零值( zero value) , 數(shù)值類(lèi)型是0, 字符串類(lèi)型是空字
符串""。 這個(gè)例子里, 聲明把s和sep隱式地初始化成空字符串。 第2章再來(lái)詳細(xì)地講解變量和
聲明。
對(duì)數(shù)值類(lèi)型, Go語(yǔ)言提供了常規(guī)的數(shù)值和邏輯運(yùn)算符。 而對(duì)string類(lèi)型, + 運(yùn)算符連接字符串( 譯注: 和C++或者js是一樣的) 。 所以表達(dá)式:
sep + os.Args[i]表示連接字符串sep和os.Args。 程序中使用的語(yǔ)句:
s += sep + os.Args[i]是一條賦值語(yǔ)句, 將s的舊值跟sep與os.Args[i]連接后賦值回s, 等價(jià)于:
s = s + sep + os.Args[i]運(yùn)算符 += 是賦值運(yùn)算符( assignment operator) , 每種數(shù)值運(yùn)算符或邏輯運(yùn)算符,如 + 或 * , 都有對(duì)應(yīng)的賦值運(yùn)算符。
echo程序可以每循環(huán)一次輸出一個(gè)參數(shù), 這個(gè)版本卻是不斷地把新文本追加到末尾來(lái)構(gòu)造字符串。 字符串s開(kāi)始為空, 即值為"", 每次循環(huán)會(huì)添加一些文本; 第一次迭代之后, 還會(huì)再插入一個(gè)空格, 因此循環(huán)結(jié)束時(shí)每個(gè)參數(shù)中間都有一個(gè)空格。 這是一種二次加工( quadraticprocess) , 當(dāng)參數(shù)數(shù)量龐大時(shí), 開(kāi)銷(xiāo)很大, 但是對(duì)于echo, 這種情形不大可能出現(xiàn)。 本章會(huì)介紹echo的若干改進(jìn)版, 下一章解決低效問(wèn)題。
循環(huán)索引變量i在for循環(huán)的第一部分中定義。 符號(hào) := 是短變量聲明( short variable declaration) 的一部分, 這是定義一個(gè)或多個(gè)變量并根據(jù)它們的初始值為這些變量賦予適當(dāng)類(lèi)型的語(yǔ)句。 下一章有這方面更多說(shuō)明。
自增語(yǔ)句 i++ 給 i 加1; 這和 i += 1 以及 i = i + 1 都是等價(jià)的。 對(duì)應(yīng)的還有 i-- 給 i 減1。 它們是語(yǔ)句, 而不像C系的其它語(yǔ)言那樣是表達(dá)式。 所以 j = i++ 非法, 而且++和–都只能放在變量名后面, 因此 --i 也非法。
Go語(yǔ)言只有for循環(huán)這一種循環(huán)語(yǔ)句。 for循環(huán)有多種形式, 其中一種如下所示:
for initialization; condition; post { // zero or more statements }for循環(huán)三個(gè)部分不需括號(hào)包圍。 大括號(hào)強(qiáng)制要求, 左大括號(hào)必須和post語(yǔ)句在同一行。
initialization語(yǔ)句是可選的, 在循環(huán)開(kāi)始前執(zhí)行。 initalization如果存在, 必須是一條簡(jiǎn)單語(yǔ)句( simple statement) , 即, 短變量聲明、 自增語(yǔ)句、 賦值語(yǔ)句或函數(shù)調(diào)用。 condition 是一個(gè)布爾表達(dá)式( boolean expression) , 其值在每次循環(huán)迭代開(kāi)始時(shí)計(jì)算。 如果為 true 則執(zhí)行循環(huán)體語(yǔ)句。 post 語(yǔ)句在循環(huán)體執(zhí)行結(jié)束后執(zhí)行, 之后再次對(duì) conditon 求值。 condition 值為 false 時(shí), 循環(huán)結(jié)束。
for循環(huán)的這三個(gè)部分每個(gè)都可以省略, 如果省略 initialization 和 post , 分號(hào)也可以省略:
如果連 condition 也省略了, 像下面這樣:
這就變成一個(gè)無(wú)限循環(huán), 盡管如此, 還可以用其他方式終止循環(huán), 如一條 break 或 return 語(yǔ)
句。
for 循環(huán)的另一種形式, 在某種數(shù)據(jù)類(lèi)型的區(qū)間( range) 上遍歷, 如字符串或切
片。 echo 的第二版本展示了這種形式:
gopl.io/ch1/echo2
每次循環(huán)迭代, range 產(chǎn)生一對(duì)值; 索引以及在該索引處的元素值。 這個(gè)例子不需要索引,但 range 的語(yǔ)法要求, 要處理元素, 必須處理索引。 一種思路是把索引賦值給一個(gè)臨時(shí)變量,如 temp , 然后忽略它的值, 但Go語(yǔ)言不允許使用無(wú)用的局部變量( local variables) , 因?yàn)檫@會(huì)導(dǎo)致編譯錯(cuò)誤。
Go語(yǔ)言中這種情況的解決方法是用 空標(biāo)識(shí)符 ( blank identifier) , 即 _ ( 也就是下劃線) 。空標(biāo)識(shí)符可用于任何語(yǔ)法需要變量名但程序邏輯不需要的時(shí)候, 例如, 在循環(huán)里, 丟棄不需要的循環(huán)索引, 保留元素值。 大多數(shù)的Go程序員都會(huì)像上面這樣使用 range 和 _ 寫(xiě) echo 程序, 因?yàn)殡[式地而非顯示地索引os.Args, 容易寫(xiě)對(duì)。
echo 的這個(gè)版本使用一條短變量聲明來(lái)聲明并初始化 s 和 seps , 也可以將這兩個(gè)變量分開(kāi)聲明, 聲明一個(gè)變量有好幾種方式, 下面這些都等價(jià):
用哪種不用哪種, 為什么呢? 第一種形式, 是一條短變量聲明, 最簡(jiǎn)潔, 但只能用在函數(shù)內(nèi)部, 而不能用于包變量。 第二種形式依賴(lài)于字符串的默認(rèn)初始化零值機(jī)制, 被初始化為""。 第三種形式用得很少, 除非同時(shí)聲明多個(gè)變量。 第四種形式顯式地標(biāo)明變量的類(lèi)型, 當(dāng)變量類(lèi)型與初值類(lèi)型相同時(shí), 類(lèi)型冗余, 但如果兩者類(lèi)型不同, 變量類(lèi)型就必須了。 實(shí)踐中一般使用前兩種形式中的某個(gè), 初始值重要的話(huà)就顯式地指定變量的類(lèi)型, 否則使用隱式初始化。
如前文所述, 每次循環(huán)迭代字符串s的內(nèi)容都會(huì)更新。 += 連接原字符串、 空格和下個(gè)參數(shù),產(chǎn)生新字符串, 并把它賦值給 s 。 s 原來(lái)的內(nèi)容已經(jīng)不再使用, 將在適當(dāng)時(shí)機(jī)對(duì)它進(jìn)行垃圾回收。
如果連接涉及的數(shù)據(jù)量很大, 這種方式代價(jià)高昂。 一種簡(jiǎn)單且高效的解決方案是使用 strings 包的 Join 函數(shù):
gopl.io/ch1/echo3
最后, 如果不關(guān)心輸出格式, 只想看看輸出值, 或許只是為了調(diào)試, 可以用 Println 為我們格式化輸出。
這條語(yǔ)句的輸出結(jié)果跟 strings.Join 得到的結(jié)果很像, 只是被放到了一對(duì)方括號(hào)里。 切片都
會(huì)被打印成這種格式。
3. 查找重復(fù)的行
對(duì)文件做拷貝、 打印、 搜索、 排序、 統(tǒng)計(jì)或類(lèi)似事情的程序都有一個(gè)差不多的程序結(jié)構(gòu): 一個(gè)處理輸入的循環(huán), 在每個(gè)元素上執(zhí)行計(jì)算處理, 在處理的同時(shí)或最后產(chǎn)生輸出。 我們會(huì)展示一個(gè)名為 dup 的程序的三個(gè)版本; 靈感來(lái)自于Unix的 uniq 命令, 其尋找相鄰的重復(fù)行。該程序使用的結(jié)構(gòu)和包是個(gè)參考范例, 可以方便地修改。
dup 的第一個(gè)版本打印標(biāo)準(zhǔn)輸入中多次出現(xiàn)的行, 以重復(fù)次數(shù)開(kāi)頭。 該程序?qū)⒁?if 語(yǔ)句, map 數(shù)據(jù)類(lèi)型以及 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 mainimport ("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 imput.Err()for line, n := range counts {if n > 1 {fmt.Println("%d\t%s\n", n, line)}} }正如 for 循環(huán)一樣, if 語(yǔ)句條件兩邊也不加括號(hào), 但是主體部分需要加。 if 語(yǔ)句的 else 部分是可選的, 在 if 的條件為 false 時(shí)執(zhí)行。
map存儲(chǔ)了鍵/值( key/value) 的集合, 對(duì)集合元素, 提供常數(shù)時(shí)間的存、 取或測(cè)試操作。 鍵可以是任意類(lèi)型, 只要其值能用 == 運(yùn)算符比較, 最常見(jiàn)的例子是字符串; 值則可以是任意類(lèi)型。 這個(gè)例子中的鍵是字符串, 值是整數(shù)。 內(nèi)置函數(shù) make 創(chuàng)建空 map , 此外, 它還有別的作用。 4.3節(jié)討論 map 。
每次 dup 讀取一行輸入, 該行被當(dāng)做 map , 其對(duì)應(yīng)的值遞增。 counts[input.Text()]++ 語(yǔ)句等價(jià)下面兩句:
map 中不含某個(gè)鍵時(shí)不用擔(dān)心, 首次讀到新行時(shí), 等號(hào)右邊的表達(dá)式 counts[line] 的值將被計(jì)算為其類(lèi)型的零值, 對(duì)于int即為0。
為了打印結(jié)果, 我們使用了基于 range 的循環(huán), 并在 counts 這個(gè) map 上迭代。 跟之前類(lèi)似, 每次迭代得到兩個(gè)結(jié)果, 鍵和其在map 中對(duì)應(yīng)的值。 map 的迭代順序并不確定, 從實(shí)踐來(lái)看, 該順序隨機(jī), 每次運(yùn)行都會(huì)變化。 這種設(shè)計(jì)是有意為之的, 因?yàn)槟芊乐钩绦蛞蕾?lài)特定遍歷順序, 而這是無(wú)法保證的。
繼續(xù)來(lái)看 bufio 包, 它使處理輸入和輸出方便又高效。 Scanner 類(lèi)型是該包最有用的特性之一, 它讀取輸入并將其拆成行或單詞; 通常是處理行形式的輸入最簡(jiǎn)單的方法。
程序使用短變量聲明創(chuàng)建 bufio.Scanner 類(lèi)型的變量 input 。
該變量從程序的標(biāo)準(zhǔn)輸入中讀取內(nèi)容。 每次調(diào)用 input.Scanner , 即讀入下一行, 并移除行末的換行符; 讀取的內(nèi)容可以調(diào)用 input.Text() 得到。 Scan 函數(shù)在讀到一行時(shí)返回 true , 在無(wú)輸入時(shí)返回 false 。
類(lèi)似于C或其它語(yǔ)言里的 printf 函數(shù), fmt.Printf 函數(shù)對(duì)一些表達(dá)式產(chǎn)生格式化輸出。 該函數(shù)的首個(gè)參數(shù)是個(gè)格式字符串, 指定后續(xù)參數(shù)被如何格式化。 各個(gè)參數(shù)的格式取決于“轉(zhuǎn)換字符”( conversion character) , 形式為百分號(hào)后跟一個(gè)字母。 舉個(gè)例子, %d 表示以十進(jìn)制形式打印一個(gè)整型操作數(shù), 而 %s 則表示把字符串型操作數(shù)的值展開(kāi)。
Printf 有一大堆這種轉(zhuǎn)換, Go程序員稱(chēng)之為動(dòng)詞( verb) 。 下面的表格雖然遠(yuǎn)不是完整的規(guī)范, 但展示了可用的很多特性:
| %d | 十進(jìn)制整數(shù) |
| %x, %o, %b | 十六進(jìn)制, 八進(jìn)制, 二進(jìn)制整數(shù)。 |
| %f, %g, %e | 浮點(diǎn)數(shù): 3.141593 3.141592653589793 3.141593e+00 |
| %t | 布爾: true或false |
| %c | 字符( rune) (Unicode碼點(diǎn)) |
| %s | 字符串 |
| %q | 帶雙引號(hào)的字符串"abc"或帶單引號(hào)的字符’c’ |
| %v | 變量的自然形式( natural format) |
| %T | 變量的類(lèi)型 |
| %% | 字面上的百分號(hào)標(biāo)志( 無(wú)操作數(shù)) |
dup1 的格式字符串中還含有制表符 \t 和換行符 \n 。 字符串字面上可能含有這些代表不可見(jiàn)字符的轉(zhuǎn)義字符( escap sequences) 。 默認(rèn)情況下, Printf 不會(huì)換行。 按照慣例, 以字母 f 結(jié)尾的格式化函數(shù), 如 log.Printf 和 fmt.Errorf , 都采用 fmt.Printf 的格式化準(zhǔn)則。而以 ln 結(jié)尾的格式化函數(shù), 則遵循 Println 的方式, 以跟 %v 差不多的方式格式化參數(shù), 并在最后添加一個(gè)換行符。 ( 譯注: 后綴 f 指 fomart , ln 指 line 。 )
很多程序要么從標(biāo)準(zhǔn)輸入中讀取數(shù)據(jù), 如上面的例子所示, 要么從一系列具名文件中讀取數(shù)據(jù)。 dup 程序的下個(gè)版本讀取標(biāo)準(zhǔn)輸入或是使用 os.Open 打開(kāi)各個(gè)具名文件, 并操作它們。
gopl.io/ch1/dup2
os.Open 函數(shù)返回兩個(gè)值。 第一個(gè)值是被打開(kāi)的文件( *os.File ) , 其后被 Scanner 讀取。
os.Open 返回的第二個(gè)值是內(nèi)置 error 類(lèi)型的值。 如果 err 等于內(nèi)置值 nil ( 譯注: 相當(dāng)于其它語(yǔ)言里的NULL) , 那么文件被成功打開(kāi)。 讀取文件, 直到文件結(jié)束, 然后調(diào)用 Close 關(guān)閉該文件, 并釋放占用的所有資源。 相反的話(huà), 如果 err 的值不是 nil , 說(shuō)明打開(kāi)文件時(shí)出錯(cuò)了。 這種情況下, 錯(cuò)誤值描述了所遇到的問(wèn)題。 我們的錯(cuò)誤處理非常簡(jiǎn)單, 只是使用 Fprintf 與表示任意類(lèi)型默認(rèn)格式值的動(dòng)詞 %v , 向標(biāo)準(zhǔn)錯(cuò)誤流打印一條信息, 然后 dup 繼續(xù)處理下一個(gè)文件; continue 語(yǔ)句直接跳到 for 循環(huán)的下個(gè)迭代開(kāi)始執(zhí)行。
為了使示例代碼保持合理的大小, 本書(shū)開(kāi)始的一些示例有意簡(jiǎn)化了錯(cuò)誤處理, 顯而易見(jiàn)的是, 應(yīng)該檢查 os.Open 返回的錯(cuò)誤值, 然而, 使用 input.Scan 讀取文件過(guò)程中, 不大可能出現(xiàn)錯(cuò)誤, 因此我們忽略了錯(cuò)誤處理。 我們會(huì)在跳過(guò)錯(cuò)誤檢查的地方做說(shuō)明。 5.4節(jié)中深入介紹錯(cuò)誤處理。
注意 countLines 函數(shù)在其聲明前被調(diào)用。 函數(shù)和包級(jí)別的變量( package-level entities) 可以任意順序聲明, 并不影響其被調(diào)用。 ( 譯注: 最好還是遵循一定的規(guī)范)
map 是一個(gè)由 make 函數(shù)創(chuàng)建的數(shù)據(jù)結(jié)構(gòu)的引用。 map 作為為參數(shù)傳遞給某函數(shù)時(shí), 該函數(shù)接收這個(gè)引用的一份拷貝( copy, 或譯為副本) , 被調(diào)用函數(shù)對(duì) map 底層數(shù)據(jù)結(jié)構(gòu)的任何修改, 調(diào)用者函數(shù)都可以通過(guò)持有的 map 引用看到。 在我們的例子中, countLines 函數(shù)向 counts 插入的值, 也會(huì)被 main 函數(shù)看到。 ( 譯注: 類(lèi)似于C++里的引用傳遞, 實(shí)際上指針是另一個(gè)指針了, 但內(nèi)部存的值指向同一塊內(nèi)存)
dup 的前兩個(gè)版本以"流”模式讀取輸入, 并根據(jù)需要拆分成多個(gè)行。 理論上, 這些程序可以處理任意數(shù)量的輸入數(shù)據(jù)。 還有另一個(gè)方法, 就是一口氣把全部輸入數(shù)據(jù)讀到內(nèi)存中, 一次分割為多行, 然后處理它們。 下面這個(gè)版本, dup3 , 就是這么操作的。 這個(gè)例子引入了 ReadFile 函數(shù)( 來(lái)自于 io/ioutil 包) , 其讀取指定文件的全部?jī)?nèi)容, strings.Split 函數(shù)把字符串分割成子串的切片。 ( Split 的作用與前文提到的 strings.Join 相反。 )
我們略微簡(jiǎn)化了 dup3 。 首先, 由于 ReadFile 函數(shù)需要文件名作為參數(shù), 因此只讀指定文件, 不讀標(biāo)準(zhǔn)輸入。 其次, 由于行計(jì)數(shù)代碼只在一處用到, 故將其移回 main 函數(shù)。
gopl.io/ch1/dup3
ReadFile 函數(shù)返回一個(gè)字節(jié)切片( byte slice) , 必須把它轉(zhuǎn)換為 string , 才能用 strings.Split 分割。 我們會(huì)在3.5.4節(jié)詳細(xì)講解字符串和字節(jié)切片。
實(shí)際上, bufio.Scanner 、ioutil.ReadFile 和 ioutil.WriteFile 都使用 *os.File 的 Read 和 Write 方法, 但是, 大多數(shù)程序員很少需要直接調(diào)用那些低級(jí)( lower-level) 函數(shù)。 高級(jí)( higher-level) 函數(shù), 像 bufio 和 io/ioutil 包中所提供的那些, 用起來(lái)要容易點(diǎn)。
4. GIF動(dòng)畫(huà)
下面的程序會(huì)演示Go語(yǔ)言標(biāo)準(zhǔn)庫(kù)里的image這個(gè)package的用法, 我們會(huì)用這個(gè)包來(lái)生成一系列的bit-mapped圖, 然后將這些圖片編碼為一個(gè)GIF動(dòng)畫(huà)。 我們生成的圖形名字叫利薩如圖(Lissajous figures), 這種效果是在1960年代的老電影里出現(xiàn)的一種視覺(jué)特效。 它們是協(xié)振子在兩個(gè)緯度上振動(dòng)所產(chǎn)生的曲線, 比如兩個(gè)sin正弦波分別在x軸和y軸輸入會(huì)產(chǎn)生的曲線。 圖1.1是這樣的一個(gè)例子:
譯注: 要看這個(gè)程序的結(jié)果, 需要將標(biāo)準(zhǔn)輸出重定向到一個(gè)GIF圖像文件( 使用 ./lissajous> output.gif 命令) 。 下面是GIF圖像動(dòng)畫(huà)效果:
這段代碼里我們用了一些新的結(jié)構(gòu), 包括const聲明, struct結(jié)構(gòu)體類(lèi)型, 復(fù)合聲明。 和我們舉的其它的例子不太一樣, 這一個(gè)例子包含了浮點(diǎn)數(shù)運(yùn)算。 這些概念我們只在這里簡(jiǎn)單地說(shuō)明一下, 之后的章節(jié)會(huì)更詳細(xì)地講解。
gopl.io/ch1/lissajous
// Lissajous generates GIF animations of random Lissajous figures. package mainimport ("image""image/color""image/gif""io""math""math/rand""os" )var palette = []color.Color{color.White, color.Black}const (whiteIndex = 0 // first color in paletteblackIndex = 1 // next color in palette )func main() {lissajous(os.Stdout) }func lissajous(out io.Writer) {const (cycles = 5 //number of complete x oscillator revolutionsres = 0.001 // angular resolutionsize = 100 // image canvas covers [-size..+size]nframes = 64 // number of animation framesdelay = 8 // delay between frames in 10ms units)freq := rand.Float64() * 3.0 // relative frequency of y oscillatoranim := gif.GIF{LoopCount: nframes}phase := 0.0for 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.1anim.Delay = append(anim.Delay, delay)anim.Image = append(anim.Image, img)}gif.EncodeAll(out, &anim) }當(dāng)我們import了一個(gè)包路徑包含有多個(gè)單詞的package時(shí), 比如image/color( image和color兩個(gè)單詞) , 通常我們只需要用最后那個(gè)單詞表示這個(gè)包就可以。 所以當(dāng)我們寫(xiě)color.White時(shí), 這個(gè)變量指向的是image/color包里的變量, 同理gif.GIF是屬于image/gif包里的變量
這個(gè)程序里的常量聲明給出了一系列的常量值, 常量是指在程序編譯后運(yùn)行時(shí)始終都不會(huì)變化的值, 比如圈數(shù)、 幀數(shù)、 延遲值。 常量聲明和變量聲明一般都會(huì)出現(xiàn)在包級(jí)別, 所以這些常量在整個(gè)包中都是可以共享的, 或者你也可以把常量聲明定義在函數(shù)體內(nèi)部, 那么這種常量就只能在函數(shù)體內(nèi)用。 目前常量聲明的值必須是一個(gè)數(shù)字值、 字符串或者一個(gè)固定的boolean值。
[]color.Color{…}和gif.GIF{…}這兩個(gè)表達(dá)式就是我們說(shuō)的復(fù)合聲明( 4.2和4.4.1節(jié)有說(shuō)明) 。 這是實(shí)例化Go語(yǔ)言里的復(fù)合類(lèi)型的一種寫(xiě)法。 這里的前者生成的是一個(gè)slice切片, 后者生成的是一個(gè)struct結(jié)構(gòu)體。
gif.GIF是一個(gè)struct類(lèi)型( 參考4.4節(jié)) 。 struct是一組值或者叫字段的集合, 不同的類(lèi)型集合在一個(gè)struct可以讓我們以一個(gè)統(tǒng)一的單元進(jìn)行處理。 anim是一個(gè)gif.GIF類(lèi)型的struct變量。這種寫(xiě)法會(huì)生成一個(gè)struct變量, 并且其內(nèi)部變量LoopCount字段會(huì)被設(shè)置為nframes; 而其它的字段會(huì)被設(shè)置為各自類(lèi)型默認(rèn)的零值。 struct內(nèi)部的變量可以以一個(gè)點(diǎn)(.)來(lái)進(jìn)行訪問(wèn), 就像在最后兩個(gè)賦值語(yǔ)句中顯式地更新了anim這個(gè)struct的Delay和Image字段。
lissajous函數(shù)內(nèi)部有兩層嵌套的for循環(huán)。 外層循環(huán)會(huì)循環(huán)64次, 每一次都會(huì)生成一個(gè)單獨(dú)的動(dòng)畫(huà)幀。 它生成了一個(gè)包含兩種顏色的201&201大小的圖片, 白色和黑色。 所有像素點(diǎn)都會(huì)被默認(rèn)設(shè)置為其零值( 也就是調(diào)色板palette里的第0個(gè)值) , 這里我們?cè)O(shè)置的是白色。 每次外層循環(huán)都會(huì)生成一張新圖片, 并將一些像素設(shè)置為黑色。 其結(jié)果會(huì)append到之前結(jié)果之后。這里我們用到了append(參考4.2.1)內(nèi)置函數(shù), 將結(jié)果append到anim中的幀列表末尾, 并設(shè)置一個(gè)默認(rèn)的80ms的延遲值。 循環(huán)結(jié)束后所有的延遲值被編碼進(jìn)了GIF圖片中, 并將結(jié)果寫(xiě)入到輸出流。 out這個(gè)變量是io.Writer類(lèi)型, 這個(gè)類(lèi)型支持把輸出結(jié)果寫(xiě)到很多目標(biāo), 很快我們就可以看到例子。
內(nèi)層循環(huán)設(shè)置兩個(gè)偏振值。 x軸偏振使用sin函數(shù)。 y軸偏振也是正弦波, 但其相對(duì)x軸的偏振是一個(gè)0-3的隨機(jī)值, 初始偏振值是一個(gè)零值, 隨著動(dòng)畫(huà)的每一幀逐漸增加。 循環(huán)會(huì)一直跑到x軸完成五次完整的循環(huán)。 每一步它都會(huì)調(diào)用SetColorIndex來(lái)為(x, y)點(diǎn)來(lái)染黑色。
main函數(shù)調(diào)用lissajous函數(shù), 用它來(lái)向標(biāo)準(zhǔn)輸出流打印信息, 所以下面這個(gè)命令會(huì)像圖1.1中產(chǎn)生一個(gè)GIF動(dòng)畫(huà)。
5. 獲取URL
對(duì)于很多現(xiàn)代應(yīng)用來(lái)說(shuō), 訪問(wèn)互聯(lián)網(wǎng)上的信息和訪問(wèn)本地文件系統(tǒng)一樣重要。 Go語(yǔ)言在net這個(gè)強(qiáng)大package的幫助下提供了一系列的package來(lái)做這件事情, 使用這些包可以更簡(jiǎn)單地用網(wǎng)絡(luò)收發(fā)信息, 還可以建立更底層的網(wǎng)絡(luò)連接, 編寫(xiě)服務(wù)器程序。 在這些情景下, Go語(yǔ)言原生的并發(fā)特性( 在第八章中會(huì)介紹) 顯得尤其好用。
為了最簡(jiǎn)單地展示基于HTTP獲取信息的方式, 下面給出一個(gè)示例程序fetch, 這個(gè)程序?qū)@取對(duì)應(yīng)的url, 并將其源文本打印出來(lái); 這個(gè)例子的靈感來(lái)源于curl工具( 譯注: unix下的一個(gè)用來(lái)發(fā)http請(qǐng)求的工具, 具體可以man curl) 。 當(dāng)然, curl提供的功能更為復(fù)雜豐富, 這里只編寫(xiě)最簡(jiǎn)單的樣例。 這個(gè)樣例之后還會(huì)多次被用到。
gopl.io/ch1/fetch
package mainimport ("fmt""io/ioutil""net/http""os" )func main() {for _, url := range os.Args[1:] {resp, err := http.Get(url)if err != nil {fmt.Fprint(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)} }這個(gè)程序從兩個(gè)package中導(dǎo)入了函數(shù), net/http和io/ioutil包, http.Get函數(shù)是創(chuàng)建HTTP請(qǐng)求的函數(shù), 如果獲取過(guò)程沒(méi)有出錯(cuò), 那么會(huì)在resp這個(gè)結(jié)構(gòu)體中得到訪問(wèn)的請(qǐng)求結(jié)果。 resp的Body字段包括一個(gè)可讀的服務(wù)器響應(yīng)流。 ioutil.ReadAll函數(shù)從response中讀取到全部?jī)?nèi)容;
將其結(jié)果保存在變量b中。 resp.Body.Close關(guān)閉resp的Body流, 防止資源泄露, Printf函數(shù)會(huì)將結(jié)果b寫(xiě)出到標(biāo)準(zhǔn)輸出流中
HTTP請(qǐng)求如果失敗了的話(huà), 會(huì)得到下面這樣的結(jié)果:
在大天朝的網(wǎng)絡(luò)環(huán)境下很容易重現(xiàn)這種錯(cuò)誤, 下面是Windows下運(yùn)行得到的錯(cuò)誤信息:
無(wú)論哪種失敗原因, 我們的程序都用了os.Exit函數(shù)來(lái)終止進(jìn)程, 并且返回一個(gè)status錯(cuò)誤碼,其值為1。
6. 并發(fā)獲取多個(gè)URL
7. Web服務(wù)
Go語(yǔ)言的內(nèi)置庫(kù)使得寫(xiě)一個(gè)類(lèi)似fetch的web服務(wù)器變得異常地簡(jiǎn)單。 在本節(jié)中, 我們會(huì)展示一個(gè)微型服務(wù)器, 這個(gè)服務(wù)器的功能是返回當(dāng)前用戶(hù)正在訪問(wèn)的URL。 比如用戶(hù)訪問(wèn)的是http://localhost:8000/hello , 那么響應(yīng)是URL.Path = “hello”。
gopl.io/ch1/server1
// Server1 is a minimal "echo" server. package mainimport ("fmt""log""net/http" )func main() {http.HandleFunc("/", handler) //each request calls handlerlog.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) }我們只用了八九行代碼就實(shí)現(xiàn)了一個(gè)Web服務(wù)程序, 這都是多虧了標(biāo)準(zhǔn)庫(kù)里的方法已經(jīng)幫我們完成了大量工作。 main函數(shù)將所有發(fā)送到/路徑下的請(qǐng)求和handler函數(shù)關(guān)聯(lián)起來(lái), /開(kāi)頭的請(qǐng)求其實(shí)就是所有發(fā)送到當(dāng)前站點(diǎn)上的請(qǐng)求, 服務(wù)監(jiān)聽(tīng)8000端口。 發(fā)送到這個(gè)服務(wù)的“請(qǐng)求”是一個(gè)http.Request類(lèi)型的對(duì)象, 這個(gè)對(duì)象中包含了請(qǐng)求中的一系列相關(guān)字段, 其中就包括我們需要的URL。 當(dāng)請(qǐng)求到達(dá)服務(wù)器時(shí), 這個(gè)請(qǐng)求會(huì)被傳給handler函數(shù)來(lái)處理, 這個(gè)函數(shù)會(huì)將/hello這個(gè)路徑從請(qǐng)求的URL中解析出來(lái), 然后把其發(fā)送到響應(yīng)中, 這里我們用的是標(biāo)準(zhǔn)輸出流的fmt.Fprintf。 Web服務(wù)會(huì)在第7.7節(jié)中做更詳細(xì)的闡述。
讓我們?cè)诤笈_(tái)運(yùn)行這個(gè)服務(wù)程序。 如果你的操作系統(tǒng)是Mac OS X或者Linux, 那么在運(yùn)行命令的末尾加上一個(gè)&符號(hào), 即可讓程序簡(jiǎn)單地跑在后臺(tái), windows下可以在另外一個(gè)命令行窗口去運(yùn)行這個(gè)程序。
現(xiàn)在可以通過(guò)命令行來(lái)發(fā)送客戶(hù)端請(qǐng)求了:
還可以直接在瀏覽器里訪問(wèn)這個(gè)URL, 然后得到返回結(jié)果, 如圖1.2:
在這個(gè)服務(wù)的基礎(chǔ)上疊加特性是很容易的。 一種比較實(shí)用的修改是為訪問(wèn)的url添加某種狀態(tài)。 比如, 下面這個(gè)版本輸出了同樣的內(nèi)容, 但是會(huì)對(duì)請(qǐng)求的次數(shù)進(jìn)行計(jì)算; 對(duì)URL的請(qǐng)求結(jié)果會(huì)包含各種URL被訪問(wèn)的總次數(shù), 直接對(duì)/count這個(gè)URL的訪問(wèn)要除外。
gopl.io/ch1/server2
package mainimport ("fmt""log""net/http""sync" )var mu sync.Mutex var count intfunc main() {http.HandleFunc("/", handler1)http.HandleFunc("/count", counter)log.Fatal(http.ListenAndServe("localhost:8001", nil)) }// 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() }// handler echoes the Path component of the requested URL. func handler1(w http.ResponseWriter, r *http.Request) {mu.Lock()count++mu.Unlock()fmt.Fprintf(w, "URL.Path = %q\n", r.URL.Path) }這個(gè)服務(wù)器有兩個(gè)請(qǐng)求處理函數(shù), 根據(jù)請(qǐng)求的url不同會(huì)調(diào)用不同的函數(shù): 對(duì)/count這個(gè)url的請(qǐng)求會(huì)調(diào)用到count這個(gè)函數(shù), 其它的url都會(huì)調(diào)用默認(rèn)的處理函數(shù)。 如果你的請(qǐng)求pattern是以/結(jié)尾, 那么所有以該url為前綴的url都會(huì)被這條規(guī)則匹配。 在這些代碼的背后, 服務(wù)器每一次接收請(qǐng)求處理時(shí)都會(huì)另起一個(gè)goroutine, 這樣服務(wù)器就可以同一時(shí)間處理多個(gè)請(qǐng)求。 然而在并發(fā)情況下, 假如真的有兩個(gè)請(qǐng)求同一時(shí)刻去更新count, 那么這個(gè)值可能并不會(huì)被正確地增加; 這個(gè)程序可能會(huì)引發(fā)一個(gè)嚴(yán)重的bug: 競(jìng)態(tài)條件( 參見(jiàn)9.1) 。 為了避免這個(gè)問(wèn)題, 我們必須保證每次修改變量的最多只能有一個(gè)goroutine, 這也就是代碼里的mu.Lock()和mu.Unlock()調(diào)用將修改count的所有行為包在中間的目的。 第九章中我們會(huì)進(jìn)一步講解共享變量。
下面是一個(gè)更為豐富的例子, handler函數(shù)會(huì)把請(qǐng)求的http頭和請(qǐng)求的form數(shù)據(jù)都打印出來(lái),這樣可以使檢查和調(diào)試這個(gè)服務(wù)更為方便:
gopl.io/ch1/server3
我們用http.Request這個(gè)struct里的字段來(lái)輸出下面這樣的內(nèi)容:
可以看到這里的ParseForm被嵌套在了if語(yǔ)句中。 Go語(yǔ)言允許這樣的一個(gè)簡(jiǎn)單的語(yǔ)句結(jié)果作為循環(huán)的變量聲明出現(xiàn)在if語(yǔ)句的最前面, 這一點(diǎn)對(duì)錯(cuò)誤處理很有用處。 我們還可以像下面這樣寫(xiě)( 當(dāng)然看起來(lái)就長(zhǎng)了一些)
用if和ParseForm結(jié)合可以讓代碼更加簡(jiǎn)單, 并且可以限制err這個(gè)變量的作用域, 這么做是很不錯(cuò)的。 我們會(huì)在2.7節(jié)中講解作用域。
在這些程序中, 我們看到了很多不同的類(lèi)型被輸出到標(biāo)準(zhǔn)輸出流中。 比如前面的fetch程序,把HTTP的響應(yīng)數(shù)據(jù)拷貝到了os.Stdout, lissajous程序里我們輸出的是一個(gè)文件。 fetchall程序則完全忽略到了HTTP的響應(yīng)Body, 只是計(jì)算了一下響應(yīng)Body的大小, 這個(gè)程序中把響應(yīng)Body拷貝到了ioutil.Discard。 在本節(jié)的web服務(wù)器程序中則是用fmt.Fprintf直接寫(xiě)到了http.ResponseWriter中。
盡管三種具體的實(shí)現(xiàn)流程并不太一樣, 他們都實(shí)現(xiàn)一個(gè)共同的接口, 即當(dāng)它們被調(diào)用需要一個(gè)標(biāo)準(zhǔn)流輸出時(shí)都可以滿(mǎn)足。 這個(gè)接口叫作io.Writer, 在7.1節(jié)中會(huì)詳細(xì)討論。
Go語(yǔ)言的接口機(jī)制會(huì)在第7章中講解, 為了在這里簡(jiǎn)單說(shuō)明接口能做什么, 讓我們簡(jiǎn)單地將這里的web服務(wù)器和之前寫(xiě)的lissajous函數(shù)結(jié)合起來(lái), 這樣GIF動(dòng)畫(huà)可以被寫(xiě)到HTTP的客戶(hù)端, 而不是之前的標(biāo)準(zhǔn)輸出流。 只要在web服務(wù)器的代碼里加入下面這幾行。
或者另一種等價(jià)形式:
HandleFunc函數(shù)的第二個(gè)參數(shù)是一個(gè)函數(shù)的字面值, 也就是一個(gè)在使用時(shí)定義的匿名函數(shù)。這些內(nèi)容我們會(huì)在5.6節(jié)中講解。
做完這些修改之后, 在瀏覽器里訪問(wèn) http://localhost:8000 。 每次你載入這個(gè)頁(yè)面都可以看到一個(gè)像圖1.3那樣的動(dòng)畫(huà)。
8. 本章要點(diǎn)
總結(jié)
以上是生活随笔為你收集整理的《Go语言圣经》学习笔记 第一章 Go语言入门的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: LeetCode——树:BST
- 下一篇: 《Go语言圣经》学习笔记 第二章 程序结