日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問(wèn) 生活随笔!

生活随笔

當(dāng)前位置: 首頁(yè) > 编程资源 > 编程问答 >内容正文

编程问答

《Go语言圣经》学习笔记 第一章 Go语言入门

發(fā)布時(shí)間:2024/4/11 编程问答 40 豆豆
生活随笔 收集整理的這篇文章主要介紹了 《Go语言圣经》学习笔记 第一章 Go语言入门 小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,幫大家做個(gè)參考.

Go語(yǔ)言圣經(jīng)學(xué)習(xí)筆記 第一章 Go語(yǔ)言入門(mén)


目錄

  • Hello, World
  • 命令行參數(shù)
  • 查找重復(fù)的行
  • GIF動(dòng)畫(huà)
  • 獲取URL
  • 并發(fā)獲取多個(gè)URL
  • Web服務(wù)
  • 本章要點(diǎ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子命令:

    $ go build helloworld.go
  • 這個(gè)命令生成一個(gè)名為helloworld的可執(zhí)行的二進(jìn)制文件, 之后你可以隨時(shí)運(yùn)行它, 不需任
    何處理(注:顯示的是win命令行,不是Linux)。

  • 本書(shū)中, 所有的示例代碼上都有一行標(biāo)記, 利用這些標(biāo)記, 可以從gopl.io網(wǎng)站上本書(shū)源碼倉(cāng)庫(kù)
    里獲取代碼:

    gopl.io/ch1/helloworld
  • 執(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é)注釋

  • 本書(shū)作者之一Brian W. Kernighan也是《The C Programming Language》 一書(shū)的作
    者。
  • Windows系統(tǒng)下生成的可執(zhí)行文件是helloworld.exe, 增加了.exe后綴名。
  • 在Windows系統(tǒng)下在命令行直接輸入helloworld.exe命令運(yùn)行。
  • 因?yàn)殪o態(tài)編譯, 所以不用擔(dān)心在系統(tǒng)庫(kù)更新的時(shí)候沖突, 幸福感滿(mǎn)滿(mǎn)。
  • 需要先安裝Git或Hg之類(lèi)的版本管理工具, 并將對(duì)應(yīng)的命令添加到PATH環(huán)境變量中。序言已經(jīng)提及, 需要先設(shè)置好GOPATH環(huán)境變量, 下載的代碼會(huì)放在 $GOPATH/src/gopl.io/ch1/helloworld 目錄。
  • 以+結(jié)尾的話(huà)不會(huì)被插入分號(hào)分隔符, 但是以x結(jié)尾的話(huà)則會(huì)被分號(hào)分隔符, 從而導(dǎo)致
    編譯錯(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

    // Echo2 prints its command-line arguments. package mainimport ("fmt""os" )func main() {s, sep := "", ""for _, arg := range os.Args[1:] {s += sep + argsep = " "}fmt.Println(s) }
  • 每次循環(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

    func main() { fmt.Println(strings.Join(os.Args[1:], " ")) }
  • 最后, 如果不關(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

    package mainimport ("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.Fprint(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 函數(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

    package mainimport ("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.Fprint(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.Println("%d\t%s\n", n, line)}} }
  • 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

  • Go語(yǔ)言最有意思并且最新奇的特性就是對(duì)并發(fā)編程的支持。 并發(fā)編程是一個(gè)大話(huà)題, 在第八章和第九章中會(huì)專(zhuān)門(mén)講到。 這里我們只淺嘗輒止地來(lái)體驗(yàn)一下Go語(yǔ)言里的goroutine和channel。
  • 下面的例子fetchall, 和前面小節(jié)的fetch程序所要做的工作基本一致, fetchall的特別之處在于它會(huì)同時(shí)去獲取所有的URL, 所以這個(gè)程序的總執(zhí)行時(shí)間不會(huì)超過(guò)執(zhí)行時(shí)間最長(zhǎng)的那一個(gè)任務(wù), 前面的fetch程序執(zhí)行時(shí)間則是所有任務(wù)執(zhí)行時(shí)間之和。 fetchall程序只會(huì)打印獲取的內(nèi)容大小和經(jīng)過(guò)的時(shí)間, 不會(huì)像之前那樣打印獲取的內(nèi)容。
  • gopl.io/ch1/fetchall// Fetchall fetches URLs in parallel and reports their times and sizes. package mainimport ("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 chreturn}nbytes, err := io.Copy(ioutil.Discard, resp.Body)resp.Body.Close() //don't leak resourcesif err != nil {ch <- fmt.Sprintf("while reading %s: %v", url, err)}secs := time.Since(start).Seconds()ch <- fmt.Sprintf("%.2fs %7d %s", secs, nbytes, url) }
  • 下面使用fetchall來(lái)請(qǐng)求幾個(gè)地址:
  • goroutine是一種函數(shù)的并發(fā)執(zhí)行方式, 而channel是用來(lái)在goroutine之間進(jìn)行參數(shù)傳遞。main函數(shù)本身也運(yùn)行在一個(gè)goroutine中, 而go function則表示創(chuàng)建一個(gè)新的goroutine, 并在這個(gè)新的goroutine中執(zhí)行這個(gè)函數(shù)。
  • main函數(shù)中用make函數(shù)創(chuàng)建了一個(gè)傳遞string類(lèi)型參數(shù)的channel, 對(duì)每一個(gè)命令行參數(shù), 我們都用go這個(gè)關(guān)鍵字來(lái)創(chuàng)建一個(gè)goroutine, 并且讓函數(shù)在這個(gè)goroutine異步執(zhí)行http.Get方法。 這個(gè)程序里的io.Copy會(huì)把響應(yīng)的Body內(nèi)容拷貝到ioutil.Discard輸出流中( 譯注: 可以把這個(gè)變量看作一個(gè)垃圾桶, 可以向里面寫(xiě)一些不需要的數(shù)據(jù)) , 因?yàn)槲覀冃枰@個(gè)方法返回的字節(jié)數(shù), 但是又不想要其內(nèi)容。 每當(dāng)請(qǐng)求返回內(nèi)容時(shí), fetch函數(shù)都會(huì)往ch這個(gè)channel里寫(xiě)入一個(gè)字符串, 由main函數(shù)里的第二個(gè)for循環(huán)來(lái)處理并打印channel里的這個(gè)字符串。
  • 當(dāng)一個(gè)goroutine嘗試在一個(gè)channel上做send或者receive操作時(shí), 這個(gè)goroutine會(huì)阻塞在調(diào)用處, 直到另一個(gè)goroutine往這個(gè)channel里寫(xiě)入、 或者接收值, 這樣兩個(gè)goroutine才會(huì)繼續(xù)執(zhí)行channel操作之后的邏輯。 在這個(gè)例子中, 每一個(gè)fetch函數(shù)在執(zhí)行時(shí)都會(huì)往channel里發(fā)送一個(gè)值(ch <- expression), 主函數(shù)負(fù)責(zé)接收這些值(<-ch)。 這個(gè)程序中我們用main函數(shù)來(lái)接收所有fetch函數(shù)傳回的字符串, 可以避免在goroutine異步執(zhí)行還沒(méi)有完成時(shí)main函數(shù)提前退出。

  • 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

    func handler1(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這個(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)

  • 控制流: 在本章我們只介紹了if控制和for, 但是沒(méi)有提到switch多路選擇。 這里是一個(gè)簡(jiǎn)單的switch的例子:
  • 在翻轉(zhuǎn)硬幣的時(shí)候, 例子里的coinflip函數(shù)返回幾種不同的結(jié)果, 每一個(gè)case都會(huì)對(duì)應(yīng)一個(gè)返回結(jié)果, 這里需要注意, Go語(yǔ)言并不需要顯式地在每一個(gè)case后寫(xiě)break, 語(yǔ)言默認(rèn)執(zhí)行完case后的邏輯語(yǔ)句會(huì)自動(dòng)退出。 當(dāng)然了, 如果你想要相鄰的幾個(gè)case都執(zhí)行同一邏輯的話(huà),需要自己顯式地寫(xiě)上一個(gè)fallthrough語(yǔ)句來(lái)覆蓋這種默認(rèn)行為。 不過(guò)fallthrough語(yǔ)句在一般的程序中很少用到。
  • Go語(yǔ)言里的switch還可以不帶操作對(duì)象( 譯注: switch不帶操作對(duì)象時(shí)默認(rèn)用true值代替, 然后將每個(gè)case的表達(dá)式和true值進(jìn)行比較) ; 可以直接羅列多種條件, 像其它語(yǔ)言里面的多個(gè)if else一樣, 下面是一個(gè)例子:
  • 這種形式叫做無(wú)tag switch(tagless switch); 這和switch true是等價(jià)的。
  • 像for和if控制語(yǔ)句一樣, switch也可以緊跟一個(gè)簡(jiǎn)短的變量聲明, 一個(gè)自增表達(dá)式、 賦值語(yǔ)句, 或者一個(gè)函數(shù)調(diào)用(譯注: 比其它語(yǔ)言豐富)。
  • break和continue語(yǔ)句會(huì)改變控制流。 和其它語(yǔ)言中的break和continue一樣, break會(huì)中斷當(dāng)前的循環(huán), 并開(kāi)始執(zhí)行循環(huán)之后的內(nèi)容, 而continue會(huì)中跳過(guò)當(dāng)前循環(huán), 并開(kāi)始執(zhí)行下一次循環(huán)。 這兩個(gè)語(yǔ)句除了可以控制for循環(huán), 還可以用來(lái)控制switch和select語(yǔ)句(之后會(huì)講到), 在1.3節(jié)中我們看到, continue會(huì)跳過(guò)內(nèi)層的循環(huán), 如果我們想跳過(guò)的是更外層的循環(huán)的話(huà), 我們可以在相應(yīng)的位置加上label, 這樣break和continue就可以根據(jù)我們的想法來(lái)continue和break任意循環(huán)。 這看起來(lái)甚至有點(diǎn)像goto語(yǔ)句的作用了。 當(dāng)然, 一般程序員也不會(huì)用到這種操作。 這兩種行為更多地被用到機(jī)器生成的代碼中。
  • 命名類(lèi)型: 類(lèi)型聲明使得我們可以很方便地給一個(gè)特殊類(lèi)型一個(gè)名字。 因?yàn)閟truct類(lèi)型聲明通常非常地長(zhǎng), 所以我們總要給這種struct取一個(gè)名字。 本章中就有這樣一個(gè)例子, 二維點(diǎn)類(lèi)型:
  • 類(lèi)型聲明和命名類(lèi)型會(huì)在第二章中介紹。
  • 指針: Go語(yǔ)言提供了指針。 指針是一種直接存儲(chǔ)了變量的內(nèi)存地址的數(shù)據(jù)類(lèi)型。 在其它語(yǔ)言中, 比如C語(yǔ)言, 指針操作是完全不受約束的。 在另外一些語(yǔ)言中, 指針一般被處理為“引用”, 除了到處傳遞這些指針之外, 并不能對(duì)這些指針做太多事情。 Go語(yǔ)言在這兩種范圍中取了一種平衡。 指針是可見(jiàn)的內(nèi)存地址, &操作符可以返回一個(gè)變量的內(nèi)存地址, 并且*操作符可以獲取指針指向的變量?jī)?nèi)容, 但是在Go語(yǔ)言里沒(méi)有指針運(yùn)算, 也就是不能像c語(yǔ)言里可以對(duì)指針進(jìn)行加或減操作。 我們會(huì)在2.3.2中進(jìn)行詳細(xì)介紹。
  • 方法和接口: 方法是和命名類(lèi)型關(guān)聯(lián)的一類(lèi)函數(shù)。 Go語(yǔ)言里比較特殊的是方法可以被關(guān)聯(lián)到任意一種命名類(lèi)型。 在第六章我們會(huì)詳細(xì)地講方法。 接口是一種抽象類(lèi)型, 這種類(lèi)型可以讓我們以同樣的方式來(lái)處理不同的固有類(lèi)型, 不用關(guān)心它們的具體實(shí)現(xiàn), 而只需要關(guān)注它們提供的方法。 第七章中會(huì)詳細(xì)說(shuō)明這些內(nèi)容。
  • 包( packages) : Go語(yǔ)言提供了一些很好用的package, 并且這些package是可以擴(kuò)展的。Go語(yǔ)言社區(qū)已經(jīng)創(chuàng)造并且分享了很多很多。 所以Go語(yǔ)言編程大多數(shù)情況下就是用已有的package來(lái)寫(xiě)我們自己的代碼。 通過(guò)這本書(shū), 我們會(huì)講解一些重要的標(biāo)準(zhǔn)庫(kù)內(nèi)的package, 但是還是有很多限于篇幅沒(méi)有去說(shuō)明, 因?yàn)槲覀儧](méi)法在這樣的厚度的書(shū)里去做一部代碼大全。
  • 在你開(kāi)始寫(xiě)一個(gè)新程序之前, 最好先去檢查一下是不是已經(jīng)有了現(xiàn)成的庫(kù)可以幫助你更高效地完成這件事情。 你可以在 https://golang.org/pkg 和 https://godoc.org 中找到標(biāo)準(zhǔn)庫(kù)和社區(qū)寫(xiě)的package。 godoc這個(gè)工具可以讓你直接在本地命令行閱讀標(biāo)準(zhǔn)庫(kù)的文檔。 比如下面這個(gè)例子。
  • 注釋: 我們之前已經(jīng)提到過(guò)了在源文件的開(kāi)頭寫(xiě)的注釋是這個(gè)源文件的文檔。 在每一個(gè)函數(shù)之前寫(xiě)一個(gè)說(shuō)明函數(shù)行為的注釋也是一個(gè)好習(xí)慣。 這些慣例很重要, 因?yàn)檫@些內(nèi)容會(huì)被像godoc這樣的工具檢測(cè)到, 并且在執(zhí)行命令時(shí)顯示這些注釋。 具體可以參考10.7.4。
  • 多行注釋可以用 /* … */ 來(lái)包裹, 和其它大多數(shù)語(yǔ)言一樣。 在文件一開(kāi)頭的注釋一般都是這種形式, 或者一大段的解釋性的注釋文字也會(huì)被這符號(hào)包住, 來(lái)避免每一行都需要加//。 在注釋中//和/*是沒(méi)什么意義的, 所以不要在注釋中再嵌入注釋.
  • 總結(jié)

    以上是生活随笔為你收集整理的《Go语言圣经》学习笔记 第一章 Go语言入门的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。

    如果覺(jué)得生活随笔網(wǎng)站內(nèi)容還不錯(cuò),歡迎將生活随笔推薦給好友。