Merge pull request #1111 from brchiu/correct-zh-tw-translation-chapter-2

Editorial changes for chapter 2 (zh-tw)
This commit is contained in:
astaxie
2019-09-18 23:55:40 +08:00
committed by GitHub
8 changed files with 208 additions and 209 deletions

View File

@@ -4,7 +4,7 @@
## 程式
這就像一個傳統,在學習大部分語言之前,你先學會如何編寫一個可以輸出`hello world`的程式。
這就像一個傳統,在學習大部分語言之前,你先學會如何編寫一個可以輸出 `hello world` 的程式。
準備好了嗎Let's Go!
@@ -22,30 +22,30 @@ func main() {
Hello, world or 你好,世界 or καλημ ́ρα κóσμ or こんにちはせかい
## 詳解
首先我們要了解一個概念Go 程式是透過 `package` 來組織的
首先我們要了解一個概念Go 程式是透過 `package` 來組織的
`package <pkgName>`(在我們的例子中是`package main`)這一行告訴我們當前檔案屬於哪個套件,而套件名 `main` 則告訴我們它是一個可獨立執行的套件,它在編譯後會產生可執行檔案。除了 `main` 套件之外,其它的套件最後都會產生`*.a`檔案(也就是套件檔案)並放置在`$GOPATH/pkg/$GOOS_$GOARCH`中(以 Mac 為例就是`$GOPATH/pkg/darwin_amd64`)。
`package <pkgName>`(在我們的例子中是`package main`)這一行告訴我們當前檔案屬於哪個套件,而套件名 `main` 則告訴我們它是一個可獨立執行的套件,它在編譯後會產生可執行檔案。除了 `main` 套件之外,其它的套件最後都會產生 `*.a` 檔案(也就是套件檔案)並放置在 `$GOPATH/pkg/$GOOS_$GOARCH` 中(以 Mac 為例就是 `$GOPATH/pkg/darwin_amd64`)。
>每一個可獨立執行的 Go 程式,必定包含一個`package main`,在這個 `main` 套件中必定包含一個入口函式`main`,而這個函式既沒有參數,也沒有回傳值。
>每一個可獨立執行的 Go 程式,必定包含一個 `package main`,在這個 `main` 套件中必定包含一個入口函式 `main`,而這個函式既沒有參數,也沒有回傳值。
為了列印`Hello, world...`,我們呼叫了一個函式`Printf`,這個函式來自於 `fmt` 套件,所以我們在第三行中匯入了系統級別的 `fmt` 套件:`import "fmt"`
為了列印 `Hello, world...` ,我們呼叫了一個函式 `Printf`,這個函式來自於 `fmt` 套件,所以我們在第三行中匯入了系統級別的 `fmt` 套件:`import "fmt"`
套件的概念和 Python 中的 package 類似,它們都有一些特別的好處:模組化(能夠把你的程式分成多個模組)和可重用性(每個模組都能被其它應用程式反覆使用)。我們在這裡只是先了解一下套件的概念,後面我們將會編寫自己的套件。
在第五行中,我們透過關鍵字 `func` 定義了一個 `main` 函式,函式體被放在`{}`(大括號)中,就像我們平時寫 C、C++或 Java 時一樣。
在第五行中,我們透過關鍵字 `func` 定義了一個 `main` 函式,函式體被放在 `{}`(大括號)中,就像我們平時寫 C、C++ 或 Java 時一樣。
大家可以看到 `main` 函式是沒有任何的參數的,我們接下來就學習如何編寫帶參數的、回傳 0 個或多個值的函式。
第六行,我們呼叫了 `fmt` 套件裡面定義的函式`Printf`。大家可以看到,這個函式是透過`<pkgName>.<funcName>`的方式呼叫的,這一點和 Python 十分相似。
第六行,我們呼叫了 `fmt` 套件裡面定義的函式 `Printf`。大家可以看到,這個函式是透過 `<pkgName>.<funcName>` 的方式呼叫的,這一點和 Python 十分相似。
>前面提到過,套件名和套件所在的資料夾名可以是不同的,此處的 `<pkgName>` 即為透過`package <pkgName>`宣告的套件名,而非資料夾名。
>前面提到過,套件名和套件所在的資料夾名可以是不同的,此處的 `<pkgName>` 即為透過 `package <pkgName>` 宣告的套件名,而非資料夾名。
最後大家可以看到我們輸出的內容裡面包含了很多非 ASCII 碼字元。實際上Go 是天生支援 UTF-8 的,任何字元都可以直接輸出,你甚至可以用 UTF-8 中的任何字元作為識別符號。
## 結論
Go 使用`package`(和 Python 的模組類似)來組織程式碼。`main.main()`函式(這個函式位於主套件是每一個獨立的可執行程式的入口點。Go 使用 UTF-8 字串和識別符號(因為 UTF-8 的發明者也就是 Go 的發明者之一),所以它天生支援多語言。
Go 使用 `package`(和 Python 的模組類似)來組織程式碼。`main.main()` 函式這個函式位於主套件是每一個獨立的可執行程式的入口點。Go 使用 UTF-8 字串和識別符號因為 UTF-8 的發明者也就是 Go 的發明者之一,所以它天生支援多語言。
## links
* [目錄](<preface.md>)

View File

@@ -9,29 +9,29 @@ Go 語言裡面定義變數有多種方式。
使用 `var` 關鍵字是 Go 最基本的定義變數方式,與 C 語言不同的是 Go 把變數型別放在變數名後面:
```Go
//定義一個名稱為“variableName”型別為"type"的變數
// 定義一個名稱為 “variableName”型別為 "type" 的變數
var variableName type
```
定義多個變數
```Go
//定義三個型別都是“type”的變數
// 定義三個型別都是 “type” 的變數
var vname1, vname2, vname3 type
```
定義變數並初始化值
```Go
//初始化“variableName”的變數為“value”值型別是“type”
// 初始化 “variableName” 的變數為 “value” 值,型別是 “type”
var variableName type = value
```
同時初始化多個變數
```Go
/*
定義三個型別都是"type"的變數,並且分別初始化為相應的值
定義三個型別都是 "type" 的變數,並且分別初始化為相應的值
vname1 為 v1vname2 為 v2vname3 為 v3
*/
var vname1, vname2, vname3 type= v1, v2, v3
var vname1, vname2, vname3 type = v1, v2, v3
```
你是不是覺得上面這樣的定義有點繁瑣?沒關係,因為 Go 語言的設計者也發現了,有一種寫法可以讓它變得簡單一點。我們可以直接忽略型別宣告,那麼上面的程式碼變成這樣了:
@@ -53,9 +53,9 @@ var vname1, vname2, vname3 = v1, v2, v3
*/
vname1, vname2, vname3 := v1, v2, v3
```
現在是不是看上去非常簡潔了?`:=`這個符號直接取代了 `var``type`,這種形式叫做簡短宣告。不過它有一個限制,那就是它只能用在函式內部;在函式外部使用則會無法編譯透過,所以一般用 `var` 方式來定義全域性變數。
現在是不是看上去非常簡潔了?`:=` 這個符號直接取代了 `var` `type`,這種形式叫做簡短宣告。不過它有一個限制,那就是它只能用在函式內部;在函式外部使用則會無法編譯透過,所以一般用 `var` 方式來定義全域性變數。
`_`(下劃線)是個特殊的變數名,任何賦予它的值都會被丟棄。在這個例子中,我們將值 `35` 賦予`b`,並同時丟棄`34`
`_`(下劃線)是個特殊的變數名,任何賦予它的值都會被丟棄。在這個例子中,我們將值 `35` 賦予 `b`,並同時丟棄 `34`
_, b := 34, 35
@@ -87,17 +87,17 @@ const i = 10000
const MaxThread = 10
const prefix = "astaxie_"
```
Go 常數和一般程式語言不同的是,可以指定相當多的小數位數(例如 200 位)
若指定給 float32 自動縮短為 32bit指定給 float64 自動縮短為 64bit詳情參考 [ 連結](http://golang.org/ref/spec#Constants)
Go 常數和一般程式語言不同的是,可以指定相當多的小數位數例如 200 位
若指定給 float32 自動縮短為 32bit指定給 float64 自動縮短為 64bit詳情參考 [連結](http://golang.org/ref/spec#Constants)
## 內建基礎型別
### Boolean
在 Go 中,布林值的型別為`bool`,值是 `true``false`,預設為`false`
在 Go 中,布林值的型別為 `bool`,值是 `true` `false`,預設為 `false`
```Go
//範例程式碼
// 範例程式碼
var isActive bool // 全域性變數宣告
var enabled, disabled = true, false // 忽略型別的宣告
func test() {
@@ -109,7 +109,7 @@ func test() {
### 數值型別
整數型別有無符號和帶符號兩種。Go 同時支援 `int``uint`這兩種型別的長度相同但具體長度取決於不同編譯器的實現。Go 裡面也有直接定義好位數的型別:`rune`, `int8`, `int16`, `int32`, `int64``byte`, `uint8`, `uint16`, `uint32`, `uint64`。其中 `rune``int32`的別稱,`byte``uint8` 的別稱。
整數型別有無符號和帶符號兩種。Go 同時支援 `int` `uint`這兩種型別的長度相同但具體長度取決於不同編譯器的實現。Go 裡面也有直接定義好位數的型別:`rune`, `int8`, `int16`, `int32`, `int64``byte`, `uint8`, `uint16`, `uint32`, `uint64`。其中 `rune` `int32` 的別稱,`byte``uint8` 的別稱。
>需要注意的一點是,這些型別的變數之間不允許互相賦值或操作,不然會在編譯時引起編譯器報錯。
>
@@ -123,22 +123,22 @@ func test() {
>
>另外,儘管 int 的長度是 32 bit, 但 int 與 int32 並不可以互用。
浮點數的型別有 `float32``float64`兩種(沒有 `float` 型別),預設是`float64`
浮點數的型別有 `float32` `float64` 兩種(沒有 `float` 型別),預設是 `float64`
這就是全部嗎NoGo 還支援複數。它的預設型別是`complex128`64 位實數+64 位虛數)。如果需要小一些的,也有`complex64`(32 位實數+32 位虛數)。複數的形式為`RE + IMi`,其中 `RE` 是實數部分,`IM`是虛數部分,而最後的 `i` 是虛數單位。下面是一個使用複數的例子:
這就是全部嗎NoGo 還支援複數。它的預設型別是 `complex128`64 位實數 + 64 位虛數)。如果需要小一些的,也有 `complex64`(32 位實數+32 位虛數)。複數的形式為 `RE + IMi`,其中 `RE` 是實數部分,`IM` 是虛數部分,而最後的 `i` 是虛數單位。下面是一個使用複數的例子:
```Go
var c complex64 = 5+5i
//output: (5+5i)
// output: (5+5i)
fmt.Printf("Value is: %v", c)
```
### 字串
我們在上一節中講過Go 中的字串都是採用`UTF-8`字符集編碼。字串是用一對雙引號(`""`)或反引號(`` ` `` `` ` ``)括起來定義,它的型別是`string`。
我們在上一節中講過Go 中的字串都是採用 `UTF-8` 字符集編碼。字串是用一對雙引號(`""`)或反引號(`` ` `` `` ` ``)括起來定義,它的型別是 `string`。
```Go
//範例程式碼
// 範例程式碼
var frenchHello string // 宣告變數為字串的一般方法
var emptyString string = "" // 宣告了一個字串變數,初始化為空字串
func test() {
@@ -164,7 +164,7 @@ s2 := string(c) // 再轉換回 string 型別
fmt.Printf("%s\n", s2)
```
Go 中可以使用`+`運算子來連線兩個字串:
Go 中可以使用 `+` 運算子來連線兩個字串:
```Go
s := "hello,"
@@ -176,7 +176,7 @@ fmt.Printf("%s\n", a)
```Go
s := "hello"
s = "c" + s[1:] // 字串雖不能更改,但可進行切片(slice)操作
s = "c" + s[1:] // 字串雖不能更改,但可進行切片 (slice) 操作
fmt.Printf("%s\n", s)
```
如果要宣告一個多行的字串怎麼辦?可以透過`` ` ``來宣告:
@@ -261,7 +261,7 @@ const (
x = iota // x == 0
y = iota // y == 1
z = iota // z == 2
w // 常數宣告省略值時,預設和之前一個值的字面相同。這裡隱含的說 w = iota因此 w == 3。其實上面 y 和 z 可同樣不用"= iota"
w // 常數宣告省略值時,預設和之前一個值的字面相同。這裡隱含的說 w = iota因此 w == 3。其實上面 y 和 z 可同樣不用 "= iota"
)
const v = iota // 每遇到一個 const 關鍵字iota 就會重置,此時 v == 0
@@ -271,18 +271,18 @@ const (
)
const (
a = iota //a=0
a = iota // a=0
b = "B"
c = iota //c=2
d, e, f = iota, iota, iota //d=3,e=3,f=3
g = iota //g = 4
c = iota // c=2
d, e, f = iota, iota, iota // d=3, e=3, f=3
g = iota // g = 4
)
func main() {
fmt.Println(a, b, c, d, e, f, g, h, i, j, x, y, z, w, v)
}
```
>除非被明確的設定為其它值或`iota`,每個 `const` 分組的第一個常數被預設設定為它的 0 值,第二及後續的常數被預設設定為它前面那個常數的值,如果前面那個常數的值是`iota`,則它也被設定為`iota`。
>除非被明確的設定為其它值或`iota`,每個 `const` 分組的第一個常數被預設設定為它的 0 值,第二及後續的常數被預設設定為它前面那個常數的值,如果前面那個常數的值是 `iota`,則它也被設定為 `iota`。
### Go 程式設計的一些規則
Go 之所以會那麼簡潔,是因為它有一些預設的行為:
@@ -297,7 +297,7 @@ Go 之所以會那麼簡潔,是因為它有一些預設的行為:
```Go
var arr [n]type
```
在 `[n]type` 中,`n`表示陣列的長度,`type`表示儲存元素的型別。對陣列的操作和其它語言類似,都是透過 `[]` 來進行讀取或賦值:
在 `[n]type` 中,`n` 表示陣列的長度,`type` 表示儲存元素的型別。對陣列的操作和其它語言類似,都是透過 `[]` 來進行讀取或賦值:
```Go
var arr [10]int // 宣告了一個 int 型別的陣列
@@ -306,7 +306,7 @@ arr[1] = 13 // 賦值操作
fmt.Printf("The first element is %d\n", arr[0]) // 取得資料,回傳 42
fmt.Printf("The last element is %d\n", arr[9]) //回傳未賦值的最後一個元素,預設回傳 0
```
由於長度也是陣列型別的一部分,因此 `[3]int` 與`[4]int`是不同的型別,陣列也就不能改變長度。陣列之間的賦值是值的賦值,即當把一個陣列作為參數傳入函式的時候,傳入的其實是該陣列的副本,而不是它的指標。如果要使用指標,那麼就需要用到後面介紹的 `slice` 型別了。
由於長度也是陣列型別的一部分,因此 `[3]int` 與 `[4]int` 是不同的型別,陣列也就不能改變長度。陣列之間的賦值是值的賦值,即當把一個陣列作為參數傳入函式的時候,傳入的其實是該陣列的副本,而不是它的指標。如果要使用指標,那麼就需要用到後面介紹的 `slice` 型別了。
陣列可以使用另一種 `:=` 來宣告
@@ -315,7 +315,7 @@ a := [3]int{1, 2, 3} // 宣告了一個長度為 3 的 int 陣列
b := [10]int{1, 2, 3} // 宣告了一個長度為 10 的 int 陣列,其中前三個元素初始化為 1、2、3其它預設為 0
c := [...]int{4, 5, 6} // 可以省略長度而採用`...`的方式Go 會自動根據元素個數來計算長度
c := [...]int{4, 5, 6} // 可以省略長度而採用 `...` 的方式Go 會自動根據元素個數來計算長度
```
也許你會說我想陣列裡面的值還是陣列能實現嗎當然囉Go 支援巢狀陣列,即多維陣列。比如下面的程式碼就宣告了一個二維陣列:
@@ -335,20 +335,20 @@ easyArray := [2][4]int{{1, 2, 3, 4}, {5, 6, 7, 8}}
### slice
在很多應用場景中,陣列並不能滿足我們的需求。在初始定義陣列時,我們並不知道需要多大的陣列,因此我們就需要“動態陣列”。在 Go 裡面這種資料結構叫`slice`
在很多應用場景中,陣列並不能滿足我們的需求。在初始定義陣列時,我們並不知道需要多大的陣列,因此我們就需要 “動態陣列” 。在 Go 裡面這種資料結構叫 `slice`
`slice`並不是真正意義上的動態陣列,而是一個參考型別。`slice`總是指向一個底層`array``slice`的宣告也可以像 `array` 一樣,只是不需要長度。
`slice` 並不是真正意義上的動態陣列,而是一個參考型別。`slice` 總是指向一個底層 `array``slice` 的宣告也可以像 `array` 一樣,只是不需要長度。
```Go
// 和宣告 array 一樣,只是少了長度
var fslice []int
```
接下來我們可以宣告一個`slice`,並初始化資料,如下所示:
接下來我們可以宣告一個 `slice`,並初始化資料,如下所示:
```Go
slice := []byte {'a', 'b', 'c', 'd'}
```
`slice`可以從一個陣列或一個已經存在的 `slice` 中再次宣告。`slice`透過 `array[i:j]` 來取得,其中 `i` 是陣列的開始位置,`j`是結束位置,但不包含`array[j]`,它的長度是`j-i`。
`slice` 可以從一個陣列或一個已經存在的 `slice` 中再次宣告。`slice` 透過 `array[i:j]` 來取得,其中 `i` 是陣列的開始位置,`j` 是結束位置,但不包含 `array[j]`,它的長度是 `j-i`。
```Go
// 宣告一個含有 10 個元素元素型別為 byte 的陣列
@@ -365,7 +365,7 @@ a = ar[2:5]
b = ar[3:5]
// b 的元素是ar[3]和 ar[4]
```
>注意 `slice` 和陣列在宣告時的區別:宣告陣列時,方括號內寫明了陣列的長度或使用`...`自動計算長度,而宣告 `slice` 時,方括號內沒有任何字元。
>注意 `slice` 和陣列在宣告時的區別:宣告陣列時,方括號內寫明了陣列的長度或使用 `...` 自動計算長度,而宣告 `slice` 時,方括號內沒有任何字元。
它們的資料結構如下所示
@@ -375,9 +375,9 @@ b = ar[3:5]
slice 有一些簡便的操作
- `slice`的預設開始位置是 0`ar[:n]`等價於`ar[0:n]`
- `slice`的第二個序列預設是陣列的長度,`ar[n:]`等價於`ar[n:len(ar)]`
- 如果從一個陣列裡面直接取得`slice`,可以這樣`ar[:]`,因為預設第一個序列是 0第二個是陣列的長度即等價於`ar[0:len(ar)]`
- `slice` 的預設開始位置是 0`ar[:n]` 等價於 `ar[0:n]`
- `slice` 的第二個序列預設是陣列的長度,`ar[n:]` 等價於 `ar[n:len(ar)]`
- 如果從一個陣列裡面直接取得 `slice`,可以這樣 `ar[:]`,因為預設第一個序列是 0第二個是陣列的長度即等價於 `ar[0:len(ar)]`
下面這個例子展示了更多關於 `slice` 的操作:
@@ -399,7 +399,7 @@ bSlice = aSlice[:3] // bSlice 包含 aSlice[0], aSlice[1], aSlice[2] 也就是
bSlice = aSlice[0:5] // 對 slice 的 slice 可以在 cap 範圍內擴充套件,此時 bSlice 包含d,e,f,g,h
bSlice = aSlice[:] // bSlice 包含所有 aSlice 的元素: d,e,f,g
```
`slice`是參考型別,所以當參考改變其中元素的值時,其它的所有參考都會改變該值,例如上面的 `aSlice` 和`bSlice`,如果修改了 `aSlice` 中元素的值,那麼 `bSlice` 相對應的值也會改變。
`slice` 是參考型別,所以當參考改變其中元素的值時,其它的所有參考都會改變該值,例如上面的 `aSlice` 和 `bSlice`,如果修改了 `aSlice` 中元素的值,那麼 `bSlice` 相對應的值也會改變。
從概念上面來說 `slice` 像一個結構體,這個結構體包含了三個元素:
- 一個指標,指向陣列中 `slice` 指定的開始位置
@@ -423,8 +423,8 @@ bSlice = aSlice[:] // bSlice 包含所有 aSlice 的元素: d,e,f,g
- `append` 向 `slice` 裡面追加一個或者多個元素,然後回傳一個和 `slice` 一樣型別的`slice`
- `copy` 函式 `copy` 從源 `slice` 的`src`中複製元素到目標`dst`,並且回傳複製的元素的個數
注:`append`函式會改變 `slice` 所參考的陣列的內容,從而影響到參考同一陣列的其它`slice`。
但當 `slice` 中沒有剩餘空間(即`(cap-len) == 0`)時,此時將動態分配新的陣列空間。回傳的 `slice` 陣列指標將指向這個空間,而原陣列的內容將保持不變;其它參考此陣列的 `slice` 則不受影響。
注:`append` 函式會改變 `slice` 所參考的陣列的內容,從而影響到參考同一陣列的其它 `slice`。
但當 `slice` 中沒有剩餘空間(即 `(cap-len) == 0`)時,此時將動態分配新的陣列空間。回傳的 `slice` 陣列指標將指向這個空間,而原陣列的內容將保持不變;其它參考此陣列的 `slice` 則不受影響。
從 Go1.2 開始 slice 支援了三個參數的 slice之前我們一直採用這種方式在 slice 或者 array 基礎上來取得一個 slice
@@ -436,15 +436,15 @@ slice := array[2:4]
slice = array[2:4:7]
上面這個的容量就是`7-2`,即 5。這樣這個產生的新的 slice 就沒辦法訪問最後的三個元素。
上面這個的容量就是 `7-2`,即 5。這樣這個產生的新的 slice 就沒辦法訪問最後的三個元素。
如果 slice 是這樣的形式`array[:i:j]`,即第一個參數為空,預設值就是 0。
如果 slice 是這樣的形式 `array[:i:j]`,即第一個參數為空,預設值就是 0。
### map
`map`也就是 Python 中字典的概念,它的格式為`map[keyType]valueType`
`map` 也就是 Python 中字典的概念,它的格式為 `map[keyType]valueType`
我們看下面的程式碼,`map`的讀取和設定也類似 `slice` 一樣,透過 `key` 來操作,只是 `slice` 的`index`只能是int型別,而 `map` 多了很多型別,可以是`int`,可以是 `string` 及所有完全定義了 `==` 與`!=`操作的型別。
我們看下面的程式碼,`map` 的讀取和設定也類似 `slice` 一樣,透過 `key` 來操作,只是 `slice` 的 `index` 只能是 `int` 型別,而 `map` 多了很多型別,可以是 `int`,可以是 `string` 及所有完全定義了 `==` 與 `!=` 操作的型別。
```Go
// 宣告一個 key 是字串,值為 int 的字典,這種方式的宣告需要在使用之前使用 make 初始化
@@ -459,16 +459,16 @@ fmt.Println("第三個數字是: ", numbers["three"]) // 讀取資料
// 顯示出來如 : 第三個數字是: 3
```
這個 `map` 就像我們平常看到的表格一樣,左邊列是`key`,右邊列是值
這個 `map` 就像我們平常看到的表格一樣,左邊列是 `key`,右邊列是值
使用 map 過程中需要注意的幾點:
- `map`是無序的,每次顯示出來的 `map` 都會不一樣,它不能透過 `index` 取得,而必須透過 `key` 取得
- `map`的長度是不固定的,也就是和 `slice` 一樣,也是一種參考型別
- 內建的 `len` 函式同樣適用於`map`,回傳 `map` 擁有的 `key` 的數量
- `map`的值可以很方便的修改,透過`numbers["one"]=11`可以很容易的把 key 為`one`的字典值改為`11`
- `map`和其他基本型別不同,它不是 thread-safe在多個 go-routine 存取時,必須使用 mutex lock 機制
- `map` 是無序的,每次顯示出來的 `map` 都會不一樣,它不能透過 `index` 取得,而必須透過 `key` 取得
- `map` 的長度是不固定的,也就是和 `slice` 一樣,也是一種參考型別
- 內建的 `len` 函式同樣適用於 `map`,回傳 `map` 擁有的 `key` 的數量
- `map` 的值可以很方便的修改,透過 `numbers["one"]=11` 可以很容易的把 key 為 `one` 的字典值改為 `11`
- `map` 和其他基本型別不同,它不是 thread-safe在多個 go-routine 存取時,必須使用 mutex lock 機制
`map`的初始化可以透過 `key:val` 的方式初始化值,同時 `map` 內建有判斷是否存在 `key` 的方式
`map` 的初始化可以透過 `key:val` 的方式初始化值,同時 `map` 內建有判斷是否存在 `key` 的方式
透過 `delete` 刪除 `map` 的元素:
@@ -486,7 +486,7 @@ if ok {
delete(rating, "C") // 刪除 key 為 C 的元素
```
上面說過了,`map`也是一種參考型別,如果兩個 `map` 同時指向一個底層,那麼一個改變,另一個也相應的改變:
上面說過了,`map` 也是一種參考型別,如果兩個 `map` 同時指向一個底層,那麼一個改變,另一個也相應的改變:
```Go
m := make(map[string]string)
@@ -497,17 +497,17 @@ m1["Hello"] = "Salut" // 現在 m["hello"]的值已經是 Salut 了
### make、new 操作
`make`用於內建型別(`map`、`slice` 和`channel`)的記憶體分配。`new`用於各種型別的記憶體分配。
`make` 用於內建型別(`map`、`slice` 和 `channel`)的記憶體分配。`new` 用於各種型別的記憶體分配。
內建函式 `new` 本質上說跟其它語言中的同名函式功能一樣:`new(T)`分配了零值填充的 `T` 型別的記憶體空間,並且回傳其地址,即一個`*T`型別的值。用 Go 的術語說,它回傳了一個指標,指向新分配的型別 `T` 的零值。有一點非常重要:
內建函式 `new` 本質上說跟其它語言中的同名函式功能一樣:`new(T)` 分配了零值填充的 `T` 型別的記憶體空間,並且回傳其地址,即一個 `*T` 型別的值。用 Go 的術語說,它回傳了一個指標,指向新分配的型別 `T` 的零值。有一點非常重要:
>`new`回傳指標。
>`new` 回傳指標。
內建函式`make(T, args)``new(T)`有著不同的功能make 只能建立`slice`、`map``channel`,並且回傳一個有初始值(非零)的 `T` 型別,而不是`*T`。本質來講,導致這三個型別有所不同的原因是指向資料結構的參考在使用前必須被初始化。例如,一個`slice`,是一個包含指向資料(內部`array`)的指標、長度和容量的三項描述符;在這些專案被初始化之前,`slice``nil`。對於`slice`、`map`和 `channel` 來說,`make`初始化了內部的資料結構,填充適當的值。
內建函式 `make(T, args)``new(T)` 有著不同的功能make 只能建立 `slice`、`map``channel`,並且回傳一個有初始值(非零)的 `T` 型別,而不是 `*T`。本質來講,導致這三個型別有所不同的原因是指向資料結構的參考在使用前必須被初始化。例如,一個 `slice`,是一個包含指向資料(內部 `array`)的指標、長度和容量的三項描述符;在這些專案被初始化之前,`slice``nil`。對於 `slice`、`map` 和 `channel` 來說,`make` 初始化了內部的資料結構,填充適當的值。
>`make`回傳初始化後的(非零)值。
>`make` 回傳初始化後的(非零)值。
下面這個圖詳細的解釋了 `new` 和`make`之間的區別。
下面這個圖詳細的解釋了 `new` 和 `make` 之間的區別。
![](images/2.2.makenew.png)

View File

@@ -3,9 +3,9 @@
## 流程控制
流程控制在程式語言中是最偉大的發明了因為有了它你可以透過很簡單的流程描述來表達很複雜的邏輯。Go 中流程控制分三大類別:條件判斷,迴圈控制和無條件跳轉。
### if
`if`也許是各種程式語言中最常見的了,它的語法概括起來就是:如果滿足條件就做某事,否則做另一件事。
`if` 也許是各種程式語言中最常見的了,它的語法概括起來就是:如果滿足條件就做某事,否則做另一件事。
Go 裡面 `if` 條件判斷語句中不需要括號,如下程式碼所示
Go 裡面 `if` 條件判斷語句中不需要括號,如下程式碼所示
```Go
if x > 10 {
@@ -14,7 +14,7 @@ if x > 10 {
fmt.Println("x is less than 10")
}
```
Go 的 `if` 還有一個強大的地方就是條件判斷語句裡面允許宣告一個變數,這個變數的作用域只能在該條件邏輯區塊內,其他地方就無法使用,如下所示
Go 的 `if` 還有一個強大的地方就是條件判斷語句裡面允許宣告一個變數,這個變數的作用域只能在該條件邏輯區塊內,其他地方就無法使用,如下所示
```Go
// 計算取得值 x然後根據 x 回傳的大小,判斷是否大於 10。
@@ -24,7 +24,7 @@ if x := computedValue(); x > 10 {
fmt.Println("x is less than 10")
}
//這個地方如果這樣呼叫就編譯出錯了,因為 x 是條件裡面的變數
// 這個地方如果這樣呼叫就編譯出錯了,因為 x 是條件裡面的變數
fmt.Println(x)
```
多個條件的時候如下所示:
@@ -51,17 +51,17 @@ Here: //這行的第一個詞,以冒號結束作為標籤
goto Here //跳轉到 Here 去
}
```
>標籤名稱(label)是區分大小寫的的。
>標籤名稱 (label) 是區分大小寫的的。
### for
Go 裡面最強大的一個控制邏輯就是`for`,它既可以用來迴圈讀取資料,又可以當作 `while` 來控制邏輯,還能迭代操作。它的語法如下:
Go 裡面最強大的一個控制邏輯就是 `for`,它既可以用來迴圈讀取資料,又可以當作 `while` 來控制邏輯,還能迭代操作。它的語法如下:
```Go
for expression1; expression2; expression3 {
//...
}
```
`expression1``expression2``expression3` 都是表示式,其中 `expression1``expression3`是變數宣告或者函式呼叫回傳值之類別的,`expression2`是用來條件判斷,`expression1`在迴圈開始之前呼叫,`expression3`在每輪迴圈結束之時呼叫。
`expression1``expression2` `expression3` 都是表示式,其中 `expression1` `expression3` 是變數宣告或者函式呼叫回傳值之類別的,`expression2` 是用來條件判斷,`expression1` 在迴圈開始之前呼叫,`expression3`在每輪迴圈結束之時呼叫。
一個例子比上面講那麼多更有用,那麼我們看看下面的例子吧:
@@ -79,10 +79,9 @@ func main(){
}
// 輸出sum is equal to 45
```
有些時候需要進行多個賦值操作,由於 Go 裡面沒有`,`運算子,那麼可以使用平行賦值`i, j = i+1, j-1`
有些時候需要進行多個賦值操作,由於 Go 裡面沒有 `,` 運算子,那麼可以使用平行賦值 `i, j = i+1, j-1`
有些時候如果我們忽略 `expression1``expression3`
有些時候如果我們忽略 `expression1``expression3`
```Go
sum := 1
@@ -98,7 +97,7 @@ for sum < 1000 {
sum += sum
}
```
在迴圈裡面有兩個關鍵操作 `break``continue` ,`break`操作是跳出當前迴圈,`continue`是跳過本次迴圈。當巢狀過深的時候,`break`可以配合標籤使用,即跳轉至標籤所指定的位置,詳細參考如下例子:
在迴圈裡面有兩個關鍵操作 `break` `continue`, `break` 操作是跳出當前迴圈,`continue` 是跳過本次迴圈。當巢狀過深的時候,`break` 可以配合標籤使用,即跳轉至標籤所指定的位置,詳細參考如下例子:
```Go
for index := 10; index>0; index-- {
@@ -110,9 +109,9 @@ for index := 10; index>0; index-- {
// break 顯示出來 10、9、8、7、6
// continue 顯示出來 10、9、8、7、6、4、3、2、1
```
`break``continue` 還可以跟著標號,用來跳到多重迴圈中的外層迴圈
`break` `continue` 還可以跟著標號,用來跳到多重迴圈中的外層迴圈
`for`配合 `range` 可以用於讀取 `slice``map`的資料:
`for` 配合 `range` 可以用於讀取 `slice``map`的資料:
```Go
for k,v:=range map {
@@ -120,7 +119,7 @@ for k,v:=range map {
fmt.Println("map's val:",v)
}
```
由於 Go 支援 “多值回傳”, 而對於“宣告而未被呼叫”的變數, 編譯器會報錯, 在這種情況下, 可以使用 `_` 來丟棄不需要的回傳值
由於 Go 支援 “多值回傳”, 而對於 “宣告而未被呼叫” 的變數, 編譯器會報錯, 在這種情況下, 可以使用 `_` 來丟棄不需要的回傳值
例如
```Go
@@ -130,7 +129,7 @@ for _, v := range map{
```
### switch
有些時候你需要寫很多的`if-else`來實現一些邏輯處理,這個時候程式碼看上去就很醜很冗長,而且也不易於以後的維護,這個時候 `switch` 就能很好的解決這個問題。它的語法如下
有些時候你需要寫很多的 `if-else` 來實現一些邏輯處理,這個時候程式碼看上去就很醜很冗長,而且也不易於以後的維護,這個時候 `switch` 就能很好的解決這個問題。它的語法如下
```Go
switch sExpr {
@@ -144,7 +143,7 @@ default:
other code
}
```
`sExpr``expr1``expr2``expr3`的型別必須一致。Go 的 `switch` 非常靈活,表示式不必是常數或整數,執行的過程從上至下,直到找到匹配項;而如果 `switch` 沒有表示式,它會匹配`true`
`sExpr``expr1``expr2``expr3` 的型別必須一致。Go 的 `switch` 非常靈活,表示式不必是常數或整數,執行的過程從上至下,直到找到匹配項;而如果 `switch` 沒有表示式,它會匹配 `true`
```Go
i := 10
@@ -159,7 +158,7 @@ default:
fmt.Println("All I know is that i is an integer")
}
```
在第 5 行中,我們把很多值聚合在了一個 `case` 裡面同時Go 裡面 `switch` 預設相當於每個 `case` 最後帶有`break`,匹配成功後不會自動向下執行其他 case而是跳出整個`switch`, 但是可以使用 `fallthrough` 強制執行後面的 case 程式碼。
在第 5 行中,我們把很多值聚合在了一個 `case` 裡面同時Go 裡面 `switch` 預設相當於每個 `case` 最後帶有`break`,匹配成功後不會自動向下執行其他 case而是跳出整個 `switch`, 但是可以使用 `fallthrough` 強制執行後面的 case 程式碼。
```Go
integer := 6
@@ -197,17 +196,17 @@ default case
```Go
func funcName(input1 type1, input2 type2) (output1 type1, output2 type2) {
//這裡是處理邏輯程式碼
//回傳多個值
// 這裡是處理邏輯程式碼
// 回傳多個值
return value1, value2
}
```
上面的程式碼我們看出
- 關鍵字 `func` 用來宣告一個函式`funcName`
- 函式可以有一個或者多個參數,每個參數後面帶有型別,透過`,`分隔
- 關鍵字 `func` 用來宣告一個函式 `funcName`
- 函式可以有一個或者多個參數,每個參數後面帶有型別,透過 `,` 分隔
- 函式可以回傳多個值
- 上面回傳值宣告了兩個變數 `output1``output2`,如果你不想宣告也可以,直接就兩個型別
- 上面回傳值宣告了兩個變數 `output1` `output2`,如果你不想宣告也可以,直接就兩個型別
- 如果只有一個回傳值且不宣告回傳值變數,那麼你可以省略 包括回傳值 的括號
- 如果沒有回傳值,那麼就直接省略最後的回傳資訊
- 如果有回傳值, 那麼必須在函式的外層新增 return 語句
@@ -232,15 +231,15 @@ func main() {
y := 4
z := 5
max_xy := max(x, y) //呼叫函式 max(x, y)
max_xz := max(x, z) //呼叫函式 max(x, z)
max_xy := max(x, y) // 呼叫函式 max(x, y)
max_xz := max(x, z) // 呼叫函式 max(x, z)
fmt.Printf("max(%d, %d) = %d\n", x, y, max_xy)
fmt.Printf("max(%d, %d) = %d\n", x, z, max_xz)
fmt.Printf("max(%d, %d) = %d\n", y, z, max(y,z)) // 也可在這直接呼叫它
}
```
上面這個裡面我們可以看到 `max` 函式有兩個參數,它們的型別都是`int`,那麼第一個變數的型別可以省略(即 a,b int而非 a int, b int),預設為離它最近的型別,同理多於 2 個同類型的變數或者回傳值。同時我們注意到它的回傳值就是一個型別,這個就是省略寫法。
上面這個裡面我們可以看到 `max` 函式有兩個參數,它們的型別都是 `int`,那麼第一個變數的型別可以省略(即 a,b int而非 a int, b int),預設為離它最近的型別,同理多於 2 個同類型的變數或者回傳值。同時我們注意到它的回傳值就是一個型別,這個就是省略寫法。
### 多個回傳值
Go 語言比 C 更先進的特性,其中一點就是函式能夠回傳多個值。
@@ -252,7 +251,7 @@ package main
import "fmt"
//回傳 A+B 和 A*B
// 回傳 A+B 和 A*B
func SumAndProduct(A, B int) (int, int) {
return A+B, A*B
}
@@ -267,7 +266,7 @@ func main() {
fmt.Printf("%d * %d = %d\n", x, y, xTIMESy)
}
```
上面的例子我們可以看到直接回傳了兩個參數,當然我們也可以命名回傳參數的變數,這個例子裡面只是用了兩個型別,我們也可以改成如下這樣的定義,然後回傳的時候不用帶上變數名,因為直接在函式裡面初始化了。但如果你的函式是匯出的(首字母大寫),官方建議:最好命名回傳值,因為不命名回傳值,雖然使得程式碼更加簡潔了,但是會造成產生的文件可讀性差。
上面的例子我們可以看到直接回傳了兩個參數,當然我們也可以命名回傳參數的變數,這個例子裡面只是用了兩個型別,我們也可以改成如下這樣的定義,然後回傳的時候不用帶上變數名,因為直接在函式裡面初始化了。但如果你的函式是匯出的首字母大寫,官方建議:最好命名回傳值,因為不命名回傳值,雖然使得程式碼更加簡潔了,但是會造成產生的文件可讀性差。
```Go
func SumAndProduct(A, B int) (add int, Multiplied int) {
@@ -304,10 +303,10 @@ package main
import "fmt"
//簡單的一個函式,實現了參數+1 的操作
// 簡單的一個函式,實現了參數+1 的操作
func add1(a int) int {
a = a+1 // 我們改變了 a 的值
return a //回傳一個新值
return a // 回傳一個新值
}
func main() {
@@ -321,20 +320,20 @@ func main() {
fmt.Println("x = ", x) // 應該輸出"x = 3"
}
```
看到了嗎?雖然我們呼叫了 `add1` 函式,並且在 `add1` 中執行`a = a+1`操作,但是上面例子中 `x` 變數的值沒有發生變化
看到了嗎?雖然我們呼叫了 `add1` 函式,並且在 `add1` 中執行 `a = a+1` 操作,但是上面例子中 `x` 變數的值沒有發生變化
理由很簡單:因為當我們呼叫 `add1` 的時候,`add1`接收的參數其實是 `x` 的 copy而不是 `x` 本身。
理由很簡單:因為當我們呼叫 `add1` 的時候,`add1` 接收的參數其實是 `x` 的 copy而不是 `x` 本身。
那你也許會問了,如果真的需要傳這個 `x` 本身,該怎麼辦呢?
這就牽扯到了所謂的指標。我們知道,變數在記憶體中是存放於一定地址上的,修改變數實際是修改變數地址處的記憶體。只有 `add1` 函式知道 `x` 變數所在的地址,才能修改 `x` 變數的值。所以我們需要將 `x` 所在地址`&x`傳入函式,並將函式的參數的型別由 `int` 改為`*int`,即改為指標型別,才能在函式中修改 `x` 變數的值。此時參數仍然是按 copy 傳遞的,只是 copy 的是一個指標。請看下面的例子
這就牽扯到了所謂的指標。我們知道,變數在記憶體中是存放於一定地址上的,修改變數實際是修改變數地址處的記憶體。只有 `add1` 函式知道 `x` 變數所在的地址,才能修改 `x` 變數的值。所以我們需要將 `x` 所在地址 `&x` 傳入函式,並將函式的參數的型別由 `int` 改為 `*int`,即改為指標型別,才能在函式中修改 `x` 變數的值。此時參數仍然是按 copy 傳遞的,只是 copy 的是一個指標。請看下面的例子
```Go
package main
import "fmt"
//簡單的一個函式,實現了參數+1 的操作
// 簡單的一個函式,實現了參數+1 的操作
func add1(a *int) int { // 請注意,
*a = *a+1 // 修改了 a 的值
return *a // 回傳新值
@@ -355,7 +354,7 @@ func main() {
- 傳指標使得多個函式能操作同一個物件。
- 傳指標比較輕量級 (8bytes),只是傳記憶體地址,我們可以用指標傳遞體積大的結構體。如果用參數值傳遞的話, 在每次 copy 上面就會花費相對較多的系統開銷(記憶體和時間)。所以當你要傳遞大的結構體的時候,用指標是一個明智的選擇。
- Go 語言中`channel``slice``map`這三種類型的實現機制類似指標,所以可以直接傳遞,而不用取地址後傳遞指標。(注:若函式需改變 `slice` 的長度,則仍需要取地址傳遞指標)
- Go 語言中 `channel``slice``map` 這三種類型的實現機制類似指標,所以可以直接傳遞,而不用取地址後傳遞指標。(注:若函式需改變 `slice` 的長度,則仍需要取地址傳遞指標)
### defer
Go 語言中有種不錯的設計即延遲defer語句你可以在函式中新增多個 defer 語句。當函式執行到最後時,這些 defer 語句會按照逆序執行,最後該函式回傳。特別是當你在進行一些開啟資源的操作時,遇到錯誤需要提前回傳,在回傳前你需要關閉相應的資源,不然很容易造成資源洩露等問題。如下程式碼所示,我們一般寫開啟一個資源是這樣操作的:
@@ -393,7 +392,7 @@ func ReadWrite() bool {
return true
}
```
如果有很多呼叫`defer`,那麼 `defer` 是採用後進先出模式,所以如下程式碼會輸出`4 3 2 1 0`
如果有很多呼叫 `defer`,那麼 `defer` 是採用後進先出模式,所以如下程式碼會輸出 `4 3 2 1 0`
```Go
for i := 0; i < 5; i++ {
@@ -454,15 +453,15 @@ func main(){
### Panic 和 Recover
Go 沒有像 Java 那樣的異常機制,它不能丟擲異常,而是使用了 `panic``recover`機制。一定要記住,你應當把它作為最後的手段來使用,也就是說,你的程式碼中應當沒有,或者很少有 `panic` 的東西。這是個強大的工具,請明智地使用它。那麼,我們應該如何使用它呢?
Go 沒有像 Java 那樣的異常機制,它不能丟擲異常,而是使用了 `panic` `recover` 機制。一定要記住,你應當把它作為最後的手段來使用,也就是說,你的程式碼中應當沒有,或者很少有 `panic` 的東西。這是個強大的工具,請明智地使用它。那麼,我們應該如何使用它呢?
Panic
>是一個內建函式,可以中斷原有的控制流程,進入一個 `panic` 狀態中。當函式 `F` 呼叫`panic`,函式 F 的執行被中斷,但是 `F` 中的延遲函式會正常執行,然後 F 回傳到呼叫它的地方。在呼叫的地方,`F`的行為就像呼叫了`panic`。這一過程繼續向上,直到發生 `panic``goroutine`中所有呼叫的函式回傳,此時程式退出。`panic`可以直接呼叫 `panic` 產生。也可以由執行時錯誤產生,例如訪問越界的陣列。
>是一個內建函式,可以中斷原有的控制流程,進入一個 `panic` 狀態中。當函式 `F` 呼叫 `panic`,函式 `F` 的執行被中斷,但是 `F` 中的延遲函式會正常執行,然後 `F` 回傳到呼叫它的地方。在呼叫的地方,`F` 的行為就像呼叫了 `panic`。這一過程繼續向上,直到發生 `panic` `goroutine` 中所有呼叫的函式回傳,此時程式退出。`panic` 可以直接呼叫 `panic` 產生。也可以由執行時錯誤產生,例如訪問越界的陣列。
Recover
>是一個內建的函式,可以讓進入 `panic` 狀態的 `goroutine` 恢復過來。`recover`僅在延遲函式中有效。在正常的執行過程中,呼叫 `recover` 會回傳`nil`,並且沒有其它任何效果。如果當前的 `goroutine` 陷入 `panic` 狀態,呼叫 `recover` 可以捕獲到 `panic` 的輸入值,並且恢復正常的執行。
>是一個內建的函式,可以讓進入 `panic` 狀態的 `goroutine` 恢復過來。`recover` 僅在延遲函式中有效。在正常的執行過程中,呼叫 `recover` 會回傳 `nil`,並且沒有其它任何效果。如果當前的 `goroutine` 陷入 `panic` 狀態,呼叫 `recover` 可以捕獲到 `panic` 的輸入值,並且恢復正常的執行。
下面這個函式示範了如何在過程中使用`panic`
下面這個函式示範了如何在過程中使用 `panic`
```Go
var user = os.Getenv("USER")
@@ -473,7 +472,7 @@ func init() {
}
}
```
下面這個函式檢查作為其參數的函式在執行時是否會產生`panic`
下面這個函式檢查作為其參數的函式在執行時是否會產生 `panic`
```Go
func throwsPanic(f func()) (b bool) {
@@ -488,9 +487,9 @@ func throwsPanic(f func()) (b bool) {
```
### `main`函式和 `init` 函式
Go 裡面有兩個保留的函式:`init`函式(能夠應用於所有的`package`)和 `main` 函式(只能應用於`package main`)。這兩個函式在定義時不能有任何的參數和回傳值。雖然一個 `package` 裡面可以寫任意多個 `init` 函式,但這無論是對於可讀性還是以後的可維護性來說,我們都強烈建議使用者在一個 `package` 中每個檔案只寫一個 `init` 函式。
Go 裡面有兩個保留的函式:`init` 函式(能夠應用於所有的 `package`)和 `main` 函式(只能應用於 `package main`)。這兩個函式在定義時不能有任何的參數和回傳值。雖然一個 `package` 裡面可以寫任意多個 `init` 函式,但這無論是對於可讀性還是以後的可維護性來說,我們都強烈建議使用者在一個 `package` 中每個檔案只寫一個 `init` 函式。
Go 程式會自動呼叫`init()``main()`,所以你不需要在任何地方呼叫這兩個函式。每個 `package` 中的 `init` 函式都是可選的,但`package main`就必須包含一個 `main` 函式。
Go 程式會自動呼叫 `init()``main()`,所以你不需要在任何地方呼叫這兩個函式。每個 `package` 中的 `init` 函式都是可選的,但 `package main` 就必須包含一個 `main` 函式。
程式的初始化和執行都起始於 `main` 套件。如果 `main` 套件還匯入了其它的套件,那麼就會在編譯時將它們依次匯入。有時一個套件會被多個套件同時匯入,那麼它只會被匯入一次(例如很多套件可能都會用到 `fmt` 套件,但它只會被匯入一次,因為沒有必要匯入多次)。當一個套件被匯入時,如果該套件還匯入了其它的套件,那麼會先將其它套件匯入進來,然後再對這些套件中的 "套件級" (package-level) 常數和變數進行初始化,接著執行 `init` 函式(如果有的話),依次類別推。等所有被匯入的套件都載入完畢了,就會開始對 `main` 套件中的 "套件級" 常數和變數進行初始化,然後執行 `main` 套件中的 `init` 函式(如果存在的話),最後執行 `main` 函式。下圖詳細地解釋了整個執行過程:
@@ -533,7 +532,7 @@ fmt.Println("hello world")
. "fmt"
)
這個點操作的含義就是這個套件匯入之後在你呼叫這個套件的函式時,你可以省略字首的套件名,也就是前面你呼叫的 fmt.Println("hello world")可以省略的寫成 Println("hello world")
這個點操作的含義就是這個套件匯入之後在你呼叫這個套件的函式時,你可以省略字首的套件名,也就是前面你呼叫的 fmt.Println("hello world") 可以省略的寫成 Println("hello world")
2. 別名操作

View File

@@ -1,6 +1,6 @@
# 2.4 struct 型別
## struct
Go 語言中,也和 C 或者其他語言一樣,我們可以宣告新的型別,作為其它型別的屬性或欄位的容器。例如,我們可以建立一個自訂型別 `person` 代表一個人的實體。這個實體擁有屬性:姓名和年齡。這樣的型別我們稱之`struct`。如下程式碼所示:
Go 語言中,也和 C 或者其他語言一樣,我們可以宣告新的型別,作為其它型別的屬性或欄位的容器。例如,我們可以建立一個自訂型別 `person` 代表一個人的實體。這個實體擁有屬性:姓名和年齡。這樣的型別我們稱之 `struct`。如下程式碼所示
```Go
type person struct {
@@ -12,7 +12,7 @@ type person struct {
- 一個 string 型別的欄位 name用來儲存使用者名稱稱這個屬性
- 一個 int 型別的欄位 age用來儲存使用者年齡這個屬性
如何使用 struct 呢?請看下面的程式碼
如何使用 struct 呢?請看下面的程式碼
```Go
type person struct {
@@ -22,25 +22,25 @@ type person struct {
var P person // P 現在就是 person 型別的變量了
P.name = "Astaxie" // 賦值"Astaxie"給 P 的 name 屬性.
P.name = "Astaxie" // 賦值"Astaxie"給 P 的 name 屬性
P.age = 25 // 賦值"25"給變數 P 的 age 屬性
fmt.Printf("The person's name is %s", P.name) // 訪問 P 的 name 屬性.
fmt.Printf("The person's name is %s", P.name) // 訪問 P 的 name 屬性
```
除了上面這種 P 的宣告使用之外,還有另外幾種宣告使用方式:
- 1.按照順序提供初始化值
- 按照順序提供初始化值
P := person{"Tom", 25}
- 2.透過 `field:value` 的方式初始化,這樣可以任意順序
- 透過 `field:value` 的方式初始化,這樣可以任意順序
P := person{age:24, name:"Tom"}
- 3.當然也可以透過 `new` 函式分配一個指標,此處 P 的型別為*person
- 當然也可以透過 `new` 函式分配一個指標,此處 P 的型別為*person
P := new(person)
下面我們看一個完整的使用 struct 的例子
下面我們看一個完整的使用 struct 的例子
```Go
package main
@@ -93,7 +93,7 @@ func main() {
當匿名欄位是一個 struct 的時候,那麼這個 struct 所擁有的全部欄位都被隱含的引入了當前定義的這個 struct。
讓我們來看一個例子,讓上面說的這些更具體化
讓我們來看一個例子,讓上面說的這些更具體化
```Go
package main
@@ -134,7 +134,7 @@ func main() {
fmt.Println("His weight is", mark.weight)
}
```
圖例如下:
圖例如下
![](images/2.4.student_struct.png)
@@ -146,7 +146,7 @@ func main() {
mark.Human = Human{"Marcus", 55, 220}
mark.Human.age -= 1
```
透過匿名訪問和修改欄位相當的有用,但是不僅僅是 struct 欄位哦,所有的內建型別和自訂型別都是可以作為匿名欄位的。請看下面的例子
透過匿名訪問和修改欄位相當的有用,但是不僅僅是 struct 欄位哦,所有的內建型別和自訂型別都是可以作為匿名欄位的。請看下面的例子
```Go
package main
@@ -191,9 +191,9 @@ func main() {
這裡有一個問題:如果 human 裡面有一個欄位叫做 phone而 student 也有一個欄位叫做 phone那麼該怎麼辦呢
Go 裡面很簡單的解決了這個問題,最外層的優先訪問,也就是當你透過`student.phone`訪問的時候,是訪問 student 裡面的欄位,而不是 human 裡面的欄位。
Go 裡面很簡單的解決了這個問題,最外層的優先訪問,也就是當你透過 `student.phone` 訪問的時候,是訪問 student 裡面的欄位,而不是 human 裡面的欄位。
這樣就允許我們去過載透過匿名欄位繼承的一些欄位,當然如果我們想訪問過載後對應匿名型別裡面的欄位,可以透過匿名欄位名來訪問。請看下面的例子
這樣就允許我們去過載透過匿名欄位繼承的一些欄位,當然如果我們想訪問過載後對應匿名型別裡面的欄位,可以透過匿名欄位名來訪問。請看下面的例子
```Go
package main

View File

@@ -1,10 +1,10 @@
# 2.5 物件導向
前面兩章我們介紹了函式和 struct那你是否想過函式當作 struct 的欄位一樣來處理呢?今天我們就講解一下函式的另一種形態,帶有接收者的函式,我們稱為`method`
前面兩章我們介紹了函式和 struct那你是否想過函式當作 struct 的欄位一樣來處理呢?今天我們就講解一下函式的另一種形態,帶有接收者的函式,我們稱為 `method`
## method
現在假設有這麼一個場景,你定義了一個 struct 叫做長方形,你現在想要計算他的面積,那麼按照我們一般的思路應該會用下面的方式來實現
現在假設有這麼一個場景,你定義了一個 struct 叫做長方形,你現在想要計算他的面積,那麼按照我們一般的思路應該會用下面的方式來實現
```Go
package main
@@ -28,9 +28,9 @@ func main() {
```
這段程式碼可以計算出來長方形的面積,但是 area()不是作為 Rectangle 的方法實現的(類似物件導向裡面的方法),而是將 Rectangle 的物件(如 r1,r2作為參數傳入函式計算面積的。
這樣實現當然沒有問題囉,但是當需要增加圓形、正方形、五邊形甚至其它多邊形的時候,你想計算他們的面積的時候怎麼辦啊?那就只能增加新的函式囉,但是函式名你就必須要跟著換了,變成`area_rectangle, area_circle, area_triangle...`
這樣實現當然沒有問題囉,但是當需要增加圓形、正方形、五邊形甚至其它多邊形的時候,你想計算他們的面積的時候怎麼辦啊?那就只能增加新的函式囉,但是函式名你就必須要跟著換了,變成 `area_rectangle, area_circle, area_triangle...`
像下圖所表示的那樣, 橢圓代表函式, 而這些函式並不從屬於 struct(或者以物件導向的術語來說,並不屬於 class),他們是單獨存在於 struct 外圍,而非在概念上屬於某個 struct 的。
像下圖所表示的那樣, 橢圓代表函式, 而這些函式並不從屬於 struct或者以物件導向的術語來說,並不屬於 class,他們是單獨存在於 struct 外圍,而非在概念上屬於某個 struct 的。
![](images/2.5.rect_func_without_receiver.png)
@@ -38,9 +38,9 @@ func main() {
很顯然,這樣的實現並不優雅,並且從概念上來說"面積"是"形狀"的一個屬性,它是屬於這個特定的形狀的,就像長方形的長和寬一樣。
基於上面的原因所以就有了 `method` 的概念,`method`是附屬在一個給定的型別上的,他的語法和函式的宣告語法幾乎一樣,只是在 `func` 後面增加了一個 receiver (也就是 method 所依從的主體)
基於上面的原因所以就有了 `method` 的概念,`method` 是附屬在一個給定的型別上的,他的語法和函式的宣告語法幾乎一樣,只是在 `func` 後面增加了一個 receiver也就是 method 所依從的主體
用上面提到的形狀的例子來說method `area()` 是依賴於某個形狀(比如說 Rectangle)來發生作用的。Rectangle.area()的發出者是 Rectangle area()是屬於 Rectangle 的方法,而非一個外圍函式。
用上面提到的形狀的例子來說method `area()` 是依賴於某個形狀比如說 Rectangle來發生作用的。Rectangle.area()的發出者是 Rectangle area() 是屬於 Rectangle 的方法,而非一個外圍函式。
更具體地說Rectangle 存在欄位 height 和 width, 同時存在方法 area(), 這些欄位和方法都屬於 Rectangle。
@@ -97,15 +97,15 @@ func main() {
- 雖然 method 的名字一模一樣,但是如果接收者不一樣,那麼 method 就不一樣
- method 裡面可以訪問接收者的欄位
- 呼叫 method 透過`.`訪問,就像 struct 裡面訪問欄位一樣
- 呼叫 method 透過 `.` 訪問,就像 struct 裡面訪問欄位一樣
圖示如下:
圖示如下
![](images/2.5.shapes_func_with_receiver_cp.png)
圖 2.9 不同 struct 的 method 不同
在上例method area() 分別屬於 Rectangle 和 Circle 於是他們的 Receiver 就變成了 Rectangle 和 Circle, 或者說,這個 area()方法 是由 Rectangle/Circle 發出的。
在上例method area() 分別屬於 Rectangle 和 Circle於是他們的 Receiver 就變成了 Rectangle 和 Circle或者說,這個 area()方法 是由 Rectangle/Circle 發出的。
>值得說明的一點是,圖示中 method 用虛線標出,意思是此處方法的 Receiver 是以值傳遞而非參考傳遞是的Receiver 還可以是指標, 兩者的差別在於, 指標作為 Receiver 會對實體物件的內容發生操作,而普通型別作為 Receiver 僅僅是以副本作為操作物件,並不對原實體物件發生操作。後文對此會有詳細論述。
@@ -130,11 +130,11 @@ m := months {
"December":31,
}
```
看到了嗎?簡單的很吧,這樣你就可以在自己的程式碼裡面定義有意義的型別了,實際上只是一個定義了一個別名,有點類似於 c 中的 typedef例如上面 ages 替代了 int
看到了嗎?簡單的很吧,這樣你就可以在自己的程式碼裡面定義有意義的型別了,實際上只是一個定義了一個別名,有點類似於 c 中的 typedef例如上面 ages 替代了 int
好了,讓我們回到`method`
好了,讓我們回到 `method`
你可以在任何的自訂型別中定義任意多的`method`,接下來讓我們看一個複雜一點的例子
你可以在任何的自訂型別中定義任意多的 `method`,接下來讓我們看一個複雜一點的例子
```Go
package main
@@ -156,7 +156,7 @@ type Box struct {
color Color
}
type BoxList []Box //a slice of boxes
type BoxList []Box // a slice of boxes
func (b Box) Volume() float64 {
return b.width * b.height * b.depth
@@ -211,49 +211,49 @@ func main() {
fmt.Println("Obviously, now, the biggest one is", boxes.BiggestColor().String())
}
```
上面的程式碼透過 const 定義了一些常數,然後定義了一些自訂型別
上面的程式碼透過 const 定義了一些常數,然後定義了一些自訂型別
- Color 作為 byte 的別名
- 定義了一個 struct:Box含有三個長寬高欄位和一個顏色屬性
- 定義了一個 slice:BoxList含有 Box
然後以上面的自訂型別為接收者定義了一些 method
然後以上面的自訂型別為接收者定義了一些 method
- Volume()定義了接收者為 Box回傳 Box 的容量
- Volume() 定義了接收者為 Box回傳 Box 的容量
- SetColor(c Color),把 Box 的顏色改為 c
- BiggestColor()定在在 BoxList 上面,回傳 list 裡面容量最大的顏色
- PaintItBlack()把 BoxList 裡面所有 Box 的顏色全部變成黑色
- String()定義在 Color 上面,回傳 Color 的具體顏色(字串格式)
- BiggestColor() 定在在 BoxList 上面,回傳 list 裡面容量最大的顏色
- PaintItBlack() 把 BoxList 裡面所有 Box 的顏色全部變成黑色
- String() 定義在 Color 上面,回傳 Color 的具體顏色字串格式
上面的程式碼透過文字描述出來之後是不是很簡單?我們一般解決問題都是透過問題的描述,去寫相應的程式碼實現。
### 指標作為 receiver
現在讓我們回過頭來看看 SetColor 這個 method它的 receiver 是一個指向 Box 的指標,是的,你可以使用*Box。想想為啥要使用指標而不是 Box 本身呢?
現在讓我們回過頭來看看 SetColor 這個 method它的 receiver 是一個指向 Box 的指標,是的,你可以使用 *Box。想想為啥要使用指標而不是 Box 本身呢?
我們定義 SetColor 的真正目的是想改變這個 Box 的顏色,如果不傳 Box 的指標,那麼 SetColor 接受的其實是 Box 的一個 copy也就是說 method 內對於顏色值的修改,其實只作用於 Box 的 copy而不是真正的 Box。所以我們需要傳入指標。
這裡可以把 receiver 當作 method 的第一個參數來看,然後結合前面函式講解的傳值和傳參考就不難理解
這裡你也許會問了那 SetColor 函式裡面應該這樣定義`*b.Color=c`,而不是`b.Color=c`,因為我們需要讀取到指標相應的值。
這裡你也許會問了那 SetColor 函式裡面應該這樣定義 `*b.Color=c`,而不是 `b.Color=c`,因為我們需要讀取到指標相應的值。
你是對的,其實 Go 裡面這兩種方式都是正確的,當你用指標去訪問相應的欄位時(雖然指標沒有任何的欄位)Go 知道你要透過指標去取得這個值看到了吧Go 的設計是不是越來越吸引你了。
你是對的,其實 Go 裡面這兩種方式都是正確的,當你用指標去訪問相應的欄位時雖然指標沒有任何的欄位Go 知道你要透過指標去取得這個值看到了吧Go 的設計是不是越來越吸引你了。
也許細心的讀者會問這樣的問題PaintItBlack 裡面呼叫 SetColor 的時候是不是應該寫成`(&bl[i]).SetColor(BLACK)`,因為 SetColor 的 receiver 是*Box而不是 Box。
也許細心的讀者會問這樣的問題PaintItBlack 裡面呼叫 SetColor 的時候是不是應該寫成 `(&bl[i]).SetColor(BLACK)`,因為 SetColor 的 receiver 是 *Box而不是 Box。
你又說對了,這兩種方式都可以,因為 Go 知道 receiver 是指標,他自動幫你轉了。
也就是說:
>如果一個 method 的 receiver 是 *T你可以在一個 T 型別的變數 V 上面呼叫這個 method而不需要 &V 去呼叫這個 method
>如果一個 method 的 receiver 是 *T你可以在一個 T 型別的變數 V 上面呼叫這個 method而不需要 &V 去呼叫這個 method
類似的
>如果一個 method 的 receiver 是 T你可以在一個*T 型別的變數 P 上面呼叫這個 method而不需要 *P 去呼叫這個 method
>如果一個 method 的 receiver 是 T你可以在一個 *T 型別的變數 P 上面呼叫這個 method而不需要 *P 去呼叫這個 method
所以,你不用擔心你是呼叫的指標的 method 還是不是指標的 methodGo 知道你要做的一切,這對於有多年 C/C++程式設計經驗的同學來說,真是解決了一個很大的痛苦。
### method 繼承
前面一章我們學習了欄位的繼承,那麼你也會發現 Go 的一個神奇之處method 也是可以繼承的。如果匿名欄位實現了一個 method那麼包含這個匿名欄位的 struct 也能呼叫該 method。讓我們來看下面這個例子
前面一章我們學習了欄位的繼承,那麼你也會發現 Go 的一個神奇之處method 也是可以繼承的。如果匿名欄位實現了一個 method那麼包含這個匿名欄位的 struct 也能呼叫該 method。讓我們來看下面這個例子
```Go
package main
@@ -267,16 +267,16 @@ type Human struct {
}
type Student struct {
Human //匿名欄位
Human // 匿名欄位
school string
}
type Employee struct {
Human //匿名欄位
Human // 匿名欄位
company string
}
//在 human 上面定義了一個 method
// 在 human 上面定義了一個 method
func (h *Human) SayHi() {
fmt.Printf("Hi, I am %s you can call me on %s\n", h.name, h.phone)
}
@@ -306,21 +306,21 @@ type Human struct {
}
type Student struct {
Human //匿名欄位
Human // 匿名欄位
school string
}
type Employee struct {
Human //匿名欄位
Human // 匿名欄位
company string
}
//Human 定義 method
// Human 定義 method
func (h *Human) SayHi() {
fmt.Printf("Hi, I am %s you can call me on %s\n", h.name, h.phone)
}
//Employee 的 method 重寫 Human 的 method
// Employee 的 method 重寫 Human 的 method
func (e *Employee) SayHi() {
fmt.Printf("Hi, I am %s, I work at %s. Call me on %s\n", e.name,
e.company, e.phone) //Yes you can split into 2 lines here.
@@ -336,7 +336,7 @@ func main() {
```
上面的程式碼設計的是如此的美妙,讓人不自覺的為 Go 的設計驚歎!
透過這些內容,我們可以設計出基本的物件導向的程式了,但是 Go 裡面的物件導向是如此的簡單,沒有任何的私有、公有關鍵字,透過大小寫來實現(大寫開頭的為公有,小寫開頭的為私有),方法也同樣適用這個原則。
透過這些內容,我們可以設計出基本的物件導向的程式了,但是 Go 裡面的物件導向是如此的簡單,沒有任何的私有、公有關鍵字,透過大小寫來實現大寫開頭的為公有,小寫開頭的為私有,方法也同樣適用這個原則。
## links

View File

@@ -6,15 +6,15 @@ Go 語言裡面設計最精妙的應該算 interface它讓物件導向
簡單的說interface 是一組 method 簽名的組合,我們透過 interface 來定義物件的一組行為。
我們前面一章最後一個例子中 Student 和 Employee 都能 SayHi雖然他們的內部實現不一樣但是那不重要重要的是他們都能`say hi`
我們前面一章最後一個例子中 Student 和 Employee 都能 SayHi雖然他們的內部實現不一樣但是那不重要重要的是他們都能 `say hi`
讓我們來繼續做更多的擴充套件Student 和 Employee 實現另一個方法`Sing`,然後 Student 實現方法 BorrowMoney 而 Employee 實現 SpendSalary。
讓我們來繼續做更多的擴充套件Student 和 Employee 實現另一個方法 `Sing`,然後 Student 實現方法 BorrowMoney 而 Employee 實現 SpendSalary。
這樣 Student 實現了三個方法SayHi、Sing、BorrowMoney而 Employee 實現了 SayHi、Sing、SpendSalary。
上面這些方法的組合稱為 interface(被物件 Student 和 Employee 實現)。例如 Student 和 Employee 都實現了 interfaceSayHi 和 Sing也就是這兩個物件是該 interface 型別。而 Employee 沒有實現這個 interfaceSayHi、Sing 和 BorrowMoney因為 Employee 沒有實現 BorrowMoney 這個方法。
上面這些方法的組合稱為 interface被物件 Student 和 Employee 實現。例如 Student 和 Employee 都實現了 interfaceSayHi 和 Sing也就是這兩個物件是該 interface 型別。而 Employee 沒有實現這個 interfaceSayHi、Sing 和 BorrowMoney因為 Employee 沒有實現 BorrowMoney 這個方法。
### interface 型別
interface 型別定義了一組方法,如果某個物件實現了某個介面的所有方法,則此物件就實現了此介面。詳細的語法參考下面這個例子
interface 型別定義了一組方法,如果某個物件實現了某個介面的所有方法,則此物件就實現了此介面。詳細的語法參考下面這個例子
```Go
@@ -38,7 +38,7 @@ type Employee struct {
money float32
}
//Human 物件實現 Sayhi 方法
// Human 物件實現 Sayhi 方法
func (h *Human) SayHi() {
fmt.Printf("Hi, I am %s you can call me on %s\n", h.name, h.phone)
}
@@ -48,7 +48,7 @@ func (h *Human) Sing(lyrics string) {
fmt.Println("La la, la la la, la la la la la...", lyrics)
}
//Human 物件實現 Guzzle 方法
// Human 物件實現 Guzzle 方法
func (h *Human) Guzzle(beerStein string) {
fmt.Println("Guzzle Guzzle Guzzle...", beerStein)
}
@@ -59,12 +59,12 @@ func (e *Employee) SayHi() {
e.company, e.phone) //此句可以分成多行
}
//Student 實現 BorrowMoney 方法
// Student 實現 BorrowMoney 方法
func (s *Student) BorrowMoney(amount float32) {
s.loan += amount // (again and again and...)
}
//Employee 實現 SpendSalary 方法
// Employee 實現 SpendSalary 方法
func (e *Employee) SpendSalary(amount float32) {
e.money -= amount // More vodka please!!! Get me through the day!
}
@@ -91,14 +91,14 @@ type ElderlyGent interface {
```
透過上面的程式碼我們可以知道interface 可以被任意的物件實現。我們看到上面的 Men interface 被 Human、Student 和 Employee 實現。同理,一個物件可以實現任意多個 interface例如上面的 Student 實現了 Men 和 YoungChap 兩個 interface。
最後,任意的型別都實現了空 interface(我們這樣定義interface{}),也就是包含 0 個 method 的 interface。
最後,任意的型別都實現了空 interface我們這樣定義interface{},也就是包含 0 個 method 的 interface。
### interface 值
那麼 interface 裡面到底能存什麼值呢?如果我們定義了一個 interface 的變數,那麼這個變數裡面可以存實現這個 interface 的任意型別的物件。例如上面例子中,我們定義了一個 Men interface 型別的變數 m那麼 m 裡面可以存 Human、Student 或者 Employee 值。
因為 m 能夠持有這三種類型的物件,所以我們可以定義一個包含 Men 型別元素的 slice這個 slice 可以被賦予實現了 Men 介面的任意結構的物件,這個和我們傳統意義上面的 slice 有所不同。
讓我們來看一下下面這個例子:
讓我們來看一下下面這個例子
```Go
@@ -124,17 +124,17 @@ type Employee struct {
money float32
}
//Human 實現 SayHi 方法
// Human 實現 SayHi 方法
func (h Human) SayHi() {
fmt.Printf("Hi, I am %s you can call me on %s\n", h.name, h.phone)
}
//Human 實現 Sing 方法
// Human 實現 Sing 方法
func (h Human) Sing(lyrics string) {
fmt.Println("La la la la...", lyrics)
}
//Employee 過載 Human 的 SayHi 方法
// Employee 過載 Human 的 SayHi 方法
func (e Employee) SayHi() {
fmt.Printf("Hi, I am %s, I work at %s. Call me on %s\n", e.name,
e.company, e.phone)
@@ -153,28 +153,28 @@ func main() {
sam := Employee{Human{"Sam", 36, "444-222-XXX"}, "Golang Inc.", 1000}
tom := Employee{Human{"Tom", 37, "222-444-XXX"}, "Things Ltd.", 5000}
//定義 Men 型別的變數 i
// 定義 Men 型別的變數 i
var i Men
//i 能儲存 Student
// i 能儲存 Student
i = mike
fmt.Println("This is Mike, a Student:")
i.SayHi()
i.Sing("November rain")
//i 也能儲存 Employee
// i 也能儲存 Employee
i = tom
fmt.Println("This is tom, an Employee:")
i.SayHi()
i.Sing("Born to be wild")
//定義了 slice Men
// 定義了 slice Men
fmt.Println("Let's use a slice of Men and see what happens")
x := make([]Men, 3)
//這三個都是不同型別的元素,但是他們實現了 interface 同一個介面
// 這三個都是不同型別的元素,但是他們實現了 interface 同一個介面
x[0], x[1], x[2] = paul, sam, mike
for _, value := range x{
@@ -182,11 +182,11 @@ func main() {
}
}
```
透過上面的程式碼,你會發現 interface 就是一組抽象方法的集合,它必須由其他非 interface 型別實現,而不能自我實現, Go 透過 interface 實現了 duck-typing:即"當看到一隻鳥走起來像鴨子、游泳起來像鴨子、叫起來也像鴨子,那麼這隻鳥就可以被稱為鴨子"
透過上面的程式碼,你會發現 interface 就是一組抽象方法的集合,它必須由其他非 interface 型別實現,而不能自我實現, Go 透過 interface 實現了 duck-typing:即「當看到一隻鳥走起來像鴨子、游泳起來像鴨子、叫起來也像鴨子,那麼這隻鳥就可以被稱為鴨子
### 空 interface
空 interface(interface{})不包含任何的 method正因為如此所有的型別都實現了空 interface。空 interface 對於描述起不到任何的作用(因為它不包含任何的 method但是空 interface 在我們需要儲存任意型別的數值的時候相當有用,因為它可以儲存任意型別的數值。它有點類似於 C 語言的 void*型別。
空 interface(interface{}) 不包含任何的 method正因為如此所有的型別都實現了空 interface。空 interface 對於描述起不到任何的作用因為它不包含任何的 method但是空 interface 在我們需要儲存任意型別的數值的時候相當有用,因為它可以儲存任意型別的數值。它有點類似於 C 語言的 void* 型別。
```Go
@@ -240,14 +240,14 @@ func main() {
```Go
//實現同樣的功能
// 實現同樣的功能
fmt.Println("The biggest one is", boxes.BiggestsColor().String())
fmt.Println("The biggest one is", boxes.BiggestsColor())
```
注:實現了 error 介面的物件(即實現了 Error() string 的物件),使用 fmt 輸出時,會呼叫 Error()方法,因此不必再定義 String()方法了。
注:實現了 error 介面的物件(即實現了 Error() string 的物件),使用 fmt 輸出時,會呼叫 Error() 方法,因此不必再定義 String() 方法了。
### interface 變數儲存的型別
我們知道 interface 的變數裡面可以儲存任意型別的數值(該型別實現了 interface)。那麼我們怎麼反向知道這個變數裡面實際儲存了的是哪個型別的物件呢?目前常用的有兩種方法:
我們知道 interface 的變數裡面可以儲存任意型別的數值該型別實現了 interface。那麼我們怎麼反向知道這個變數裡面實際儲存了的是哪個型別的物件呢?目前常用的有兩種方法:
- Comma-ok 斷言
@@ -274,7 +274,7 @@ fmt.Println("The biggest one is", boxes.BiggestsColor())
age int
}
//定義了 String 方法,實現了 fmt.Stringer
// 定義了 String 方法,實現了 fmt.Stringer
func (p Person) String() string {
return "(name: " + p.name + " - age: "+strconv.Itoa(p.age)+ " years)"
}
@@ -303,7 +303,7 @@ fmt.Println("The biggest one is", boxes.BiggestsColor())
也許你注意到了,我們斷言的型別越多,那麼 if else 也就越多,所以才引出了下面要介紹的 switch。
- switch 測試
最好的講解就是程式碼例子,現在讓我們重寫上面的這個實現
最好的講解就是程式碼例子,現在讓我們重寫上面的這個實現
```Go
@@ -322,7 +322,7 @@ fmt.Println("The biggest one is", boxes.BiggestsColor())
age int
}
//列印
// 列印
func (p Person) String() string {
return "(name: " + p.name + " - age: "+strconv.Itoa(p.age)+ " years)"
}
@@ -347,7 +347,7 @@ fmt.Println("The biggest one is", boxes.BiggestsColor())
}
}
```
這裡有一點需要強調的是:`element.(type)`語法不能在 switch 外的任何邏輯裡面使用,如果你要在 switch 外面判斷一個型別就使用`comma-ok`
這裡有一點需要強調的是:`element.(type)` 語法不能在 switch 外的任何邏輯裡面使用,如果你要在 switch 外面判斷一個型別就使用 `comma-ok`
### 嵌入 interface
@@ -358,9 +358,9 @@ Go 裡面真正吸引人的是它內建的邏輯語法,就像我們在學習 S
```Go
type Interface interface {
sort.Interface //嵌入欄位 sort.Interface
Push(x interface{}) //a Push method to push elements into the heap
Pop() interface{} //a Pop elements that pops elements from the heap
sort.Interface // 嵌入欄位 sort.Interface
Push(x interface{}) // a Push method to push elements into the heap
Pop() interface{} // a Pop elements that pops elements from the heap
}
```
我們看到 sort.Interface 其實就是嵌入欄位,把 sort.Interface 的所有 method 給隱式的包含進來了。也就是下面三個方法:
@@ -390,21 +390,21 @@ type ReadWriter interface {
### 反射
Go 語言實現了反射,所謂反射就是能檢查程式在執行時的狀態。我們一般用到的套件是 reflect 套件。如何運用 reflect 套件,官方的這篇文章詳細的講解了 reflect 套件的實現原理,[laws of reflection](http://golang.org/doc/articles/laws_of_reflection.html)
使用 reflect 一般分成三步,下面簡要的講解一下:要去反射是一個型別的值(這些值都實現了空 interface),首先需要把它轉化成 reflect 物件(reflect.Type 或者 reflect.Value根據不同的情況呼叫不同的函式)。這兩種取得方式如下:
使用 reflect 一般分成三步,下面簡要的講解一下:要去反射是一個型別的值這些值都實現了空 interface,首先需要把它轉化成 reflect 物件reflect.Type 或者 reflect.Value根據不同的情況呼叫不同的函式。這兩種取得方式如下:
```Go
t := reflect.TypeOf(i) //得到型別的 Meta 資料,透過 t 我們能取得型別定義裡面的所有元素
v := reflect.ValueOf(i) //得到實際的值,透過 v 我們取得儲存在裡面的值,還可以去改變值
t := reflect.TypeOf(i) // 得到型別的 Meta 資料,透過 t 我們能取得型別定義裡面的所有元素
v := reflect.ValueOf(i) // 得到實際的值,透過 v 我們取得儲存在裡面的值,還可以去改變值
```
轉化為 reflect 物件之後我們就可以進行一些操作了,也就是將 reflect 物件轉化成相應的值,例如
轉化為 reflect 物件之後我們就可以進行一些操作了,也就是將 reflect 物件轉化成相應的值,例如
```Go
tag := t.Elem().Field(0).Tag //取得定義在 struct 裡面的標籤
name := v.Elem().Field(0).String() //取得儲存在第一個欄位裡面的值
tag := t.Elem().Field(0).Tag // 取得定義在 struct 裡面的標籤
name := v.Elem().Field(0).String() // 取得儲存在第一個欄位裡面的值
```
取得反射值能回傳相應的型別和數值
取得反射值能回傳相應的型別和數值
```Go
@@ -414,7 +414,7 @@ fmt.Println("type:", v.Type())
fmt.Println("kind is float64:", v.Kind() == reflect.Float64)
fmt.Println("value:", v.Float())
```
最後,反射的話,那麼反射的欄位必須是可修改的,我們前面學習過傳值和傳參考,這個裡面也是一樣的道理。反射的欄位必須是可讀寫的意思是,如果下面這樣寫,那麼會發生錯誤
最後,反射的話,那麼反射的欄位必須是可修改的,我們前面學習過傳值和傳參考,這個裡面也是一樣的道理。反射的欄位必須是可讀寫的意思是,如果下面這樣寫,那麼會發生錯誤
```Go
@@ -422,7 +422,7 @@ var x float64 = 3.4
v := reflect.ValueOf(x)
v.SetFloat(7.1)
```
如果要修改相應的值,必須這樣寫
如果要修改相應的值,必須這樣寫
```Go

View File

@@ -4,7 +4,7 @@
## goroutine
goroutine 是 Go 並行設計的核心。goroutine 說到底其實就是[協程](https://zh.wikipedia.org/wiki/%E5%8D%8F%E7%A8%8B) (Coroutine),但是它比執行緒更小,十幾個 goroutine 可能體現在底層就是五六個執行緒Go 語言內部幫你實現了這些 goroutine 之間的記憶體共享。執行 goroutine 只需極少的棧記憶體(大概是 4~5KB)當然會根據相應的資料伸縮。也正因為如此可同時執行成千上萬個併發任務。goroutine 比 thread 更易用、更高效、更輕便。
goroutine 是 Go 並行設計的核心。goroutine 說到底其實就是[協程](https://zh.wikipedia.org/wiki/%E5%8D%8F%E7%A8%8B) (Coroutine),但是它比執行緒更小,十幾個 goroutine 可能體現在底層就是五六個執行緒Go 語言內部幫你實現了這些 goroutine 之間的記憶體共享。執行 goroutine 只需極少的棧記憶體大概是 4~5KB當然會根據相應的資料伸縮。也正因為如此可同時執行成千上萬個併發任務。goroutine 比 thread 更易用、更高效、更輕便。
goroutine 是透過 Go 的 runtime 管理的一個執行緒管理器。goroutine 透過 `go` 關鍵字實現了,其實就是一個普通的函式。
@@ -12,7 +12,7 @@ goroutine 是透過 Go 的 runtime 管理的一個執行緒管理器。goroutine
go hello(a, b, c)
```
透過關鍵字 go 就啟動了一個 goroutine。我們來看一個例子
透過關鍵字 go 就啟動了一個 goroutine。我們來看一個例子
```Go
@@ -49,14 +49,14 @@ func main() {
我們可以看到 go 關鍵字很方便的就實現了併發程式設計。
上面的多個 goroutine 執行在同一個程序裡面,共享記憶體資料,不過設計上我們要遵循:不要透過共享來通訊,而要透過通訊來共享。
> runtime.Gosched()表示讓 CPU 把時間片讓給別人,下次某個時候繼續恢復執行該 goroutine。
> runtime.Gosched() 表示讓 CPU 把時間片讓給別人,下次某個時候繼續恢復執行該 goroutine。
>預設情況下,在 Go 1.5 將標識併發系統執行緒個數的 runtime.GOMAXPROCS 的初始值由 1 改為了執行環境的 CPU 核數。
但在 Go 1.5 以前排程器僅使用單執行緒,也就是說只實現了併發。想要發揮多核處理器的並行,需要在我們的程式中明確的呼叫 runtime.GOMAXPROCS(n) 告訴排程器同時使用多個執行緒。GOMAXPROCS 設定了同時執行邏輯程式碼的系統執行緒的最大數量,並回傳之前的設定。如果 n < 1不會改變當前設定。
## channels
goroutine 執行在相同的地址空間,因此訪問共享記憶體必須做好同步。那麼 goroutine 之間如何進行資料的通訊呢Go 提供了一個很好的通訊機制 channel。channel 可以與 Unix shell 中的雙向管道做類別比可以透過它傳送或者接收值。這些值只能是特定的型別channel 型別。定義一個 channel 時,也需要定義傳送到 channel 的值的型別。注意,必須使用 make 建立 channel
goroutine 執行在相同的地址空間,因此訪問共享記憶體必須做好同步。那麼 goroutine 之間如何進行資料的通訊呢Go 提供了一個很好的通訊機制 channel。channel 可以與 Unix shell 中的雙向管道做類別比可以透過它傳送或者接收值。這些值只能是特定的型別channel 型別。定義一個 channel 時,也需要定義傳送到 channel 的值的型別。注意,必須使用 make 建立 channel
```Go
@@ -64,7 +64,7 @@ ci := make(chan int)
cs := make(chan string)
cf := make(chan interface{})
```
channel 透過運算子`<-`來接收和傳送資料
channel 透過運算子 `<-` 來接收和傳送資料
```Go
@@ -110,7 +110,7 @@ ch := make(chan type, value)
```
當 value = 0 時channel 是無緩衝阻塞讀寫的,當 value > 0 時channel 有緩衝、是非阻塞的,直到寫滿 value 個元素才阻塞寫入。
我們看一下下面這個例子,你可以在自己本機測試一下,修改相應的 value 值
我們看一下下面這個例子,你可以在自己本機測試一下,修改相應的 value 值
```Go
@@ -119,14 +119,14 @@ package main
import "fmt"
func main() {
c := make(chan int, 2)//修改 2 為 1 就報錯,修改 2 為 3 可以正常執行
c := make(chan int, 2) // 修改 2 為 1 就報錯,修改 2 為 3 可以正常執行
c <- 1
c <- 2
fmt.Println(<-c)
fmt.Println(<-c)
}
//修改為 1 報如下的錯誤:
//fatal error: all goroutines are asleep - deadlock!
// 修改為 1 報如下的錯誤:
// fatal error: all goroutines are asleep - deadlock!
```
## Range 和 Close
@@ -157,15 +157,15 @@ func main() {
}
}
```
`for i := range c`能夠不斷的讀取 channel 裡面的資料,直到該 channel 被明確的關閉。上面程式碼我們看到可以明確的關閉 channel生產者透過內建函式 `close` 關閉 channel。關閉 channel 之後就無法再發送任何資料了,在消費方可以透過語法`v, ok := <-ch`測試 channel 是否被關閉。如果 ok 回傳 false那麼說明 channel 已經沒有任何資料並且已經被關閉。
`for i := range c` 能夠不斷的讀取 channel 裡面的資料,直到該 channel 被明確的關閉。上面程式碼我們看到可以明確的關閉 channel生產者透過內建函式 `close` 關閉 channel。關閉 channel 之後就無法再發送任何資料了,在消費方可以透過語法 `v, ok := <-ch` 測試 channel 是否被關閉。如果 ok 回傳 false那麼說明 channel 已經沒有任何資料並且已經被關閉。
>記住應該在生產者的地方關閉 channel而不是消費的地方去關閉它這樣容易引起 panic
>另外記住一點的就是 channel 不像檔案之類別的,不需要經常去關閉,只有當你確實沒有任何傳送資料了,或者你想明確的結束 range 迴圈之類別的
>另外記住一點的就是 channel 不像檔案之類別的,不需要經常去關閉,只有當你確實沒有任何傳送資料了,或者你想明確的結束 range 迴圈之類別的時候。
## Select
我們上面介紹的都是隻有一個 channel 的情況,那麼如果存在多個 channel 的時候我們該如何操作呢Go 裡面提供了一個關鍵字`select`,透過 `select` 可以監聽 channel 上的資料流動。
我們上面介紹的都是隻有一個 channel 的情況,那麼如果存在多個 channel 的時候我們該如何操作呢Go 裡面提供了一個關鍵字 `select`,透過 `select` 可以監聽 channel 上的資料流動。
`select` 預設是阻塞的,只有當監聽的 channel 中有傳送或接收可以進行時才會執行,當多個 channel 都準備好的時候select 會隨機選擇其中一個執行。
@@ -200,7 +200,7 @@ func main() {
fibonacci(c, quit)
}
```
`select` 裡面還有 default 語法,`select`其實就是類似 switch 的功能default 就是當監聽的 channel 都沒有準備好的時候預設執行的select 不再阻塞等待 channel
`select` 裡面還有 default 語法,`select` 其實就是類似 switch 的功能default 就是當監聽的 channel 都沒有準備好的時候預設執行的select 不再阻塞等待 channel
```Go

View File

@@ -9,7 +9,7 @@ chan else goto package switch
const fallthrough if range type
continue for import return var
```
- var 和 const 參考 2.2Go 語言基礎裡面的變數和常數宣告
- var 和 const 參考 2.2 Go 語言基礎裡面的變數和常數宣告
- package 和 import 已經有過短暫的接觸
- func 用於定義函式和方法
- return 用於從函式回傳