mirror of
https://github.com/gopl-zh/gopl-zh.github.com.git
synced 2025-12-17 19:24:19 +08:00
make loop
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
## 2.1. 命名
|
||||
|
||||
Go語言中的的函數名, 變量名, 常量名, 類型名, 語句段標簽名, 和 包名 等所有的命名, 都遵循一個命名規則: 一個名字必鬚以一個字母(Unicode字母)或下劃綫開頭, 後面可以跟任意數量的字母,數字或下劃綫. 不同大小寫字母是不同的: `heapSort` 和 `Heapsort` 是兩個不同的名字.
|
||||
Go語言中的的函數名, 變量名, 常量名, 類型名, 語句段標籤名, 和 包名 等所有的命名, 都遵循一個命名規則: 一個名字必鬚以一個字母(Unicode字母)或下劃線開頭, 後面可以跟任意數量的字母,數字或下劃線. 不同大小寫字母是不同的: `heapSort` 和 `Heapsort` 是兩個不同的名字.
|
||||
|
||||
Go語言類似 `if` 和 `switch` 的關鍵字有25個; 關鍵字不能用於自定義名字, 隻能在特定語法中使用.
|
||||
|
||||
@@ -31,6 +31,6 @@ Functions: make len cap new append copy close delete
|
||||
|
||||
如果一個實體是在函數內部定義, 那麽它的就隻在函數內部有效. 如果是在函數外部定義, 那麽將在當前包的所有文件中都可以訪問. 名字的開頭字母的大小寫決定了名字在包外的可見性. 如果一個名字是大寫字母開頭的, 那麽它將是導齣的, 也就是可以被外部的包訪問, 例如 `fmt` 包的 `Printf` 函數就是導齣的, 可以在 `fmt` 包外部訪問. 包本身的名字一般總是用小寫字母.
|
||||
|
||||
名字的長度沒有限製, 但是Go的風格是儘量使用短小的名字, 對於侷部變量尤其是這樣; 你會經常看到 `i` 之類的名字, 而是冗長的 `theLoopIndex`. 通常來說, 如果一個名字的作用域比較大, 生命週期較長, 那麽用長的名字將更有意義.
|
||||
名字的長度沒有限製, 但是Go的風格是盡量使用短小的名字, 對於局部變量尤其是這樣; 你會經常看到 `i` 之類的名字, 而是冗長的 `theLoopIndex`. 通常來説, 如果一個名字的作用域比較大, 生命週期較長, 那麽用長的名字將更有意義.
|
||||
|
||||
在習慣上, Go程序員推薦使用`駝峯式`命名, 當名字有幾個單詞的時優先使用大小寫分隔, 而不是優先用下劃綫分隔. 因此, 標準庫有 `QuoteRuneToASCII` 和 `parseRequestLine` 這樣的函數命名, 但是不會用 `quote_rune_to_ASCII` 和 `parse_request_line` 這樣的命名. 像 `ASCII` 和 `HTML` 這樣的縮略詞避免使用大小寫混合, 它們可能被稱爲 `htmlEscape`, `HTMLEscape` 或 `escapeHTML`, 但不會是 `escapeHtml`.
|
||||
在習慣上, Go程序員推薦使用`駝峯式`命名, 當名字有幾個單詞的時優先使用大小寫分隔, 而不是優先用下劃線分隔. 因此, 標準庫有 `QuoteRuneToASCII` 和 `parseRequestLine` 這樣的函數命名, 但是不會用 `quote_rune_to_ASCII` 和 `parse_request_line` 這樣的命名. 像 `ASCII` 和 `HTML` 這樣的縮略詞避免使用大小寫混合, 它們可能被稱爲 `htmlEscape`, `HTMLEscape` 或 `escapeHTML`, 但不會是 `escapeHtml`.
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
## 2.2. 聲明
|
||||
|
||||
聲明定義了程序的入口以及部分或全部的屬性. Go主要有四種聲明類型: var, const, type, 和 func, 分別對應 變量, 常量, 類型, 和 函數的 聲明. 這一章我們重點討論變量和類型的聲明, 第三章將討論常量的聲明, 第五章將討論函數的聲明.
|
||||
聲明定義了程序的入口以及部分或全部的屬性. Go主要有四種聲明類型: var, const, type, 和 func, 分别對應 變量, 常量, 類型, 和 函數的 聲明. 這一章我們重點討論變量和類型的聲明, 第三章將討論常量的聲明, 第五章將討論函數的聲明.
|
||||
|
||||
一個Go程序存儲在一個或多個以`.go`爲後綴名的文件中. 每個文件以個包的聲明開始, 以說明文件是屬於包的一部分.
|
||||
一個Go程序存儲在一個或多個以`.go`爲後綴名的文件中. 每個文件以個包的聲明開始, 以説明文件是屬於包的一部分.
|
||||
包聲明之後是 import 導入聲明, 然後是包一級的類型/變量/常量/函數的聲明, 聲明的順序無關緊要. 例如, 下面的例子聲明了一個常量, 一個函數和兩個變量:
|
||||
|
||||
```Go
|
||||
@@ -23,11 +23,11 @@ func main() {
|
||||
}
|
||||
```
|
||||
|
||||
其中 常量 `boilingF` 是在包一級聲明的, 然後 `f` 和 `c` 是在 main 函數內部聲明的. 在包一級聲明的名字可在整個包訪問, 而不僅僅在其聲明的文件中訪問. 相比之下, 侷部聲明的名字就隻能在函數內部很小的部分可訪問.
|
||||
其中 常量 `boilingF` 是在包一級聲明的, 然後 `f` 和 `c` 是在 main 函數內部聲明的. 在包一級聲明的名字可在整個包訪問, 而不僅僅在其聲明的文件中訪問. 相比之下, 局部聲明的名字就隻能在函數內部很小的部分可訪問.
|
||||
|
||||
一個函數的聲明有一個函數名字, 參數列表(由函數的調用者提供參數變量的具體值), 一個可選的返迴值列表, 和包含函數語句定義的函數體. 如果函數沒有返迴值, 那麽返迴值列表是省略的. 執行函數從函數的第一個語句開始, 但是順序執行直到遇到 renturn 返迴語言, 如果沒有返迴語句則是到函數末尾, 然後返迴到調用者.
|
||||
|
||||
我們已經看到過很多函數的例子了, 在第五章將深入討論函數的細節, 這裡隻粗略說下. 下面的 `fToC` 函數封裝了溫度轉換的邏輯, 這樣它隻需要定義一次, 就可以在多個地方多次使用. 這個例子中, main 函數就調用了兩次 `fToC` 函數, 分別是使用侷部定義的兩個常量作爲函數參數.
|
||||
我們已經看到過很多函數的例子了, 在第五章將深入討論函數的細節, 這里隻粗略説下. 下面的 `fToC` 函數封裝了溫度轉換的邏輯, 這樣它隻需要定義一次, 就可以在多個地方多次使用. 這個例子中, main 函數就調用了兩次 `fToC` 函數, 分别是使用局部定義的兩個常量作爲函數參數.
|
||||
|
||||
|
||||
```Go
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
### 2.3.1. 簡短變量聲明
|
||||
|
||||
在函數內部, 有一種稱爲簡短變量聲明的形式可用於聲明和初始化侷部變量. 以 `名字 := 表達式` 方式聲明變量, 變量的類型根據表達式來推導. 這裡函數中是三個簡短變量聲明語句(§1.4):
|
||||
在函數內部, 有一種稱爲簡短變量聲明的形式可用於聲明和初始化局部變量. 以 `名字 := 表達式` 方式聲明變量, 變量的類型根據表達式來推導. 這里函數中是三個簡短變量聲明語句(§1.4):
|
||||
|
||||
```Go
|
||||
anim := gif.GIF{LoopCount: nframes}
|
||||
@@ -8,7 +8,7 @@ freq := rand.Float64() * 3.0
|
||||
t := 0.0
|
||||
```
|
||||
|
||||
因爲簡潔和靈活性, 簡短變量聲明用於大部分的侷部變量的聲明和初始化. var 方式的聲明往往是用於需要顯示指定類型的侷部變量, 或者因爲稍後會被賦值而初始值無關緊要的變量.
|
||||
因爲簡潔和靈活性, 簡短變量聲明用於大部分的局部變量的聲明和初始化. var 方式的聲明往往是用於需要顯示指定類型的局部變量, 或者因爲稍後會被賦值而初始值無關緊要的變量.
|
||||
|
||||
|
||||
```Go
|
||||
@@ -44,7 +44,7 @@ if err != nil {
|
||||
f.Close()
|
||||
```
|
||||
|
||||
這裡有一個比較微妙的地方: 簡短變量聲明左邊的全部變量可能並不是全部都是剛剛聲明的. 如果有一些已經在相同的詞法塊聲明過了(§2.7), 那麽簡短變量聲明對這些已經聲明過的變量就隻有賦值行爲了.
|
||||
這里有一個比較微妙的地方: 簡短變量聲明左邊的全部變量可能併不是全部都是剛剛聲明的. 如果有一些已經在相同的詞法塊聲明過了(§2.7), 那麽簡短變量聲明對這些已經聲明過的變量就隻有賦值行爲了.
|
||||
|
||||
在下面的代碼中, 第一個語句聲明了 in 和 err 變量. 第二個語句隻聲明了 out, 然後對已經聲明的 err 進行賦值.
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
### 2.3.2 指鍼
|
||||
### 2.3.2 指針
|
||||
|
||||
一個變量對應一個保存了一個值的內存空間. 變量在聲明語句創建時綁定一個名字, 比如 x, 但是還有很多變量始終以表達式方式引入, 例如 x[i] 或 x.f. 所有這些表達式都讀取一個變量的值, 除非它們是齣現在賦值語句的左邊, 這種時候是給變量賦予一個新值.
|
||||
|
||||
一個指鍼的值是一個變量的地址. 一個指鍼對應變量在內存中的存儲位置. 並不是每一個值都會有一個地址, 但是對於每一個變量必然有對應的地址. 通過指鍼, 我們可以直接讀或更新變量的值, 而不需要知道變量的名字(卽使變量有名字的話).
|
||||
一個指針的值是一個變量的地址. 一個指針對應變量在內存中的存儲位置. 併不是每一個值都會有一個地址, 但是對於每一個變量必然有對應的地址. 通過指針, 我們可以直接讀或更新變量的值, 而不需要知道變量的名字(卽使變量有名字的話).
|
||||
|
||||
如果這樣聲明一個變量 `var x int`, 那麽 `&x` 表達式(x的地址)將產生一個指向整數變量的指鍼, 對應的數據類型是 `*int`, 稱之爲 "指向 int 的指鍼". 如果指鍼名字爲 p, 那麽可以說 "p 指鍼指向 x", 或者說 "p 指鍼保存了 x 變量的地址". `*p` 對應 p 指鍼指向的變量的值. `*p` 表達式讀取變量的值, 爲 int 類型, 同時因爲 `*p` 對應一個變量, 所以可以齣現在賦值語句的左邊, 用於更新所指向的變量的值.
|
||||
如果這樣聲明一個變量 `var x int`, 那麽 `&x` 表達式(x的地址)將産生一個指向整數變量的指針, 對應的數據類型是 `*int`, 稱之爲 "指向 int 的指針". 如果指針名字爲 p, 那麽可以説 "p 指針指向 x", 或者説 "p 指針保存了 x 變量的地址". `*p` 對應 p 指針指向的變量的值. `*p` 表達式讀取變量的值, 爲 int 類型, 同時因爲 `*p` 對應一個變量, 所以可以齣現在賦值語句的左邊, 用於更新所指向的變量的值.
|
||||
|
||||
```Go
|
||||
x := 1
|
||||
@@ -14,18 +14,18 @@ fmt.Println(*p) // "1"
|
||||
fmt.Println(x) // "2"
|
||||
```
|
||||
|
||||
對於聚合類型, 比如結構體的每個字段, 或者是數組的每個元素, 也都是對應一個變量, 並且可以被獲取地址.
|
||||
對於聚合類型, 比如結構體的每個字段, 或者是數組的每個元素, 也都是對應一個變量, 併且可以被穫取地址.
|
||||
|
||||
變量有時候被稱爲可尋址的值. 如果變量由表達式臨時生成, 那麽表達式必鬚能接受 `&` 取地址操作.
|
||||
|
||||
任何類型的指鍼的零值都是 nil. 如果 `p != nil` 測試爲眞, 那麽 p 是指向變量. 指鍼直接也是可以進行相等測試的, 隻有當它們指向同一個變量或全部是 nil 時纔相等.
|
||||
任何類型的指針的零值都是 nil. 如果 `p != nil` 測試爲眞, 那麽 p 是指向變量. 指針直接也是可以進行相等測試的, 隻有當它們指向同一個變量或全部是 nil 時纔相等.
|
||||
|
||||
```Go
|
||||
var x, y int
|
||||
fmt.Println(&x == &x, &x == &y, &x == nil) // "true false false"
|
||||
```
|
||||
|
||||
在Go語言中, 返迴函數中侷部變量的地址是安全的. 例如下面的代碼, 調用 f 函數時創建 v 侷部變量, 在地址被返迴之後依然有效, 因爲指鍼 p 依然引用這個變量.
|
||||
在Go語言中, 返迴函數中局部變量的地址是安全的. 例如下面的代碼, 調用 f 函數時創建 v 局部變量, 在地址被返迴之後依然有效, 因爲指針 p 依然引用這個變量.
|
||||
|
||||
```Go
|
||||
var p = f()
|
||||
@@ -42,7 +42,7 @@ func f() *int {
|
||||
fmt.Println(f() == f()) // "false"
|
||||
```
|
||||
|
||||
因爲指鍼包含了一個變量的地址, 因此將指鍼作爲參數調用函數, 將可以在函數中通過指鍼更新變量的值. 例如這個通過指鍼來更新變量的值, 然後返迴更新後的值, 可用在一個表達式中:
|
||||
因爲指針包含了一個變量的地址, 因此將指針作爲參數調用函數, 將可以在函數中通過指針更新變量的值. 例如這個通過指針來更新變量的值, 然後返迴更新後的值, 可用在一個表達式中:
|
||||
|
||||
```Go
|
||||
func incr(p *int) int {
|
||||
@@ -55,9 +55,9 @@ incr(&v) // side effect: v is now 2
|
||||
fmt.Println(incr(&v)) // "3" (and v is 3)
|
||||
```
|
||||
|
||||
每次我們對變量取地址, 或者復製指鍼, 我們都創建了變量的新的別名. 例如, *p 是 變量 v 的別名. 指鍼特別有加載的地方在於我們可以不用名字而訪問一個變量, 但是這是一把雙刃劍: 要找到一個變量的所有訪問者, 我們必鬚知道變量全部的別名. 不僅僅是指鍼創建別名, 很多其他引用類型也會創建別名, 例如 切片, 字典和管道, 甚至結構體, 數組和接口都會創建所引用變量的別名.
|
||||
每次我們對變量取地址, 或者複製指針, 我們都創建了變量的新的别名. 例如, *p 是 變量 v 的别名. 指針特别有加載的地方在於我們可以不用名字而訪問一個變量, 但是這是一把雙刃劍: 要找到一個變量的所有訪問者, 我們必鬚知道變量全部的别名. 不僅僅是指針創建别名, 很多其他引用類型也會創建别名, 例如 切片, 字典和管道, 甚至結構體, 數組和接口都會創建所引用變量的别名.
|
||||
|
||||
指鍼是 flag 包的關鍵, 它使用命令行參數來設置對應的變量, 而這些分佈在整個程序中. 爲了說明這一點, 在早些的echo版本中, 包含了兩個可選的命令行參數: `-n` 用於忽略行尾的換行符, `-s sep` 用於指定分隔字符(默認是空格). 這是第四個版本, 對應包 gopl.io/ch2/echo4.
|
||||
指針是 flag 包的關鍵, 它使用命令行參數來設置對應的變量, 而這些分布在整個程序中. 爲了説明這一點, 在早些的echo版本中, 包含了兩個可選的命令行參數: `-n` 用於忽略行尾的換行符, `-s sep` 用於指定分隔字符(默認是空格). 這是第四個版本, 對應包 gopl.io/ch2/echo4.
|
||||
|
||||
```Go
|
||||
gopl.io/ch2/echo4
|
||||
@@ -82,7 +82,7 @@ func main() {
|
||||
}
|
||||
```
|
||||
|
||||
`flag.Bool` 函數調用創建了一個新的佈爾型標誌參數變量. 它有三個屬性: 第一個是的名字"n", 然後是標誌的默認值(這裡是false), 最後是對應的描述信息. 如果用戶輸入了無效的標誌參數, 或者輸入 `-h` 或 `-help` 標誌參數, 將打印標誌參數的名字, 默認值和描述信息. 類似的, flag.String 用於創建一個字符串類型的標誌參數變量, 同樣包含參數名, 默認值, 和描述信息. 變量 `sep` 和 `n` 是一個指向標誌參數變量的指鍼, 因此必鬚用 *sep 和 *n 的方式間接引用.
|
||||
`flag.Bool` 函數調用創建了一個新的布爾型標誌參數變量. 它有三個屬性: 第一個是的名字"n", 然後是標誌的默認值(這里是false), 最後是對應的描述信息. 如果用戶輸入了無效的標誌參數, 或者輸入 `-h` 或 `-help` 標誌參數, 將打印標誌參數的名字, 默認值和描述信息. 類似的, flag.String 用於創建一個字符串類型的標誌參數變量, 同樣包含參數名, 默認值, 和描述信息. 變量 `sep` 和 `n` 是一個指向標誌參數變量的指針, 因此必鬚用 *sep 和 *n 的方式間接引用.
|
||||
|
||||
|
||||
當程序運行時, 必鬚在標誌參數變量使用之前調用 flag.Parse 函數更新標誌參數變量的值(之前是默認值). 非標誌參數的普通類型參數可以用 flag.Args() 訪問, 對應一個 字符串切片. 如果 flag.Parse 解析遇到錯誤, 將打印提示信息, 然後調用 os.Exit(2) 終止程序.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
### 2.3.3 new 函數
|
||||
|
||||
|
||||
另一個創建變量的方法是用內建的 new 函數. 表達式 `new(T)` 創建一個T類型的匿名變量, 初始化爲T類型的零值, 返迴返迴變量地址, 返迴指鍼類型爲 `*T`.
|
||||
另一個創建變量的方法是用內建的 new 函數. 表達式 `new(T)` 創建一個T類型的匿名變量, 初始化爲T類型的零值, 返迴返迴變量地址, 返迴指針類型爲 `*T`.
|
||||
|
||||
```Go
|
||||
p := new(int) // p, *int 類型, 指向匿名的 int 變量
|
||||
@@ -11,7 +11,7 @@ fmt.Println(*p) // "2"
|
||||
```
|
||||
|
||||
|
||||
從 new 創建變量和普通聲明方式創建變量沒有什麽區別, 除了不需要聲明一個臨時變量的名字外, 我們還可以在表達式中使用 `new(T)`. 換言之, new 類似是一種語法醣, 而不是一個新的基礎概唸.
|
||||
從 new 創建變量和普通聲明方式創建變量沒有什麽區别, 除了不需要聲明一個臨時變量的名字外, 我們還可以在表達式中使用 `new(T)`. 換言之, new 類似是一種語法醣, 而不是一個新的基礎概念.
|
||||
|
||||
下面的兩個 newInt 函數有着相同的行爲:
|
||||
|
||||
@@ -30,11 +30,11 @@ q := new(int)
|
||||
fmt.Println(p == q) // "false"
|
||||
```
|
||||
|
||||
當然也有特殊情況: 如果兩個類型都是空的, 也就是說類型的大小是0, 例如 `struct{}` 和 `[0]int`, 有可能有相同的地址(依賴具體的語言實現).
|
||||
當然也有特殊情況: 如果兩個類型都是空的, 也就是説類型的大小是0, 例如 `struct{}` 和 `[0]int`, 有可能有相同的地址(依賴具體的語言實現).
|
||||
|
||||
new 函數使用相對比較少, 因爲對應結構體來說, 可以直接用字面量語法創建新變量的方法更靈活 (§4.4.1).
|
||||
new 函數使用相對比較少, 因爲對應結構體來説, 可以直接用字面量語法創建新變量的方法更靈活 (§4.4.1).
|
||||
|
||||
由於 new 隻是一個預定義的函數, 它並不是一個關鍵字, 因此我們可以將 new 重新定義爲別的類型. 例如:
|
||||
由於 new 隻是一個預定義的函數, 它併不是一個關鍵字, 因此我們可以將 new 重新定義爲别的類型. 例如:
|
||||
|
||||
```Go
|
||||
func delta(old, new int) int { return new - old }
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
### 2.3.4. 變量的生命週期
|
||||
|
||||
變量的生命週期指的是程序運行期間變量存在的有效時間間隔. 包級聲明的變量的生命週期和程序的生命週期是一緻的. 相比之下, 侷部變量的聲明週期是動態的: 從每次創建一個新變量的聲明語句被執行開始, 直到變量不在被引用爲止, 然後變量的存儲空間可能被迴收. 函數的參數變量和返迴值變量都是侷部變量. 它們在函數每次被調用的時候創建.
|
||||
變量的生命週期指的是程序運行期間變量存在的有效時間間隔. 包級聲明的變量的生命週期和程序的生命週期是一致的. 相比之下, 局部變量的聲明週期是動態的: 從每次創建一個新變量的聲明語句被執行開始, 直到變量不在被引用爲止, 然後變量的存儲空間可能被迴收. 函數的參數變量和返迴值變量都是局部變量. 它們在函數每次被調用的時候創建.
|
||||
|
||||
例如, 下面是從 1.4 節的 Lissajous 程序摘録的代碼片段:
|
||||
|
||||
@@ -15,11 +15,11 @@ for t := 0.0; t < cycles*2*math.Pi; t += res {
|
||||
|
||||
在每次循環的開始創建變量 t, 然後在每次循環迭代中創建 x 和 y.
|
||||
|
||||
那麽垃圾收集器是如何知道一個變量是何時可以被迴收的呢? 這裡我們先避開完整的技術細節, 但是基本的思路是, 從每個包級的變量和每個當前運行函數的每一個侷部變量開始, 通過指鍼或引用的路徑, 是否可以找到該變量. 如果不存在這樣的路徑, 那麽說明該變量是不可達的, 也就是說它並不會影響其餘的計算.
|
||||
那麽垃圾收集器是如何知道一個變量是何時可以被迴收的呢? 這里我們先避開完整的技術細節, 但是基本的思路是, 從每個包級的變量和每個當前運行函數的每一個局部變量開始, 通過指針或引用的路徑, 是否可以找到該變量. 如果不存在這樣的路徑, 那麽説明該變量是不可達的, 也就是説它併不會影響其餘的計算.
|
||||
|
||||
因爲一個變量的聲明週期隻取決於是否可達, 因此一個循環迭代內部的侷部變量的生命週期可能超齣其侷部作用域. 它可能在函數返迴之後依然存在.
|
||||
因爲一個變量的聲明週期隻取決於是否可達, 因此一個循環迭代內部的局部變量的生命週期可能超齣其局部作用域. 它可能在函數返迴之後依然存在.
|
||||
|
||||
編譯器會選擇在棧上還是在堆上分配侷部變量的存儲空間, 但可能令人驚訝的是, 這個選擇並不是由 var 或 new 來決定的.
|
||||
編譯器會選擇在棧上還是在堆上分配局部變量的存儲空間, 但可能令人驚訝的是, 這個選擇併不是由 var 或 new 來決定的.
|
||||
|
||||
```Go
|
||||
var global *int
|
||||
@@ -31,10 +31,10 @@ func f() { func g() {
|
||||
}
|
||||
```
|
||||
|
||||
這裡的 x 必鬚在堆上分配, 因爲它在函數退齣後依然可以通過包的 global 變量找到, 雖然它是在函數內部定義的; 我們說這個 x 侷部變量從 函數 f 中逃逸了. 相反, 當 g 函數返迴時, 變量 `*y` 將是不可達的, 也就是可以被迴收的. 因此, `*y` 並沒有從 函數 g 逃逸, 編譯器可以選擇在棧上分配 `*y` 的存儲空間, 雖然這裡用的是 new 方式.
|
||||
在任何時候, 你並不需爲了編寫正確的代碼而要考慮變量的逃逸行爲, 要記住的是, 逃逸的變量需要額外分配內存, 同時對性能的優化會產生一定的影響.
|
||||
這里的 x 必鬚在堆上分配, 因爲它在函數退齣後依然可以通過包的 global 變量找到, 雖然它是在函數內部定義的; 我們説這個 x 局部變量從 函數 f 中逃逸了. 相反, 當 g 函數返迴時, 變量 `*y` 將是不可達的, 也就是可以被迴收的. 因此, `*y` 併沒有從 函數 g 逃逸, 編譯器可以選擇在棧上分配 `*y` 的存儲空間, 雖然這里用的是 new 方式.
|
||||
在任何時候, 你併不需爲了編寫正確的代碼而要考慮變量的逃逸行爲, 要記住的是, 逃逸的變量需要額外分配內存, 同時對性能的優化會産生一定的影響.
|
||||
|
||||
垃圾收集器對編寫正確的代碼是一個鉅大的幫助, 但並不是說你完全不用考慮內存了. 你雖然不需要顯式地分配和釋放內存, 但是要編寫高效的程序你還是需要知道變量的生命週期. 例如, 將指向短生命週期對象的指鍼保存到具有長生命週期的對象中, 特別是全侷變量時, 會阻止對短生命週期對象的垃圾迴收.
|
||||
垃圾收集器對編寫正確的代碼是一個鉅大的幫助, 但併不是説你完全不用考慮內存了. 你雖然不需要顯式地分配和釋放內存, 但是要編寫高效的程序你還是需要知道變量的生命週期. 例如, 將指向短生命週期對象的指針保存到具有長生命週期的對象中, 特别是全局變量時, 會阻止對短生命週期對象的垃圾迴收.
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
## 2.3. 變量
|
||||
|
||||
var 聲明可以創建一個特定類型的變量, 然後給變量附加一個名字, 並且設置變量的初始值. 變量聲明的一般語法:
|
||||
var 聲明可以創建一個特定類型的變量, 然後給變量附加一個名字, 併且設置變量的初始值. 變量聲明的一般語法:
|
||||
|
||||
```Go
|
||||
var name type = 表達式
|
||||
```
|
||||
|
||||
其中類型或 `= 表達式` 可以省略其中的一個. 如果省略的是類型信息, 那麽將根據初始化表達式類推導類型信息. 如果初始化表達式被省略, 那麽將用零值初始化變量. 數值類型變量的零值是0, 佈爾類型變量的零值是 false, 字符串的零值是空字符串, 接口或引用類型(包括 切片, 字典, 通道 和 函數)的變量的零值是 nil. 數組或結構體等聚合類型的零值是每個元素或字段都是零值.
|
||||
其中類型或 `= 表達式` 可以省略其中的一個. 如果省略的是類型信息, 那麽將根據初始化表達式類推導類型信息. 如果初始化表達式被省略, 那麽將用零值初始化變量. 數值類型變量的零值是0, 布爾類型變量的零值是 false, 字符串的零值是空字符串, 接口或引用類型(包括 切片, 字典, 通道 和 函數)的變量的零值是 nil. 數組或結構體等聚合類型的零值是每個元素或字段都是零值.
|
||||
|
||||
零值機製可以確保每個聲明的變量總是有一個良好定義的值, 在 Go 中不存在未初始化的變量. 這個可以簡化很多代碼, 在沒有增加額外工作的前提下確保邊界條件下的合理行爲. 例如:
|
||||
|
||||
@@ -15,9 +15,9 @@ var s string
|
||||
fmt.Println(s) // ""
|
||||
```
|
||||
|
||||
這段代碼將打印一個空字符串, 而不是導緻錯誤或產生不可預知的行爲. Go 程序員經常讓一些聚合類型的零值也有意義, 這樣不管任何類型的變量總是有一個合理的零值狀態.
|
||||
這段代碼將打印一個空字符串, 而不是導致錯誤或産生不可預知的行爲. Go 程序員經常讓一些聚合類型的零值也有意義, 這樣不管任何類型的變量總是有一個合理的零值狀態.
|
||||
|
||||
可以在一個聲明語句中同時聲明一組變量, 或用一組初始化表達式聲明並初始化一組變量.
|
||||
可以在一個聲明語句中同時聲明一組變量, 或用一組初始化表達式聲明併初始化一組變量.
|
||||
如果省略每個變量的類型, 將可以聲明多個不同類型的變量(類型由初始化表達式推導):
|
||||
|
||||
```Go
|
||||
@@ -25,7 +25,7 @@ var i, j, k int // int, int, int
|
||||
var b, f, s = true, 2.3, "four" // bool, float64, string
|
||||
```
|
||||
|
||||
初始化可以是字面量或任意的表達式. 包級別聲明的變量會在 main 函數執行前完成初始化 (§2.6.2), 侷部變量將在聲明語句被執行到的時候初始化.
|
||||
初始化可以是字面量或任意的表達式. 包級别聲明的變量會在 main 函數執行前完成初始化 (§2.6.2), 局部變量將在聲明語句被執行到的時候初始化.
|
||||
|
||||
一組變量的初始化也可以通過調用一個函數, 由函數返迴的多個返迴值初始化:
|
||||
|
||||
|
||||
@@ -31,22 +31,22 @@ func fib(n int) int {
|
||||
}
|
||||
```
|
||||
|
||||
元組賦值也可以使一繫列瑣碎賦值更緊湊(譯註: 特別是在for循環的初始化部分),
|
||||
元組賦值也可以使一繫列瑣碎賦值更緊湊(譯註: 特别是在for循環的初始化部分),
|
||||
|
||||
```Go
|
||||
i, j, k = 2, 3, 5
|
||||
```
|
||||
|
||||
但如果表達式太復雜的話, 應該儘量避免元組賦值; 因爲一個個單獨的賦值語句的可讀性會更好.
|
||||
但如果表達式太複雜的話, 應該盡量避免元組賦值; 因爲一個個單獨的賦值語句的可讀性會更好.
|
||||
|
||||
某些表達式會產生多個值, 比如調用一個有多個返迴值的函數.
|
||||
當這樣一個函數調用齣現在元組賦值右邊的表達式中時(譯註: 右邊不能再有其他表達式), 左邊變量的數目必鬚和右邊一緻.
|
||||
某些表達式會産生多個值, 比如調用一個有多個返迴值的函數.
|
||||
當這樣一個函數調用齣現在元組賦值右邊的表達式中時(譯註: 右邊不能再有其他表達式), 左邊變量的數目必鬚和右邊一致.
|
||||
|
||||
```Go
|
||||
f, err = os.Open("foo.txt") // function call returns two values
|
||||
```
|
||||
|
||||
通常, 這類函數會用額外的返迴值表達某種錯誤類型, 例如 os.Open 是返迴一個 error 類型的錯誤, 還有一些是返迴佈爾值, 通常被稱爲ok. 在稍後我們看到的三個操作都是類似的行爲. 如果 字典査找(§4.3), 類型斷言(§7.10), 或 通道接收(§8.4.2) 齣現在賦值語句的右邊, 它們都將產生兩個結果, 有一個額外的佈爾結果表示操作是否成功:
|
||||
通常, 這類函數會用額外的返迴值表達某種錯誤類型, 例如 os.Open 是返迴一個 error 類型的錯誤, 還有一些是返迴布爾值, 通常被稱爲ok. 在稍後我們看到的三個操作都是類似的行爲. 如果 字典査找(§4.3), 類型斷言(§7.10), 或 通道接收(§8.4.2) 齣現在賦值語句的右邊, 它們都將産生兩個結果, 有一個額外的布爾結果表示操作是否成功:
|
||||
|
||||
```Go
|
||||
v, ok = m[key] // map lookup
|
||||
@@ -54,7 +54,7 @@ v, ok = x.(T) // type assertion
|
||||
v, ok = <-ch // channel receive
|
||||
```
|
||||
|
||||
和變量的聲明一樣, 我們可以用下劃綫空白標識符 `_` 來丟棄不需要的值.
|
||||
和變量的聲明一樣, 我們可以用下劃線空白標識符 `_` 來丟棄不需要的值.
|
||||
|
||||
```Go
|
||||
_, err = io.Copy(dst, src) // 丟棄字節數
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
### 2.4.2. 可賦值性
|
||||
|
||||
賦值語句是顯示的賦值形式, 但是程序中還有很多地方會發送隱式的賦值行爲: 函數調用將隱式地將調用參數的值賦值給函數的參數變量, 一個返迴語句將隱式地將返迴操作的值賦值給結果變量, 一個復合類型的字面量(§4.2)也會產生賦值行爲. 例如下面的語句:
|
||||
賦值語句是顯示的賦值形式, 但是程序中還有很多地方會發送隱式的賦值行爲: 函數調用將隱式地將調用參數的值賦值給函數的參數變量, 一個返迴語句將隱式地將返迴操作的值賦值給結果變量, 一個複合類型的字面量(§4.2)也會産生賦值行爲. 例如下面的語句:
|
||||
|
||||
```Go
|
||||
medals := []string{"gold", "silver", "bronze"}
|
||||
@@ -16,10 +16,10 @@ medals[2] = "bronze"
|
||||
|
||||
字典和管道的元素, 雖然不是普通的變量, 但是也有類似的隱式賦值行爲.
|
||||
|
||||
不管是隱式還是顯示地賦值, 在賦值語句坐標的變量和右邊最終的求到的值必鬚有相同的數據類型. 更直白地說, 隻有右邊的值對於左邊的變量是可賦值的, 賦值語句纔是允許的.
|
||||
不管是隱式還是顯示地賦值, 在賦值語句坐標的變量和右邊最終的求到的值必鬚有相同的數據類型. 更直白地説, 隻有右邊的值對於左邊的變量是可賦值的, 賦值語句纔是允許的.
|
||||
|
||||
可賦值性的規則對於不同類型有不同要求, 對每個新類型有關的地方我們會專門解釋.
|
||||
對於目前我們已經討論過的類型, 它的規則是簡單的: 類型必鬚完全匹配, nil 可以賦值給任何指鍼或引用類型的變量. 常量(§3.6)有更靈活的規則, 這樣可以避免不必要的顯示類型轉換.
|
||||
對於目前我們已經討論過的類型, 它的規則是簡單的: 類型必鬚完全匹配, nil 可以賦值給任何指針或引用類型的變量. 常量(§3.6)有更靈活的規則, 這樣可以避免不必要的顯示類型轉換.
|
||||
|
||||
對於兩個值是否可以用 `==` 或 `!=` 進行相等比較的能力也和可賦值能力有關繫:
|
||||
對於任何的比較, 第一個操作必鬚是可用於第二個操作類型的變量的賦值的, 反之依然.
|
||||
|
||||
@@ -4,18 +4,18 @@
|
||||
|
||||
```Go
|
||||
x = 1 // 命令變量的賦值
|
||||
*p = true // 通過指鍼間接賦值
|
||||
*p = true // 通過指針間接賦值
|
||||
person.name = "bob" // 結構體字段賦值
|
||||
count[x] = count[x] * scale // 數組, 切片 或 字典的 元素賦值
|
||||
```
|
||||
|
||||
特定的賦值語句和二元算術復合操作有一個簡潔形式, 例如上面最後的語句可以重寫爲:
|
||||
特定的賦值語句和二元算術複合操作有一個簡潔形式, 例如上面最後的語句可以重寫爲:
|
||||
|
||||
```Go
|
||||
count[x] *= scale
|
||||
```
|
||||
|
||||
這樣可以省去對變量表達式的重復計算.
|
||||
這樣可以省去對變量表達式的重複計算.
|
||||
|
||||
數值變量也可以支持 `++` 遞增和 `--` 遞減語句:
|
||||
|
||||
|
||||
@@ -2,22 +2,22 @@
|
||||
|
||||
變量或表達式的類型定義了對應存儲值的特徵, 例如數值的存儲大小(或者是元素的bit個數), 它們在內部是如何表達的, 是否支持一些操作符, 以及它們自己關聯的方法集,
|
||||
|
||||
在任何程序中都會有一些變量有着相同的內部實現, 但是表示完全不同的概唸.
|
||||
在任何程序中都會有一些變量有着相同的內部實現, 但是表示完全不同的概念.
|
||||
例如, int 類型的變量可以用來表示一個循環的迭代索引, 或者一個時間戳, 或者一個文件描述符, 或者一個月份; 一個 float64 類型的變量可以用來表示每秒幾米的速度, 或者是不同溫度單位的溫度;
|
||||
一個字符串可以用來表示一個密碼或者一個顏色的名稱.
|
||||
一個字符串可以用來表示一個密碼或者一個顔色的名稱.
|
||||
|
||||
一個類型的聲明創建了一個新的類型名稱, 和現有類型具有相同的底層結構.
|
||||
新命名的類型提供了一個方法, 用來分隔不同概唸的類型, 卽使它們底層類型相同也是不兼容的.
|
||||
新命名的類型提供了一個方法, 用來分隔不同概念的類型, 卽使它們底層類型相同也是不兼容的.
|
||||
|
||||
```Go
|
||||
type name underlying-type
|
||||
```
|
||||
|
||||
類型的聲明一般齣現在包級別, 因此如果新創建的類型名字名字的首字符大寫, 則在外部包也可以使用.
|
||||
類型的聲明一般齣現在包級别, 因此如果新創建的類型名字名字的首字符大寫, 則在外部包也可以使用.
|
||||
|
||||
爲了說明類型聲明, 我們將不同溫度單位分別定義爲不同的類型:
|
||||
爲了説明類型聲明, 我們將不同溫度單位分别定義爲不同的類型:
|
||||
|
||||
爲了說明類型聲明,讓我們把不同溫度範圍分爲不同的類型:
|
||||
爲了説明類型聲明,讓我們把不同溫度范圍分爲不同的類型:
|
||||
|
||||
```Go
|
||||
gopl.io/ch2/tempconv0
|
||||
@@ -30,7 +30,7 @@ type Celsius float64 // 攝氏溫度
|
||||
type Fahrenheit float64 // 華氏溫度
|
||||
|
||||
const (
|
||||
AbsoluteZeroC Celsius = -273.15 // 絕對零度
|
||||
AbsoluteZeroC Celsius = -273.15 // 絶對零度
|
||||
FreezingC Celsius = 0 // 結冰點溫度
|
||||
BoilingC Celsius = 100 // 沸水問題
|
||||
)
|
||||
@@ -40,13 +40,13 @@ func CToF(c Celsius) Fahrenheit { return Fahrenheit(c*9/5 + 32) }
|
||||
func FToC(f Fahrenheit) Celsius { return Celsius((f - 32) * 5 / 9) }
|
||||
```
|
||||
|
||||
這個包定義了兩種類型, Celsius 和 Fahrenheit 分別對應不同的溫度單位. 它們都有着相同的底層類型 float64, 但是它們是不同的數據類型, 因此它們不可以被相互比較或混在一個表達式計算. 可以區分類型, 可以避免一些像無意中結合單位的溫度進行計算的錯誤; 因爲需要一個類似 Celsius(t) 或 Fahrenheit(t) 顯式的轉型操作纔能將 float64 轉爲對應的類型. Celsius(t) 和 Fahrenheit(t) 是類型轉換操作, 並不是函數調用. 類型轉換不會改變值本身, 但是會使它們的語義發生變化. 另一方面, 函數 CToF 和 FToC 則是對兩個不同的溫度單位進行轉換, 它們會返迴不同的值.
|
||||
這個包定義了兩種類型, Celsius 和 Fahrenheit 分别對應不同的溫度單位. 它們都有着相同的底層類型 float64, 但是它們是不同的數據類型, 因此它們不可以被相互比較或混在一個表達式計算. 可以區分類型, 可以避免一些像無意中結合單位的溫度進行計算的錯誤; 因爲需要一個類似 Celsius(t) 或 Fahrenheit(t) 顯式的轉型操作纔能將 float64 轉爲對應的類型. Celsius(t) 和 Fahrenheit(t) 是類型轉換操作, 併不是函數調用. 類型轉換不會改變值本身, 但是會使它們的語義發生變化. 另一方面, 函數 CToF 和 FToC 則是對兩個不同的溫度單位進行轉換, 它們會返迴不同的值.
|
||||
|
||||
對於每一個類型 T, 都有一個對應的類型轉換操作 T(x), 用於將 x 轉爲 T 類型.
|
||||
隻有當兩個類型的底層基礎類型相同時, 纔允許這種轉型操作, 或者是兩者都是指向相同底層結構的指鍼類型,
|
||||
隻有當兩個類型的底層基礎類型相同時, 纔允許這種轉型操作, 或者是兩者都是指向相同底層結構的指針類型,
|
||||
這些轉換隻改變類型而不會影響值本身. 如果x是可以賦值給T類型的, 那麽x必然可以被轉爲T類型, 但是一般沒有必要.
|
||||
|
||||
數值類型之間的轉型也是允許的, 並且在字符串和一些特定切片之間也是可以轉換的, 在下一章我們會看到這樣的例子. 這類轉換可能改變值的表現. 例如, 將一個浮點數轉爲整數將丟棄小數部分, 將一個字符串轉爲 []byte 切片將拷貝一個字符串數據的副本. 在任何情況下, 運行時不會發送轉換失敗的錯誤(譯註: 錯誤隻會發生在編譯階段).
|
||||
數值類型之間的轉型也是允許的, 併且在字符串和一些特定切片之間也是可以轉換的, 在下一章我們會看到這樣的例子. 這類轉換可能改變值的表現. 例如, 將一個浮點數轉爲整數將丟棄小數部分, 將一個字符串轉爲 []byte 切片將拷貝一個字符串數據的副本. 在任何情況下, 運行時不會發送轉換失敗的錯誤(譯註: 錯誤隻會發生在編譯階段).
|
||||
|
||||
底層數據類型決定了內部結構和表達方式, 也包決定是否可以像底層類型一樣對內置運算符的支持.
|
||||
這意味着, Celsius 和 Fahrenheit 類型的算術行爲和底層的 float64 類型一樣, 正如你所期望的.
|
||||
@@ -70,11 +70,11 @@ fmt.Println(c == f) // compile error: type mismatch
|
||||
fmt.Println(c == Celsius(f)) // "true"!
|
||||
```
|
||||
|
||||
註意最後那個語句. 儘管看起來想函數調用, 但是Celsius(f)類型轉換, 並不會改變值, 它僅僅是改變值的類型而已. 測試爲眞的原因是因爲 c 和 g 都是零值.
|
||||
註意最後那個語句. 盡管看起來想函數調用, 但是Celsius(f)類型轉換, 併不會改變值, 它僅僅是改變值的類型而已. 測試爲眞的原因是因爲 c 和 g 都是零值.
|
||||
|
||||
一個命名的類型可以提供符號方便, 特別是可以避免一遍又一遍地書寫復雜類型(譯註: 例如用匿名的結構體定義變量). 雖然對於像float64這種簡單的底層類型沒有簡潔很多, 但是如果是復雜的類型將會簡潔很多, 正如我們卽將討論的結構體類型:
|
||||
一個命名的類型可以提供符號方便, 特别是可以避免一遍又一遍地書寫複雜類型(譯註: 例如用匿名的結構體定義變量). 雖然對於像float64這種簡單的底層類型沒有簡潔很多, 但是如果是複雜的類型將會簡潔很多, 正如我們卽將討論的結構體類型:
|
||||
|
||||
命名類型還可以爲該類型的值定義新的行爲. 這些行爲表示爲一組關聯到類型的函數, 我們成爲類型的方法集. 我們將在第六章討論方法的細節, 這裡值說寫簡單用法.
|
||||
命名類型還可以爲該類型的值定義新的行爲. 這些行爲表示爲一組關聯到類型的函數, 我們成爲類型的方法集. 我們將在第六章討論方法的細節, 這里值説寫簡單用法.
|
||||
|
||||
下面的聲明, Celsius 類型的參數 c 齣現在了函數名的前面, 表示聲明一個 Celsius 類型的 名叫 String 的方法, 方法返迴 帶着 °C 溫度單位 的參數 c 的數字打印字符串:
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
### 2.6.1. 導入包
|
||||
|
||||
在Go程序中, 每個包都是有一個全侷唯一的導入路徑. 聲明中類似 "gopl.io/ch2/tempconv" 的字符串對應導入路徑. 語言的規範並沒有定義這些字符串的具體含義或包來自哪裡, 它們是由工具來解釋. 當使用 go 工具箱時(第十章), 一個導入路徑代表一個目録中的一個或多個Go源文件.
|
||||
在Go程序中, 每個包都是有一個全局唯一的導入路徑. 聲明中類似 "gopl.io/ch2/tempconv" 的字符串對應導入路徑. 語言的規范併沒有定義這些字符串的具體含義或包來自哪里, 它們是由工具來解釋. 當使用 go 工具箱時(第十章), 一個導入路徑代表一個目録中的一個或多個Go源文件.
|
||||
|
||||
除了到導入路徑, 每個包還有一個包名, 包名一般是短小的(也不要求是是唯一的), 包名在包的聲明處指定. 按照慣例, 一個包的名字和包的導入路徑的最後一個字段相同, 例如 gopl.io/ch2/tempconv 包的名字是 tempconv.
|
||||
|
||||
@@ -34,7 +34,7 @@ func main() {
|
||||
}
|
||||
```
|
||||
|
||||
導入聲明將導入的包綁定到一個短小的名字, 然後通過該名字就可以引用包中導齣的全部內容. 上面的導入聲明將允許我們以 tempconv.CToF 的方式來訪問 gopl.io/ch2/tempconv 包中的內容. 默認情況下, 導入的包綁定到 tempconv 名字, 但是我們也可以綁定到另一個名稱, 以避免名字沖突(§10.3).
|
||||
導入聲明將導入的包綁定到一個短小的名字, 然後通過該名字就可以引用包中導齣的全部內容. 上面的導入聲明將允許我們以 tempconv.CToF 的方式來訪問 gopl.io/ch2/tempconv 包中的內容. 默認情況下, 導入的包綁定到 tempconv 名字, 但是我們也可以綁定到另一個名稱, 以避免名字衝突(§10.3).
|
||||
|
||||
cf 程序將命令行輸入的一個溫度在 Celsius 和 Fahrenheit 之間轉換:
|
||||
|
||||
@@ -48,7 +48,7 @@ $ ./cf -40
|
||||
-40°F = -40°C, -40°C = -40°F
|
||||
```
|
||||
|
||||
如果導入一個包, 但是沒有使用該包將被當作一個錯誤. 這種強製檢測可以有效減少不必要的依賴, 雖然在調試期間會讓人討厭, 因爲刪除一個類似 log.Print("got here!") 的打印可能導緻需要同時刪除 log 包導入聲明, 否則, 編譯器將會發齣一個錯誤. 在這種情況下, 我們需要將不必要的導入刪除或註釋掉.
|
||||
如果導入一個包, 但是沒有使用該包將被當作一個錯誤. 這種強製檢測可以有效減少不必要的依賴, 雖然在調試期間會讓人討厭, 因爲刪除一個類似 log.Print("got here!") 的打印可能導致需要同時刪除 log 包導入聲明, 否則, 編譯器將會發齣一個錯誤. 在這種情況下, 我們需要將不必要的導入刪除或註釋掉.
|
||||
|
||||
不過有更好的解決方案, 我們可以使用 golang.org/x/tools/cmd/goimports 工具, 它可以根據需要自動添加或刪除導入的包; 許多編輯器都可以集成 goimports 工具, 然後在保存文件的時候自動允許它. 類似的還有 gofmt 工具, 可以用來格式化Go源文件.
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ func f() int { return c + 1 }
|
||||
|
||||
如果包中含有多個 .go 文件, 它們按照發給編譯器的順序進行初始化, Go的構建工具首先將 .go 文件根據文件名排序, 然後依次調用編譯器編譯.
|
||||
|
||||
對於在包級別聲明的變量, 如果有初始化表達式則用表達式初始化, 還有一些沒有初始化表達式的, 例如 某些表格數據 初始化並不是一個簡單的賦值過程. 在這種情況下, 我們可以用 init 初始化函數來簡化工作. 每個文件都可以包含多個 init 初始化函數
|
||||
對於在包級别聲明的變量, 如果有初始化表達式則用表達式初始化, 還有一些沒有初始化表達式的, 例如 某些表格數據 初始化併不是一個簡單的賦值過程. 在這種情況下, 我們可以用 init 初始化函數來簡化工作. 每個文件都可以包含多個 init 初始化函數
|
||||
|
||||
```Go
|
||||
func init() { /* ... */ }
|
||||
@@ -22,7 +22,7 @@ func init() { /* ... */ }
|
||||
|
||||
每個包在解決依賴的前提下, 以導入聲明的順序初始化, 每個包隻會被初始化一次. 因此, 如果一個 p 包導入了 q 包, 那麽在 p 包初始化的時候可以認爲 q 包已經初始化過了. 初始化工作是自下而上進行的, main 包最後被初始化. 以這種方式, 確保 在 main 函數執行之前, 所有的包都已經初始化了.
|
||||
|
||||
下面的代碼定義了一個 PopCount 函數, 用於返迴一個數字中含二進製1bit的個數. 它使用 init 初始化函數來生成輔助表格 pc, pc 表格用於處理每個8bit寬度的數字含二進製的1bit的個數, 這樣的話在處理64bit寬度的數字時就沒有必要循環64次, 隻需要8次査表就可以了. (這並不是最快的統計1bit數目的算法, 但是他可以方便演示init函數的用法, 並且演示了如果預生成輔助表格, 這是編程中常用的技術.)
|
||||
下面的代碼定義了一個 PopCount 函數, 用於返迴一個數字中含二進製1bit的個數. 它使用 init 初始化函數來生成輔助表格 pc, pc 表格用於處理每個8bit寬度的數字含二進製的1bit的個數, 這樣的話在處理64bit寬度的數字時就沒有必要循環64次, 隻需要8次査表就可以了. (這併不是最快的統計1bit數目的算法, 但是他可以方便演示init函數的用法, 併且演示了如果預生成輔助表格, 這是編程中常用的技術.)
|
||||
|
||||
```Go
|
||||
gopl.io/ch2/popcount
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
## 2.6. 包和文件
|
||||
|
||||
Go語言中的包和其他語言的庫或模塊概唸類似, 目的都是爲了支持模塊好, 封裝, 單獨編譯和代碼重用. 一個包的源代碼保存在一個或多個以.爲後綴名的文件中, 通常一個包所在目録路徑的後綴是包的導入路徑; 例如包 gopl.io/ch1/helloworld 對應的目録路徑是 $GOPATH/src/gopl.io/ch1/helloworld.
|
||||
Go語言中的包和其他語言的庫或模塊概念類似, 目的都是爲了支持模塊好, 封裝, 單獨編譯和代碼重用. 一個包的源代碼保存在一個或多個以.爲後綴名的文件中, 通常一個包所在目録路徑的後綴是包的導入路徑; 例如包 gopl.io/ch1/helloworld 對應的目録路徑是 $GOPATH/src/gopl.io/ch1/helloworld.
|
||||
|
||||
每個包作爲一個獨立的名字空間. 例如, 在 image 包中的 Decode 函數 和 unicode/utf16 包中的 Decode 函數是不同的. 要在外部包引用該函數, 必鬚顯式使用 image.Decode 或 utf16.Decode 訪問.
|
||||
|
||||
@@ -8,7 +8,7 @@ Go語言中的包和其他語言的庫或模塊概唸類似, 目的都是爲了
|
||||
|
||||
爲了演示基本的用法, 假設我們的溫度轉換軟件已經很流行, 我們希望到Go社區也能使用這個包. 我們該如何做呢?
|
||||
|
||||
讓我們創建一個名爲 gopl.io/ch2/tempconv 的包, 是前面例子的一個改進版本. (我們約定我們的例子都是以章節順序來編號的, 這樣的路徑更容易閱讀.) 包代碼存儲在兩個文件, 用來演示如何在一個文件聲明然後在其他的文件訪問; 在現實中, 這樣小的包一般值需要一個文件.
|
||||
讓我們創建一個名爲 gopl.io/ch2/tempconv 的包, 是前面例子的一個改進版本. (我們約定我們的例子都是以章節順序來編號的, 這樣的路徑更容易閲讀.) 包代碼存儲在兩個文件, 用來演示如何在一個文件聲明然後在其他的文件訪問; 在現實中, 這樣小的包一般值需要一個文件.
|
||||
|
||||
我們把變量的聲明, 對應的常量, 還有方法都放到 tempconv.go 文件:
|
||||
|
||||
@@ -44,10 +44,10 @@ func CToF(c Celsius) Fahrenheit { return Fahrenheit(c*9/5 + 32) }
|
||||
func FToC(f Fahrenheit) Celsius { return Celsius((f - 32) * 5 / 9) }
|
||||
```
|
||||
|
||||
每個文件都是以包的聲明語句開始, 用來指定包的名字. 當包被導入的時候, 包內部的成員將通過類似 tempconv.CToF 的方式訪問. 包級別的名字, 例如在一個文件聲明的類型和常量, 在同一個包的其他文件也是可以直接訪問的,
|
||||
就好像所有代碼都在一個文件一樣. 要註意的是 tempconv.go 文件導入了 fmt 包, 但是 conv.go 文件並沒有, 因爲它並沒有用到 fmt 包.
|
||||
每個文件都是以包的聲明語句開始, 用來指定包的名字. 當包被導入的時候, 包內部的成員將通過類似 tempconv.CToF 的方式訪問. 包級别的名字, 例如在一個文件聲明的類型和常量, 在同一個包的其他文件也是可以直接訪問的,
|
||||
就好像所有代碼都在一個文件一樣. 要註意的是 tempconv.go 文件導入了 fmt 包, 但是 conv.go 文件併沒有, 因爲它併沒有用到 fmt 包.
|
||||
|
||||
因爲包級別的常量名都是以大寫字母開頭, 它們也是可以像 tempconv.AbsoluteZeroC 這樣被訪問的:
|
||||
因爲包級别的常量名都是以大寫字母開頭, 它們也是可以像 tempconv.AbsoluteZeroC 這樣被訪問的:
|
||||
|
||||
```Go
|
||||
fmt.Printf("Brrrr! %v\n", tempconv.AbsoluteZeroC) // "Brrrr! -273.15°C"
|
||||
@@ -62,8 +62,8 @@ fmt.Println(tempconv.CToF(tempconv.BoilingC)) // "212°F"
|
||||
在每個文件的包聲明前僅跟着的註釋是包註釋(§10.7.4). 通常, 第一句應該先是包的功能概要.
|
||||
一個包通常隻有一個文件有包註釋. 如果包註釋很大, 通常會放到一個獨立的 doc.go 文件中.
|
||||
|
||||
**練習 2.1:** 向 tempconv 包 添加類型, 常量和函數用來處理 Kelvin 絕對溫度的轉換,
|
||||
Kelvin 絕對零度是 −273.15°C, Kelvin 絕對溫度1K和攝氏度1°C的單位間隔是一樣的.
|
||||
**練習 2.1:** 向 tempconv 包 添加類型, 常量和函數用來處理 Kelvin 絶對溫度的轉換,
|
||||
Kelvin 絶對零度是 −273.15°C, Kelvin 絶對溫度1K和攝氏度1°C的單位間隔是一樣的.
|
||||
|
||||
{% include "./ch2-06-1.md" %}
|
||||
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
## 2.7. 作用域
|
||||
|
||||
一個聲明語句將程序中的實體和一個名字關聯, 比如一個函數或一個變量. 聲明的作用域是指源代碼中可以有效使用這個名字的範圍.
|
||||
一個聲明語句將程序中的實體和一個名字關聯, 比如一個函數或一個變量. 聲明的作用域是指源代碼中可以有效使用這個名字的范圍.
|
||||
|
||||
不要將作用域和生命週期混爲一談. 聲明的作用域對應的是一個源代碼的文本區域; 它是一個編譯時的屬性. 一個變量的生命週期是程序運行時變量存在的有效時間段, 在此時間區域內存它可以被程序的其他部分引用. 是一個運行時的概唸.
|
||||
不要將作用域和生命週期混爲一談. 聲明的作用域對應的是一個源代碼的文本區域; 它是一個編譯時的屬性. 一個變量的生命週期是程序運行時變量存在的有效時間段, 在此時間區域內存它可以被程序的其他部分引用. 是一個運行時的概念.
|
||||
|
||||
語法塊是由花括弧所包含的一繫列語句, 就像函數體或循環體那樣. 語法塊內部聲明的名字是無法被外部語法塊訪問的. 語法決定了內部聲明的名字的作用域範圍. 我們可以這樣理解, 語法塊可以包含其他類似組批量聲明等沒有用花括弧包含的代碼, 我們稱之爲詞滙塊. 有一個語法決爲整個源代碼, 稱爲全侷塊; 然後是每個包的語法決; 每個 for, if 和 switch 語句的語法決; 每個 switch 或 select 分支的 語法決; 當然也包含顯示編寫的語法塊(花括弧包含).
|
||||
語法塊是由花括弧所包含的一繫列語句, 就像函數體或循環體那樣. 語法塊內部聲明的名字是無法被外部語法塊訪問的. 語法決定了內部聲明的名字的作用域范圍. 我們可以這樣理解, 語法塊可以包含其他類似組批量聲明等沒有用花括弧包含的代碼, 我們稱之爲詞滙塊. 有一個語法決爲整個源代碼, 稱爲全局塊; 然後是每個包的語法決; 每個 for, if 和 switch 語句的語法決; 每個 switch 或 select 分支的 語法決; 當然也包含顯示編寫的語法塊(花括弧包含).
|
||||
|
||||
聲明的詞法域決定了作用域範圍是大還是小. 內置的類型, 函數和常量, 比如 int, len 和 true 等是在全侷作用域的, 可以在整個程序中直接使用. 任何在在函數外部(也就是包級作用域)聲明的名字可以在同一個包的任何Go文件訪問. 導入的包, 例如 tempconv 導入的 fmt 包, 則是對應文件級的作用域, 因此隻能在當前的文件中訪問 fmt 包, 當前包的其它文件無法訪問當前文件導入的包. 還有許多聲明, 比如 tempconv.CToF 函數中的變量 c, 則是侷部作用域的, 它隻能在函數內部(甚至隻能是某些部分)訪問.
|
||||
聲明的詞法域決定了作用域范圍是大還是小. 內置的類型, 函數和常量, 比如 int, len 和 true 等是在全局作用域的, 可以在整個程序中直接使用. 任何在在函數外部(也就是包級作用域)聲明的名字可以在同一個包的任何Go文件訪問. 導入的包, 例如 tempconv 導入的 fmt 包, 則是對應文件級的作用域, 因此隻能在當前的文件中訪問 fmt 包, 當前包的其它文件無法訪問當前文件導入的包. 還有許多聲明, 比如 tempconv.CToF 函數中的變量 c, 則是局部作用域的, 它隻能在函數內部(甚至隻能是某些部分)訪問.
|
||||
|
||||
控製流標簽, 例如 break, continue 或 goto 後面跟着的那種標簽, 則是函數級的作用域.
|
||||
控製流標籤, 例如 break, continue 或 goto 後面跟着的那種標籤, 則是函數級的作用域.
|
||||
|
||||
一個程序可能包含多個同名的聲明, 隻有它們在不同的詞法域就沒有關繫. 例如, 你可以聲明一個侷部變量, 和包級的變量同名. 或者是 2.3.3節的那樣, 你可以將一個函數參數的名字聲明爲 new, 雖然內置的new是全侷作用域的. 但是物極必反, 如果濫用重名的特性, 可能導緻程序很難閱讀.
|
||||
一個程序可能包含多個同名的聲明, 隻有它們在不同的詞法域就沒有關繫. 例如, 你可以聲明一個局部變量, 和包級的變量同名. 或者是 2.3.3節的那樣, 你可以將一個函數參數的名字聲明爲 new, 雖然內置的new是全局作用域的. 但是物極必反, 如果濫用重名的特性, 可能導致程序很難閲讀.
|
||||
|
||||
當編譯器遇到一個名字引用, 它看起來像一個聲明, 它首先從最內層的詞法域向全侷的作用域査找. 如果査找失敗, 則報告 "未聲明的名字" 這樣的錯誤. 如果名字在內部和外部的塊分別聲明, 則內部塊的聲明首先被找到. 在這種情況下, 內部聲明屏蔽了外部同名的聲明, 讓外部的聲明無法被訪問:
|
||||
當編譯器遇到一個名字引用, 它看起來像一個聲明, 它首先從最內層的詞法域向全局的作用域査找. 如果査找失敗, 則報告 "未聲明的名字" 這樣的錯誤. 如果名字在內部和外部的塊分别聲明, 則內部塊的聲明首先被找到. 在這種情況下, 內部聲明屏蔽了外部同名的聲明, 讓外部的聲明無法被訪問:
|
||||
|
||||
```Go
|
||||
func f() {}
|
||||
@@ -42,9 +42,9 @@ func main() {
|
||||
}
|
||||
```
|
||||
|
||||
在 `x[i]` 和 `x + 'A' - 'a'` 聲明初始化的表達式中都引用了外部作用域聲明的x變量, 稍後我們會解釋這個. (註意, 後面的表達式和unicode.ToUpper並不等價.)
|
||||
在 `x[i]` 和 `x + 'A' - 'a'` 聲明初始化的表達式中都引用了外部作用域聲明的x變量, 稍後我們會解釋這個. (註意, 後面的表達式和unicode.ToUpper併不等價.)
|
||||
|
||||
正如上面所示, 並不是所有的詞法域都顯示地對應到由花括弧包含的語句; 還有一些隱含的規則. 上面的for語句創建了兩個詞法域: 花括弧包含的是顯式的部分是for的循環體, 另外一個隱式的部分則是循環的初始化部分, 比如用於迭代變量 i 的初始化. 隱式的部分的作用域還包含條件測試部分和循環後的迭代部分(i++), 當然也包含循環體.
|
||||
正如上面所示, 併不是所有的詞法域都顯示地對應到由花括弧包含的語句; 還有一些隱含的規則. 上面的for語句創建了兩個詞法域: 花括弧包含的是顯式的部分是for的循環體, 另外一個隱式的部分則是循環的初始化部分, 比如用於迭代變量 i 的初始化. 隱式的部分的作用域還包含條件測試部分和循環後的迭代部分(i++), 當然也包含循環體.
|
||||
|
||||
下面的例子同樣有三個不同的x變量, 每個聲明在不同的塊, 一個在函數體塊, 一個在for語句塊, 一個在循環體塊; 隻有兩個塊是顯式創建的:
|
||||
|
||||
@@ -58,7 +58,7 @@ func main() {
|
||||
}
|
||||
```
|
||||
|
||||
和彿如循環類似, if和switch語句也會在條件部分創建隱式塊, 還有它們對應的執行體塊. 下面的 if-else 測試鏈演示的 x 和 y 的作用域範圍:
|
||||
和彿如循環類似, if和switch語句也會在條件部分創建隱式塊, 還有它們對應的執行體塊. 下面的 if-else 測試鏈演示的 x 和 y 的作用域范圍:
|
||||
|
||||
```Go
|
||||
if x := f(); x == 0 {
|
||||
@@ -73,7 +73,7 @@ fmt.Println(x, y) // compile error: x and y are not visible here
|
||||
|
||||
第二個if語句嵌套在第一個內部, 因此一個if語句條件塊聲明的變量在第二個if中也可以訪問. switch語句的每個分支也有類似的規則: 條件部分爲一個隱式塊, 然後每個是每個分支的主體塊.
|
||||
|
||||
在包級別, 聲明的順序並不會影響作用域範圍, 因此一個先聲明的可以引用它自身或者是引用後面的一個聲明, 這可以讓我們定義一些相互嵌套或遞歸的類型或函數. 但是如果一個變量或常量遞歸引用了自身, 則會產生編譯錯誤.
|
||||
在包級别, 聲明的順序併不會影響作用域范圍, 因此一個先聲明的可以引用它自身或者是引用後面的一個聲明, 這可以讓我們定義一些相互嵌套或遞歸的類型或函數. 但是如果一個變量或常量遞歸引用了自身, 則會産生編譯錯誤.
|
||||
|
||||
在這個程序中:
|
||||
|
||||
@@ -85,7 +85,7 @@ f.ReadByte() // compile error: undefined f
|
||||
f.Close() // compile error: undefined f
|
||||
```
|
||||
|
||||
變量 f 的作用域隻有if語句內, 因此後面的語句將無法引入它, 將導緻編譯錯誤. 你可能會收到一個侷部變量f沒有聲明的錯誤提示, 具體錯誤信息依賴編譯器的實現.
|
||||
變量 f 的作用域隻有if語句內, 因此後面的語句將無法引入它, 將導致編譯錯誤. 你可能會收到一個局部變量f沒有聲明的錯誤提示, 具體錯誤信息依賴編譯器的實現.
|
||||
|
||||
通常需要在if之前聲明變量, 這樣可以確保後面的語句依然可以訪問變量:
|
||||
|
||||
@@ -112,7 +112,7 @@ if f, err := os.Open(fname); err != nil {
|
||||
|
||||
但這不是Go推薦的做法, Go的習慣是在if中處理錯誤然後直接返迴, 這樣可以確保正常成功執行的語句不需要代碼縮進.
|
||||
|
||||
要特別註意短的變量聲明的作用域範圍, 考慮下面的程序, 它的目的是獲取當前的工作目録然後保存到一個包級的變量中. 這可以通過直接調用 os.Getwd 完成, 但是將這個從主邏輯中分離齣來可能會更好, 特別是在需要處理錯誤的時候. 函數 log.Fatalf 打印信息, 然後調用 os.Exit(1) 終止程序.
|
||||
要特别註意短的變量聲明的作用域范圍, 考慮下面的程序, 它的目的是穫取當前的工作目録然後保存到一個包級的變量中. 這可以通過直接調用 os.Getwd 完成, 但是將這個從主邏輯中分離齣來可能會更好, 特别是在需要處理錯誤的時候. 函數 log.Fatalf 打印信息, 然後調用 os.Exit(1) 終止程序.
|
||||
|
||||
```Go
|
||||
var cwd string
|
||||
@@ -125,9 +125,9 @@ func init() {
|
||||
}
|
||||
```
|
||||
|
||||
雖然cwd在外部已經聲明過, 但是 `:=` 語句還是將 cwd 和 err 重新聲明爲侷部變量. 內部聲明的 cwd 將屏蔽外部的聲明, 因此上面的代碼並不會更新包級聲明的 cwd 變量.
|
||||
雖然cwd在外部已經聲明過, 但是 `:=` 語句還是將 cwd 和 err 重新聲明爲局部變量. 內部聲明的 cwd 將屏蔽外部的聲明, 因此上面的代碼併不會更新包級聲明的 cwd 變量.
|
||||
|
||||
當前的編譯器將檢測到侷部聲明的cwd並沒有本使用, 然後報告這可能是一個錯誤, 但是這種檢測並不可靠. 一些小的代碼變更, 例如增加一個侷部cwd的打印語句, 就可能導緻這種檢測失效.
|
||||
當前的編譯器將檢測到局部聲明的cwd併沒有本使用, 然後報告這可能是一個錯誤, 但是這種檢測併不可靠. 一些小的代碼變更, 例如增加一個局部cwd的打印語句, 就可能導致這種檢測失效.
|
||||
|
||||
```Go
|
||||
var cwd string
|
||||
@@ -141,7 +141,7 @@ func init() {
|
||||
}
|
||||
```
|
||||
|
||||
全侷的cwd變量依然是沒有被正確初始化的, 而且看似正常的日誌輸齣更是這個BUG更加隱晦.
|
||||
全局的cwd變量依然是沒有被正確初始化的, 而且看似正常的日誌輸齣更是這個BUG更加隱晦.
|
||||
|
||||
有許多方式可以避免齣現類似潛在的問題. 最直接的是通過單獨聲明err變量, 來避免使用 `:=` 的簡短聲明方式:
|
||||
|
||||
@@ -159,4 +159,4 @@ func init() {
|
||||
|
||||
我們已經看到包, 文件, 聲明和語句如何來表達一個程序結構. 在下面的兩個章節, 我們將探討數據的結構.
|
||||
|
||||
**譯註: 本章的詞法域和作用域概唸有些混淆, 需要重譯一遍.**
|
||||
**譯註: 本章的詞法域和作用域概念有些混淆, 需要重譯一遍.**
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# 第2章 程序結構
|
||||
|
||||
Go語言和任何其他語言一樣, 一個大的程序是有很多小的基礎構件組成的. 變量保存值. 簡單的加法和減法運算被組合成較大的表達式. 基礎類型被聚合爲數組或結構體. 然後使用if和for之類的控製語句來組織和控製表達式的執行順序. 然後多個語句被組織到函數中, 以便代碼的隔離和復用. 函數以源文件和包的方式組織.
|
||||
Go語言和任何其他語言一樣, 一個大的程序是有很多小的基礎構件組成的. 變量保存值. 簡單的加法和減法運算被組合成較大的表達式. 基礎類型被聚合爲數組或結構體. 然後使用if和for之類的控製語句來組織和控製表達式的執行順序. 然後多個語句被組織到函數中, 以便代碼的隔離和複用. 函數以源文件和包的方式組織.
|
||||
|
||||
我們已經在前面的章節的例子中看到了大部分的例子. 在本章中, 我們將深入討論Go程序的基礎結構的一些細節. 每個示例程序都是刻意寫的簡單, 這樣我們可以減少被復雜的算法和數據結構所乾擾, 從而專註於語言本身的學習.
|
||||
我們已經在前面的章節的例子中看到了大部分的例子. 在本章中, 我們將深入討論Go程序的基礎結構的一些細節. 每個示例程序都是刻意寫的簡單, 這樣我們可以減少被複雜的算法和數據結構所榦擾, 從而專註於語言本身的學習.
|
||||
|
||||
Reference in New Issue
Block a user