mirror of
https://github.com/gopl-zh/gopl-zh.github.com.git
synced 2025-12-17 19:24:19 +08:00
回到简体
This commit is contained in:
@@ -1,8 +1,8 @@
|
||||
## 2.1. 命名
|
||||
|
||||
Go語言中的函數名、變量名、常量名、類型名、語句標號和包名等所有的命名,都遵循一個簡單的命名規則:一個名字必須以一個字母(Unicode字母)或下劃線開頭,後面可以跟任意數量的字母、數字或下劃線。大寫字母和小寫字母是不同的:heapSort和Heapsort是兩個不同的名字。
|
||||
Go语言中的函数名、变量名、常量名、类型名、语句标号和包名等所有的命名,都遵循一个简单的命名规则:一个名字必须以一个字母(Unicode字母)或下划线开头,后面可以跟任意数量的字母、数字或下划线。大写字母和小写字母是不同的:heapSort和Heapsort是两个不同的名字。
|
||||
|
||||
Go語言中類似if和switch的關鍵字有25個;關鍵字不能用於自定義名字,隻能在特定語法結構中使用。
|
||||
Go语言中类似if和switch的关键字有25个;关键字不能用于自定义名字,只能在特定语法结构中使用。
|
||||
|
||||
```
|
||||
break default func interface select
|
||||
@@ -12,25 +12,25 @@ const fallthrough if range type
|
||||
continue for import return var
|
||||
```
|
||||
|
||||
此外,還有大約30多個預定義的名字,比如int和true等,主要對應內建的常量、類型和函數。
|
||||
此外,还有大约30多个预定义的名字,比如int和true等,主要对应内建的常量、类型和函数。
|
||||
|
||||
```
|
||||
內建常量: true false iota nil
|
||||
内建常量: true false iota nil
|
||||
|
||||
內建類型: int int8 int16 int32 int64
|
||||
内建类型: int int8 int16 int32 int64
|
||||
uint uint8 uint16 uint32 uint64 uintptr
|
||||
float32 float64 complex128 complex64
|
||||
bool byte rune string error
|
||||
|
||||
內建函數: make len cap new append copy close delete
|
||||
内建函数: make len cap new append copy close delete
|
||||
complex real imag
|
||||
panic recover
|
||||
```
|
||||
|
||||
這些內部預先定義的名字併不是關鍵字,你可以再定義中重新使用它們。在一些特殊的場景中重新定義它們也是有意義的,但是也要註意避免過度而引起語義混亂。
|
||||
这些内部预先定义的名字并不是关键字,你可以再定义中重新使用它们。在一些特殊的场景中重新定义它们也是有意义的,但是也要注意避免过度而引起语义混乱。
|
||||
|
||||
如果一個名字是在函數內部定義,那麽它的就隻在函數內部有效。如果是在函數外部定義,那麽將在當前包的所有文件中都可以訪問。名字的開頭字母的大小寫決定了名字在包外的可見性。如果一個名字是大寫字母開頭的(譯註:必須是在函數外部定義的包級名字;包級函數名本身也是包級名字),那麽它將是導出的,也就是説可以被外部的包訪問,例如fmt包的Printf函數就是導出的,可以在fmt包外部訪問。包本身的名字一般總是用小寫字母。
|
||||
如果一个名字是在函数内部定义,那么它的就只在函数内部有效。如果是在函数外部定义,那么将在当前包的所有文件中都可以访问。名字的开头字母的大小写决定了名字在包外的可见性。如果一个名字是大写字母开头的(译注:必须是在函数外部定义的包级名字;包级函数名本身也是包级名字),那么它将是导出的,也就是说可以被外部的包访问,例如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. 聲明
|
||||
## 2.2. 声明
|
||||
|
||||
聲明語句定義了程序的各種實體對象以及部分或全部的屬性。Go語言主要有四種類型的聲明語句:var、const、type和func,分别對應變量、常量、類型和函數實體對象的聲明。這一章我們重點討論變量和類型的聲明,第三章將討論常量的聲明,第五章將討論函數的聲明。
|
||||
声明语句定义了程序的各种实体对象以及部分或全部的属性。Go语言主要有四种类型的声明语句:var、const、type和func,分别对应变量、常量、类型和函数实体对象的声明。这一章我们重点讨论变量和类型的声明,第三章将讨论常量的声明,第五章将讨论函数的声明。
|
||||
|
||||
一個Go語言編寫的程序對應一個或多個以.go爲文件後綴名的源文件中。每個源文件以包的聲明語句開始,説明該源文件是屬於哪個包。包聲明語句之後是import語句導入依賴的其它包,然後是包一級的類型、變量、常量、函數的聲明語句,包一級的各種類型的聲明語句的順序無關緊要(譯註:函數內部的名字則必須先聲明之後才能使用)。例如,下面的例子中聲明了一個常量、一個函數和兩個變量:
|
||||
一个Go语言编写的程序对应一个或多个以.go为文件后缀名的源文件中。每个源文件以包的声明语句开始,说明该源文件是属于哪个包。包声明语句之后是import语句导入依赖的其它包,然后是包一级的类型、变量、常量、函数的声明语句,包一级的各种类型的声明语句的顺序无关紧要(译注:函数内部的名字则必须先声明之后才能使用)。例如,下面的例子中声明了一个常量、一个函数和两个变量:
|
||||
|
||||
<u><i>gopl.io/ch2/boiling</i></u>
|
||||
```Go
|
||||
@@ -22,11 +22,11 @@ func main() {
|
||||
}
|
||||
```
|
||||
|
||||
其中常量boilingF是在包一級范圍聲明語句聲明的,然後f和c兩個變量是在main函數內部聲明的聲明語句聲明的。在包一級聲明語句聲明的名字可在整個包對應的每個源文件中訪問,而不是僅僅在其聲明語句所在的源文件中訪問。相比之下,局部聲明的名字就隻能在函數內部很小的范圍被訪問。
|
||||
其中常量boilingF是在包一级范围声明语句声明的,然后f和c两个变量是在main函数内部声明的声明语句声明的。在包一级声明语句声明的名字可在整个包对应的每个源文件中访问,而不是仅仅在其声明语句所在的源文件中访问。相比之下,局部声明的名字就只能在函数内部很小的范围被访问。
|
||||
|
||||
一個函數的聲明由一個函數名字、參數列表(由函數的調用者提供參數變量的具體值)、一個可選的返迴值列表和包含函數定義的函數體組成。如果函數沒有返迴值,那麽返迴值列表是省略的。執行函數從函數的第一個語句開始,依次順序執行直到遇到renturn返迴語句,如果沒有返迴語句則是執行到函數末尾,然後返迴到函數調用者。
|
||||
一个函数的声明由一个函数名字、参数列表(由函数的调用者提供参数变量的具体值)、一个可选的返回值列表和包含函数定义的函数体组成。如果函数没有返回值,那么返回值列表是省略的。执行函数从函数的第一个语句开始,依次顺序执行直到遇到renturn返回语句,如果没有返回语句则是执行到函数末尾,然后返回到函数调用者。
|
||||
|
||||
我們已經看到過很多函數聲明和函數調用的例子了,在第五章將深入討論函數的相關細節,這里隻簡單解釋下。下面的fToC函數封裝了溫度轉換的處理邏輯,這樣它隻需要被定義一次,就可以在多個地方多次被使用。在這個例子中,main函數就調用了兩次fToC函數,分别是使用在局部定義的兩個常量作爲調用函數的參數。
|
||||
我们已经看到过很多函数声明和函数调用的例子了,在第五章将深入讨论函数的相关细节,这里只简单解释下。下面的fToC函数封装了温度转换的处理逻辑,这样它只需要被定义一次,就可以在多个地方多次被使用。在这个例子中,main函数就调用了两次fToC函数,分别是使用在局部定义的两个常量作为调用函数的参数。
|
||||
|
||||
<u><i>gopl.io/ch2/ftoc</i></u>
|
||||
```Go
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
### 2.3.1. 簡短變量聲明
|
||||
### 2.3.1. 简短变量声明
|
||||
|
||||
在函數內部,有一種稱爲簡短變量聲明語句的形式可用於聲明和初始化局部變量。它以“名字 := 表達式”形式聲明變量,變量的類型根據表達式來自動推導。下面是lissajous函數中的三個簡短變量聲明語句(§1.4):
|
||||
在函数内部,有一种称为简短变量声明语句的形式可用于声明和初始化局部变量。它以“名字 := 表达式”形式声明变量,变量的类型根据表达式来自动推导。下面是lissajous函数中的三个简短变量声明语句(§1.4):
|
||||
|
||||
```Go
|
||||
anim := gif.GIF{LoopCount: nframes}
|
||||
@@ -8,7 +8,7 @@ freq := rand.Float64() * 3.0
|
||||
t := 0.0
|
||||
```
|
||||
|
||||
因爲簡潔和靈活的特點,簡短變量聲明被廣泛用於大部分的局部變量的聲明和初始化。var形式的聲明語句往往是用於需要顯式指定變量類型地方,或者因爲變量稍後會被重新賦值而初始值無關緊要的地方。
|
||||
因为简洁和灵活的特点,简短变量声明被广泛用于大部分的局部变量的声明和初始化。var形式的声明语句往往是用于需要显式指定变量类型地方,或者因为变量稍后会被重新赋值而初始值无关紧要的地方。
|
||||
|
||||
```Go
|
||||
i := 100 // an int
|
||||
@@ -18,21 +18,21 @@ var err error
|
||||
var p Point
|
||||
```
|
||||
|
||||
和var形式聲明變語句一樣,簡短變量聲明語句也可以用來聲明和初始化一組變量:
|
||||
和var形式声明变语句一样,简短变量声明语句也可以用来声明和初始化一组变量:
|
||||
|
||||
```Go
|
||||
i, j := 0, 1
|
||||
```
|
||||
|
||||
但是這種同時聲明多個變量的方式應該限製隻在可以提高代碼可讀性的地方使用,比如for語句的循環的初始化語句部分。
|
||||
但是这种同时声明多个变量的方式应该限制只在可以提高代码可读性的地方使用,比如for语句的循环的初始化语句部分。
|
||||
|
||||
請記住“:=”是一個變量聲明語句,而“=‘是一個變量賦值操作。也不要混淆多個變量的聲明和元組的多重賦值(§2.4.1),後者是將右邊各個的表達式值賦值給左邊對應位置的各個變量:
|
||||
请记住“:=”是一个变量声明语句,而“=‘是一个变量赋值操作。也不要混淆多个变量的声明和元组的多重赋值(§2.4.1),后者是将右边各个的表达式值赋值给左边对应位置的各个变量:
|
||||
|
||||
```Go
|
||||
i, j = j, i // 交換 i 和 j 的值
|
||||
i, j = j, i // 交换 i 和 j 的值
|
||||
```
|
||||
|
||||
和普通var形式的變量聲明語句一樣,簡短變量聲明語句也可以用函數的返迴值來聲明和初始化變量,像下面的os.Open函數調用將返迴兩個值:
|
||||
和普通var形式的变量声明语句一样,简短变量声明语句也可以用函数的返回值来声明和初始化变量,像下面的os.Open函数调用将返回两个值:
|
||||
|
||||
```Go
|
||||
f, err := os.Open(name)
|
||||
@@ -43,9 +43,9 @@ if err != nil {
|
||||
f.Close()
|
||||
```
|
||||
|
||||
這里有一個比較微妙的地方:簡短變量聲明左邊的變量可能併不是全部都是剛剛聲明的。如果有一些已經在相同的詞法域聲明過了(§2.7),那麽簡短變量聲明語句對這些已經聲明過的變量就隻有賦值行爲了。
|
||||
这里有一个比较微妙的地方:简短变量声明左边的变量可能并不是全部都是刚刚声明的。如果有一些已经在相同的词法域声明过了(§2.7),那么简短变量声明语句对这些已经声明过的变量就只有赋值行为了。
|
||||
|
||||
在下面的代碼中,第一個語句聲明了in和err兩個變量。在第二個語句隻聲明了out一個變量,然後對已經聲明的err進行了賦值操作。
|
||||
在下面的代码中,第一个语句声明了in和err两个变量。在第二个语句只声明了out一个变量,然后对已经声明的err进行了赋值操作。
|
||||
|
||||
```Go
|
||||
in, err := os.Open(infile)
|
||||
@@ -53,7 +53,7 @@ in, err := os.Open(infile)
|
||||
out, err := os.Create(outfile)
|
||||
```
|
||||
|
||||
簡短變量聲明語句中必須至少要聲明一個新的變量,下面的代碼將不能編譯通過:
|
||||
简短变量声明语句中必须至少要声明一个新的变量,下面的代码将不能编译通过:
|
||||
|
||||
```Go
|
||||
f, err := os.Open(infile)
|
||||
@@ -61,9 +61,9 @@ f, err := os.Open(infile)
|
||||
f, err := os.Create(outfile) // compile error: no new variables
|
||||
```
|
||||
|
||||
解決的方法是第二個簡短變量聲明語句改用普通的多重賦值語言。
|
||||
解决的方法是第二个简短变量声明语句改用普通的多重赋值语言。
|
||||
|
||||
簡短變量聲明語句隻有對已經在同級詞法域聲明過的變量才和賦值操作語句等價,如果變量是在外部詞法域聲明的,那麽簡短變量聲明語句將會在當前詞法域重新聲明一個新的變量。我們在本章後面將會看到類似的例子。
|
||||
简短变量声明语句只有对已经在同级词法域声明过的变量才和赋值操作语句等价,如果变量是在外部词法域声明的,那么简短变量声明语句将会在当前词法域重新声明一个新的变量。我们在本章后面将会看到类似的例子。
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
### 2.3.2. 指針
|
||||
### 2.3.2. 指针
|
||||
|
||||
一個變量對應一個保存了變量對應類型值的內存空間。普通變量在聲明語句創建時被綁定到一個變量名,比如叫x的變量,但是還有很多變量始終以表達式方式引入,例如x[i]或x.f變量。所有這些表達式一般都是讀取一個變量的值,除非它們是出現在賦值語句的左邊,這種時候是給對應變量賦予一個新的值。
|
||||
一个变量对应一个保存了变量对应类型值的内存空间。普通变量在声明语句创建时被绑定到一个变量名,比如叫x的变量,但是还有很多变量始终以表达式方式引入,例如x[i]或x.f变量。所有这些表达式一般都是读取一个变量的值,除非它们是出现在赋值语句的左边,这种时候是给对应变量赋予一个新的值。
|
||||
|
||||
一個指針的值是另一個變量的地址。一個指針對應變量在內存中的存儲位置。併不是每一個值都會有一個內存地址,但是對於每一個變量必然有對應的內存地址。通過指針,我們可以直接讀或更新對應變量的值,而不需要知道該變量的名字(如果變量有名字的話)。
|
||||
一个指针的值是另一个变量的地址。一个指针对应变量在内存中的存储位置。并不是每一个值都会有一个内存地址,但是对于每一个变量必然有对应的内存地址。通过指针,我们可以直接读或更新对应变量的值,而不需要知道该变量的名字(如果变量有名字的话)。
|
||||
|
||||
如果用“var x int”聲明語句聲明一個x變量,那麽&x表達式(取x變量的內存地址)將産生一個指向該整數變量的指針,指針對應的數據類型是`*int`,指針被稱之爲“指向int類型的指針”。如果指針名字爲p,那麽可以説“p指針指向變量x”,或者説“p指針保存了x變量的內存地址”。同時`*p`表達式對應p指針指向的變量的值。一般`*p`表達式讀取指針指向的變量的值,這里爲int類型的值,同時因爲`*p`對應一個變量,所以該表達式也可以出現在賦值語句的左邊,表示更新指針所指向的變量的值。
|
||||
如果用“var x int”声明语句声明一个x变量,那么&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()
|
||||
@@ -36,17 +36,17 @@ func f() *int {
|
||||
}
|
||||
```
|
||||
|
||||
每次調用f函數都將返迴不同的結果:
|
||||
每次调用f函数都将返回不同的结果:
|
||||
|
||||
```Go
|
||||
fmt.Println(f() == f()) // "false"
|
||||
```
|
||||
|
||||
因爲指針包含了一個變量的地址,因此如果將指針作爲參數調用函數,那將可以在函數中通過該指針來更新變量的值。例如下面這個例子就是通過指針來更新變量的值,然後返迴更新後的值,可用在一個表達式中(譯註:這是對C語言中`++v`操作的模擬,這里隻是爲了説明指針的用法,incr函數模擬的做法併不推薦):
|
||||
因为指针包含了一个变量的地址,因此如果将指针作为参数调用函数,那将可以在函数中通过该指针来更新变量的值。例如下面这个例子就是通过指针来更新变量的值,然后返回更新后的值,可用在一个表达式中(译注:这是对C语言中`++v`操作的模拟,这里只是为了说明指针的用法,incr函数模拟的做法并不推荐):
|
||||
|
||||
```Go
|
||||
func incr(p *int) int {
|
||||
*p++ // 非常重要:隻是增加p指向的變量的值,併不改變p指針!!!
|
||||
*p++ // 非常重要:只是增加p指向的变量的值,并不改变p指针!!!
|
||||
return *p
|
||||
}
|
||||
|
||||
@@ -55,9 +55,9 @@ incr(&v) // side effect: v is now 2
|
||||
fmt.Println(incr(&v)) // "3" (and v is 3)
|
||||
```
|
||||
|
||||
每次我們對一個變量取地址,或者複製指針,我們都是爲原變量創建了新的别名。例如,`*p`就是是 變量v的别名。指針特别有價值的地方在於我們可以不用名字而訪問一個變量,但是這是一把雙刃劍:要找到一個變量的所有訪問者併不容易,我們必須知道變量全部的别名(譯註:這是Go語言的垃圾迴收器所做的工作)。不僅僅是指針會創建别名,很多其他引用類型也會創建别名,例如slice、map和chan,甚至結構體、數組和接口都會創建所引用變量的别名。
|
||||
每次我们对一个变量取地址,或者复制指针,我们都是为原变量创建了新的别名。例如,`*p`就是是 变量v的别名。指针特别有价值的地方在于我们可以不用名字而访问一个变量,但是这是一把双刃剑:要找到一个变量的所有访问者并不容易,我们必须知道变量全部的别名(译注:这是Go语言的垃圾回收器所做的工作)。不仅仅是指针会创建别名,很多其他引用类型也会创建别名,例如slice、map和chan,甚至结构体、数组和接口都会创建所引用变量的别名。
|
||||
|
||||
指針是實現標準庫中flag包的關鍵技術,它使用命令行參數來設置對應變量的值,而這些對應命令行標誌參數的變量可能會零散分布在整個程序中。爲了説明這一點,在早些的echo版本中,就包含了兩個可選的命令行參數:`-n`用於忽略行尾的換行符,`-s sep`用於指定分隔字符(默認是空格)。下面這是第四個版本,對應包路徑爲gopl.io/ch2/echo4。
|
||||
指针是实现标准库中flag包的关键技术,它使用命令行参数来设置对应变量的值,而这些对应命令行标志参数的变量可能会零散分布在整个程序中。为了说明这一点,在早些的echo版本中,就包含了两个可选的命令行参数:`-n`用于忽略行尾的换行符,`-s sep`用于指定分隔字符(默认是空格)。下面这是第四个版本,对应包路径为gopl.io/ch2/echo4。
|
||||
|
||||
<u><i>gopl.io/ch2/echo4</i></u>
|
||||
```Go
|
||||
@@ -82,11 +82,11 @@ 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()函數來訪問,返迴值對應對應一個字符串類型的slice。如果在flag.Parse函數解析命令行參數時遇到錯誤,默認將打印相關的提示信息,然後調用os.Exit(2)終止程序。
|
||||
当程序运行时,必须在使用标志参数对应的变量之前调用先flag.Parse函数,用于更新每个标志参数对应变量的值(之前是默认值)。对于非标志参数的普通命令行参数可以通过调用flag.Args()函数来访问,返回值对应对应一个字符串类型的slice。如果在flag.Parse函数解析命令行参数时遇到错误,默认将打印相关的提示信息,然后调用os.Exit(2)终止程序。
|
||||
|
||||
讓我們運行一些echo測試用例:
|
||||
让我们运行一些echo测试用例:
|
||||
|
||||
```
|
||||
$ go build gopl.io/ch2/echo4
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
### 2.3.3. new函數
|
||||
### 2.3.3. new函数
|
||||
|
||||
另一個創建變量的方法是調用用內建的new函數。表達式new(T)將創建一個T類型的匿名變量,初始化爲T類型的零值,然後返迴變量地址,返迴的指針類型爲`*T`。
|
||||
另一个创建变量的方法是调用用内建的new函数。表达式new(T)将创建一个T类型的匿名变量,初始化为T类型的零值,然后返回变量地址,返回的指针类型为`*T`。
|
||||
|
||||
```Go
|
||||
p := new(int) // p, *int 類型, 指向匿名的 int 變量
|
||||
p := new(int) // p, *int 类型, 指向匿名的 int 变量
|
||||
fmt.Println(*p) // "0"
|
||||
*p = 2 // 設置 int 匿名變量的值爲 2
|
||||
*p = 2 // 设置 int 匿名变量的值为 2
|
||||
fmt.Println(*p) // "2"
|
||||
```
|
||||
|
||||
用new創建變量和普通變量聲明語句方式創建變量沒有什麽區别,除了不需要聲明一個臨時變量的名字外,我們還可以在表達式中使用new(T)。換言之,new函數類似是一種語法糖,而不是一個新的基礎概念。
|
||||
用new创建变量和普通变量声明语句方式创建变量没有什么区别,除了不需要声明一个临时变量的名字外,我们还可以在表达式中使用new(T)。换言之,new函数类似是一种语法糖,而不是一个新的基础概念。
|
||||
|
||||
下面的兩個newInt函數有着相同的行爲:
|
||||
下面的两个newInt函数有着相同的行为:
|
||||
|
||||
```Go
|
||||
func newInt() *int {
|
||||
@@ -24,7 +24,7 @@ func newInt() *int {
|
||||
}
|
||||
```
|
||||
|
||||
每次調用new函數都是返迴一個新的變量的地址,因此下面兩個地址是不同的:
|
||||
每次调用new函数都是返回一个新的变量的地址,因此下面两个地址是不同的:
|
||||
|
||||
```Go
|
||||
p := new(int)
|
||||
@@ -32,15 +32,15 @@ q := new(int)
|
||||
fmt.Println(p == q) // "false"
|
||||
```
|
||||
|
||||
當然也可能有特殊情況:如果兩個類型都是空的,也就是説類型的大小是0,例如`struct{}`和 `[0]int`, 有可能有相同的地址(依賴具體的語言實現)(譯註:請謹慎使用大小爲0的類型,因爲如果類型的大小位0好話,可能導致Go語言的自動垃圾迴收器有不同的行爲,具體請査看`runtime.SetFinalizer`函數相關文檔)。
|
||||
当然也可能有特殊情况:如果两个类型都是空的,也就是说类型的大小是0,例如`struct{}`和 `[0]int`, 有可能有相同的地址(依赖具体的语言实现)(译注:请谨慎使用大小为0的类型,因为如果类型的大小位0好话,可能导致Go语言的自动垃圾回收器有不同的行为,具体请查看`runtime.SetFinalizer`函数相关文档)。
|
||||
|
||||
new函數使用常見相對比較少,因爲對應結構體來説,可以直接用字面量語法創建新變量的方法會更靈活(§4.4.1)。
|
||||
new函数使用常见相对比较少,因为对应结构体来说,可以直接用字面量语法创建新变量的方法会更灵活(§4.4.1)。
|
||||
|
||||
由於new隻是一個預定義的函數,它併不是一個關鍵字,因此我們可以將new名字重新定義爲别的類型。例如下面的例子:
|
||||
由于new只是一个预定义的函数,它并不是一个关键字,因此我们可以将new名字重新定义为别的类型。例如下面的例子:
|
||||
|
||||
```Go
|
||||
func delta(old, new int) int { return new - old }
|
||||
```
|
||||
|
||||
由於new被定義爲int類型的變量名,因此在delta函數內部是無法使用內置的new函數的。
|
||||
由于new被定义为int类型的变量名,因此在delta函数内部是无法使用内置的new函数的。
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
### 2.3.4. 變量的生命週期
|
||||
### 2.3.4. 变量的生命周期
|
||||
|
||||
變量的生命週期指的是在程序運行期間變量有效存在的時間間隔。對於在包一級聲明的變量來説,它們的生命週期和整個程序的運行週期是一致的。而相比之下,在局部變量的聲明週期則是動態的:從每次創建一個新變量的聲明語句開始,直到該變量不再被引用爲止,然後變量的存儲空間可能被迴收。函數的參數變量和返迴值變量都是局部變量。它們在函數每次被調用的時候創建。
|
||||
变量的生命周期指的是在程序运行期间变量有效存在的时间间隔。对于在包一级声明的变量来说,它们的生命周期和整个程序的运行周期是一致的。而相比之下,在局部变量的声明周期则是动态的:从每次创建一个新变量的声明语句开始,直到该变量不再被引用为止,然后变量的存储空间可能被回收。函数的参数变量和返回值变量都是局部变量。它们在函数每次被调用的时候创建。
|
||||
|
||||
例如,下面是從1.4節的Lissajous程序摘録的代碼片段:
|
||||
例如,下面是从1.4节的Lissajous程序摘录的代码片段:
|
||||
|
||||
```Go
|
||||
for t := 0.0; t < cycles*2*math.Pi; t += res {
|
||||
@@ -13,7 +13,7 @@ for t := 0.0; t < cycles*2*math.Pi; t += res {
|
||||
}
|
||||
```
|
||||
|
||||
譯註:函數的有右小括弧也可以另起一行縮進,同時爲了防止編譯器在行尾自動插入分號而導致的編譯錯誤,可以在末尾的參數變量後面顯式插入逗號。像下面這樣:
|
||||
译注:函数的有右小括弧也可以另起一行缩进,同时为了防止编译器在行尾自动插入分号而导致的编译错误,可以在末尾的参数变量后面显式插入逗号。像下面这样:
|
||||
|
||||
```Go
|
||||
for t := 0.0; t < cycles*2*math.Pi; t += res {
|
||||
@@ -21,18 +21,18 @@ for t := 0.0; t < cycles*2*math.Pi; t += res {
|
||||
y := math.Sin(t*freq + phase)
|
||||
img.SetColorIndex(
|
||||
size+int(x*size+0.5), size+int(y*size+0.5),
|
||||
blackIndex, // 最後插入的逗號不會導致編譯錯誤,這是Go編譯器的一個特性
|
||||
) // 小括弧另起一行縮進,和大括弧的風格保存一致
|
||||
blackIndex, // 最后插入的逗号不会导致编译错误,这是Go编译器的一个特性
|
||||
) // 小括弧另起一行缩进,和大括弧的风格保存一致
|
||||
}
|
||||
```
|
||||
|
||||
在每次循環的開始會創建臨時變量t,然後在每次循環迭代中創建臨時變量x和y。
|
||||
在每次循环的开始会创建临时变量t,然后在每次循环迭代中创建临时变量x和y。
|
||||
|
||||
那麽垃Go語言的自動圾收集器是如何知道一個變量是何時可以被迴收的呢?這里我們可以避開完整的技術細節,基本的實現思路是,從每個包級的變量和每個當前運行函數的每一個局部變量開始,通過指針或引用的訪問路徑遍歷,是否可以找到該變量。如果不存在這樣的訪問路徑,那麽説明該變量是不可達的,也就是説它是否存在併不會影響程序後續的計算結果。
|
||||
那么垃Go语言的自动圾收集器是如何知道一个变量是何时可以被回收的呢?这里我们可以避开完整的技术细节,基本的实现思路是,从每个包级的变量和每个当前运行函数的每一个局部变量开始,通过指针或引用的访问路径遍历,是否可以找到该变量。如果不存在这样的访问路径,那么说明该变量是不可达的,也就是说它是否存在并不会影响程序后续的计算结果。
|
||||
|
||||
因爲一個變量的有效週期隻取決於是否可達,因此一個循環迭代內部的局部變量的生命週期可能超出其局部作用域。同時,局部變量可能在函數返迴之後依然存在。
|
||||
因为一个变量的有效周期只取决于是否可达,因此一个循环迭代内部的局部变量的生命周期可能超出其局部作用域。同时,局部变量可能在函数返回之后依然存在。
|
||||
|
||||
編譯器會自動選擇在棧上還是在堆上分配局部變量的存儲空間,但可能令人驚訝的是,這個選擇併不是由用var還是new聲明變量的方式決定的。
|
||||
编译器会自动选择在栈上还是在堆上分配局部变量的存储空间,但可能令人惊讶的是,这个选择并不是由用var还是new声明变量的方式决定的。
|
||||
|
||||
```Go
|
||||
var global *int
|
||||
@@ -49,9 +49,9 @@ func g() {
|
||||
}
|
||||
```
|
||||
|
||||
f函數里的x變量必須在堆上分配,因爲它在函數退出後依然可以通過包一級的global變量找到,雖然它是在函數內部定義的;用Go語言的術語説,這個x局部變量從函數f中逃逸了。相反,當g函數返迴時,變量`*y`將是不可達的,也就是説可以馬上被迴收的。因此,`*y`併沒有從函數g中逃逸,編譯器可以選擇在棧上分配`*y`的存儲空間(譯註:也可以選擇在堆上分配,然後由Go語言的GC迴收這個變量的內存空間),雖然這里用的是new方式。其實在任何時候,你併不需爲了編寫正確的代碼而要考慮變量的逃逸行爲,要記住的是,逃逸的變量需要額外分配內存,同時對性能的優化可能會産生細微的影響。
|
||||
f函数里的x变量必须在堆上分配,因为它在函数退出后依然可以通过包一级的global变量找到,虽然它是在函数内部定义的;用Go语言的术语说,这个x局部变量从函数f中逃逸了。相反,当g函数返回时,变量`*y`将是不可达的,也就是说可以马上被回收的。因此,`*y`并没有从函数g中逃逸,编译器可以选择在栈上分配`*y`的存储空间(译注:也可以选择在堆上分配,然后由Go语言的GC回收这个变量的内存空间),虽然这里用的是new方式。其实在任何时候,你并不需为了编写正确的代码而要考虑变量的逃逸行为,要记住的是,逃逸的变量需要额外分配内存,同时对性能的优化可能会产生细微的影响。
|
||||
|
||||
Go語言的自動垃圾收集器對編寫正確的代碼是一個鉅大的幫助,但也併不是説你完全不用考慮內存了。你雖然不需要顯式地分配和釋放內存,但是要編寫高效的程序你依然需要了解變量的生命週期。例如,如果將指向短生命週期對象的指針保存到具有長生命週期的對象中,特别是保存到全局變量時,會阻止對短生命週期對象的垃圾迴收(從而可能影響程序的性能)。
|
||||
Go语言的自动垃圾收集器对编写正确的代码是一个巨大的帮助,但也并不是说你完全不用考虑内存了。你虽然不需要显式地分配和释放内存,但是要编写高效的程序你依然需要了解变量的生命周期。例如,如果将指向短生命周期对象的指针保存到具有长生命周期的对象中,特别是保存到全局变量时,会阻止对短生命周期对象的垃圾回收(从而可能影响程序的性能)。
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,32 +1,32 @@
|
||||
## 2.3. 變量
|
||||
## 2.3. 变量
|
||||
|
||||
var聲明語句可以創建一個特定類型的變量,然後給變量附加一個名字,併且設置變量的初始值。變量聲明的一般語法如下:
|
||||
var声明语句可以创建一个特定类型的变量,然后给变量附加一个名字,并且设置变量的初始值。变量声明的一般语法如下:
|
||||
|
||||
```Go
|
||||
var 變量名字 類型 = 表達式
|
||||
var 变量名字 类型 = 表达式
|
||||
```
|
||||
|
||||
其中“*類型*”或“*= 表達式*”兩個部分可以省略其中的一個。如果省略的是類型信息,那麽將根據初始化表達式來推導變量的類型信息。如果初始化表達式被省略,那麽將用零值初始化該變量。 數值類型變量對應的零值是0,布爾類型變量對應的零值是false,字符串類型對應的零值是空字符串,接口或引用類型(包括slice、map、chan和函數)變量對應的零值是nil。數組或結構體等聚合類型對應的零值是每個元素或字段都是對應該類型的零值。
|
||||
其中“*类型*”或“*= 表达式*”两个部分可以省略其中的一个。如果省略的是类型信息,那么将根据初始化表达式来推导变量的类型信息。如果初始化表达式被省略,那么将用零值初始化该变量。 数值类型变量对应的零值是0,布尔类型变量对应的零值是false,字符串类型对应的零值是空字符串,接口或引用类型(包括slice、map、chan和函数)变量对应的零值是nil。数组或结构体等聚合类型对应的零值是每个元素或字段都是对应该类型的零值。
|
||||
|
||||
零值初始化機製可以確保每個聲明的變量總是有一個良好定義的值,因此在Go語言中不存在未初始化的變量。這個特性可以簡化很多代碼,而且可以在沒有增加額外工作的前提下確保邊界條件下的合理行爲。例如:
|
||||
零值初始化机制可以确保每个声明的变量总是有一个良好定义的值,因此在Go语言中不存在未初始化的变量。这个特性可以简化很多代码,而且可以在没有增加额外工作的前提下确保边界条件下的合理行为。例如:
|
||||
|
||||
```Go
|
||||
var s string
|
||||
fmt.Println(s) // ""
|
||||
```
|
||||
|
||||
這段代碼將打印一個空字符串,而不是導致錯誤或産生不可預知的行爲。Go語言程序員應該讓一些聚合類型的零值也具有意義,這樣可以保證不管任何類型的變量總是有一個合理有效的零值狀態。
|
||||
这段代码将打印一个空字符串,而不是导致错误或产生不可预知的行为。Go语言程序员应该让一些聚合类型的零值也具有意义,这样可以保证不管任何类型的变量总是有一个合理有效的零值状态。
|
||||
|
||||
也可以在一個聲明語句中同時聲明一組變量,或用一組初始化表達式聲明併初始化一組變量。如果省略每個變量的類型,將可以聲明多個類型不同的變量(類型由初始化表達式推導):
|
||||
也可以在一个声明语句中同时声明一组变量,或用一组初始化表达式声明并初始化一组变量。如果省略每个变量的类型,将可以声明多个类型不同的变量(类型由初始化表达式推导):
|
||||
|
||||
```Go
|
||||
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),局部变量将在声明语句被执行到的时候完成初始化。
|
||||
|
||||
一組變量也可以通過調用一個函數,由函數返迴的多個返迴值初始化:
|
||||
一组变量也可以通过调用一个函数,由函数返回的多个返回值初始化:
|
||||
|
||||
```Go
|
||||
var f, err = os.Open(name) // os.Open returns a file and an error
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
### 2.4.1. 元組賦值
|
||||
### 2.4.1. 元组赋值
|
||||
|
||||
元組賦值是另一種形式的賦值語句,它允許同時更新多個變量的值。在賦值之前,賦值語句右邊的所有表達式將會先進行求值,然後再統一更新左邊對應變量的值。這對於處理有些同時出現在元組賦值語句左右兩邊的變量很有幫助,例如我們可以這樣交換兩個變量的值:
|
||||
元组赋值是另一种形式的赋值语句,它允许同时更新多个变量的值。在赋值之前,赋值语句右边的所有表达式将会先进行求值,然后再统一更新左边对应变量的值。这对于处理有些同时出现在元组赋值语句左右两边的变量很有帮助,例如我们可以这样交换两个变量的值:
|
||||
|
||||
```Go
|
||||
x, y = y, x
|
||||
@@ -8,7 +8,7 @@ x, y = y, x
|
||||
a[i], a[j] = a[j], a[i]
|
||||
```
|
||||
|
||||
或者是計算兩個整數值的的最大公約數(GCD)(譯註:GCD不是那個敏感字,而是greatest common divisor的縮寫,歐幾里德的GCD是最早的非平凡算法):
|
||||
或者是计算两个整数值的的最大公约数(GCD)(译注:GCD不是那个敏感字,而是greatest common divisor的缩写,欧几里德的GCD是最早的非平凡算法):
|
||||
|
||||
```Go
|
||||
func gcd(x, y int) int {
|
||||
@@ -19,7 +19,7 @@ func gcd(x, y int) int {
|
||||
}
|
||||
```
|
||||
|
||||
或者是計算斐波納契數列(Fibonacci)的第N個數:
|
||||
或者是计算斐波纳契数列(Fibonacci)的第N个数:
|
||||
|
||||
```Go
|
||||
func fib(n int) int {
|
||||
@@ -31,21 +31,21 @@ 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。在稍後我們將看到的三個操作都是類似的用法。如果map査找(§4.3)、類型斷言(§7.10)或通道接收(§8.4.2)出現在賦值語句的右邊,它們都可能會産生兩個結果,有一個額外的布爾結果表示操作是否成功:
|
||||
通常,这类函数会用额外的返回值来表达某种错误类型,例如os.Open是用额外的返回值返回一个error类型的错误,还有一些是用来返回布尔值,通常被称为ok。在稍后我们将看到的三个操作都是类似的用法。如果map查找(§4.3)、类型断言(§7.10)或通道接收(§8.4.2)出现在赋值语句的右边,它们都可能会产生两个结果,有一个额外的布尔结果表示操作是否成功:
|
||||
|
||||
```Go
|
||||
v, ok = m[key] // map lookup
|
||||
@@ -53,22 +53,22 @@ v, ok = x.(T) // type assertion
|
||||
v, ok = <-ch // channel receive
|
||||
```
|
||||
|
||||
譯註:map査找(§4.3)、類型斷言(§7.10)或通道接收(§8.4.2)出現在賦值語句的右邊時,併不一定是産生兩個結果,也可能隻産生一個結果。對於值産生一個結果的情形,map査找失敗時會返迴零值,類型斷言失敗時會發送運行時panic異常,通道接收失敗時會返迴零值(阻塞不算是失敗)。例如下面的例子:
|
||||
译注:map查找(§4.3)、类型断言(§7.10)或通道接收(§8.4.2)出现在赋值语句的右边时,并不一定是产生两个结果,也可能只产生一个结果。对于值产生一个结果的情形,map查找失败时会返回零值,类型断言失败时会发送运行时panic异常,通道接收失败时会返回零值(阻塞不算是失败)。例如下面的例子:
|
||||
|
||||
```Go
|
||||
v = m[key] // map査找,失敗時返迴零值
|
||||
v = x.(T) // type斷言,失敗時panic異常
|
||||
v = <-ch // 管道接收,失敗時返迴零值(阻塞不算是失敗)
|
||||
v = m[key] // map查找,失败时返回零值
|
||||
v = x.(T) // type断言,失败时panic异常
|
||||
v = <-ch // 管道接收,失败时返回零值(阻塞不算是失败)
|
||||
|
||||
_, ok = m[key] // map返迴2個值
|
||||
_, ok = mm[""], false // map返迴1個值
|
||||
_ = mm[""] // map返迴1個值
|
||||
_, ok = m[key] // map返回2个值
|
||||
_, ok = mm[""], false // map返回1个值
|
||||
_ = mm[""] // map返回1个值
|
||||
```
|
||||
|
||||
和變量聲明一樣,我們可以用下劃線空白標識符`_`來丟棄不需要的值。
|
||||
和变量声明一样,我们可以用下划线空白标识符`_`来丢弃不需要的值。
|
||||
|
||||
```Go
|
||||
_, err = io.Copy(dst, src) // 丟棄字節數
|
||||
_, ok = x.(T) // 隻檢測類型,忽略具體值
|
||||
_, err = io.Copy(dst, src) // 丢弃字节数
|
||||
_, ok = x.(T) // 只检测类型,忽略具体值
|
||||
```
|
||||
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
### 2.4.2. 可賦值性
|
||||
### 2.4.2. 可赋值性
|
||||
|
||||
賦值語句是顯式的賦值形式,但是程序中還有很多地方會發生隱式的賦值行爲:函數調用會隱式地將調用參數的值賦值給函數的參數變量,一個返迴語句將隱式地將返迴操作的值賦值給結果變量,一個複合類型的字面量(§4.2)也會産生賦值行爲。例如下面的語句:
|
||||
赋值语句是显式的赋值形式,但是程序中还有很多地方会发生隐式的赋值行为:函数调用会隐式地将调用参数的值赋值给函数的参数变量,一个返回语句将隐式地将返回操作的值赋值给结果变量,一个复合类型的字面量(§4.2)也会产生赋值行为。例如下面的语句:
|
||||
|
||||
```Go
|
||||
medals := []string{"gold", "silver", "bronze"}
|
||||
```
|
||||
|
||||
隱式地對slice的每個元素進行賦值操作,類似這樣寫的行爲:
|
||||
隐式地对slice的每个元素进行赋值操作,类似这样写的行为:
|
||||
|
||||
```Go
|
||||
medals[0] = "gold"
|
||||
@@ -14,12 +14,12 @@ medals[1] = "silver"
|
||||
medals[2] = "bronze"
|
||||
```
|
||||
|
||||
map和chan的元素,雖然不是普通的變量,但是也有類似的隱式賦值行爲。
|
||||
map和chan的元素,虽然不是普通的变量,但是也有类似的隐式赋值行为。
|
||||
|
||||
不管是隱式還是顯式地賦值,在賦值語句左邊的變量和右邊最終的求到的值必須有相同的數據類型。更直白地説,隻有右邊的值對於左邊的變量是可賦值的,賦值語句才是允許的。
|
||||
不管是隐式还是显式地赋值,在赋值语句左边的变量和右边最终的求到的值必须有相同的数据类型。更直白地说,只有右边的值对于左边的变量是可赋值的,赋值语句才是允许的。
|
||||
|
||||
可賦值性的規則對於不同類型有着不同要求,對每個新類型特殊的地方我們會專門解釋。對於目前我們已經討論過的類型,它的規則是簡單的:類型必須完全匹配,nil可以賦值給任何指針或引用類型的變量。常量(§3.6)則有更靈活的賦值規則,因爲這樣可以避免不必要的顯式的類型轉換。
|
||||
可赋值性的规则对于不同类型有着不同要求,对每个新类型特殊的地方我们会专门解释。对于目前我们已经讨论过的类型,它的规则是简单的:类型必须完全匹配,nil可以赋值给任何指针或引用类型的变量。常量(§3.6)则有更灵活的赋值规则,因为这样可以避免不必要的显式的类型转换。
|
||||
|
||||
對於兩個值是否可以用`==`或`!=`進行相等比較的能力也和可賦值能力有關繫:對於任何類型的值的相等比較,第二個值必須是對第一個值類型對應的變量是可賦值的,反之依然。和前面一樣,我們會對每個新類型比較特殊的地方做專門的解釋。
|
||||
对于两个值是否可以用`==`或`!=`进行相等比较的能力也和可赋值能力有关系:对于任何类型的值的相等比较,第二个值必须是对第一个值类型对应的变量是可赋值的,反之依然。和前面一样,我们会对每个新类型比较特殊的地方做专门的解释。
|
||||
|
||||
|
||||
|
||||
@@ -1,28 +1,28 @@
|
||||
## 2.4. 賦值
|
||||
## 2.4. 赋值
|
||||
|
||||
使用賦值語句可以更新一個變量的值,最簡單的賦值語句是將要被賦值的變量放在=的左邊,新值的表達式放在=的右邊。
|
||||
使用赋值语句可以更新一个变量的值,最简单的赋值语句是将要被赋值的变量放在=的左边,新值的表达式放在=的右边。
|
||||
|
||||
```Go
|
||||
x = 1 // 命名變量的賦值
|
||||
*p = true // 通過指針間接賦值
|
||||
person.name = "bob" // 結構體字段賦值
|
||||
count[x] = count[x] * scale // 數組、slice或map的元素賦值
|
||||
x = 1 // 命名变量的赋值
|
||||
*p = true // 通过指针间接赋值
|
||||
person.name = "bob" // 结构体字段赋值
|
||||
count[x] = count[x] * scale // 数组、slice或map的元素赋值
|
||||
```
|
||||
|
||||
特定的二元算術運算符和賦值語句的複合操作有一個簡潔形式,例如上面最後的語句可以重寫爲:
|
||||
特定的二元算术运算符和赋值语句的复合操作有一个简洁形式,例如上面最后的语句可以重写为:
|
||||
|
||||
```Go
|
||||
count[x] *= scale
|
||||
```
|
||||
|
||||
這樣可以省去對變量表達式的重複計算。
|
||||
这样可以省去对变量表达式的重复计算。
|
||||
|
||||
數值變量也可以支持`++`遞增和`--`遞減語句(譯註:自增和自減是語句,而不是表達式,因此`x = i++`之類的表達式是錯誤的):
|
||||
数值变量也可以支持`++`递增和`--`递减语句(译注:自增和自减是语句,而不是表达式,因此`x = i++`之类的表达式是错误的):
|
||||
|
||||
```Go
|
||||
v := 1
|
||||
v++ // 等價方式 v = v + 1;v 變成 2
|
||||
v-- // 等價方式 v = v - 1;v 變成 1
|
||||
v++ // 等价方式 v = v + 1;v 变成 2
|
||||
v-- // 等价方式 v = v - 1;v 变成 1
|
||||
```
|
||||
|
||||
{% include "./ch2-04-1.md" %}
|
||||
|
||||
@@ -1,24 +1,24 @@
|
||||
## 2.5. 類型
|
||||
## 2.5. 类型
|
||||
|
||||
變量或表達式的類型定義了對應存儲值的屬性特徵,例如數值在內存的存儲大小(或者是元素的bit個數),它們在內部是如何表達的,是否支持一些操作符,以及它們自己關聯的方法集等。
|
||||
变量或表达式的类型定义了对应存储值的属性特征,例如数值在内存的存储大小(或者是元素的bit个数),它们在内部是如何表达的,是否支持一些操作符,以及它们自己关联的方法集等。
|
||||
|
||||
在任何程序中都會存在一些變量有着相同的內部結構,但是卻表示完全不同的概念。例如,一個int類型的變量可以用來表示一個循環的迭代索引、或者一個時間戳、或者一個文件描述符、或者一個月份;一個float64類型的變量可以用來表示每秒移動幾米的速度、或者是不同溫度單位下的溫度;一個字符串可以用來表示一個密碼或者一個顔色的名稱。
|
||||
在任何程序中都会存在一些变量有着相同的内部结构,但是却表示完全不同的概念。例如,一个int类型的变量可以用来表示一个循环的迭代索引、或者一个时间戳、或者一个文件描述符、或者一个月份;一个float64类型的变量可以用来表示每秒移动几米的速度、或者是不同温度单位下的温度;一个字符串可以用来表示一个密码或者一个颜色的名称。
|
||||
|
||||
一個類型聲明語句創建了一個新的類型名稱,和現有類型具有相同的底層結構。新命名的類型提供了一個方法,用來分隔不同概念的類型,這樣卽使它們底層類型相同也是不兼容的。
|
||||
一个类型声明语句创建了一个新的类型名称,和现有类型具有相同的底层结构。新命名的类型提供了一个方法,用来分隔不同概念的类型,这样即使它们底层类型相同也是不兼容的。
|
||||
|
||||
```Go
|
||||
type 類型名字 底層類型
|
||||
type 类型名字 底层类型
|
||||
```
|
||||
|
||||
類型聲明語句一般出現在包一級,因此如果新創建的類型名字的首字符大寫,則在外部包也可以使用。
|
||||
类型声明语句一般出现在包一级,因此如果新创建的类型名字的首字符大写,则在外部包也可以使用。
|
||||
|
||||
譯註:對於中文漢字,Unicode標誌都作爲小寫字母處理,因此中文的命名默認不能導出;不過国內的用戶針對該問題提出了不同的看法,根據RobPike的迴複,在Go2中有可能會將中日韓等字符當作大寫字母處理。下面是RobPik在 [Issue763](https://github.com/golang/go/issues/5763) 的迴複:
|
||||
译注:对于中文汉字,Unicode标志都作为小写字母处理,因此中文的命名默认不能导出;不过国内的用户针对该问题提出了不同的看法,根据RobPike的回复,在Go2中有可能会将中日韩等字符当作大写字母处理。下面是RobPik在 [Issue763](https://github.com/golang/go/issues/5763) 的回复:
|
||||
|
||||
> A solution that's been kicking around for a while:
|
||||
>
|
||||
> For Go 2 (can't do it before then): Change the definition to “lower case letters and _ are package-local; all else is exported”. Then with non-cased languages, such as Japanese, we can write 日本語 for an exported name and _日本語 for a local name. This rule has no effect, relative to the Go 1 rule, with cased languages. They behave exactly the same.
|
||||
> For Go 2 (can't do it before then): Change the definition to “lower case letters and _ are package-local; all else is exported”. Then with non-cased languages, such as Japanese, we can write 日本语 for an exported name and _日本语 for a local name. This rule has no effect, relative to the Go 1 rule, with cased languages. They behave exactly the same.
|
||||
|
||||
爲了説明類型聲明,我們將不同溫度單位分别定義爲不同的類型:
|
||||
为了说明类型声明,我们将不同温度单位分别定义为不同的类型:
|
||||
|
||||
<u><i>gopl.io/ch2/tempconv0</i></u>
|
||||
```Go
|
||||
@@ -27,13 +27,13 @@ package tempconv
|
||||
|
||||
import "fmt"
|
||||
|
||||
type Celsius float64 // 攝氏溫度
|
||||
type Fahrenheit float64 // 華氏溫度
|
||||
type Celsius float64 // 摄氏温度
|
||||
type Fahrenheit float64 // 华氏温度
|
||||
|
||||
const (
|
||||
AbsoluteZeroC Celsius = -273.15 // 絶對零度
|
||||
FreezingC Celsius = 0 // 結冰點溫度
|
||||
BoilingC Celsius = 100 // 沸水溫度
|
||||
AbsoluteZeroC Celsius = -273.15 // 绝对零度
|
||||
FreezingC Celsius = 0 // 结冰点温度
|
||||
BoilingC Celsius = 100 // 沸水温度
|
||||
)
|
||||
|
||||
func CToF(c Celsius) Fahrenheit { return Fahrenheit(c*9/5 + 32) }
|
||||
@@ -41,13 +41,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類型(譯註:如果T是指針類型,可能會需要用小括弧包裝T,比如`(*int)(0)`)。隻有當兩個類型的底層基礎類型相同時,才允許這種轉型操作,或者是兩者都是指向相同底層結構的指針類型,這些轉換隻改變類型而不會影響值本身。如果x是可以賦值給T類型的值,那麽x必然也可以被轉爲T類型,但是一般沒有這個必要。
|
||||
对于每一个类型T,都有一个对应的类型转换操作T(x),用于将x转为T类型(译注:如果T是指针类型,可能会需要用小括弧包装T,比如`(*int)(0)`)。只有当两个类型的底层基础类型相同时,才允许这种转型操作,或者是两者都是指向相同底层结构的指针类型,这些转换只改变类型而不会影响值本身。如果x是可以赋值给T类型的值,那么x必然也可以被转为T类型,但是一般没有这个必要。
|
||||
|
||||
數值類型之間的轉型也是允許的,併且在字符串和一些特定類型的slice之間也是可以轉換的,在下一章我們會看到這樣的例子。這類轉換可能改變值的表現。例如,將一個浮點數轉爲整數將丟棄小數部分,將一個字符串轉爲`[]byte`類型的slice將拷貝一個字符串數據的副本。在任何情況下,運行時不會發生轉換失敗的錯誤(譯註: 錯誤隻會發生在編譯階段)。
|
||||
数值类型之间的转型也是允许的,并且在字符串和一些特定类型的slice之间也是可以转换的,在下一章我们会看到这样的例子。这类转换可能改变值的表现。例如,将一个浮点数转为整数将丢弃小数部分,将一个字符串转为`[]byte`类型的slice将拷贝一个字符串数据的副本。在任何情况下,运行时不会发生转换失败的错误(译注: 错误只会发生在编译阶段)。
|
||||
|
||||
底層數據類型決定了內部結構和表達方式,也決定是否可以像底層類型一樣對內置運算符的支持。這意味着,Celsius和Fahrenheit類型的算術運算行爲和底層的float64類型是一樣的,正如我們所期望的那樣。
|
||||
底层数据类型决定了内部结构和表达方式,也决定是否可以像底层类型一样对内置运算符的支持。这意味着,Celsius和Fahrenheit类型的算术运算行为和底层的float64类型是一样的,正如我们所期望的那样。
|
||||
|
||||
```Go
|
||||
fmt.Printf("%g\n", BoilingC-FreezingC) // "100" °C
|
||||
@@ -56,7 +56,7 @@ fmt.Printf("%g\n", boilingF-CToF(FreezingC)) // "180" °F
|
||||
fmt.Printf("%g\n", boilingF-FreezingC) // compile error: type mismatch
|
||||
```
|
||||
|
||||
比較運算符`==`和`<`也可以用來比較一個命名類型的變量和另一個有相同類型的變量,或有着相同底層類型的未命名類型的值之間做比較。但是如果兩個值有着不同的類型,則不能直接進行比較:
|
||||
比较运算符`==`和`<`也可以用来比较一个命名类型的变量和另一个有相同类型的变量,或有着相同底层类型的未命名类型的值之间做比较。但是如果两个值有着不同的类型,则不能直接进行比较:
|
||||
|
||||
```Go
|
||||
var c Celsius
|
||||
@@ -67,19 +67,19 @@ 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溫度單位的字符串:
|
||||
下面的声明语句,Celsius类型的参数c出现在了函数名的前面,表示声明的是Celsius类型的一个叫名叫String的方法,该方法返回该类型对象c带着°C温度单位的字符串:
|
||||
|
||||
```Go
|
||||
func (c Celsius) String() string { return fmt.Sprintf("%g°C", c) }
|
||||
```
|
||||
|
||||
許多類型都會定義一個String方法,因爲當使用fmt包的打印方法時,將會優先使用該類型對應的String方法返迴的結果打印,我們將在7.1節講述。
|
||||
许多类型都会定义一个String方法,因为当使用fmt包的打印方法时,将会优先使用该类型对应的String方法返回的结果打印,我们将在7.1节讲述。
|
||||
|
||||
```Go
|
||||
c := FToC(212.0)
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
### 2.6.1. 導入包
|
||||
### 2.6.1. 导入包
|
||||
|
||||
在Go語言程序中,每個包都是有一個全局唯一的導入路徑。導入語句中類似"gopl.io/ch2/tempconv"的字符串對應包的導入路徑。Go語言的規范併沒有定義這些字符串的具體含義或包來自哪里,它們是由構建工具來解釋的。當使用Go語言自帶的go工具箱時(第十章),一個導入路徑代表一個目録中的一個或多個Go源文件。
|
||||
在Go语言程序中,每个包都是有一个全局唯一的导入路径。导入语句中类似"gopl.io/ch2/tempconv"的字符串对应包的导入路径。Go语言的规范并没有定义这些字符串的具体含义或包来自哪里,它们是由构建工具来解释的。当使用Go语言自带的go工具箱时(第十章),一个导入路径代表一个目录中的一个或多个Go源文件。
|
||||
|
||||
除了包的導入路徑,每個包還有一個包名,包名一般是短小的名字(併不要求包名是唯一的),包名在包的聲明處指定。按照慣例,一個包的名字和包的導入路徑的最後一個字段相同,例如gopl.io/ch2/tempconv包的名字一般是tempconv。
|
||||
除了包的导入路径,每个包还有一个包名,包名一般是短小的名字(并不要求包名是唯一的),包名在包的声明处指定。按照惯例,一个包的名字和包的导入路径的最后一个字段相同,例如gopl.io/ch2/tempconv包的名字一般是tempconv。
|
||||
|
||||
要使用gopl.io/ch2/tempconv包,需要先導入:
|
||||
要使用gopl.io/ch2/tempconv包,需要先导入:
|
||||
|
||||
<u><i>gopl.io/ch2/cf</i></u>
|
||||
```Go
|
||||
@@ -34,9 +34,9 @@ func main() {
|
||||
}
|
||||
```
|
||||
|
||||
導入語句將導入的包綁定到一個短小的名字,然後通過該短小的名字就可以引用包中導出的全部內容。上面的導入聲明將允許我們以tempconv.CToF的形式來訪問gopl.io/ch2/tempconv包中的內容。在默認情況下,導入的包綁定到tempconv名字(譯註:這包聲明語句指定的名字),但是我們也可以綁定到另一個名稱,以避免名字衝突(§10.4)。
|
||||
导入语句将导入的包绑定到一个短小的名字,然后通过该短小的名字就可以引用包中导出的全部内容。上面的导入声明将允许我们以tempconv.CToF的形式来访问gopl.io/ch2/tempconv包中的内容。在默认情况下,导入的包绑定到tempconv名字(译注:这包声明语句指定的名字),但是我们也可以绑定到另一个名称,以避免名字冲突(§10.4)。
|
||||
|
||||
cf程序將命令行輸入的一個溫度在Celsius和Fahrenheit溫度單位之間轉換:
|
||||
cf程序将命令行输入的一个温度在Celsius和Fahrenheit温度单位之间转换:
|
||||
|
||||
```
|
||||
$ go build gopl.io/ch2/cf
|
||||
@@ -48,8 +48,8 @@ $ ./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源文件。
|
||||
不过有更好的解决方案,我们可以使用golang.org/x/tools/cmd/goimports导入工具,它可以根据需要自动添加或删除导入的包;许多编辑器都可以集成goimports工具,然后在保存文件的时候自动运行。类似的还有gofmt工具,可以用来格式化Go源文件。
|
||||
|
||||
**練習 2.2:** 寫一個通用的單位轉換程序,用類似cf程序的方式從命令行讀取參數,如果缺省的話則是從標準輸入讀取參數,然後做類似Celsius和Fahrenheit的單位轉換,長度單位可以對應英尺和米,重量單位可以對應磅和公斤等。
|
||||
**练习 2.2:** 写一个通用的单位转换程序,用类似cf程序的方式从命令行读取参数,如果缺省的话则是从标准输入读取参数,然后做类似Celsius和Fahrenheit的单位转换,长度单位可以对应英尺和米,重量单位可以对应磅和公斤等。
|
||||
|
||||
@@ -1,28 +1,28 @@
|
||||
### 2.6.2. 包的初始化
|
||||
|
||||
包的初始化首先是解決包級變量的依賴順序,然後安照包級變量聲明出現的順序依次初始化:
|
||||
包的初始化首先是解决包级变量的依赖顺序,然后安照包级变量声明出现的顺序依次初始化:
|
||||
|
||||
```Go
|
||||
var a = b + c // a 第三個初始化, 爲 3
|
||||
var b = f() // b 第二個初始化, 爲 2, 通過調用 f (依賴c)
|
||||
var c = 1 // c 第一個初始化, 爲 1
|
||||
var a = b + c // a 第三个初始化, 为 3
|
||||
var b = f() // b 第二个初始化, 为 2, 通过调用 f (依赖c)
|
||||
var c = 1 // c 第一个初始化, 为 1
|
||||
|
||||
func f() int { return c + 1 }
|
||||
```
|
||||
|
||||
如果包中含有多個.go源文件,它們將按照發給編譯器的順序進行初始化,Go語言的構建工具首先會將.go文件根據文件名排序,然後依次調用編譯器編譯。
|
||||
如果包中含有多个.go源文件,它们将按照发给编译器的顺序进行初始化,Go语言的构建工具首先会将.go文件根据文件名排序,然后依次调用编译器编译。
|
||||
|
||||
對於在包級别聲明的變量,如果有初始化表達式則用表達式初始化,還有一些沒有初始化表達式的,例如某些表格數據初始化併不是一個簡單的賦值過程。在這種情況下,我們可以用一個特殊的init初始化函數來簡化初始化工作。每個文件都可以包含多個init初始化函數
|
||||
对于在包级别声明的变量,如果有初始化表达式则用表达式初始化,还有一些没有初始化表达式的,例如某些表格数据初始化并不是一个简单的赋值过程。在这种情况下,我们可以用一个特殊的init初始化函数来简化初始化工作。每个文件都可以包含多个init初始化函数
|
||||
|
||||
```Go
|
||||
func init() { /* ... */ }
|
||||
```
|
||||
|
||||
這樣的init初始化函數除了不能被調用或引用外,其他行爲和普通函數類似。在每個文件中的init初始化函數,在程序開始執行時按照它們聲明的順序被自動調用。
|
||||
这样的init初始化函数除了不能被调用或引用外,其他行为和普通函数类似。在每个文件中的init初始化函数,在程序开始执行时按照它们声明的顺序被自动调用。
|
||||
|
||||
每個包在解決依賴的前提下,以導入聲明的順序初始化,每個包隻會被初始化一次。因此,如果一個p包導入了q包,那麽在p包初始化的時候可以認爲q包必然已經初始化過了。初始化工作是自下而上進行的,main包最後被初始化。以這種方式,可以確保在main函數執行之前,所有依然的包都已經完成初始化工作了。
|
||||
每个包在解决依赖的前提下,以导入声明的顺序初始化,每个包只会被初始化一次。因此,如果一个p包导入了q包,那么在p包初始化的时候可以认为q包必然已经初始化过了。初始化工作是自下而上进行的,main包最后被初始化。以这种方式,可以确保在main函数执行之前,所有依然的包都已经完成初始化工作了。
|
||||
|
||||
下面的代碼定義了一個PopCount函數,用於返迴一個數字中含二進製1bit的個數。它使用init初始化函數來生成輔助表格pc,pc表格用於處理每個8bit寬度的數字含二進製的1bit的bit個數,這樣的話在處理64bit寬度的數字時就沒有必要循環64次,隻需要8次査表就可以了。(這併不是最快的統計1bit數目的算法,但是它可以方便演示init函數的用法,併且演示了如果預生成輔助表格,這是編程中常用的技術)。
|
||||
下面的代码定义了一个PopCount函数,用于返回一个数字中含二进制1bit的个数。它使用init初始化函数来生成辅助表格pc,pc表格用于处理每个8bit宽度的数字含二进制的1bit的bit个数,这样的话在处理64bit宽度的数字时就没有必要循环64次,只需要8次查表就可以了。(这并不是最快的统计1bit数目的算法,但是它可以方便演示init函数的用法,并且演示了如果预生成辅助表格,这是编程中常用的技术)。
|
||||
|
||||
<u><i>gopl.io/ch2/popcount</i></u>
|
||||
```Go
|
||||
@@ -50,7 +50,7 @@ func PopCount(x uint64) int {
|
||||
}
|
||||
```
|
||||
|
||||
譯註:對於pc這類需要複雜處理的初始化,可以通過將初始化邏輯包裝爲一個匿名函數處理,像下面這樣:
|
||||
译注:对于pc这类需要复杂处理的初始化,可以通过将初始化逻辑包装为一个匿名函数处理,像下面这样:
|
||||
|
||||
```Go
|
||||
// pc[i] is the population count of i.
|
||||
@@ -62,16 +62,16 @@ var pc [256]byte = func() (pc [256]byte) {
|
||||
}()
|
||||
```
|
||||
|
||||
要註意的是在init函數中,range循環隻使用了索引,省略了沒有用到的值部分。循環也可以這樣寫:
|
||||
要注意的是在init函数中,range循环只使用了索引,省略了没有用到的值部分。循环也可以这样写:
|
||||
|
||||
```Go
|
||||
for i, _ := range pc {
|
||||
```
|
||||
|
||||
我們在下一節和10.5節還將看到其它使用init函數的地方。
|
||||
我们在下一节和10.5节还将看到其它使用init函数的地方。
|
||||
|
||||
**練習 2.3:** 重寫PopCount函數,用一個循環代替單一的表達式。比較兩個版本的性能。(11.4節將展示如何繫統地比較兩個不同實現的性能。)
|
||||
**练习 2.3:** 重写PopCount函数,用一个循环代替单一的表达式。比较两个版本的性能。(11.4节将展示如何系统地比较两个不同实现的性能。)
|
||||
|
||||
**練習 2.4:** 用移位算法重寫PopCount函數,每次測試最右邊的1bit,然後統計總數。比較和査表算法的性能差異。
|
||||
**练习 2.4:** 用移位算法重写PopCount函数,每次测试最右边的1bit,然后统计总数。比较和查表算法的性能差异。
|
||||
|
||||
**練習 2.5:** 表達式`x&(x-1)`用於將x的最低的一個非零的bit位清零。使用這個算法重寫PopCount函數,然後比較性能。
|
||||
**练习 2.5:** 表达式`x&(x-1)`用于将x的最低的一个非零的bit位清零。使用这个算法重写PopCount函数,然后比较性能。
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
## 2.6. 包和文件
|
||||
|
||||
Go語言中的包和其他語言的庫或模塊的概念類似,目的都是爲了支持模塊化、封裝、單獨編譯和代碼重用。一個包的源代碼保存在一個或多個以.go爲文件後綴名的源文件中,通常一個包所在目録路徑的後綴是包的導入路徑;例如包gopl.io/ch1/helloworld對應的目録路徑是$GOPATH/src/gopl.io/ch1/helloworld。
|
||||
Go语言中的包和其他语言的库或模块的概念类似,目的都是为了支持模块化、封装、单独编译和代码重用。一个包的源代码保存在一个或多个以.go为文件后缀名的源文件中,通常一个包所在目录路径的后缀是包的导入路径;例如包gopl.io/ch1/helloworld对应的目录路径是$GOPATH/src/gopl.io/ch1/helloworld。
|
||||
|
||||
每個包都對應一個獨立的名字空間。例如,在image包中的Decode函數和在unicode/utf16包中的 Decode函數是不同的。要在外部引用該函數,必須顯式使用image.Decode或utf16.Decode形式訪問。
|
||||
每个包都对应一个独立的名字空间。例如,在image包中的Decode函数和在unicode/utf16包中的 Decode函数是不同的。要在外部引用该函数,必须显式使用image.Decode或utf16.Decode形式访问。
|
||||
|
||||
包還可以讓我們通過控製哪些名字是外部可見的來隱藏內部實現信息。在Go語言中,一個簡單的規則是:如果一個名字是大寫字母開頭的,那麽該名字是導出的(譯註:因爲漢字不區分大小寫,因此漢字開頭的名字是沒有導出的)。
|
||||
包还可以让我们通过控制哪些名字是外部可见的来隐藏内部实现信息。在Go语言中,一个简单的规则是:如果一个名字是大写字母开头的,那么该名字是导出的(译注:因为汉字不区分大小写,因此汉字开头的名字是没有导出的)。
|
||||
|
||||
爲了演示包基本的用法,先假設我們的溫度轉換軟件已經很流行,我們希望到Go語言社區也能使用這個包。我們該如何做呢?
|
||||
为了演示包基本的用法,先假设我们的温度转换软件已经很流行,我们希望到Go语言社区也能使用这个包。我们该如何做呢?
|
||||
|
||||
讓我們創建一個名爲gopl.io/ch2/tempconv的包,這是前面例子的一個改進版本。(我們約定我們的例子都是以章節順序來編號的,這樣的路徑更容易閲讀)包代碼存儲在兩個源文件中,用來演示如何在一個源文件聲明然後在其他的源文件訪問;雖然在現實中,這樣小的包一般隻需要一個文件。
|
||||
让我们创建一个名为gopl.io/ch2/tempconv的包,这是前面例子的一个改进版本。(我们约定我们的例子都是以章节顺序来编号的,这样的路径更容易阅读)包代码存储在两个源文件中,用来演示如何在一个源文件声明然后在其他的源文件访问;虽然在现实中,这样小的包一般只需要一个文件。
|
||||
|
||||
我們把變量的聲明、對應的常量,還有方法都放到tempconv.go源文件中:
|
||||
我们把变量的声明、对应的常量,还有方法都放到tempconv.go源文件中:
|
||||
|
||||
<u></i>gopl.io/ch2/tempconv</i></u>
|
||||
```Go
|
||||
@@ -32,7 +32,7 @@ func (c Celsius) String() string { return fmt.Sprintf("%g°C", c) }
|
||||
func (f Fahrenheit) String() string { return fmt.Sprintf("%g°F", f) }
|
||||
```
|
||||
|
||||
轉換函數則放在另一個conv.go源文件中:
|
||||
转换函数则放在另一个conv.go源文件中:
|
||||
|
||||
```Go
|
||||
package tempconv
|
||||
@@ -44,23 +44,23 @@ 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"
|
||||
```
|
||||
|
||||
要將攝氏溫度轉換爲華氏溫度,需要先用import語句導入gopl.io/ch2/tempconv包,然後就可以使用下面的代碼進行轉換了:
|
||||
要将摄氏温度转换为华氏温度,需要先用import语句导入gopl.io/ch2/tempconv包,然后就可以使用下面的代码进行转换了:
|
||||
|
||||
```Go
|
||||
fmt.Println(tempconv.CToF(tempconv.BoilingC)) // "212°F"
|
||||
```
|
||||
|
||||
在每個源文件的包聲明前僅跟着的註釋是包註釋(§10.7.4)。通常,包註釋的第一句應該先是包的功能概要説明。一個包通常隻有一個源文件有包註釋(譯註:如果有多個包註釋,目前的文檔工具會根據源文件名的先後順序將它們鏈接爲一個包註釋)。如果包註釋很大,通常會放到一個獨立的doc.go文件中。
|
||||
在每个源文件的包声明前仅跟着的注释是包注释(§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等是在全局作用域的,因此可以在整個程序中直接使用。任何在在函數外部(也就是包級語法域)聲明的名字可以在同一個包的任何源文件中訪問的。對於導入的包,例如tempconv導入的fmt包,則是對應源文件級的作用域,因此隻能在當前的文件中訪問導入的fmt包,當前包的其它源文件無法訪問在當前源文件導入的包。還有許多聲明語句,比如tempconv.CToF函數中的變量c,則是局部作用域的,它隻能在函數內部(甚至隻能是局部的某些部分)訪問。
|
||||
声明语句对应的词法域决定了作用域范围的大小。对于内置的类型、函数和常量,比如int、len和true等是在全局作用域的,因此可以在整个程序中直接使用。任何在在函数外部(也就是包级语法域)声明的名字可以在同一个包的任何源文件中访问的。对于导入的包,例如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() {}
|
||||
@@ -27,7 +27,7 @@ func main() {
|
||||
}
|
||||
```
|
||||
|
||||
在函數中詞法域可以深度嵌套,因此內部的一個聲明可能屏蔽外部的聲明。還有許多語法塊是if或for等控製流語句構造的。下面的代碼有三個不同的變量x,因爲它們是定義在不同的詞法域(這個例子隻是爲了演示作用域規則,但不是好的編程風格)。
|
||||
在函数中词法域可以深度嵌套,因此内部的一个声明可能屏蔽外部的声明。还有许多语法块是if或for等控制流语句构造的。下面的代码有三个不同的变量x,因为它们是定义在不同的词法域(这个例子只是为了演示作用域规则,但不是好的编程风格)。
|
||||
|
||||
```Go
|
||||
func main() {
|
||||
@@ -42,11 +42,11 @@ 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隱式的初始化詞法域,一個在for循環體詞法域;隻有兩個塊是顯式創建的:
|
||||
下面的例子同样有三个不同的x变量,每个声明在不同的词法域,一个在函数体词法域,一个在for隐式的初始化词法域,一个在for循环体词法域;只有两个块是显式创建的:
|
||||
|
||||
```Go
|
||||
func main() {
|
||||
@@ -58,7 +58,7 @@ func main() {
|
||||
}
|
||||
```
|
||||
|
||||
和for循環類似,if和switch語句也會在條件部分創建隱式詞法域,還有它們對應的執行體詞法域。下面的if-else測試鏈演示了x和y的有效作用域范圍:
|
||||
和for循环类似,if和switch语句也会在条件部分创建隐式词法域,还有它们对应的执行体词法域。下面的if-else测试链演示了x和y的有效作用域范围:
|
||||
|
||||
```Go
|
||||
if x := f(); x == 0 {
|
||||
@@ -71,11 +71,11 @@ if x := f(); x == 0 {
|
||||
fmt.Println(x, y) // compile error: x and y are not visible here
|
||||
```
|
||||
|
||||
第二個if語句嵌套在第一個內部,因此第一個if語句條件初始化詞法域聲明的變量在第二個if中也可以訪問。switch語句的每個分支也有類似的詞法域規則:條件部分爲一個隱式詞法域,然後每個是每個分支的詞法域。
|
||||
第二个if语句嵌套在第一个内部,因此第一个if语句条件初始化词法域声明的变量在第二个if中也可以访问。switch语句的每个分支也有类似的词法域规则:条件部分为一个隐式词法域,然后每个是每个分支的词法域。
|
||||
|
||||
在包級别,聲明的順序併不會影響作用域范圍,因此一個先聲明的可以引用它自身或者是引用後面的一個聲明,這可以讓我們定義一些相互嵌套或遞歸的類型或函數。但是如果一個變量或常量遞歸引用了自身,則會産生編譯錯誤。
|
||||
在包级别,声明的顺序并不会影响作用域范围,因此一个先声明的可以引用它自身或者是引用后面的一个声明,这可以让我们定义一些相互嵌套或递归的类型或函数。但是如果一个变量或常量递归引用了自身,则会产生编译错误。
|
||||
|
||||
在這個程序中:
|
||||
在这个程序中:
|
||||
|
||||
```Go
|
||||
if f, err := os.Open(fname); err != nil { // compile error: unused: f
|
||||
@@ -85,9 +85,9 @@ f.ReadByte() // compile error: undefined f
|
||||
f.Close() // compile error: undefined f
|
||||
```
|
||||
|
||||
變量f的作用域隻有在if語句內,因此後面的語句將無法引入它,這將導致編譯錯誤。你可能會收到一個局部變量f沒有聲明的錯誤提示,具體錯誤信息依賴編譯器的實現。
|
||||
变量f的作用域只有在if语句内,因此后面的语句将无法引入它,这将导致编译错误。你可能会收到一个局部变量f没有声明的错误提示,具体错误信息依赖编译器的实现。
|
||||
|
||||
通常需要在if之前聲明變量,這樣可以確保後面的語句依然可以訪問變量:
|
||||
通常需要在if之前声明变量,这样可以确保后面的语句依然可以访问变量:
|
||||
|
||||
```Go
|
||||
f, err := os.Open(fname)
|
||||
@@ -98,7 +98,7 @@ f.ReadByte()
|
||||
f.Close()
|
||||
```
|
||||
|
||||
你可能會考慮通過將ReadByte和Close移動到if的else塊來解決這個問題:
|
||||
你可能会考虑通过将ReadByte和Close移动到if的else块来解决这个问题:
|
||||
|
||||
```Go
|
||||
if f, err := os.Open(fname); err != nil {
|
||||
@@ -110,9 +110,9 @@ if f, err := os.Open(fname); err != nil {
|
||||
}
|
||||
```
|
||||
|
||||
但這不是Go語言推薦的做法,Go語言的習慣是在if中處理錯誤然後直接返迴,這樣可以確保正常執行的語句不需要代碼縮進。
|
||||
但这不是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,9 +141,9 @@ func init() {
|
||||
}
|
||||
```
|
||||
|
||||
全局的cwd變量依然是沒有被正確初始化的,而且看似正常的日誌輸出更是讓這個BUG更加隱晦。
|
||||
全局的cwd变量依然是没有被正确初始化的,而且看似正常的日志输出更是让这个BUG更加隐晦。
|
||||
|
||||
有許多方式可以避免出現類似潛在的問題。最直接的方法是通過單獨聲明err變量,來避免使用`:=`的簡短聲明方式:
|
||||
有许多方式可以避免出现类似潜在的问题。最直接的方法是通过单独声明err变量,来避免使用`:=`的简短声明方式:
|
||||
|
||||
```Go
|
||||
var cwd string
|
||||
@@ -157,5 +157,5 @@ func init() {
|
||||
}
|
||||
```
|
||||
|
||||
我們已經看到包、文件、聲明和語句如何來表達一個程序結構。在下面的兩個章節,我們將探討數據的結構。
|
||||
我们已经看到包、文件、声明和语句如何来表达一个程序结构。在下面的两个章节,我们将探讨数据的结构。
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# 第2章 程序結構
|
||||
# 第2章 程序结构
|
||||
|
||||
Go語言和其他編程語言一樣,一個大的程序是由很多小的基礎構件組成的。變量保存值,簡單的加法和減法運算被組合成較複雜的表達式。基礎類型被聚合爲數組或結構體等更複雜的數據結構。然後使用if和for之類的控製語句來組織和控製表達式的執行流程。然後多個語句被組織到一個個函數中,以便代碼的隔離和複用。函數以源文件和包的方式被組織。
|
||||
Go语言和其他编程语言一样,一个大的程序是由很多小的基础构件组成的。变量保存值,简单的加法和减法运算被组合成较复杂的表达式。基础类型被聚合为数组或结构体等更复杂的数据结构。然后使用if和for之类的控制语句来组织和控制表达式的执行流程。然后多个语句被组织到一个个函数中,以便代码的隔离和复用。函数以源文件和包的方式被组织。
|
||||
|
||||
我們已經在前面章節的例子中看到了很多例子。在本章中,我們將深入討論Go程序基礎結構方面的一些細節。每個示例程序都是刻意寫的簡單,這樣我們可以減少複雜的算法或數據結構等不相關的問題帶來的榦擾,從而可以專註於Go語言本身的學習。
|
||||
我们已经在前面章节的例子中看到了很多例子。在本章中,我们将深入讨论Go程序基础结构方面的一些细节。每个示例程序都是刻意写的简单,这样我们可以减少复杂的算法或数据结构等不相关的问题带来的干扰,从而可以专注于Go语言本身的学习。
|
||||
|
||||
Reference in New Issue
Block a user