├── .gitignore ├── .travis.yml ├── LICENSE ├── Makefile ├── README.md ├── SUMMARY.md ├── _config.yml ├── about.md ├── book.json ├── chapter1.md ├── chapter2.md ├── chapter3.md ├── chapter4.md ├── chapter5.md ├── chapter6.md ├── conclusion.md ├── deploy.sh ├── getting-started.md └── introduction.md /.gitignore: -------------------------------------------------------------------------------- 1 | # Output of documentation 2 | _book 3 | 4 | book.pdf 5 | book.epub 6 | book.mobi 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | env: 2 | global: 3 | - secure: "GbrG1ip47iENLZK/qGfw9CsTONbpHYySd9mPBJmwo4AHhOii5ldGpxyyLd/51oLI66DoN1BJqY2gAM4NpcPBdcR2evM9BXJiIxZ8jrdAyiAE7tvqbujq689Mh+U34czsnc/tVzwVHEtXfrskO3w8ajSJQXxQ2tL0GcVrH770W8z/Xx/1At4fiiZw+7Mh9++PyLM8D7uv6c5hgF+MEr/I7gdt3LGIApRVHtQOhDAQWUBndK4J5+A58icD9Y4LiC0WHORkBUADTibS4aTB8rmWkP0rEtcwo9Rz7Y4/FIVQRKIFFSVpSustR95xDeJ7GzFWnhD6WZIFsFJx2xjtMfxLpvh+o3IymVaSbSEGnP+c4F/OW9F7dEJ24nfSkR9KsPTFb8OzHXrgRGhHwy8fxhRUhN2H5hLZd621PFogiVkVyAR/TCypFYnh4nZpKLyhRVdD0GlW+WzOw3bE1MAmIA1sYHb2tsDewiQF883uIVM++sQybS+AK2MZ9Vx30eroIyoEJdUCO/nw9/JnHF8D3o+NxbVeEyoBiAhSZKA1Dz1710BuMn4m+0VkDWUPBj6u7wZRF49TnDUxjyrBT5Kjnq/DI644VltQazO+dWXtspZYgn0o/7P8SAFmkudiaZxv5BTegLuIuWHphJOe5VpXXXRkj9xuyts8wJER22rsKpQmB9k=" 4 | 5 | language: node_js 6 | node_js: 7 | - '6' 8 | script: npm install gitbook-cli -g 9 | after_success: 10 | - bash deploy.sh -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 kevingo 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: all build review update clean 2 | 3 | all: install build 4 | 5 | install: 6 | gitbook install 7 | 8 | build: 9 | gitbook build 10 | 11 | review: 12 | gitbook serve 13 | 14 | update: 15 | git pull origin master --rebase 16 | 17 | deploy: 18 | gitbook install 19 | gitbook build -g kevingo/the-little-go-book 20 | 21 | clean: 22 | rm -rf _book/ node_modules/ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # The Little Go Book 繁體中文翻譯版 2 | 3 | [The Little Go Book](https://github.com/karlseguin/the-little-go-book) 繁體中文翻譯版。 4 | 5 | # 閱讀本書 6 | 7 | 你可以透過 GitBook 或 Github Page 來閱讀本書,請參考以下連結: 8 | 9 | - [The Little Go Book - Github Page](https://kevingo.github.io/the-little-go-book/) 10 | - [The Little Go Book - GitBook](https://kevingo.gitbooks.io/the-little-go-book/content/) 11 | 12 | # 目錄 13 | 14 | - [關於本書](./about.md) 15 | - [簡介](./introduction.md) 16 | - [入門](./getting-started.md) 17 | - [第一章 - 基本概念](./chapter1.md) 18 | - [第二章 - 結構](./chapter2.md) 19 | - [第三章 - Map、Array、和 Slice](./chapter3.md) 20 | - [第四章 - 組織程式碼和介面](./chapter4.md) 21 | - [第五章 - 花絮](./chapter5.md) 22 | - [第六章 - 並行](./chapter6.md) 23 | - [結論](./conclusion.md) 24 | 25 | # 致謝 26 | 27 | - 作者 28 | - [Karl Seguin](http://openmymind.net/) 29 | - 翻譯人員 30 | - [kevingo](https://github.com/kevingo) 31 | - 貢獻人員 32 | - [neighborhood999](https://github.com/neighborhood999) 33 | 34 | # 貢獻 35 | 36 | 如果您發現任何翻譯錯誤或語意錯誤,歡迎隨時提交 Pull Request,我會將您加入貢獻人員名單中。 37 | 38 | # 授權 39 | 40 | 本書內容採用 [CC BY-NC-SA 4.0](https://creativecommons.org/licenses/by-nc-sa/4.0/) 授權。 41 | -------------------------------------------------------------------------------- /SUMMARY.md: -------------------------------------------------------------------------------- 1 | # 目錄 2 | 3 | - [關於本書](./about.md) 4 | - [簡介](./introduction.md) 5 | - [入門](./getting-started.md) 6 | - [第一章 - 基本概念](./chapter1.md) 7 | - [第二章 - 結構](./chapter2.md) 8 | - [第三章 - Map、Array、和 Slice](./chapter3.md) 9 | - [第四章 - 組織程式碼和介面](./chapter4.md) 10 | - [第五章 - 花絮](./chapter5.md) 11 | - [第六章 - 並行](./chapter6.md) 12 | - [結論](./conclusion.md) 13 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-cayman -------------------------------------------------------------------------------- /about.md: -------------------------------------------------------------------------------- 1 | # 關於本書 2 | 3 | ## 授權 4 | 5 | 本書採用 [Attribution-NonCommercial-ShareAlike 4.0 International](https://creativecommons.org/licenses/by-nc-sa/4.0/legalcode) 授權, 你不需要為本書付費。 6 | 7 | 你可以免費的複製、發佈、修改或展示本書。然而,本書的版權是屬於我,Karl Seguin,並且請勿將本書用於商業目的。 8 | 9 | 你可以透過以下連結了解授權內容的全文: 10 | 11 | [https://creativecommons.org/licenses/by-nc-sa/4.0/](https://creativecommons.org/licenses/by-nc-sa/4.0/) 12 | 13 | ## 最新版本 14 | 15 | 關於本書的最新版本請見此連結: [https://github.com/karlseguin/the-little-go-book](https://github.com/karlseguin/the-little-go-book) 16 | 17 | -------------------------------------------------------------------------------- /book.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "The Little Go Book 繁體中文翻譯版", 3 | "gitbook": "3.2.0", 4 | "pluginsConfig": { 5 | "github": { 6 | "url": "https://github.com/kevingo/the-little-go-book" 7 | }, 8 | "sharing": { 9 | "facebook": true, 10 | "twitter": true, 11 | "google": false, 12 | "weibo": false 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /chapter1.md: -------------------------------------------------------------------------------- 1 | # 第一章 - 基本概念 2 | 3 | Go 是需要編譯、靜態型別、具有類似於 C 的語法與擁有 Garbage Collection 等特性的語言。 4 | 這是什麼意思? 5 | 6 | ## 編譯 7 | 編譯是將高階程式碼轉換為低階程式碼的過程。例如:在 Go 來說就會是組合語言,或是其他的中介語言(比如說 Java 和 C#) 8 | 9 | 你可能覺得編譯語言讓你感到不愉快,因為編譯速度可能相對慢。如果你需要等待數分鐘甚至數小時來編譯你的程式碼,那要快速迭代是相當困難的。編譯速度是 Go 語言在設計上的主要考量。這對於過去使用直譯式語言並且得利於快速開發週期的人來說,是相當好的消息。 10 | 11 | 編譯語言的執行速度較快,而且執行時不需要額外的相依套件(至少,這對於 C、C++ 和 Go 這類編譯成組合語言的程式語言來說是這樣的)。 12 | 13 | ## 靜態型別 14 | 靜態型別意味著變數必須是特定類型(int、string、bool、[]byte 等)。 15 | 這可以透過在宣告變數時指定類型來實現,或者在許多情況下,讓編譯器來幫助你推斷類型 16 | (我們稍後將看看範例)。關於靜態型別有很多可以提的,但我相信透過閱讀程式碼你會有更好的理解。如果你習慣於動態類型的語言,你可能會發現這很麻煩。沒有錯,但這是有好處的。使用靜態型別系統,編譯器除了能夠檢查語法錯誤外,並能進一步進行優化。 17 | 18 | 19 | ## 類似於 C 的語法 20 | 如果一個程式語言的語法類似於 C,並且你曾經使用過其他類似語法的語言, 21 | 例如:C、C++、Java、Javascript 和 C#,那你會發現 Go 至少表面上看起來很類似。比如說,這代表了 `&&` 是 boolean 的 AND、`==` 是用來比較是否相等、`{` 和 `}` 22 | 是一個宣告的範圍,以及陣列的 index 從 0 開始。 23 | 24 | 類似於 C 的語法也代表了,分號指的是一行的結束,以及用括號來包住條件。但在 Go 語言中省去了這兩個部分, 25 | 儘管大括號還是用在控制範圍。舉例來說,一個 if 條件式會長的像: 26 | 27 | ```go 28 | if name == "Leto" { 29 | print("the spice must flow") 30 | } 31 | ``` 32 | 33 | 一個更複雜一點的例子中,括號依舊是有用的: 34 | 35 | ```go 36 | if (name == "Goku" && power > 9000) || (name == "gohan" && power < 4000) { 37 | print("super Saiyan") 38 | } 39 | ``` 40 | 41 | 除此之外,Go 比 C# 或 Java 更接近 C - 不僅在語法上,更在於其目的。當你去學習 Go 語言後,你會發現它的簡潔和簡單。 42 | 43 | ## Garbage Collected 44 | 一些變數在建立時,有一個容易定義的生命週期。例如,函式的區域變數在函式結束時消失。在其他情況下,這不是那麼顯著 - 至少對於編譯器來說。例如,由函式返回或由其他變數和物件引用的變數生命週期可能難以確定。沒有 garbage collection 機制,開發人員需要在不需要變數的時候釋放與這些變數相關的記憶體。怎麼做?在 C,你可以使用 `free(str);`。具有 garbage collection(例如:Ruby、Python、Java、JavaScript、C#、Go)機制的語言能夠追蹤變數,並在不需要使用時釋放它們。垃圾收集增加了負擔,但它也解決了一些會造成毀滅性的 bug 的問題。 45 | 46 | ## 執行 Go 程式碼 47 | 48 | 讓我們開始我們的旅程吧!建立一個簡單的程式,學習如何編譯和執行它。打開你喜歡的編輯器,並撰寫以下程式碼: 49 | 50 | ```go 51 | package main 52 | 53 | func main() { 54 | println("it's over 9000!") 55 | } 56 | ``` 57 | 58 | 將檔案另存為 `main.go`。現在,你可以儲存在任何你想要的地方。我們不需要把這個小範例放到工作目錄中。 59 | 60 | 接下來,打開 shell/command prompt,並將目錄切換到檔案的位置。對我來說,那是在 `cd ~/code`。 61 | 62 | 最後,執行程式碼: 63 | 64 | ```sh 65 | go run main.go 66 | ``` 67 | 68 | 如果一切正確,你會看到 `it's over 9000!`。 69 | 70 | 但等等,編譯的步驟呢? `go run` 是一個同時進行編譯和執行程式碼的指令。它使用一個臨時目錄來建置程式、執行它、最後砍掉自己。你可以透過以下指令查看臨時檔案的位置: 71 | 72 | ```sh 73 | go run --work main.go 74 | ``` 75 | 76 | 想要直接編譯程式碼,使用 `go build`: 77 | 78 | ```sh 79 | go build main.go 80 | ``` 81 | 82 | 這個指令會產生一個執行檔 `main`。在 Linux/OSX 系統中,你需要用 `./main` 來執行它。 83 | 84 | 在你開發的過程中,你可能會用 `go run` 或 `go build` 等指令,但在你部署程式時, 85 | 你會就直接編譯好執行檔,並且將執行檔進行部署。 86 | 87 | ## Main 88 | 89 | 希望我們上面執行的程式碼是好理解的。我們建立了一個函式,並且透過內建的 `println` 函式印出字串。當我們執行 `go run` 的時候,Go 會知道因為我們只有單一的一個檔案,而知道我們要執行的就是那隻程式嗎?不,在 Go 語言裡,程式的進入點是在 `main` package 裡面的 `main` 函式。 90 | 91 | 我們會在後面的章節討論 packege 的概念。現在,我們只要了解 Go 的基礎就好。 92 | 在這裡,我們一律將我們的程式碼寫在 `main` package 中。 93 | 94 | 如果你想要試試看,也可以變更 package 的名稱,接著一樣執行程式,看看會跑出什麼錯誤訊息。也可以把 package 改回 `main`,但是變更函式的名稱,又會跑出什麼錯誤訊息。嘗試執行一樣的變更,但不是執行,而是去編譯他,編譯時,你會發現這沒有進入點的編譯。當你在編譯一個套件時,這是相當正常的。 95 | 96 | ## Import 97 | Go 有相當多內建的函式,例如:`println` 可以不需要引用任何套件就可以使用。即使我們使用第三方套件,不使用內建的函式幾乎是不可能的。在 Go 中,使用 `import` 關鍵字可以用來引用在程式碼中使用的相關套件。 98 | 99 | 讓我們修改一下原本的程式碼: 100 | 101 | ```go 102 | package main 103 | 104 | import ( 105 | "fmt" 106 | "os" 107 | ) 108 | 109 | func main() { 110 | if len(os.Args) != 2 { 111 | os.Exit(1) 112 | } 113 | fmt.Println("It's over", os.Args[1]) 114 | } 115 | ``` 116 | 117 | 透過以下指令來執行: 118 | 119 | ```sh 120 | go run main.go 9000 121 | ``` 122 | 123 | 我們現在使用兩個 Go 的標準套件:`fmt` 和 `os`。同時也會使用另一個內建的函式 `len`。`len` 會傳回 string 的長度,或是一個 dictionary 的數量,亦或是我們這裡看到的 array 的長度。如果你不知道為什麼我們的程式是存取第二個參數,那是因為第一個參數 (index 是 0) 代表的是目前執行程式碼的路徑。 124 | 125 | 你或許注意到了我們在函數的前面多了前綴名稱 `fmt.Println`。這和其他的語言有所不同,我們會在後面談論到 package。現在,了解如何使用 import 和 package 就夠了。 126 | 127 | Go 對於如何使用 import 來引入套件的管理上是嚴格的,如果你 import 了套件, 128 | 卻沒有使用它的話,是無法編譯成功的。嘗試執行下面的程式碼: 129 | 130 | ```go 131 | package main 132 | 133 | import ( 134 | "fmt" 135 | "os" 136 | ) 137 | 138 | func main() { 139 | } 140 | ``` 141 | 142 | 你會得到兩個錯誤訊息,告訴你 `fmt` 和 `os` 這兩個套件被引用但卻無法使用。覺得困擾嗎?肯定的,但隨著不斷學習,你會習慣的(儘管你仍然會感到困擾)。Go 會這麼嚴格的原因是,引用未使用的套件會使得編譯速度變慢,儘管,大多數人可能不會有這種程度的困擾。 143 | 144 | 另一個值得一提的是,Go 的標準函式庫有相當良好的文件。你可以到 [https://golang.org/pkg/fmt/#Println](https://golang.org/pkg/fmt/#Println) 閱讀關於 `fmt` 套件中 `Println` 函式的文件,也可以點擊觀看原始碼。 145 | 146 | 如果你的電腦無法連上網路,你可以在本機端輸入: 147 | 148 | ```sh 149 | godoc -http=:6060 150 | ``` 151 | 152 | 然後在瀏覽器上到 `http://localhost:6060` 來檢視文件。 153 | 154 | ## 變數與宣告 155 | 當我們寫下 `x = 4`,這對於變數的宣告來說,也許是一個開始,也可以是結束。但不幸的,在 Go 中,事情相對複雜了些。我們會從簡單的範例開始,在下一章中使用 structure 的時候,來擴展我們的例子。你可能會想說,哇,是什麼事情會這麼複雜?讓我們先來看些例子。 在 Go 中,宣告變數最明確也是最冗長的方式是: 156 | 157 | ```go 158 | package main 159 | 160 | import ( 161 | "fmt" 162 | ) 163 | 164 | func main() { 165 | var power int 166 | power = 9000 167 | fmt.Printf("It's over %d\n", power) 168 | } 169 | ``` 170 | 171 | 這裡,我們宣告了一個 `int` 類型的 `power` 變數,預設情況下,Go 會為這個變數分配一個零值。整數的話是 `0`、布林值為 `false`、字串是 `""` 等。下一步,我們將 `9000` 指派給 `power` 這個變數。我們可以合併這兩行: 172 | 173 | ```go 174 | var power int = 9000 175 | ``` 176 | 177 | 這仍然需要打很多字。在 Go 中,有一個方便的簡短宣告運算子:`:=`,這個運算子可以進行型別的推論: 178 | 179 | ```go 180 | power := 9000 181 | ``` 182 | 183 | 這很方便,同時在函式上也能這樣使用: 184 | 185 | ```go 186 | func main() { 187 | power := getPower() 188 | } 189 | 190 | func getPower() int { 191 | return 9001 192 | } 193 | ``` 194 | 195 | 重要的是,要記住 `:=` 同時用於宣告變數以及為它賦值。為什麼? 因為變數不能被宣告兩次(在同一範圍內)。如果你嘗試執行以下操作,會收到錯誤。 196 | 197 | ```go 198 | func main() { 199 | power := 9000 200 | fmt.Printf("It's over %d\n", power) 201 | 202 | // 編譯錯誤: 203 | // 在 := 的左方沒有新的變數指派 204 | power := 9001 205 | fmt.Printf("It's also over %d\n", power) 206 | } 207 | ``` 208 | 209 | 編譯器會抱怨在 `:=` 運算子的左側沒有新的變數。這代表,當我們首次宣告一個變數時,我們使用 `:=` 運算子,但是在後續賦值中,我們使用賦值運算子 `=`。這相當合理,但會有點微妙,你需要多練習來讓你的肌肉可以順利的在這兩者之間進行轉換。 210 | 211 | 如果你仔細閱讀錯誤訊息,你會注意到變數是複數。這是因為 Go 允許你指派多個變數(使用 `=` 或 `:=` 皆可)。 212 | 213 | ```go 214 | func main() { 215 | name, power := "Goku", 9000 216 | fmt.Printf("%s's power is over %d\n", name, power) 217 | } 218 | ``` 219 | 220 | 一旦這個變數是新的,`:=` 運算子就可以被使用。看看以下的範例: 221 | 222 | ```go 223 | func main() { 224 | power := 1000 225 | fmt.Printf("default power is %d\n", power) 226 | 227 | name, power := "Goku", 9000 228 | fmt.Printf("%s's power is over %d\n", name, power) 229 | } 230 | ``` 231 | 232 | 雖然 power 這個變數被 `:=` 指派兩次,編譯器卻不會產生錯誤。編譯器會看到有一個新的變數 `name`,因此你可以正常使用 `:=`。但要注意的是,你不能變更 power 這個變數的型態,因為他被隱性的指派為整數,所以只能被整數賦值。 233 | 234 | 現在,你要知道的最後一件事情是,就跟 import 一樣,Go 不會讓你有宣告但未使用的變數。看看這個例子: 235 | 236 | ```go 237 | func main() { 238 | name, power := "Goku", 1000 239 | fmt.Printf("default power is %d\n", power) 240 | } 241 | ``` 242 | 243 | 將無法編譯成功。因為 `name` 變數宣告了但沒有被使用。就像 import 了未使用的套件一樣,這可能會讓某些人覺得沮喪,但整體來看,我認為這對於程式碼的整潔度和可讀性是有所幫助的。 244 | 245 | 關於變數的宣告和指派還有更多可以學習的部分。現在,你要記住,使用 `var NAME TYPE` 的方式來宣告一個變數會被賦予該變數的零值,使用 `NAME := VALUE` 是宣告變數並賦值,而 `NAME = VALUE` 是指派一個值給之前宣告過的變數。 246 | 247 | ## 函式宣告 248 | 249 | 在學習完變數宣告後,現在正是一個好時機讓你知道,Go 的函式是可以有多個回傳值的。讓我們來看看三個函式,一個沒有回傳值、一個回傳一個值,最後一個回傳兩個值: 250 | 251 | ```go 252 | func log(message string) { 253 | } 254 | 255 | func add(a int, b int) int { 256 | } 257 | 258 | func power(name string) (int, bool) { 259 | } 260 | ``` 261 | 262 | 我們可以這樣使用多回傳值的函式: 263 | 264 | ```go 265 | value, exists := power("goku") 266 | if exists == false { 267 | // 處理錯誤情況 268 | } 269 | ``` 270 | 271 | 有時候,你只需要其中一個回傳值。在這種情況下,你可以將不需要處理的變數,用 `_` 來取代: 272 | 273 | ```go 274 | _, exists := power("goku") 275 | if exists == false { 276 | // 處理錯誤情況 277 | } 278 | ``` 279 | 280 | 這種使用方式不僅僅是一種常規,`_` 空白識別符號是特別用在返回值不需要被實際指派的時候使用。你可以不管返回的類型,重複的使用 `_` 識別符號。 281 | 282 | 最後,如果在函式所用到的參數共用同一種類型的話,我們可以用較短的語法宣告(如下方的 a、b 變數共用整數型態的宣告): 283 | 284 | ```go 285 | func add(a, b int) int { 286 | 287 | } 288 | ``` 289 | 290 | 函式多重返回值和使用 `_` 來丟棄你不需要的回傳值是很常用到的功能。命名返回值和少量的 verbose 參數宣告不是那麼常見。不過你遲早會使用它們,所以了解他們還是很重要的。 291 | 292 | ## 在你繼續學習之前 293 | 我們學習了一些小部分的概念,你可能會覺得有點零散。我們會慢慢學習一些更大的程式,屆時這些部分將會聚集成完整的程式碼。 294 | 295 | 如果你之前是學習動態程式語言,你或許會覺得複雜的型別和宣告是一種退步。但我並不認同這樣的想法,對於某些系統,也許動態語言會更有生產力。如果你之前是學習靜態程式語言,你也許在學習 Go 的過程是覺得熟悉的,推論的型別和多回傳值是相當好的特性(儘管並非 Go 所獨有)。希望隨著我們學習更多,會對於 Go 乾淨簡潔的語法會更加欣賞。 296 | -------------------------------------------------------------------------------- /chapter2.md: -------------------------------------------------------------------------------- 1 | # 第二章 - 結構 2 | 3 | Go 不像 C++、Java、Ruby 或 C# 一樣是物件導向程式語言。他沒有物件或繼承,也沒有許多物件導向的概念,比如說多形或重載。 4 | 5 | Go 語言擁有的是結構,和方法有關。Go 也支持簡潔但有效的組合關係。整體來說,他會讓程式碼更簡潔,但你有時候也會失去一些物件導向所提供的特性。(值得一提的是,*組合優於繼承* 這樣的說法長久以來不斷被討論,而 Go 是我用的第一個可以對這個說法採取堅定立場的語言) 6 | 7 | 即使 Go 可能不像你在寫物件導向程式語言那樣熟悉,但你會發現結構和類別有很多相像之處,讓我們來看一個簡單的 `Saiyan` 結構: 8 | 9 | ```go 10 | type Saiyan struct { 11 | Name string 12 | Power int 13 | } 14 | ``` 15 | 16 | 我們很快將看到如何在這個結構中增加一個方法,就像你將方法作為類別的一部分。在我們這樣做之前,我們必須回顧宣告的用法。 17 | 18 | ## 宣告及初始化 19 | 20 | 當我們第一次看到變數和宣告時,我們只學習了內建的型態,比如說整數或字串。現在,我們來談談結構。我們同時必須來學習指針。 21 | 22 | 建立一個結構的值,最簡單的方法是: 23 | ```go 24 | goku := Saiyan{ 25 | Name: "Goku", 26 | Power: 9000, 27 | } 28 | ``` 29 | 30 | *注意:* 上面的結構中最後的 `,` 是必要的。你會感激需要逗點所代表的一致性,特別是你用過其他不需要強制使用逗號的程式語言。 31 | 32 | 在初始化結構時,我們不需要設置所有的欄位。下面兩個宣告都是合法的: 33 | 34 | ```go 35 | goku := Saiyan{} 36 | 37 | // 或者 38 | 39 | goku := Saiyan{Name: "Goku"} 40 | goku.Power = 9000 41 | ``` 42 | 43 | 就像沒有指派的變數一樣,欄位也會被指派為對應的零值。 44 | 45 | 此外,你還可以省略欄位的名稱,這樣就會按照順序來對應賦值(為了清楚起見,你應該僅僅在少量欄位名稱的時候使用這種操作): 46 | 47 | ```go 48 | goku := Saiyan{"Goku", 9000} 49 | ``` 50 | 51 | 上面的各種範例都是宣告一個 `goku` 的結構變數,並且指派對應的值。 52 | 53 | 許多時候,我們不想要一個直接關聯的變數,而是想要一個指向該變數所儲存的值的指標。指標所儲存的內容是記憶體位置,找到這個記憶體位置就可以找到對應的值。這是一種對應的關係, 54 | 就像你的房子和前往你房子的方向一樣。 55 | 56 | 為什麼我們想要值的指標,而不是值本身呢?這就必須要知道 Go 傳遞參數到一個函式:用副本的方式。了解這個之後,來看看底下會印出什麼? 57 | 58 | ```go 59 | func main() { 60 | goku := Saiyan{"Goku", 9000} 61 | Super(goku) 62 | fmt.Println(goku.Power) 63 | } 64 | 65 | func Super(s Saiyan) { 66 | s.Power += 10000 67 | } 68 | ``` 69 | 70 | 答案是 9000,而不是 19000,為什麼?因為 `Super` 改變了 `goku` 副本的值,而並非原本呼叫 `Super` 所傳進去的 `goku`。因此,在 `Super` 中的變更並不會反應到呼叫 `Super` 時所傳入的 `goku` 上。為了讓程式碼的行為如你所預期,我們必須要傳入指標: 71 | 72 | ```go 73 | func main() { 74 | goku := &Saiyan{"Goku", 9000} 75 | Super(goku) 76 | fmt.Println(goku.Power) 77 | } 78 | 79 | func Super(s *Saiyan) { 80 | s.Power += 10000 81 | } 82 | ``` 83 | 84 | 在這裡我們調整兩個部分。第一個是我們使用 `&` 運算子來取得對應值的記憶體位置(這叫做 *取得記憶體位置* 運算子)。接著,我們變更 `Super` 函式的參數。之前我們預期的參數是 `Saiyan` 結構的值,但是現在我們預期的參數是 `*Saiyan` 型態,`*X` 的意思是 *型別 X 的值的指標*。顯而易見的,`Saiyan` 和 `*Saiyan` 勢必有些關聯,但他們是兩種完全不同的型別。 85 | 86 | 注意我們仍然將 `goku` 的副本值傳給 `Super`,只是 `goku` 的值變成了記憶體位置。 87 | 88 | 我們可以試著變更這個副本指向的位置來證明他的確是個副本(不過這也許不是你本來會做的事情): 89 | 90 | ```go 91 | func main() { 92 | goku := &Saiyan{"Goku", 9000} 93 | Super(goku) 94 | fmt.Println(goku.Power) 95 | } 96 | 97 | func Super(s *Saiyan) { 98 | s = &Saiyan{"Gohan", 1000} 99 | } 100 | ``` 101 | 102 | 上面的例子中,還是會印出 9000。這個行為在許多的程式語言都是如此,包含:Ruby, Python, Java 和 C#。在 Go 和某種程度的 C# 上,只是讓這個事實更顯著。 103 | 104 | 更顯而易見的,指標的副本會比整個複雜結構的副本來的輕量多了。在 64 位元的機器上,一個指標的大小是 64 bits。如果我們有一個包含許多欄位的結構,建立一個副本會是相當昂貴的。指標的真正價值是讓你共享值,想想看,我們是想要讓 `Super` 變更 `goku` 的副本,還是共享 `goku` 值的本身呢? 105 | 106 | 這一切不是說你永遠都要使用指標。在這章節的最後,等到我們看了更多關於結構的內容後,我們會再重新看看指標和值的問題。 107 | 108 | ## 函式和結構 109 | 110 | 我們可以將一個方法與結構互相關聯: 111 | 112 | ```go 113 | type Saiyan struct { 114 | Name string 115 | Power int 116 | } 117 | 118 | func (s *Saiyan) Super() { 119 | s.Power += 10000 120 | } 121 | ``` 122 | 123 | 在上面的程式中,我們說 `*Saiyan` 型別是 `Super` 方法的**接收者**。我們可以這樣呼叫 `Super`: 124 | 125 | ```go 126 | goku := &Saiyan{"Goku", 9001} 127 | goku.Super() 128 | fmt.Println(goku.Power) // 將列印出 19001 129 | ``` 130 | 131 | ## 建構子 132 | 133 | 結構並沒有所謂的建構子。相反的,你可以建立一個函式,回傳值是你所需要型別的實例(就像工廠模式一樣): 134 | 135 | ```go 136 | func NewSaiyan(name string, power int) *Saiyan { 137 | return &Saiyan{ 138 | Name: name, 139 | Power: power, 140 | } 141 | } 142 | ``` 143 | 144 | 這種模式讓很多開發者走到錯誤的路上。一方面,它是一個很微妙的語法,另一方面,他確實感覺有點不直覺。 145 | 146 | 我們的工廠模式不一定要回傳一個指標,下面這段程式碼也是完全合法的: 147 | 148 | ```go 149 | func NewSaiyan(name string, power int) Saiyan { 150 | return Saiyan{ 151 | Name: name, 152 | Power: power, 153 | } 154 | } 155 | ``` 156 | 157 | ## New 158 | 159 | 儘管 Go 語言中沒有建構子,但 Go 卻有內建的 `new` 函式,用來分配對應型別的記憶體空間。就結果來看,`new(X)` 和 `&X{}` 是一樣的。 160 | 161 | ```go 162 | goku := new(Saiyan) 163 | // 相同於 164 | goku := &Saiyan{} 165 | ``` 166 | 167 | 要用哪一種方法都可以,但你會發現大多數人都喜歡用後者,無論他們是否有對應的欄位需要初始化。原因是他比較容易閱讀。 168 | 169 | ```go 170 | goku := new(Saiyan) 171 | goku.name = "goku" 172 | goku.power = 9001 173 | 174 | // vs 175 | 176 | goku := &Saiyan { 177 | name: "goku", 178 | power: 9000, 179 | } 180 | ``` 181 | 182 | 無論你採用哪種方法,只要遵循工廠模式,你可以放心的不去管後面如何分配記憶體位置的種種細節。 183 | 184 | ## 結構中的欄位 185 | 186 | 在我們已經看過的例子中,`Saiyan` 結構有兩個欄位,字串型別的 `Name` 和 整數型別的 `Power`。事實上,結構的欄位可以是任何型別,包括其他的結構,或是任何我們還沒有介紹過的陣列、map、介面和函式。 187 | 188 | 例如,我們可以擴展 `Saiyan` 的定義: 189 | 190 | ```go 191 | type Saiyan struct { 192 | Name string 193 | Power int 194 | Father *Saiyan 195 | } 196 | ``` 197 | 198 | 可以這樣初始化: 199 | 200 | ```go 201 | gohan := &Saiyan{ 202 | Name: "Gohan", 203 | Power: 1000, 204 | Father: &Saiyan { 205 | Name: "Goku", 206 | Power: 9001, 207 | Father: nil, 208 | }, 209 | } 210 | ``` 211 | 212 | ## 組合 213 | 214 | Go 語言支持組合,意思就是將一個結構包含到另外一個結構的行為。在某些語言中,這被叫做 `trait` 或 `mixin`。沒有明確組合機制的語言總是會用其他的方式來達成。在 Java 中是這樣做: 215 | 216 | ```java 217 | public class Person { 218 | private String name; 219 | 220 | public String getName() { 221 | return this.name; 222 | } 223 | } 224 | 225 | public class Saiyan { 226 | // 類別 Saiyan 宣告這裡有一個 person 227 | private Person person; 228 | 229 | // 我們轉向呼叫到 person 的 getName() 方法 230 | public String getName() { 231 | return this.person.getName(); 232 | } 233 | ... 234 | } 235 | ``` 236 | 237 | 這樣撰寫十分乏味。`Person` 中的每個方法都必須要在 `Saiyan` 中重複一次。Go 則避免了這樣的作法: 238 | 239 | ```go 240 | type Person struct { 241 | Name string 242 | } 243 | 244 | func (p *Person) Introduce() { 245 | fmt.Printf("Hi, I'm %s\n", p.Name) 246 | } 247 | 248 | type Saiyan struct { 249 | *Person 250 | Power int 251 | } 252 | 253 | // 並使用他: 254 | goku := &Saiyan{ 255 | Person: &Person{"Goku"}, 256 | Power: 9001, 257 | } 258 | goku.Introduce() 259 | ``` 260 | 261 | `Saiyan` 結構中有一個型態是 `*Person` 的欄位。因為我們沒有給他明確的欄位名稱,所以我們可以透過組合隱性的存取這個欄位和函式。然而,Go 的編譯器 *的確* 會給他一個欄位名稱。看看以下的例子: 262 | 263 | ```go 264 | goku := &Saiyan{ 265 | Person: &Person{"Goku"}, 266 | } 267 | fmt.Println(goku.Name) 268 | fmt.Println(goku.Person.Name) 269 | ``` 270 | 271 | 兩個都會印出 "Goku"。 272 | 273 | 組合是否比繼承好呢?許多人認為這是分享程式碼一個比較可靠的方式。當使用繼承時,你的類別會僅耦合到父類別,最終你關注的是階層結構而並非是程式碼本身的行為。 274 | 275 | ### 多載 276 | 277 | 雖然多載並不限定於在結構,但值得在這一提。簡單來說,Go 不支援多載, 278 | 所以你會看到很多函式用來做 `Load`、`LoadById`、`LoadByName`。 279 | 280 | 然而,因為隱性組合是一種編譯器的小技巧,我們可以「覆寫」組合型別的函式。例如,`Saiyan` 結構可以有自己的 `Introduce` 函式: 281 | 282 | ```go 283 | func (s *Saiyan) Introduce() { 284 | fmt.Printf("Hi, I'm %s. Ya!\n", s.Name) 285 | } 286 | ``` 287 | 288 | 而你總是可以透過 `s.Person.Introduce()` 來呼叫他。 289 | 290 | ## 指標 V.S. 值 291 | 292 | 當你在寫 Go 的程式碼時,問問自己 *這是一個值,還是一個指標指向該值* 是很正常的。有兩個好消息,第一,下面任何一個問題的答案都是一樣的: 293 | 294 | * 區域變數賦值 295 | * 結構中的欄位 296 | * 函式的回傳值 297 | * 函式的參數 298 | * 方法的接收者 299 | 300 | 第二,如果你不確定的話,用指標。 301 | 302 | 就像我們看到的,傳遞值是使得資料成為不可變的一個好方法(在被呼叫的方法中變更該值並不會反映到呼叫者上)。有時候,這是你想要的行為,但大多時候你不會想要這樣。 303 | 304 | 即使你真的不想要改變資料本身,想想看建立一個龐大結構的副本是多大的開銷。相反的,如果你有一個相對小的結構: 305 | 306 | ```go 307 | type Point struct { 308 | X int 309 | Y int 310 | } 311 | ``` 312 | 在這樣的例子中,使用結構副本的開銷可能被抵銷掉,你可以直接訪問 `X` 和 `Y`。再提醒一次,這些都是比較細微的差別,除非你反覆存取幾千或幾萬次,不然可能不會注意到這開銷的差別。 313 | 314 | ## 在你繼續學習之前 315 | 316 | 從實際的角度來說,這一章節中我們介紹了結構。學習如何讓結構的實例成為一個函式的接收者。 317 | 並且在既有 Go 的型別系統中增加了指標的知識。下一章節則會建基在我們學習到的結構繼續學習。 318 | -------------------------------------------------------------------------------- /chapter3.md: -------------------------------------------------------------------------------- 1 | # Chapter 3 - Map、Array、和 Slice 2 | 3 | 到目前為止我們看過了一些簡單的型別和結構。現在該是時候來看看 array、slice 和 map 了。 4 | 5 | ## 陣列 6 | 7 | 如果你來自 Python、Ruby、Perl、JavaScript 或 PHP(或更多其他語言),你可能在寫程式時習慣使用*動態陣列*。這些陣列是在資料加入到陣列時自行調整大小的陣列。在 Go 中,像許多其他語言一樣,陣列是固定的。宣告陣列需要我們指定大小,一旦指定大小,它就不能增加: 8 | 9 | ```go 10 | var scores [10]int 11 | scores[0] = 339 12 | ``` 13 | 14 | 上面這個陣列宣告可以透過 `scores[0]` 到 `scores[9]` 來存取 10 個分數值。如果嘗試存取超過這個範圍的陣列元素,編譯器會拋出執行時期錯誤。 15 | 16 | 我們也可以在初始化陣列的時候賦值: 17 | 18 | ```go 19 | scores := [4]int{9001, 9333, 212, 33} 20 | ``` 21 | 22 | 我們可以用 `len` 函式來取得陣列的長度。而 `range` 函式可以用來循序的存取每個元素值: 23 | 24 | ```go 25 | for index, value := range scores { 26 | 27 | } 28 | ``` 29 | 30 | 使用陣列是有效率但不夠靈活,原因是因為通常我們不知道即將要處理元素的數量。讓我們來看看 slice。 31 | 32 | ## Slice 33 | 34 | 在 Go 中,你很少會直接使用陣列,通常情況下你會使用 slice。slice 是一個輕量級的結構,這個結構被封裝後,代表了一個陣列的一部份。這裡我們列出一些建立 slice 的方法,並且指出我們會在後面什麼時候會用到他們。 35 | 36 | 第一種方式和我們建立陣列時有點相像: 37 | 38 | ```go 39 | scores := []int{1,4,293,4,9} 40 | ``` 41 | 42 | 不同於陣列的宣告,slice 在宣告時不需要宣告長度。為了理解兩者的差異,我們來看看另外一種使用 `make` 建立 slice 的方式: 43 | 44 | ```go 45 | scores := make([]int, 10) 46 | ``` 47 | 48 | 我們使用 `make` 而沒有使用 `new` 是因為建立一個 slice 不僅僅是分配一個記憶體區間而已(`new` 的作用就是分配一段記憶體區間)。明確的來說,我們幫底層的陣列建立了一段記憶體區間,同時也要初始化 slice。在上面的例子中,我們初始化一個 slice,長度和容量都是 10。長度代表 slice 的大小,而容量是底層陣列的大小。透過 `make` 函式,我們可以同時宣告長度和容量。 49 | 50 | ```go 51 | scores := make([]int, 0, 10) 52 | ``` 53 | 54 | 這會建立一個長度是 0 ,容量是 10 的 slice。(如果你有留意的話,會發現 `make` 和 `len` 同時實現了 *重載* 的功能。Go 語言的某些特性會讓你感到有點失望,因為某些部分他並沒有揭露給開發者使用。) 55 | 56 | 為了更好理解關於長度和容量之間的交互關係,讓我們來看一些例子: 57 | 58 | ```go 59 | func main() { 60 | scores := make([]int, 0, 10) 61 | scores[7] = 9033 62 | fmt.Println(scores) 63 | } 64 | ``` 65 | 66 | 上面的第一個例子是無法運作的,為什麼?因為我們的 slice 長度是 0。是的,底層的陣列有 10 個元素, 67 | 但如果想要存取元素時,我們必須明確的擴展 slice。其中一個擴展 slice 的方式是使用 `append`: 68 | 69 | ```go 70 | func main() { 71 | scores := make([]int, 0, 10) 72 | scores = append(scores, 5) 73 | fmt.Println(scores) // 列印 [5] 74 | } 75 | ``` 76 | 77 | 但這改變了我們原本程式碼的意圖,在長度為 0 的 slice 上增加一個元素會被放到 slice 的第一個元素。不管出自什麼原因,那段不能運作的程式碼會賦值給 slice 的第 8 個元素。為了達成這個目標,我們可以再切割 slice: 78 | 79 | ```go 80 | func main() { 81 | scores := make([]int, 0, 10) 82 | scores = scores[0:8] 83 | scores[7] = 9033 84 | fmt.Println(scores) 85 | } 86 | ``` 87 | 88 | 可以調整 slice 長度的上限是多少?這個上限就是根據 slice 的容量來決定,在上面的例子中,就是 10。你可能會認為這沒有解決 *固定長度陣列* 的問題。其實 `append` 是比較特別的,如果底層的陣列已經滿了,`append` 會創造一個更大的陣列,並且複製所有的值到新的陣列(這也是動態陣列的工作原理,像是:PHP、Python、Ruby、Javascript等)。這就是為什麼我們在上面的例子中使用 `append`,我們必須要將 `append` 得回傳值重新指派給 scores 變數,如果原始的 slice 沒有更多容量時,`append` 會建立一個新的。 89 | 90 | 如果我告訴你 Go 在擴展陣列時使用的是 2x 演算法,你可以猜到以下的程式碼的輸出是什麼嗎? 91 | 92 | ```go 93 | func main() { 94 | scores := make([]int, 0, 5) 95 | c := cap(scores) 96 | fmt.Println(c) 97 | 98 | for i := 0; i < 25; i++ { 99 | scores = append(scores, i) 100 | 101 | // 如果容量改變了, 102 | // Go 為了容納新的資料,會增加陣列的長度 103 | if cap(scores) != c { 104 | c = cap(scores) 105 | fmt.Println(c) 106 | } 107 | } 108 | } 109 | ``` 110 | 111 | 如果初始 `scores` 的容量是 5,為了要容納 20 個元素,slice 的容量必須要擴展 3 次,分別是10、20 和 40。 112 | 113 | 最後一個範例: 114 | 115 | ```go 116 | func main() { 117 | scores := make([]int, 5) 118 | scores = append(scores, 9332) 119 | fmt.Println(scores) 120 | } 121 | ``` 122 | 123 | 上面的程式碼輸出會是 `[0, 0, 0, 0, 0, 9332]`。從直觀來看,你可能會以為輸出是 `[9332, 0, 0, 0, 0]`?對編譯器而言,上面的程式碼代表的意思是,附加 9332 到已經有五個值的 slice 。 124 | 125 | 最後,這裡提供四種常見初始化 slice 的方式: 126 | 127 | ```go 128 | names := []string{"leto", "jessica", "paul"} 129 | checks := make([]bool, 10) 130 | var names []string 131 | scores := make([]int, 0, 20) 132 | ``` 133 | 134 | 你該使用哪一種方式? 135 | 136 | 第一種方式相當直觀,不需要太多的說明,但缺點是你必須先知道要往 slice 裡面放的元素是什麼。第二種方式在你想要往 slice 的特定位置寫入一個值的時候很有用,比如說: 137 | 138 | ```go 139 | func extractPowers(saiyans []*Saiyans) []int { 140 | powers := make([]int, len(saiyans)) 141 | for index, saiyan := range saiyans { 142 | powers[index] = saiyan.Power 143 | } 144 | return powers 145 | } 146 | ``` 147 | 148 | 第三個方式會回傳一個空的 slice,一般會和 `append` 一起使用。此時 slice 的數量是未知的。 149 | 150 | 最後一種方式讓我們指定 slice 的長度和容量。當我們大概知道需要多少元素時很有用。即使你知道元素的個數,`append` 也可以被使用。這取決於個人喜好: 151 | 152 | ```go 153 | func extractPowers(saiyans []*Saiyans) []int { 154 | powers := make([]int, 0, len(saiyans)) 155 | for _, saiyan := range saiyans { 156 | powers = append(powers, saiyan.Power) 157 | } 158 | return powers 159 | } 160 | ``` 161 | 162 | slice 作為一個陣列的封裝來說是很有用的。許多語言都有類似的概念。Javascript 和 Ruby 中都有一個 `slice` 方法。在 Ruby 中,你可以透過 `[START..END]` 來得到一個 slice,或是在 Python 中使用 `[START:END]` 來得到一個 slice。然而,在某些語言中,slice 的確是從原始陣列複製而來。如果我們使用 Ruby,下面的程式碼會輸出什麼? 163 | 164 | ```go 165 | scores = [1,2,3,4,5] 166 | slice = scores[2..4] 167 | slice[0] = 999 168 | puts scores 169 | ``` 170 | 171 | 答案是 `[1, 2, 3, 4, 5]`。因為 `slice` 是把舊的值全部複製過來的一個新陣列。現在,同樣情況下來看看 Go 會怎麼做: 172 | 173 | ```go 174 | scores := []int{1,2,3,4,5} 175 | slice := scores[2:4] 176 | slice[0] = 999 177 | fmt.Println(scores) 178 | ``` 179 | 180 | 輸出會是 `[1, 2, 999, 4, 5]`。 181 | 182 | 這種行為會如何改變你的程式碼?例如,很多函式會需要位置參數。在 JavaScript 中,如果我們想要在前五個字元後尋找一個空白(是的,slice 也可以在字串中使用),我們可以這樣寫: 183 | 184 | ```go 185 | haystack = "the spice must flow"; 186 | console.log(haystack.indexOf(" ", 5)); 187 | ``` 188 | 189 | 在 Go 語言中,我們使用 slice 來做: 190 | 191 | ```go 192 | strings.Index(haystack[5:], " ") 193 | ``` 194 | 195 | 從上面的例子中,我們可以看到 `[X:]` 是代表 *從 X 到結尾* 的縮寫。而 `[:X]` 代表的是 *從開始到 X* 的縮寫。跟其他語言不同的是,Go 不支援負索引值,如果我們想要除了最後一個以外的所有值,可以這樣寫: 196 | 197 | ```go 198 | scores := []int{1, 2, 3, 4, 5} 199 | scores = scores[:len(scores) - 1] 200 | ``` 201 | 202 | 下面的例子是從一個未排序 slice 中去除一個值的有效方法。 203 | 204 | ```go 205 | func main() { 206 | scores := []int{1, 2, 3, 4, 5} 207 | scores = removeAtIndex(scores, 2) 208 | fmt.Println(scores) 209 | } 210 | 211 | func removeAtIndex(source []int, index int) []int { 212 | lastIndex := len(source) - 1 213 | // 交換最後的值並移除我們想要除的值 214 | source[index], source[lastIndex] = source[lastIndex], source[index] 215 | return source[:lastIndex] 216 | } 217 | ``` 218 | 219 | 我們已經瞭解了 slice。最後,再來學習一下一個常見的內建函式 `copy`。`copy` 是許多的函式中顯著會改變我們如何撰寫程式碼的函式之一。一般來說,複製陣列的值到另外一個陣列會需要五個參數:`source`, `sourceStart`, `count`, `destination` 和 `destinationStart`。但使用 slice,我們只需要兩個參數: 220 | 221 | ```go 222 | import ( 223 | "fmt" 224 | "math/rand" 225 | "sort" 226 | ) 227 | 228 | func main() { 229 | scores := make([]int, 100) 230 | for i := 0; i < 100; i++ { 231 | scores[i] = int(rand.Int31n(1000)) 232 | } 233 | sort.Ints(scores) 234 | 235 | worst := make([]int, 5) 236 | copy(worst, scores[:5]) 237 | fmt.Println(worst) 238 | } 239 | ``` 240 | 241 | 花點時間研究上面的程式碼。試著改變一些部分。如果你使用 `copy(worst[2:4], scores[:5])` 方式去複製,看看會產生什麼結果?或者試著複製多於或者少於 5 個值到 `worst`。 242 | 243 | ## Map 244 | 245 | 在 Go 語言中的 Map 就如同其他語言的 hashtable 或 dictionary。他們的功用正如同你想像的:定義鍵和值,你可以從 map 中取得、設定或刪除該值。 246 | 247 | Map 和 slice 一樣,可以透過 `make` 函式來建立。讓我們來看個例子: 248 | 249 | ```go 250 | func main() { 251 | lookup := make(map[string]int) 252 | lookup["goku"] = 9001 253 | power, exists := lookup["vegeta"] 254 | 255 | // 列印 0,false 256 | // Integer 的預設值為 0 257 | fmt.Println(power, exists) 258 | } 259 | ``` 260 | 261 | 使用 `len` 可以取得鍵值的數量。可以透過 `delete` 函式來刪除特定鍵的值。 262 | 263 | ```go 264 | // 回傳 1 265 | total := len(lookup) 266 | 267 | // 沒有任何的回傳,可以呼叫一個不存在的 key 268 | delete(lookup, "goku") 269 | ``` 270 | 271 | Map 是動態增長的。然而,我們可以在 `make` 函式中透過設定第二個參數來給訂初始大小: 272 | 273 | ```go 274 | lookup := make(map[string]int, 100) 275 | ``` 276 | 277 | 如果你對於有多少鍵值有概念的話,預先定義初始化大小有助於提升效能。 278 | 279 | 當你需要把結構的欄位定義為一個 map 時,可以這樣做: 280 | 281 | ```go 282 | type Saiyan struct { 283 | Name string 284 | Friends map[string]*Saiyan 285 | } 286 | ``` 287 | 288 | 初始化上面這個結構的一種方式: 289 | 290 | ```go 291 | goku := &Saiyan{ 292 | Name: "Goku", 293 | Friends: make(map[string]*Saiyan), 294 | } 295 | goku.Friends["krillin"] = ... // 待完成的 krillin 或建立 Krillin 296 | ``` 297 | 298 | 這裡還有另外一種方式可以宣告和初始化一個 map。類似 `make`,這種方式可以用來初始化 map 和陣列。我們可以這樣宣告: 299 | 300 | ```go 301 | lookup := map[string]int{ 302 | "goku": 9001, 303 | "gohan": 2044, 304 | } 305 | ``` 306 | 307 | 我們可以使用 `for` 迴圈和 `range` 關鍵字來遍歷 map: 308 | 309 | ```go 310 | for key, value := range lookup { 311 | ... 312 | } 313 | ``` 314 | 315 | 要特別注意的是,遍歷 map 是沒有順序性的。每一次的遍歷返回的鍵值對都是隨機的。 316 | 317 | ## 指針和值 318 | 319 | 我們在第二章時已經討論過什麼時候要傳遞指針、什麼時候要傳遞值。現在我們學習到了陣列和 map,再來看看該使用以下哪一種方式? 320 | 321 | ```go 322 | a := make([]Saiyan, 10) 323 | // 或 324 | b := make([]*Saiyan, 10) 325 | ``` 326 | 327 | 很多開發者會認為傳遞 b 到一個函式,或是回傳一個 b 會比較有效率,但事實上,我們傳遞或返回的都是一個 slice 的拷貝,所以就傳遞或返回這個 slice 而言,是沒有什麼差別的。 328 | 329 | 你會看見不同的地方是在於如果你要修改 slice 或 map 的值。在這點上,同樣的邏輯我們已經在第二章看過。所以是定義一個陣列指針或陣列值,取決於你怎麼使用單個值,而不是怎麼使用陣列或 map 本身來決定。 330 | 331 | ## 在你繼續學習之前 332 | 333 | 陣列和 map 在 Go 中跟其他的語言很類似,如果你曾經使用過動態陣列,可能需要一點時間適應,但是 `append` 應該會解決掉大部分不適應的地方。如果我們拋開陣列表面的語法,你會發現 slice 是很強大的。使用 slice 對於維持程式碼的簡潔有很大幫助。 334 | 335 | 這邊有一些極端案例我們沒有提到,但你應該很少會遇到這些案例。如果你碰到了,希望我們為你打下的基礎可以讓你了解是怎麼回事。 336 | -------------------------------------------------------------------------------- /chapter4.md: -------------------------------------------------------------------------------- 1 | # 第四章 - 組織程式碼和介面 2 | 3 | 該是時候來看看怎麼組織我們的程式碼了。 4 | 5 | ## 套件 6 | 7 | 為了學習更複雜的函式庫和組織系統,我們需要學習套件。在 Go 中,套件名稱和你的工作目錄結構有關。如果我們想要建構一個購物車系統,也許我們會用 "shopping" 作為套件名稱,並且把我們的程式碼放在 `$GOPATH/src/shopping/` 目錄下。 8 | 9 | 我們不想要把所有的東西都放在這個目錄。比如說,我們可能會想要把資料庫的邏輯放在專屬他的資料夾。為了達到這樣的目的,我們可以建立一個子資料夾 `$GOPATH/src/shopping/db`。在這個資料夾中的套件名稱可以簡單的稱作 `db`,但是如果其他的套件想要存取他時,就必須要把 `shopping` 套件名稱也寫上。 10 | 11 | 換句話說,當你需要針對套件命名時,只要使用 `package` 關鍵字,並且提供一個名稱即可,而不需要把整個階層都寫上去(例如:`shopping` 或 `db`)。但是當你要引用套件時,就需要把完整的路徑寫上。 12 | 13 | 讓我們試試看,在你的工作目錄 `src` 下,建立一個新的資料夾叫做 `shopping`,接著在下面建立一個子資料夾 `db`: 14 | 15 | 在 `shopping/db` 中,建立一個 `db.go` 的檔案,並撰寫以下程式碼: 16 | 17 | ```go 18 | package db 19 | 20 | type Item struct { 21 | Price float64 22 | } 23 | 24 | func LoadItem(id int) *Item { 25 | return &Item{ 26 | Price: 9.001, 27 | } 28 | } 29 | ``` 30 | 31 | 注意這個套件的名稱跟資料夾名稱一樣。很明顯的我們並沒有實際存取資料庫,這裡只是要學習如何組織我們的程式碼而已。 32 | 33 | 接著在 `shopping` 目錄中建立一個 `pricecheck.go` 的檔案,並寫入以下程式碼: 34 | 35 | ```go 36 | package shopping 37 | 38 | import ( 39 | "shopping/db" 40 | ) 41 | 42 | func PriceCheck(itemId int) (float64, bool) { 43 | item := db.LoadItem(itemId) 44 | if item == nil { 45 | return 0, false 46 | } 47 | return item.Price, true 48 | } 49 | ``` 50 | 51 | 你可以會認為我們已經在 `shopping` 目錄下了,還要引用 `shopping/db` 會有點奇怪。事實上,我們是引用 `$GOPATH/src/shopping/db`,這意味著你可以很容易引用 `test/db` 這樣的套件,只要你有一個 `db` 的套件在你工作目錄下的 `src/test` 目錄中。 52 | 53 | 如果你想要建構一個套件,你只需要以上的步驟即可。如果想要建置可執行檔,你需要一個包含 `main` 的檔案。我建議的方式是在 `shopping` 目錄中建立一個子目錄 `main`,並在裡面建立一個 `main.go` 的檔案: 54 | 55 | ```go 56 | package main 57 | 58 | import ( 59 | "shopping" 60 | "fmt" 61 | ) 62 | 63 | func main() { 64 | fmt.Println(shopping.PriceCheck(4343)) 65 | } 66 | ``` 67 | 68 | 現在你可以執行你的 `shopping` 專案: 69 | 70 | ``` 71 | go run main/main.go 72 | ``` 73 | 74 | ### 循環引用 75 | 76 | 當你開始撰寫更複雜的系統時,你一定會遇到循環引用的問題。當 A 套件要引用 B 套件,但 B 套件又引用 A 套件時就會發生這樣的狀況(不管是直接引用或是透過其他套件間接引用)。這種情況編譯器是不會允許的。 77 | 78 | 讓我們調整我們的專案結構來模擬這樣的錯誤。 79 | 80 | 將 `Item` 的定義從 `shopping/db/db.go` 改為 `shopping/pricecheck.go`,所以你的 `pricecheck.go` 會長的像這樣: 81 | 82 | ```go 83 | package shopping 84 | 85 | import ( 86 | "shopping/db" 87 | ) 88 | 89 | type Item struct { 90 | Price float64 91 | } 92 | 93 | func PriceCheck(itemId int) (float64, bool) { 94 | item := db.LoadItem(itemId) 95 | if item == nil { 96 | return 0, false 97 | } 98 | return item.Price, true 99 | } 100 | ``` 101 | 102 | 如果你試著執行這段程式碼,你會從 `db/db.go` 得到一個關於 `Item` 未定義的錯誤。這是很合理的,因為 `Item` 不再存在於 `db` 套件了,他已經被移到 `shopping` 的套件中。我們需要調整 `shopping/db/db.go`: 103 | 104 | ```go 105 | package db 106 | 107 | import ( 108 | "shopping" 109 | ) 110 | 111 | func LoadItem(id int) *shopping.Item { 112 | return &shopping.Item{ 113 | Price: 9.001, 114 | } 115 | } 116 | ``` 117 | 118 | 現在再執行一下程式碼,你會得到*循環引用*錯誤。要解決這個問題,我們必須要導入另外一個套件,所以我們現在的目錄結構長得像這樣: 119 | 120 | ``` 121 | $GOPATH/src 122 | - shopping 123 | pricecheck.go 124 | - db 125 | db.go 126 | - models 127 | item.go 128 | - main 129 | main.go 130 | ``` 131 | 132 | `pricecheck.go` 仍然會引用 `shopping/db`,但是 `db.go` 現在會引用 `shopping/models`,而不是 `shopping`。如此一來就可以解決循環引用的問題。由於我們將共用的結構 `Item` 到 `shopping/models/item.go`,我們需要變更 `shopping/db.db.go`,讓他可以從 `models` 套件中引用 `Item` 結構。 133 | 134 | ```go 135 | package db 136 | 137 | import ( 138 | "shopping/models" 139 | ) 140 | 141 | func LoadItem(id int) *models.Item { 142 | return &models.Item{ 143 | Price: 9.001, 144 | } 145 | } 146 | ``` 147 | 148 | 你經常會共享的套件不僅僅是 `models`,可能還會有其他類似 `utilities` 這樣的套件。關於這一類共享套件的重要規則就是,他不應該從 `shopping` 套件或其他任何的子套件中引用任何東西。在一些小節中,我們會看到使用介面將會幫助我們解決這些相依關係。 149 | 150 | ### 可視性 151 | 152 | Go 使用一個簡單的規則來定義每個型態和函式是否可被外部的套件呼叫。如果你在宣告類型或函式時以大寫字母開頭,那這個函式或型態就是可被外部引用的。如果是以小寫開頭,那就是不可見的。 153 | 154 | 這樣的規則也適用於結構,如果一個結構中的欄位是小寫字母開頭,那只有在同一個套件中的程式碼才能夠存取這些欄位。例如,我們在 `items.go` 中有一個函式長這樣: 155 | 156 | ```go 157 | func NewItem() *Item { 158 | // ... 159 | } 160 | ``` 161 | 162 | 我們可以透過 `models.NewItem()` 呼叫這個函式,但如果這個函式命名為 `newItem`,那我們從其他的套件就無法呼叫這個函式。 163 | 你可以繼續修改 `shopping` 套件中的型態或欄位,例如,如果你將 `Item` 結構中的 `Price` 欄位改成 `price`,會得到錯誤訊息。 164 | 165 | ### 套件管理 166 | 167 | 我們已經學習過 go 的命令列工具,例如 `go run` 和 `go build`,還有一個 `get` 的子命令可以用來下載第三方函式庫。`go get` 支援不同的通訊協定,但在我們這個例子中,我們會嘗試透過這個命令從 Github 上下載一個函式庫,這意味著你必須在你的電腦上安裝 `git`。假設你已經安裝 `git` 了,在你的命令列上輸入: 168 | 169 | ```sh 170 | go get github.com/mattn/go-sqlite3 171 | ``` 172 | 173 | `go get` 會從遠端下載檔案並且儲存到你的工作目錄。查看你的 `$GOPATH/src`。除了我們已經建立的 `shopping` 專案外,你還會看到 `github.com` 資料夾。在這個資料夾中,你還會看見一個 `mattn` 資料夾,裡面包含了 `go-sqlite3` 的資料夾。 174 | 175 | 我們已經學習過如何引用一個套件在我們的工作目錄中,現在我們有一個全新的 `go-sqlite3` 套件,你可以透過以下方式引用: 176 | 177 | ```go 178 | import ( 179 | "github.com/mattn/go-sqlite3" 180 | ) 181 | ``` 182 | 183 | 我知道這看起來很像一個網址,但事實上,他代表引用 `go-sqlite3` 套件,而這個套件就位在你電腦中的 `$GOPATH/src/github.com/mattn/go-sqlite3` 目錄下。 184 | 185 | ### 相依管理 186 | 187 | `go get` 有一些其他有趣的地方。如果你在一個專案中執行 `go get`,他會幫你掃描所有的檔案,尋找 `import` 所引用的第三方套件,並且嘗試下載它。 188 | 某方面來說,我們自己的程式碼變成一個 `Gemfile` 或 `package.json` 檔案。(譯注:`Gemfile` 是 Ruby 用來管理第三方套件的檔案、`package.json` 是 Nodejs 用來管理第三方套件的檔案) 189 | 190 | 如果你使用 `go get -u`,他會更新所有的套件(或是你也可以透過 `go get -u FULL_PACKAGE_NAME` 更新特定的套件)。 191 | 192 | 最後,你可能會發現 `go get` 一些不足的地方。首先,他無法指定一個特定版本,他總會指向 `master/head/trunk/default`,這是一個嚴重的問題,尤其是你有兩個專案引用到同一個套件,但又需要該套件的不同版本。 193 | 194 | 為了解決這個問題,你可以使用一些第三方相依管理的工具。雖然這些工具還不太成熟,但有兩個相依管理的工具比較有未來性,那就是 [goop](https://github.com/nitrous-io/goop) 和 [godep](https://github.com/tools/godep)。 195 | 196 | 更完整的列表可以參考 [go-wiki](https://github.com/golang/go/wiki/PackageManagementTools)。 197 | 198 | ## 介面 199 | 200 | 介面是一種型態,它定義了宣告但沒有實作。底下是一個範例: 201 | 202 | ```go 203 | type Logger interface { 204 | Log(message string) 205 | } 206 | ``` 207 | 208 | 你可能會覺得這樣有什麼用處?介面可以讓你的程式碼從實作中去耦合。例如,你可能會有很多種不同的 loggers: 209 | 210 | ```go 211 | type SqlLogger struct { ... } 212 | type ConsoleLogger struct { ... } 213 | type FileLogger struct { ... } 214 | ``` 215 | 216 | 如果你在實作的時候使用介面,而不是具體的實作時,你可以很容易的改變和測試我們的程式碼。要怎麼使用?就像其他的類型一樣,你可以把介面作為結構的一個欄位宣告: 217 | 218 | ```go 219 | type Server struct { 220 | logger Logger 221 | } 222 | ``` 223 | 224 | 或是一個函式的參數(或是回傳值): 225 | 226 | ```go 227 | func process(logger Logger) { 228 | logger.Log("hello!") 229 | } 230 | ``` 231 | 232 | 在 C# 或 Java 中,當一個類別實作一個介面時,並需要明確的定義: 233 | 234 | ```go 235 | public class ConsoleLogger : Logger { 236 | public void Logger(message string) { 237 | Console.WriteLine(message) 238 | } 239 | } 240 | ``` 241 | 242 | 在 Go 中,這樣的行為是隱性的。如果你的結構有一個函式 `Log`,參數是 `string`,並且沒有回傳值,那這就可以當作是一個 `Logger`。這讓介面的使用上少了點冗餘性。 243 | 244 | ```go 245 | type ConsoleLogger struct {} 246 | func (l ConsoleLogger) Log(message string) { 247 | fmt.Println(message) 248 | } 249 | ``` 250 | 251 | 這也促成了介面具有小巧和集中的特性。Go 語言的標準函式庫中充滿著介面。尤其是在 `io` 的函式庫中有許多熱門的介面,比如說 `io.Reader`、`io.Writer` 和 `io.Closer`。 252 | 如果你撰寫一個函式,函式的參數會呼叫 `Close`,你就可以傳遞一個 `io.Closer` 的介面而不用管你使用的具體型別是什麼。 253 | 254 | 介面也可以組合,也就是說介面可以由其他的介面組成。例如 `io.ReadCloser` 就是由 `io.Reader` 介面和 `io.Closer` 介面組成。 255 | 256 | 最後,介面經常會避免循環引用。因為介面沒有具體的實作內容,所以他們的相依性是有限的。 257 | 258 | ## 在你繼續學習之前 259 | 260 | 當你開始用 Go 來撰寫一些專案時,你會習慣在 Go 工作目錄中組織程式碼的方式。最重要的是你要記住套件名稱和目錄結構有密切的關連(不只在一個專案中如此,在整個工作目錄都是這樣)。Go 語言處理可見性的方式是簡單、高效率且具有一致性的。還有一些內容我們沒有介紹到,比如說常數和全域變數,但別擔心,他們的可見性也是遵守一樣的規則。 261 | 262 | 最後,如果你不熟悉 Go 的介面,可能會需要花一點時間來學習它。然而,當你第一次看到一個類似 `io.Reader` 的函式時,你會感激作者不會要求超過他所需要的部分的。 263 | -------------------------------------------------------------------------------- /chapter5.md: -------------------------------------------------------------------------------- 1 | # 第五章 - 花絮 2 | 3 | 在這章,我們會介紹一些 Go 語言的花絮,這些特性主要是用在 Go 語言上。 4 | 5 | ## 錯誤處理 6 | 7 | Go 主要是透過返回值來處理錯誤,並沒有一般語言的異常處理。讓我們來看看 `strconv.Atoi` 函式,這個函式會將字串轉整數: 8 | 9 | ```go 10 | package main 11 | 12 | import ( 13 | "fmt" 14 | "os" 15 | "strconv" 16 | ) 17 | 18 | func main() { 19 | if len(os.Args) != 2 { 20 | os.Exit(1) 21 | } 22 | 23 | n, err := strconv.Atoi(os.Args[1]) 24 | if err != nil { 25 | fmt.Println("not a valid number") 26 | } else { 27 | fmt.Println(n) 28 | } 29 | } 30 | ``` 31 | 32 | 你可以建立自己的錯誤型別,唯一的要求就是要滿足內建的 `錯誤` 介面: 33 | 34 | ```go 35 | type error interface { 36 | Error() string 37 | } 38 | ``` 39 | 40 | 一般情況下,我們可以引用內建的 `error` 套件,並使用 `new` 函式來建立自己的錯誤型別: 41 | 42 | ```go 43 | import ( 44 | "errors" 45 | ) 46 | 47 | 48 | func process(count int) error { 49 | if count < 1 { 50 | return errors.New("Invalid count") 51 | } 52 | ... 53 | return nil 54 | } 55 | ``` 56 | 57 | Go 的標準函式庫就是透過這種模式來進行錯誤處理。例如,在 `io` 的函式庫中有一個 `EOF` 的變數用來定義錯誤(譯注:原始碼可參考:[https://github.com/golang/go/blob/master/src/io/io.go#L38](https://github.com/golang/go/blob/master/src/io/io.go#L38)): 58 | 59 | ```go 60 | var EOF = errors.New("EOF") 61 | ``` 62 | 63 | 這是一個屬於套件級別的變數(定義在函式外部),是可以是可以被公開存取的(變數是大寫開頭)。當我們從檔案中或標準輸入中讀取資料時,許多的函式都可以返回這種錯誤。如果有上下文關係的話,你也應該使用這種錯誤處理。作為使用者,你可以這樣使用: 64 | 65 | ```go 66 | package main 67 | 68 | import ( 69 | "fmt" 70 | "io" 71 | ) 72 | 73 | func main() { 74 | var input int 75 | _, err := fmt.Scan(&input) 76 | if err == io.EOF { 77 | fmt.Println("no more input!") 78 | } 79 | } 80 | ``` 81 | 82 | 最後提醒一點,Go 有 `panic` 和 `recover` 函式。`panic` 類似於拋出異常,`recover` 類似於 `catch`。不過它們很少使用。 83 | 84 | ## Defer 85 | 86 | 儘管 Go 語言提供了垃圾回收的機制,還是有一些資源需要開發者明確的去釋放。比如說,在處理文件結束時,我們需要呼叫 `Close()` 來關閉 io。這種類型的程式碼總是比較危險的,首先,當我們寫了一個函式,很容易忘記去呼叫 `Close` 函式來關閉我們在第十行開啟的檔案。此外,一個函式可能會有多個返回點。Go 提供了 `defer` 關鍵字來處理這一類的問題: 87 | 88 | ```go 89 | package main 90 | 91 | import ( 92 | "fmt" 93 | "os" 94 | ) 95 | 96 | func main() { 97 | file, err := os.Open("a_file_to_read") 98 | if err != nil { 99 | fmt.Println(err) 100 | return 101 | } 102 | defer file.Close() 103 | // 讀檔 104 | } 105 | ``` 106 | 107 | 如果你嘗試執行上面的程式碼,你可能會得到一個錯誤(因為檔案不存在)。這裡主要是想要讓你知道 `defer` 的用法。使用 `defer` 的操作,都會在函式返回前執行。這讓你可以在初始化或宣告某個操作的附近就預先宣告要釋放資源。 108 | 109 | ## go fmt 110 | 111 | 大多數用 Go 寫的程式碼都遵循相同的風格,那就是,使用 tab 進行縮排、括號和程式宣告在同一行等。 112 | 113 | 我知道你有自己的風格,也很想堅持下去。我曾經有一段時間也是這樣的,但很高興最後我還是屈服了。其中最大的原因就是 `go fmt` 命令工具。它很容使用也很具代表性(所以沒有人為了無意義的偏好而爭執)。 114 | 115 | 當你在專案的目錄下,你可以透過下面的命令行工具將所有子專案進行程式碼格式編排: 116 | 117 | ```sh 118 | go fmt ./... 119 | ``` 120 | 121 | 試試看吧,這個命令行工具會幫你的程式碼縮排,也會自動地幫你對齊,並且將引用的函式庫按照字母順序排列 122 | 123 | ## 具有初始化功能的 if 124 | 125 | Go 支援一種稍微不一樣的 if 敘述,一個變數可以在 if 條件執行前宣告並且初始化: 126 | 127 | ```go 128 | if x := 10; count > x { 129 | ... 130 | } 131 | ``` 132 | 133 | 這是一個有點愚蠢的例子,比較實際的範例如下: 134 | 135 | ```go 136 | if err := process(); err != nil { 137 | return err 138 | } 139 | ``` 140 | 141 | 有趣的是,透過 if 初始化的值在 if 的範圍以外是不能被存取的,但是在 `else if` 和 `else` 中可以被使用。 142 | 143 | ## 空的 Interface 和轉換 144 | 145 | 在大多數的物件導向程式語言中,都有內建的基礎類別,通常稱做 `物件`。它通常是所有類別的父類別。但在 Go 中不支援繼承,所以沒有類似這種父類別的概念。Go 裡面擁有的是一個沒有任何宣告的空介面 `interface{}`。由於每個型別都實作了空介面的 0 個方法,而且每個介面都是隱性實作,所以每種類型都實現了空介面的契約。 146 | 147 | 如果我們想要,可以寫一個 `add` 函式: 148 | 149 | ```go 150 | func add(a interface{}, b interface{}) interface{} { 151 | ... 152 | } 153 | ``` 154 | 155 | 將一個空介面型態的變數做顯性的轉換,可以用 `.(TYPE)`: 156 | 157 | ```go 158 | return a.(int) + b.(int) 159 | ``` 160 | 161 | 要注意的是,如果欲轉換的變數並不是 `int` 型態,上面的程式碼將會出錯。 162 | 163 | 你也可以強大的 type switch 進行轉換: 164 | 165 | ```go 166 | switch a.(type) { 167 | case int: 168 | fmt.Printf("a is now an int and equals %d\n", a) 169 | case bool, string: 170 | // ... 171 | default: 172 | // ... 173 | } 174 | ``` 175 | 176 | 你會發現空介面的使用超出你的預期。不可否認的,這會讓你的程式碼看起來不夠乾淨。某些時候不斷轉換一個值是醜陋的且危險的,但在靜態語言中,這是唯一的選擇。 177 | 178 | ## 字串和位元組陣列 179 | 180 | 字串和位元組陣列有密切的關係,我們可以很容易轉換他們: 181 | 182 | ```go 183 | stra := "the spice must flow" 184 | byts := []byte(stra) 185 | strb := string(byts) 186 | ``` 187 | 188 | 事實上,這也是大多數型態的轉換方式。某些函式會明確指定 `int32` 或 `int64`,或其他無號的部分。你可能會發現自己必須這樣寫: 189 | 190 | ```go 191 | int64(count) 192 | ``` 193 | 194 | 儘管如此,當提到位元組和字串時,這會是你經常接觸到的東西。要記住,當你使用 `[]byte(x)` 或 `string(x)` 時,你是建立資料的拷貝,這是因為字串是不可變的。字串是由 `runes` 組成,`runes` 是一個 unicode 字符。當你用 `len` 函式來取得字串的長度時,往往結果不如你預期。以下的範例將會印出 3: 195 | 196 | ```go 197 | fmt.Println(len("椒")) 198 | ``` 199 | 200 | 如果你嘗試透過 `range` 函式來遍歷一個字串,你是得到一個個的 runes,而不是位元組。當然,當你將一個字串轉成 `[]byte` 時, 201 | 你會得到正確的資料。 202 | 203 | ## 函式型別 204 | 205 | 函式是一級型別: 206 | 207 | ```go 208 | type Add func(a int, b int) int 209 | ``` 210 | 211 | 它可以用在任何地方 - 當作一個欄位型別、參數、回傳值。 212 | 213 | ```go 214 | package main 215 | 216 | import ( 217 | "fmt" 218 | ) 219 | 220 | type Add func(a int, b int) int 221 | 222 | func main() { 223 | fmt.Println(process(func(a int, b int) int{ 224 | return a + b 225 | })) 226 | } 227 | 228 | func process(adder Add) int { 229 | return adder(1, 2) 230 | } 231 | ``` 232 | 233 | 透過這種使用函式的方式,我們可以從特定的實作中減少耦合,就像我們使用介面一樣。 234 | 235 | ## 在你繼續學習之前 236 | 我們已經學習了很多 Go 語言的特性,顯而易見的,我們學習了錯誤處理、當開起檔案後如何釋放資源。許多開發者不喜歡 Go 錯誤處理的方式,它讓人覺得是一種退步。某些時候我是同意的,然而,我也會發現這樣的程式碼更容易閱讀。`defer` 在資源管理上是一個不常見但實用的手段。事實上,`defer` 不僅僅可以用在資源管理上,也可以用在其他方面,比如說你可以用在函式退出時的日誌紀錄上。 237 | 238 | 當然,我們還沒有把所有 Go 的特性都介紹完,但你應該可以在遇到任何困難時都迎刃而解。 239 | -------------------------------------------------------------------------------- /chapter6.md: -------------------------------------------------------------------------------- 1 | # 第六章 - 並行 2 | 3 | Go 經常被描述為適合用在並行化處理的程式語言。主要的原因在於,Go 在並行化上提供了兩種簡單且強大的機制:goroutine 和 channel。 4 | 5 | ## Goroutine 6 | 7 | goroutine 有點類似於執行緒,但它是由 Go 自己來調度安排的,而不是由作業系統。當你的程式碼在一個 goroutine 中執行時,它可以和其他的程式碼並行執行。讓我們來看個例子: 8 | 9 | ```go 10 | package main 11 | 12 | import ( 13 | "fmt" 14 | "time" 15 | ) 16 | 17 | func main() { 18 | fmt.Println("start") 19 | go process() 20 | time.Sleep(time.Millisecond * 10) // 這是不好的方式,請別這麼做! 21 | fmt.Println("done") 22 | } 23 | 24 | func process() { 25 | fmt.Println("processing") 26 | } 27 | ``` 28 | 29 | 上面的程式碼有幾個有趣的部分,但最重要的是我們要了解怎麼啟動一個 goroutine。我們只要將 `go` 關鍵字放在我們想要執行的函式前面即可。如果我們想要執行一小段程式碼,那我們可以使用匿名函式。要注意的是,匿名函式不僅僅在 goroutine 中可以使用,其他地方也是可以的。 30 | 31 | ```go 32 | go func() { 33 | fmt.Println("processing") 34 | }() 35 | ``` 36 | 37 | Goroutine 很容易建立而且開銷很小,最終多個 goroutine 會執行在同一個作業系統多個執行緒上 (譯注:可以參考 Effective Go 的說明:[https://golang.org/doc/effective_go.html#goroutines](https://golang.org/doc/effective_go.html#goroutines))。這也常被稱為 M:N 執行緒模型。因為我們有 M 個應用程式 goroutine,執行在 N 個作業系統的執行緒。結果就是,一個 goroutine 的開銷比起執行緒來說低很多(也許只有幾 KB),在現代的硬體上,甚至有可能同時執行幾百萬個 goroutine。 38 | 39 | 此外,這裡還隱藏了映射和調度的複雜性。我們僅需要說 *這段代碼要並行執行*,然後 Go 就會讓這件事情發生了。 40 | 41 | 回到我們剛剛的例子,你會發現我們使用 `Sleep` 函式讓程式暫停幾毫秒,原因是因為我們必須要讓 goroutine 在主程式執行完結束前被執行(主程式不會等到所有 goroutine 執行完才結束)。要解決這個問題,我們必須要讓程式碼互相協調一下。 42 | 43 | ## 同步 44 | 45 | 建立一個 goroutine 沒有太困難,而且開銷很小,所以我們可以很容易地建立很多 goroutine。但問題是,並行化的程式碼需要互相溝通。要解決這個需求,Go 提供了 `channels` 的機制。在我們學習 `channels` 之前,我們必須要先學習並行化程式的基本概念。 46 | 47 | 撰寫並行化程式碼,你需要特別注意你在哪裡以及如何讀取一個值。某些面向來說,他很像你在撰寫一個沒有垃圾回收機制的程式語言。它需要你用不同的角度重新思考資料,永遠要考慮可能的危險性。看看以下的程式碼: 48 | 49 | ```go 50 | package main 51 | 52 | import ( 53 | "fmt" 54 | "time" 55 | ) 56 | 57 | var counter = 0 58 | 59 | func main() { 60 | for i := 0; i < 20; i++ { 61 | go incr() 62 | } 63 | time.Sleep(time.Millisecond * 10) 64 | } 65 | 66 | func incr() { 67 | counter++ 68 | fmt.Println(counter) 69 | } 70 | ``` 71 | 72 | 你覺得輸出會是什麼?如果你認為輸入會是 `1, 2, ... 20`,你可能是對的,也可能是錯的。當你執行上面的程式碼,的確有時候會得到這樣的結果。然而,事實上這個結果是不確定的,為什麼?因為我們有多個(在這個例子是兩個) goroutine 同時存取單一個變數 `counter`。或更糟糕的狀況是其中一個 goroutine 正在讀取這個變數,而另一個正在寫入。 73 | 74 | 這樣真的危險嗎?的確是的。`counter++` 看起來只是一行簡單的程式碼,但它實際上被拆解成數行的組合語言,實際的狀況會取決於你執行該程式碼的軟硬體平台。如果你執行這個範例,很有可能的情況是數字印出的順序是不固定的。或有可能某些數字重複或遺失。最壞的結果也有可能造成程式錯誤或是得到任意的值。 75 | 76 | 在並行化的程式中,唯一安全的方式是讀取該變數。你可以有很多程式去讀一個變數,但寫入變數必須是同步的。這有幾種方法可以實現,包括使用依賴於 CPU 架構的原子化操作。然而,大多數的形況是使用一個互斥鎖: 77 | 78 | ```go 79 | package main 80 | 81 | import ( 82 | "fmt" 83 | "time" 84 | "sync" 85 | ) 86 | 87 | var ( 88 | counter = 0 89 | lock sync.Mutex 90 | ) 91 | 92 | func main() { 93 | for i := 0; i < 20; i++ { 94 | go incr() 95 | } 96 | time.Sleep(time.Millisecond * 10) 97 | } 98 | 99 | func incr() { 100 | lock.Lock() 101 | defer lock.Unlock() 102 | counter++ 103 | fmt.Println(counter) 104 | } 105 | ``` 106 | 107 | 互斥鎖可以讓你循序的存取程式碼。因為預設的 `sync.Mutex` 是沒有鎖定的,所以我們簡單的定義了一個 `lock sync.Mutex`。 108 | 109 | 看起來似乎很簡單?其實上面的例子有一點欺騙的意味。首先,哪些程式碼需要被保護其實並不是很明顯的。雖然它可以用一個低等的鎖(這個鎖包含了許多的程式碼),這些潛在容易出錯的部分是我們在撰寫並行化程式碼首先要考慮的。我們通常想要一個很精確的鎖,不然我們經常會發現本來是開在一個十線道的,突然轉往一個單線道一樣。 110 | 111 | 另外一個問題是死鎖問題。當我們使用一個鎖的時候,沒有問題。但如果你使用兩個或兩個以上的鎖,很容易發生一種問題是,當 goroutineA 有鎖 A,但他想要存取鎖 B,而 goroutineB 擁有鎖 B,但它想要存取鎖 A。 112 | 113 | 事實上當我們使用一個鎖的時候,如果忘了釋放它,也可能發生死鎖問題。但這和多個鎖引起的死鎖問題相比,並不嚴重(事實上這也很難發現)。你可以試著執行下面的程式碼: 114 | 115 | ```go 116 | package main 117 | 118 | import ( 119 | "time" 120 | "sync" 121 | ) 122 | 123 | var ( 124 | lock sync.Mutex 125 | ) 126 | 127 | func main() { 128 | go func() { lock.Lock() }() 129 | time.Sleep(time.Millisecond * 10) 130 | lock.Lock() 131 | } 132 | ``` 133 | 134 | 我們到目前為止還有很多並行程式沒有看過。首先,有一個常見的鎖叫做「讀寫鎖」。這個鎖提供兩個功能:一個鎖定讀、另一個鎖定寫。這個功能讓你可以同時有多個讀寫操作。在 Go 中,`sync.RWMutex` 就是這樣的功用。另外,`sync.Mutex` 除了提供 `Lock` 和 `Unlock` 外,它也提供了 `RLock` 和 `RUnlock`,這個 `R` 代表了*讀取*。雖然讀寫鎖很常用,但他們也會給開發者帶來額外的負擔:我們不僅要注意我們正在存取的資料,也要注意是如何存取的。 135 | 136 | 此外,部分的並行化程式不僅僅是循序的存取變數,也需要安排多個 goroutine。例如,等待 10 毫秒並不是一個優雅的解決方法,如果一個 goroutine 需要超過 10 毫秒呢?如果執行時間少於 10 毫秒,我們只是浪費時間呢?又或者當一個 goroutine 執行完畢後,我們要告訴另外一個 goroutine 有新的資料要給處理? 137 | 138 | 所有的這些事在沒有 `channel` 的情況都可以實現, 當然對於更簡單的例子來說,我相信你應該使用 `sync.Mutex` 和 `sync.RWMutex`。 139 | 但在下一章節中,我們將會學習到 `channel` 的主要目的是為了讓並行程式碼在撰寫時更簡單且更不容易出錯。 140 | 141 | ## Channel 142 | 143 | 撰寫並行化程式最主要的挑戰在於資料共享,如果你的 Go 程式沒有要共享資料,那就不需要擔心同步的問題。但是,對與所有其他的系統而言,這並不是不需要擔心的。事實上,許多系統反而朝向反方向設計:在多個請求之間分享資料。所有的記憶體快取或資料庫設計都是最好的例子。這已經變成越來越流行的現實了。 144 | 145 | Channel 讓並行化程式設計在共享資料上更有道理。一個 Channel 就是不同的 goroutine 之間用來傳遞資料溝通的管道。換句話說,一個 goroutine 可以藉由 Channel 來傳遞資料到另外一個 goroutine。其結果就是,在同一時間內,只有一個 goroutine 會存取到資料。 146 | 147 | Channel 一樣有型別。它的型別就是我們要在不同 goroutine 之間傳遞資料的型別。例如,我們可以建立一個用來傳遞整數的 Channel: 148 | 149 | ```go 150 | c := make(chan int) 151 | ``` 152 | 153 | 這種型別的 channel 就是 `chan int`。因此,為了要透過函式傳遞這樣的 channel,他的參數會是: 154 | 155 | ```go 156 | func worker(c chan int) { ... } 157 | ``` 158 | 159 | Channel 支持兩種操作:接收和傳送。我們可以這樣傳送資料到一個 channel: 160 | 161 | ``` 162 | CHANNEL <- DATA 163 | ``` 164 | 165 | 或是從 channel 接收資料: 166 | 167 | ``` 168 | VAR := <-CHANNEL 169 | ``` 170 | 171 | 箭頭代表了資料傳遞的方向。當傳送資料時,箭頭是指向 channel 的。當接收資料時,箭頭是從 channel 指出去的。 172 | 173 | 在我們學習第一個例子之前,我們要知道最後一件事,從 channel 接收或傳送出去是互相阻塞的。也就是說,當我們從一個 channel 接收資料時,goroutine 會等到資料接收完畢後才會繼續執行。同樣的,當我們傳送資料到一個 channel 時,在資料被接收之前,goroutine 也不會繼續執行。 174 | 175 | 考量到一個系統會需要在不同的 goroutine 來處理接收到的資料,這是一個相當常見的需求。如果我們在 goroutine 針對接收到的資料進行複雜的處理,那客戶端很有可能會超時。首先,我們撰寫我們的 worker,這是一個簡單的函式,但我們會把它變成結構的一部份,因為我們之前還沒有這樣使用過 goroutine: 176 | 177 | ```go 178 | type Worker struct { 179 | id int 180 | } 181 | 182 | func (w Worker) process(c chan int) { 183 | for { 184 | data := <-c 185 | fmt.Printf("worker %d got %d\n", w.id, data) 186 | } 187 | } 188 | ``` 189 | 190 | 我們的 worker 很簡單,他等到所有的資料都接收到了之後才處理他們。這個 worker 盡責的在一個無窮迴圈中不斷的等待更多資料,然後處理他們。 191 | 192 | 為了要使用它,第一件事就是要啟動一些 workers: 193 | 194 | ```go 195 | c := make(chan int) 196 | for i := 0; i < 5; i++ { 197 | worker := &Worker{id: i} 198 | go worker.process(c) 199 | } 200 | ``` 201 | 202 | 接著我們可以指派給他一些工作: 203 | 204 | ```go 205 | for { 206 | c <- rand.Int() 207 | time.Sleep(time.Millisecond * 50) 208 | } 209 | ``` 210 | 211 | 下面是完整的範例: 212 | 213 | ```go 214 | package main 215 | 216 | import ( 217 | "fmt" 218 | "time" 219 | "math/rand" 220 | ) 221 | 222 | func main() { 223 | c := make(chan int) 224 | for i := 0; i < 5; i++ { 225 | worker := &Worker{id: i} 226 | go worker.process(c) 227 | } 228 | 229 | for { 230 | c <- rand.Int() 231 | time.Sleep(time.Millisecond * 50) 232 | } 233 | } 234 | 235 | type Worker struct { 236 | id int 237 | } 238 | 239 | func (w *Worker) process(c chan int) { 240 | for { 241 | data := <-c 242 | fmt.Printf("worker %d got %d\n", w.id, data) 243 | } 244 | } 245 | ``` 246 | 247 | 我們並不知道哪一個 worker 會收到資料。我們知道的是,Go 會確保我們送給 channel 的資料只會有一個接收者接收。 248 | 249 | 要特別注意的是,channel 是唯一安全用來接收和傳送共享資料的方式。Channel 提供了所有我們在同步程式碼所需的功能。並且確保在同一時間只會有一個 goroutine 可以存取特定的資料。 250 | 251 | ### 具有暫存能力的 Channel 252 | 253 | 在上面的程式中,如果資料超過我們可以處理的容量會怎樣呢?你可以嘗試模擬這種狀況,在 worker 接收到資料後,讓 worker 執行 sleep 函式: 254 | 255 | ```go 256 | for { 257 | data := <-c 258 | fmt.Printf("worker %d got %d\n", w.id, data) 259 | time.Sleep(time.Millisecond * 500) 260 | } 261 | ``` 262 | 263 | 在 main 程式中會發生什麼事?接收使用者輸入的資料(在這裡指的是隨機亂數產生器)會被阻塞,因為往 channel 發送資料時並沒有接收者。 264 | 265 | 為了確保資料能夠被處理,你可能想要讓客戶端被堵塞。在一些其他的情況下,你也許不願意確保資料能夠被處理。這裡有一些常見的策略來解決這個問題,首先是將資料暫存起來,如果沒有 worker 可用,我們可以將資料先存在一個有序的佇列中。Channel 擁有這種內建的暫存能力,當我們使用 `make` 建立 channel 時,可以給定它的長度: 266 | 267 | ```go 268 | c := make(chan int, 100) 269 | ``` 270 | 271 | 你可以這樣調整,但你會發現這個過程還是蠻不穩定的。作為暫存的 channel 不能增加更多的容量,它只是提供一個佇列來處理這種突然劇增的資料。 272 | 在我們的例子中,我們可以不斷地發送資料,而且 worker 也可以處理這些資料。雖然如此,我們可以透過 channel 的 `len` 函式來了解具有暫存功能的 channel 實際的作用: 273 | 274 | ```go 275 | for { 276 | c <- rand.Int() 277 | fmt.Println(len(c)) 278 | time.Sleep(time.Millisecond * 50) 279 | } 280 | ``` 281 | 282 | 你會看到具有暫存的 channel 的長度不斷增加,直到資料裝滿為止。此時,往 channel 發送的資料又會被阻塞。 283 | 284 | ### Select 285 | 286 | 即使有了暫存機制,我們還是需要開始丟棄掉一些資料。我們不可能使用一個無限大的記憶體,並指望透過人工的方式來釋放。 287 | 因此,我們要使用 Go 的 `select`。 288 | 289 | 語法上,`select` 類似於 switch,透過 `select`,我們可以撰寫一些在 channel 下無法實現的程式碼。首先,讓我們移除我們 channel 的暫存,讓我們可以更清楚地看到 `select` 是如何運作的: 290 | 291 | ```go 292 | c := make(chan int) 293 | ``` 294 | 295 | 接下來,我們修改 `for` 迴圈: 296 | 297 | ```go 298 | for { 299 | select { 300 | case c <- rand.Int(): 301 | //以下是可選的部分 302 | default: 303 | //這裡可以留空,用來丟掉資料 304 | fmt.Println("dropped") 305 | } 306 | time.Sleep(time.Millisecond * 50) 307 | } 308 | ``` 309 | 310 | 我們每秒往 channel 送 20 個訊息,但我們的 worker 每秒只能處理 10 個訊息。因此,會有一半的訊息被丟棄。 311 | 312 | 這僅僅是我們使用 `select` 來完成的第一件事情。使用 `select` 的最主要目的是讓你可以管理多個 channel。當你有多個 channel 時,`select` 會阻擋資料,直到有一個可用的 channel。如果沒有可用的 channel 時,如果你有提供 `default` 敘述,就會跑到該地方執行。當有多個 channel 都可用時,`select` 會隨機選擇一個 channel。 313 | 314 | 這是一個比較高級的特性,很難想出一個簡單的範例來證明這個行為。我們在下一節當中來說明這個部分。 315 | 316 | ### 逾時 317 | 318 | 我們已經看過暫存的訊息,同時也學到如何簡單的丟棄他們。另外一個比較常見的做法是使用逾時機制。我們會阻塞一段時間,但不是永遠阻塞。這在 Go 當中也是很容易做到的。老實說,我認爲這個語法有點難以接受,但他是比較靈活和有用的方法,基本上我不能不使用它。 319 | 320 | 為了達到阻塞的最大值,我們可以使用 `time.After` 這個函式。讓我們來看看會發生什麼神奇的事情。我們修改一下發送部分的程式碼: 321 | 322 | ```go 323 | for { 324 | select { 325 | case c <- rand.Int(): 326 | case <-time.After(time.Millisecond * 100): 327 | fmt.Println("timed out") 328 | } 329 | time.Sleep(time.Millisecond * 50) 330 | } 331 | ``` 332 | 333 | `time.After` 會回傳一個 channel,所以我們可以對它使用 `select` 語法。這個 channel 在指定的時間後會被寫入,就這樣。沒有什麼比這個更神奇的了。如果你仍然覺得困惑,這裡實作了一個 `after`: 334 | 335 | ```go 336 | func after(d time.Duration) chan bool { 337 | c := make(chan bool) 338 | go func() { 339 | time.Sleep(d) 340 | c <- true 341 | }() 342 | return c 343 | } 344 | ``` 345 | 346 | 回來看我們的 `select` 語法,這裡有幾個有趣的地方。首先,如果你在 `select` 後面增加 `default` 會發生什麼事情?你能猜猜看嗎?試試看,如果你不確定會發生什麼事情。記住,如果 channel 無法使用的時候,`default` 的部分會被執行。 347 | 348 | 此外,`time.After` 的類型是 `chan time.Time`。在上面的例子中,我們只是簡單的把傳給 channel 的值給丟掉。如果你想要的話,也可以嘗試接收它們: 349 | 350 | ```go 351 | case t := <-time.After(time.Millisecond * 100): 352 | fmt.Println("timed out at", t) 353 | ``` 354 | 355 | 注意我們的 `select` 語法,注意我們是往 `c` 發送資料,但是是從 `time.After` 拿資料。不管我們是從 channel 中接收資料、發送資料,`select` 的機制都一樣: 356 | 357 | * 第一個可用的 channel 會被選擇。 358 | * 如果有多個 channel 可用,會隨機挑選一個。 359 | * 如果沒有 channel 可用,`default` 的部分會被執行。 360 | * 如果沒有 `default`,`select` 會阻塞。 361 | 362 | 最後,在 `for` 迴圈中使用 `select` 是常見的: 363 | 364 | ```go 365 | for { 366 | select { 367 | case data := <-c: 368 | fmt.Printf("worker %d got %d\n", w.id, data) 369 | case <-time.After(time.Millisecond * 10): 370 | fmt.Println("Break time") 371 | time.Sleep(time.Second) 372 | } 373 | } 374 | ``` 375 | 376 | ## 在你繼續學習之前 377 | 378 | 如果你才剛剛進入並行程式語言的世界,你可能會覺得它是銳不可擋的。的確,他需要你更多的關注,畢竟 Go 語言的目標就是為了讓並行程式更容易撰寫。 379 | 380 | Goroutine 有效的抽象化了並行的程式碼。Channel 幫助我們在資料需要共享時所會產生的一些嚴重的 bug。其實不僅僅是消除 bug,他還改變了我們如何撰寫我們的並行程式碼。你可以開始思考怎麼透過訊息的傳遞來撰寫並行程式,而不是透過那些容易出錯的程式碼。 381 | 382 | 雖然如此,我依舊廣泛地使用 `sync` 和 `sync/atomic` 套件中的同步機制。我認為去熟悉他們是很重要的。我鼓勵你在學習時,可以先關注 `channel`,但當你需要一些短暫的鎖的範例時,也可以考慮使用 mutex 或讀寫 mutex。 383 | -------------------------------------------------------------------------------- /conclusion.md: -------------------------------------------------------------------------------- 1 | # 結論 2 | 3 | 我最近聽到有人覺得 Go 是一個很無聊的語言。無聊的原因是因為他很容易學、容易寫,更重要的是容易讀。也許我幫了一個倒忙,畢竟我花了三個章節的時間來介紹型別和如何宣告變數。 4 | 5 | 如果你有撰寫靜態程式語言的經驗,最好的情況下就是稍微複習一下就可以了。Go 語言讓指針更容易使用,並且將陣列封裝後產生了 slice,對於經驗豐富的 Java 或 C# 開發者來說,也許這不是什麼優勢。 6 | 7 | 如果你過去是學習動態程式語言,可能會覺得有點不一樣。這是公平的學習過程,不僅僅是各種宣告和初始化的語法,儘管作為 Go 的粉絲,我發現這些所有學習的部分還是有些不太容易的地方。雖然如此,這裡也涉及到一些基本的規則(像是像是你可以使用 `:=` 宣告變數,但只能宣告一次)和基本的概念(像是 `new(X)` 或 `&X{}` 只用來分配記憶體,但是 slice、map 和 channel 的初始化則需要更多初始化,也就是 `make`)。 8 | 9 | 除此之外,Go 語言使用一個簡單但有效率的方法來管理程式碼。介面、基於回傳值的錯誤處理方法、`defer` 用在資源管理,以及用簡單的方式來實作組合。 10 | 11 | 最後但也是最重要的是,Go 語言內建就支持並行化。關於 goroutine 沒有什麼好說的了,他有效率且簡單(使用上很簡單)。這是很好的一個抽象化。Channel 更為複雜一點,我一直認為在使用複雜的封裝前,要先理解最基本的部分。同樣我也認為不透過 channel 來學習並行化程式是有用的。對我來說,Channel 的實現方式並不是一個簡單的抽象,他有一個自己建構的部分。我會這樣說是因為他改變了你在撰寫並行化程式的思考過程。考慮到撰寫並行化程式有多困難來說,這肯定是一件好事。 12 | -------------------------------------------------------------------------------- /deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | # Build the website and copy it to the build folder 6 | make deploy 7 | 8 | # Commit the website and push it 9 | cd _book 10 | 11 | git init 12 | git checkout -b gh-pages 13 | git config user.name "kevingo" 14 | git config user.email "kevingo75@gmail.com" 15 | git add . 16 | git commit -a -m "Auto-deploy by Travis CI" 17 | git push --force --quiet "https://${GH_TOKEN}@github.com/kevingo/the-little-go-book.git" gh-pages:gh-pages -------------------------------------------------------------------------------- /getting-started.md: -------------------------------------------------------------------------------- 1 | # 入門 2 | 3 | 如果你想要小試身手,可以試試看 [Go Playground](https://play.golang.org/),它可以讓你在線上撰寫並執行你的 Go 程式碼而不需要安裝任何東西。[Go Playground](https://play.golang.org/) 也是用來分享程式碼到各大論壇,比如 [StackOverflow](http://stackoverflow.com/) 最熱門的方式。 4 | 5 | 安裝 Go 是直覺的。你可以從來源安裝它,但我建議你安裝預先編譯好的執行檔。當你到 Go 的官方下載頁面,你會看到不同平台的安裝檔,讓我們省略這些步驟,你會發現其實並不困難。 6 | 7 | 撇除那些簡單的範例,Go 程式在運作時主要會被放置在一個工作目錄中。這個工作目錄包含了 `bin`、`pkg` 和 `src` 等子目錄。你可能會強迫 Go 去滿足你自已的配置風格 - 千萬別這樣做。 8 | 9 | 一般來說,我會將自己的專案放在 `~/code` 目錄下。例如,我的 blog 放在 `~/code/blog`。對於 Go 而言,我的工作目錄是放在 `~/code/go`,同時,Go 的 blog 專案則是放在 `~/code/go/src/blog`。 10 | 11 | 總結來說,建立一個 Go 的工作目錄,並且將你的任何專案放置在這個目錄下的 src 子目錄中。 12 | 13 | ## OSX / Linux 14 | 15 | 下載 `tar.gz` 壓縮檔。對於 OSX 來說,你可能會下載 `go#.#.#.darwin-amd64-osx10.8.tar.gz` 這樣的檔案,而 `#.#.#` 則是 Go 的最新版。 16 | 17 | 透過 `tar -C /usr/local -xzf go#.#.#.darwin-amd64-osx10.8.tar.gz` 指令將檔案解壓縮到 `/usr/local`。 18 | 19 | 設定兩個環境變數: 20 | 21 | GOPATH 這個環境變數指定到你的工作目錄,例如對我來說,就會是:`$HOME/code/go`。接著,我們需要附加 Go 的執行檔到 `PATH` 中,試著執行以下指令: 22 | 23 | ```sh 24 | echo 'export GOPATH=$HOME/code/go' >> $HOME/.profile 25 | echo 'export PATH=$PATH:/usr/local/go/bin' >> $HOME/.profile 26 | ``` 27 | 28 | 你會需要啟用這些變數,可以嘗試重新開啟你的 shell,或是用 `source` 指令重新引入你的 profile 檔案。 29 | 30 | 如果一切順利,試著在 shell 中輸入 `go version`,你應該會看到類似以下的輸出: `o version go1.3.3 darwin/amd64` 31 | 32 | ## Windows 33 | 34 | 下載最新的 zip 壓縮檔。如果你是 x64 的系統,下載 `go#.#.#.windows-amd64.zip`,你應該會看到類似以下的輸出:`#.#.#` 是最新的 Go 版本。 35 | 36 | 解壓縮檔案到你像要的任何地方,`c:\Go` 是個好選擇。 37 | 38 | 設定兩個環境變數: 39 | 1. GOPATH 這個環境變數指定到你的工作目錄,那可能是 `c:\users\goku\work\go`。 40 | 2. 增加 `c:\Go\bin` 到你的 PATH 變數中。環境變數的設定可以在系統控制選單中的進階選項中找到。 41 | 42 | 環境變數可以透過`系統`控制台中的`進階`的`環境變數`按鈕來做設定。有些版本的 Windows 透過系統控制台的`進階系統設定`選項來做設定。 43 | 44 | 如果一切順利,試著在命令提示字元中輸入 `go version`,你應該會看到類似以下的輸出: `go version go1.3.3 darwin/amd64` 45 | -------------------------------------------------------------------------------- /introduction.md: -------------------------------------------------------------------------------- 1 | # 簡介 2 | 3 | 每當我學習新語言時,我總是感到愛恨交加。一方面來說,程式語言對於我們的工作是如此重要,即使一個微小的改變都會造成巨大的影響。當某個突然頓悟的時刻來臨時,總是會對你的程式產生長遠的影響,並且重新定義你對其他語言的期待。另一方面,語言的設計是持續的過程,學習新的關鍵字、型別系統、程式撰寫風格、新的函式庫、新的社群和範例。和其他的事物相比,學習新的語言總是讓我們感覺投資報酬率不高。 4 | 5 | 換句話說,我們必須採取積極的態度來面對。我們需要透過漸進的步驟來學習,畢竟,語言是一切的基礎。即使這些改變經常是持續的過程,他代表了這影響了生產力、可讀性、效能、測試、相依管理、錯誤處理、文件、效能分析、社群、標準函式庫等。難道我們可以說千刀萬剮是死刑的積極方法嗎? 6 | 7 | 因此,我們產生一個重要的問題:為什麼要學 Go?對我而言,有兩個重要的理由。第一,Go 是一個具有相對簡單標準函式庫的語言。從很多方面來看,Go 嘗試去簡化在過去幾十年中語言不斷進化所產生的複雜度。第二,對於許多開發者而言,Go 會補齊你現有的武器庫。 8 | 9 | Go 被設計成一種系統程式語言(例如:作業系統、驅動程式),因此,他是針對 C/C++ 的開發者所設計。但根據 Go 的核心開發團隊,同時我也可以很肯定地說,應用程式開發者已經變成 Go 語言主要的使用者,而並非系統開發者。為什麼?我無法代表系統開發者來發表太多意見,但對於想要建構網站、服務、桌面應用程式等應用的開發者來說,Go 滿足了這一類介於底層應用與高階應用之間的需求。 10 | 11 | 也許這一類的需求是訊息傳遞、快取、計算量龐大的資料分析、命令列應用、日誌或監控。我不知道該給 Go 什麼樣的標籤,但是在我的職業生涯中,隨著系統越來越複雜,同時會有成千上萬種並行處理的方式,很顯然的,客製化基礎平台的需求越來越多。你可以使用 Ruby 或 Python 來建置(許多人也這麼做),但是這些系統將受惠於更嚴格的型別系統和更好的效能。同樣的,你也可以用 Go 來建置網站(也有許多人這樣做),但話說回來,我更喜歡用 Node 或 Ruby 來做這些事情。 12 | 13 | 還有許多其他的領域是 Go 擅長的,例如,Go 編譯過後的程式不會有任何的相依性,你不需要擔心使用者安裝 Ruby 或 JVM,甚至是什麼版本。也基於這個因素,Go 在命令列程式的應用和其他工具類的應用(例如:日誌搜集)上越來越熱門。 14 | 15 | 坦白說,學習 Go 語言讓你的時間變得更有效率。你不需要花費大量的時間或者嘗試掌握 Go,在這個過程中,你會透過付出得到許多實用的東西。 16 | 17 | ## 作者註解 18 | 19 | 基於幾個理由,我很猶豫是否要寫下這本書。第一,Go 官方的文件已經很完整了,特別是 Effective Go 這份檔案。 20 | 21 | 另外一個理由是在寫語言類的書籍我會感到不安。當我寫 Little MongoDB 這本書時,我可以很安心的假設大多數的讀者了解關聯式資料庫和模型等相關的基礎。當我寫 Little Redis 這本書時,我也可以假設讀者瞭解 key value 的儲存方式。 22 | 23 | 我想想這些章節和段落,可能無法做這些假設。你花了許多時間學習 interface,這是一個新的概念。最後,考量到這本書帶給你的價值,如果你讓我知道哪些章節太詳細或太粗淺的話,我會感到相當開心。 24 | --------------------------------------------------------------------------------