mirror of
https://github.com/gopl-zh/gopl-zh.github.com.git
synced 2025-12-17 11:14:20 +08:00
回到简体
This commit is contained in:
@@ -1,8 +1,8 @@
|
||||
## 4.1. 數組
|
||||
## 4.1. 数组
|
||||
|
||||
數組是一個由固定長度的特定類型元素組成的序列,一個數組可以由零個或多個元素組成。因爲數組的長度是固定的,因此在Go語言中很少直接使用數組。和數組對應的類型是Slice(切片),它是可以增長和收縮動態序列,slice功能也更靈活,但是要理解slice工作原理的話需要先理解數組。
|
||||
数组是一个由固定长度的特定类型元素组成的序列,一个数组可以由零个或多个元素组成。因为数组的长度是固定的,因此在Go语言中很少直接使用数组。和数组对应的类型是Slice(切片),它是可以增长和收缩动态序列,slice功能也更灵活,但是要理解slice工作原理的话需要先理解数组。
|
||||
|
||||
數組的每個元素可以通過索引下標來訪問,索引下標的范圍是從0開始到數組長度減1的位置。內置的len函數將返迴數組中元素的個數。
|
||||
数组的每个元素可以通过索引下标来访问,索引下标的范围是从0开始到数组长度减1的位置。内置的len函数将返回数组中元素的个数。
|
||||
|
||||
```Go
|
||||
var a [3]int // array of 3 integers
|
||||
@@ -20,7 +20,7 @@ for _, v := range a {
|
||||
}
|
||||
```
|
||||
|
||||
默認情況下,數組的每個元素都被初始化爲元素類型對應的零值,對於數字類型來説就是0。我們也可以使用數組字面值語法用一組值來初始化數組:
|
||||
默认情况下,数组的每个元素都被初始化为元素类型对应的零值,对于数字类型来说就是0。我们也可以使用数组字面值语法用一组值来初始化数组:
|
||||
|
||||
```Go
|
||||
var q [3]int = [3]int{1, 2, 3}
|
||||
@@ -28,30 +28,30 @@ var r [3]int = [3]int{1, 2}
|
||||
fmt.Println(r[2]) // "0"
|
||||
```
|
||||
|
||||
在數組字面值中,如果在數組的長度位置出現的是“...”省略號,則表示數組的長度是根據初始化值的個數來計算。因此,上面q數組的定義可以簡化爲
|
||||
在数组字面值中,如果在数组的长度位置出现的是“...”省略号,则表示数组的长度是根据初始化值的个数来计算。因此,上面q数组的定义可以简化为
|
||||
|
||||
```Go
|
||||
q := [...]int{1, 2, 3}
|
||||
fmt.Printf("%T\n", q) // "[3]int"
|
||||
```
|
||||
|
||||
數組的長度是數組類型的一個組成部分,因此[3]int和[4]int是兩種不同的數組類型。數組的長度必須是常量表達式,因爲數組的長度需要在編譯階段確定。
|
||||
数组的长度是数组类型的一个组成部分,因此[3]int和[4]int是两种不同的数组类型。数组的长度必须是常量表达式,因为数组的长度需要在编译阶段确定。
|
||||
|
||||
```Go
|
||||
q := [3]int{1, 2, 3}
|
||||
q = [4]int{1, 2, 3, 4} // compile error: cannot assign [4]int to [3]int
|
||||
```
|
||||
|
||||
我們將會發現,數組、slice、map和結構體字面值的寫法都很相似。上面的形式是直接提供順序初始化值序列,但是也可以指定一個索引和對應值列表的方式初始化,就像下面這樣:
|
||||
我们将会发现,数组、slice、map和结构体字面值的写法都很相似。上面的形式是直接提供顺序初始化值序列,但是也可以指定一个索引和对应值列表的方式初始化,就像下面这样:
|
||||
|
||||
```Go
|
||||
type Currency int
|
||||
|
||||
const (
|
||||
USD Currency = iota // 美元
|
||||
EUR // 歐元
|
||||
GBP // 英鎊
|
||||
RMB // 人民幣
|
||||
EUR // 欧元
|
||||
GBP // 英镑
|
||||
RMB // 人民币
|
||||
)
|
||||
|
||||
symbol := [...]string{USD: "$", EUR: "€", GBP: "£", RMB: "¥"}
|
||||
@@ -59,15 +59,15 @@ symbol := [...]string{USD: "$", EUR: "€", GBP: "£", RMB: "¥"}
|
||||
fmt.Println(RMB, symbol[RMB]) // "3 ¥"
|
||||
```
|
||||
|
||||
在這種形式的數組字面值形式中,初始化索引的順序是無關緊要的,而且沒用到的索引可以省略,和前面提到的規則一樣,未指定初始值的元素將用零值初始化。例如,
|
||||
在这种形式的数组字面值形式中,初始化索引的顺序是无关紧要的,而且没用到的索引可以省略,和前面提到的规则一样,未指定初始值的元素将用零值初始化。例如,
|
||||
|
||||
```Go
|
||||
r := [...]int{99: -1}
|
||||
```
|
||||
|
||||
定義了一個含有100個元素的數組r,最後一個元素被初始化爲-1,其它元素都是用0初始化。
|
||||
定义了一个含有100个元素的数组r,最后一个元素被初始化为-1,其它元素都是用0初始化。
|
||||
|
||||
如果一個數組的元素類型是可以相互比較的,那麽數組類型也是可以相互比較的,這時候我們可以直接通過==比較運算符來比較兩個數組,隻有當兩個數組的所有元素都是相等的時候數組才是相等的。不相等比較運算符!=遵循同樣的規則。
|
||||
如果一个数组的元素类型是可以相互比较的,那么数组类型也是可以相互比较的,这时候我们可以直接通过==比较运算符来比较两个数组,只有当两个数组的所有元素都是相等的时候数组才是相等的。不相等比较运算符!=遵循同样的规则。
|
||||
|
||||
```Go
|
||||
a := [2]int{1, 2}
|
||||
@@ -78,7 +78,7 @@ d := [3]int{1, 2}
|
||||
fmt.Println(a == d) // compile error: cannot compare [2]int == [3]int
|
||||
```
|
||||
|
||||
作爲一個眞實的例子,crypto/sha256包的Sum256函數對一個任意的字節slice類型的數據生成一個對應的消息摘要。消息摘要有256bit大小,因此對應[32]byte數組類型。如果兩個消息摘要是相同的,那麽可以認爲兩個消息本身也是相同(譯註:理論上有HASH碼碰撞的情況,但是實際應用可以基本忽略);如果消息摘要不同,那麽消息本身必然也是不同的。下面的例子用SHA256算法分别生成“x”和“X”兩個信息的摘要:
|
||||
作为一个真实的例子,crypto/sha256包的Sum256函数对一个任意的字节slice类型的数据生成一个对应的消息摘要。消息摘要有256bit大小,因此对应[32]byte数组类型。如果两个消息摘要是相同的,那么可以认为两个消息本身也是相同(译注:理论上有HASH码碰撞的情况,但是实际应用可以基本忽略);如果消息摘要不同,那么消息本身必然也是不同的。下面的例子用SHA256算法分别生成“x”和“X”两个信息的摘要:
|
||||
|
||||
<u><i>gopl.io/ch4/sha256</i></u>
|
||||
```Go
|
||||
@@ -96,11 +96,11 @@ func main() {
|
||||
}
|
||||
```
|
||||
|
||||
上面例子中,兩個消息雖然隻有一個字符的差異,但是生成的消息摘要則幾乎有一半的bit位是不相同的。需要註意Printf函數的%x副詞參數,它用於指定以十六進製的格式打印數組或slice全部的元素,%t副詞參數是用於打印布爾型數據,%T副詞參數是用於顯示一個值對應的數據類型。
|
||||
上面例子中,两个消息虽然只有一个字符的差异,但是生成的消息摘要则几乎有一半的bit位是不相同的。需要注意Printf函数的%x副词参数,它用于指定以十六进制的格式打印数组或slice全部的元素,%t副词参数是用于打印布尔型数据,%T副词参数是用于显示一个值对应的数据类型。
|
||||
|
||||
當調用一個函數的時候,函數的每個調用參數將會被賦值給函數內部的參數變量,所以函數參數變量接收的是一個複製的副本,併不是原始調用的變量。因爲函數參數傳遞的機製導致傳遞大的數組類型將是低效的,併且對數組參數的任何的脩改都是發生在複製的數組上,併不能直接脩改調用時原始的數組變量。在這個方面,Go語言對待數組的方式和其它很多編程語言不同,其它編程語言可能會隱式地將數組作爲引用或指針對象傳入被調用的函數。
|
||||
当调用一个函数的时候,函数的每个调用参数将会被赋值给函数内部的参数变量,所以函数参数变量接收的是一个复制的副本,并不是原始调用的变量。因为函数参数传递的机制导致传递大的数组类型将是低效的,并且对数组参数的任何的修改都是发生在复制的数组上,并不能直接修改调用时原始的数组变量。在这个方面,Go语言对待数组的方式和其它很多编程语言不同,其它编程语言可能会隐式地将数组作为引用或指针对象传入被调用的函数。
|
||||
|
||||
當然,我們可以顯式地傳入一個數組指針,那樣的話函數通過指針對數組的任何脩改都可以直接反饋到調用者。下面的函數用於給[32]byte類型的數組清零:
|
||||
当然,我们可以显式地传入一个数组指针,那样的话函数通过指针对数组的任何修改都可以直接反馈到调用者。下面的函数用于给[32]byte类型的数组清零:
|
||||
|
||||
```Go
|
||||
func zero(ptr *[32]byte) {
|
||||
@@ -110,7 +110,7 @@ func zero(ptr *[32]byte) {
|
||||
}
|
||||
```
|
||||
|
||||
其實數組字面值[32]byte{}就可以生成一個32字節的數組。而且每個數組的元素都是零值初始化,也就是0。因此,我們可以將上面的zero函數寫的更簡潔一點:
|
||||
其实数组字面值[32]byte{}就可以生成一个32字节的数组。而且每个数组的元素都是零值初始化,也就是0。因此,我们可以将上面的zero函数写的更简洁一点:
|
||||
|
||||
```Go
|
||||
func zero(ptr *[32]byte) {
|
||||
@@ -118,8 +118,8 @@ func zero(ptr *[32]byte) {
|
||||
}
|
||||
```
|
||||
|
||||
雖然通過指針來傳遞數組參數是高效的,而且也允許在函數內部脩改數組的值,但是數組依然是殭化的類型,因爲數組的類型包含了殭化的長度信息。上面的zero函數併不能接收指向[16]byte類型數組的指針,而且也沒有任何添加或刪除數組元素的方法。由於這些原因,除了像SHA256這類需要處理特定大小數組的特例外,數組依然很少用作函數參數;相反,我們一般使用slice來替代數組。
|
||||
虽然通过指针来传递数组参数是高效的,而且也允许在函数内部修改数组的值,但是数组依然是僵化的类型,因为数组的类型包含了僵化的长度信息。上面的zero函数并不能接收指向[16]byte类型数组的指针,而且也没有任何添加或删除数组元素的方法。由于这些原因,除了像SHA256这类需要处理特定大小数组的特例外,数组依然很少用作函数参数;相反,我们一般使用slice来替代数组。
|
||||
|
||||
**練習 4.1:** 編寫一個函數,計算兩個SHA256哈希碼中不同bit的數目。(參考2.6.2節的PopCount函數。)
|
||||
**练习 4.1:** 编写一个函数,计算两个SHA256哈希码中不同bit的数目。(参考2.6.2节的PopCount函数。)
|
||||
|
||||
**練習 4.2:** 編寫一個程序,默認打印標準輸入的以SHA256哈希碼,也可以通過命令行標準參數選擇SHA384或SHA512哈希算法。
|
||||
**练习 4.2:** 编写一个程序,默认打印标准输入的以SHA256哈希码,也可以通过命令行标准参数选择SHA384或SHA512哈希算法。
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
### 4.2.1. append函數
|
||||
### 4.2.1. append函数
|
||||
|
||||
內置的append函數用於向slice追加元素:
|
||||
内置的append函数用于向slice追加元素:
|
||||
|
||||
```Go
|
||||
var runes []rune
|
||||
@@ -10,9 +10,9 @@ for _, r := range "Hello, 世界" {
|
||||
fmt.Printf("%q\n", runes) // "['H' 'e' 'l' 'l' 'o' ',' ' ' '世' '界']"
|
||||
```
|
||||
|
||||
在循環中使用append函數構建一個由九個rune字符構成的slice,當然對應這個特殊的問題我們可以通過Go語言內置的[]rune("Hello, 世界")轉換操作完成。
|
||||
在循环中使用append函数构建一个由九个rune字符构成的slice,当然对应这个特殊的问题我们可以通过Go语言内置的[]rune("Hello, 世界")转换操作完成。
|
||||
|
||||
append函數對於理解slice底層是如何工作的非常重要,所以讓我們仔細査看究竟是發生了什麽。下面是第一個版本的appendInt函數,專門用於處理[]int類型的slice:
|
||||
append函数对于理解slice底层是如何工作的非常重要,所以让我们仔细查看究竟是发生了什么。下面是第一个版本的appendInt函数,专门用于处理[]int类型的slice:
|
||||
|
||||
<u><i>gopl.io/ch4/append</i></u>
|
||||
```Go
|
||||
@@ -37,13 +37,13 @@ func appendInt(x []int, y int) []int {
|
||||
}
|
||||
```
|
||||
|
||||
每次調用appendInt函數,必須先檢測slice底層數組是否有足夠的容量來保存新添加的元素。如果有足夠空間的話,直接擴展slice(依然在原有的底層數組之上),將新添加的y元素複製到新擴展的空間,併返迴slice。因此,輸入的x和輸出的z共享相同的底層數組。
|
||||
每次调用appendInt函数,必须先检测slice底层数组是否有足够的容量来保存新添加的元素。如果有足够空间的话,直接扩展slice(依然在原有的底层数组之上),将新添加的y元素复制到新扩展的空间,并返回slice。因此,输入的x和输出的z共享相同的底层数组。
|
||||
|
||||
如果沒有足夠的增長空間的話,appendInt函數則會先分配一個足夠大的slice用於保存新的結果,先將輸入的x複製到新的空間,然後添加y元素。結果z和輸入的x引用的將是不同的底層數組。
|
||||
如果没有足够的增长空间的话,appendInt函数则会先分配一个足够大的slice用于保存新的结果,先将输入的x复制到新的空间,然后添加y元素。结果z和输入的x引用的将是不同的底层数组。
|
||||
|
||||
雖然通過循環複製元素更直接,不過內置的copy函數可以方便地將一個slice複製另一個相同類型的slice。copy函數的第一個參數是要複製的目標slice,第二個參數是源slice,目標和源的位置順序和`dst = src`賦值語句是一致的。兩個slice可以共享同一個底層數組,甚至有重疊也沒有問題。copy函數將返迴成功複製的元素的個數(我們這里沒有用到),等於兩個slice中較小的長度,所以我們不用擔心覆蓋會超出目標slice的范圍。
|
||||
虽然通过循环复制元素更直接,不过内置的copy函数可以方便地将一个slice复制另一个相同类型的slice。copy函数的第一个参数是要复制的目标slice,第二个参数是源slice,目标和源的位置顺序和`dst = src`赋值语句是一致的。两个slice可以共享同一个底层数组,甚至有重叠也没有问题。copy函数将返回成功复制的元素的个数(我们这里没有用到),等于两个slice中较小的长度,所以我们不用担心覆盖会超出目标slice的范围。
|
||||
|
||||
爲了提高內存使用效率,新分配的數組一般略大於保存x和y所需要的最低大小。通過在每次擴展數組時直接將長度翻倍從而避免了多次內存分配,也確保了添加單個元素操的平均時間是一個常數時間。這個程序演示了效果:
|
||||
为了提高内存使用效率,新分配的数组一般略大于保存x和y所需要的最低大小。通过在每次扩展数组时直接将长度翻倍从而避免了多次内存分配,也确保了添加单个元素操的平均时间是一个常数时间。这个程序演示了效果:
|
||||
|
||||
```Go
|
||||
func main() {
|
||||
@@ -56,7 +56,7 @@ func main() {
|
||||
}
|
||||
```
|
||||
|
||||
每一次容量的變化都會導致重新分配內存和copy操作:
|
||||
每一次容量的变化都会导致重新分配内存和copy操作:
|
||||
|
||||
```
|
||||
0 cap=1 [0]
|
||||
@@ -71,21 +71,21 @@ func main() {
|
||||
9 cap=16 [0 1 2 3 4 5 6 7 8 9]
|
||||
```
|
||||
|
||||
讓我們仔細査看i=3次的迭代。當時x包含了[0 1 2]三個元素,但是容量是4,因此可以簡單將新的元素添加到末尾,不需要新的內存分配。然後新的y的長度和容量都是4,併且和x引用着相同的底層數組,如圖4.2所示。
|
||||
让我们仔细查看i=3次的迭代。当时x包含了[0 1 2]三个元素,但是容量是4,因此可以简单将新的元素添加到末尾,不需要新的内存分配。然后新的y的长度和容量都是4,并且和x引用着相同的底层数组,如图4.2所示。
|
||||
|
||||

|
||||
|
||||
在下一次迭代時i=4,現在沒有新的空餘的空間了,因此appendInt函數分配一個容量爲8的底層數組,將x的4個元素[0 1 2 3]複製到新空間的開頭,然後添加新的元素i,新元素的值是4。新的y的長度是5,容量是8;後面有3個空閒的位置,三次迭代都不需要分配新的空間。當前迭代中,y和x是對應不同底層數組的view。這次操作如圖4.3所示。
|
||||
在下一次迭代时i=4,现在没有新的空余的空间了,因此appendInt函数分配一个容量为8的底层数组,将x的4个元素[0 1 2 3]复制到新空间的开头,然后添加新的元素i,新元素的值是4。新的y的长度是5,容量是8;后面有3个空闲的位置,三次迭代都不需要分配新的空间。当前迭代中,y和x是对应不同底层数组的view。这次操作如图4.3所示。
|
||||
|
||||

|
||||
|
||||
內置的append函數可能使用比appendInt更複雜的內存擴展策略。因此,通常我們併不知道append調用是否導致了內存的重新分配,因此我們也不能確認新的slice和原始的slice是否引用的是相同的底層數組空間。同樣,我們不能確認在原先的slice上的操作是否會影響到新的slice。因此,通常是將append返迴的結果直接賦值給輸入的slice變量:
|
||||
内置的append函数可能使用比appendInt更复杂的内存扩展策略。因此,通常我们并不知道append调用是否导致了内存的重新分配,因此我们也不能确认新的slice和原始的slice是否引用的是相同的底层数组空间。同样,我们不能确认在原先的slice上的操作是否会影响到新的slice。因此,通常是将append返回的结果直接赋值给输入的slice变量:
|
||||
|
||||
```Go
|
||||
runes = append(runes, r)
|
||||
```
|
||||
|
||||
更新slice變量不僅對調用append函數是必要的,實際上對應任何可能導致長度、容量或底層數組變化的操作都是必要的。要正確地使用slice,需要記住盡管底層數組的元素是間接訪問的,但是slice對應結構體本身的指針、長度和容量部分是直接訪問的。要更新這些信息需要像上面例子那樣一個顯式的賦值操作。從這個角度看,slice併不是一個純粹的引用類型,它實際上是一個類似下面結構體的聚合類型:
|
||||
更新slice变量不仅对调用append函数是必要的,实际上对应任何可能导致长度、容量或底层数组变化的操作都是必要的。要正确地使用slice,需要记住尽管底层数组的元素是间接访问的,但是slice对应结构体本身的指针、长度和容量部分是直接访问的。要更新这些信息需要像上面例子那样一个显式的赋值操作。从这个角度看,slice并不是一个纯粹的引用类型,它实际上是一个类似下面结构体的聚合类型:
|
||||
|
||||
```Go
|
||||
type IntSlice struct {
|
||||
@@ -94,7 +94,7 @@ type IntSlice struct {
|
||||
}
|
||||
```
|
||||
|
||||
我們的appendInt函數每次隻能向slice追加一個元素,但是內置的append函數則可以追加多個元素,甚至追加一個slice。
|
||||
我们的appendInt函数每次只能向slice追加一个元素,但是内置的append函数则可以追加多个元素,甚至追加一个slice。
|
||||
|
||||
```Go
|
||||
var x []int
|
||||
@@ -105,7 +105,7 @@ x = append(x, x...) // append the slice x
|
||||
fmt.Println(x) // "[1 2 3 4 5 6 1 2 3 4 5 6]"
|
||||
```
|
||||
|
||||
通過下面的小脩改,我們可以可以達到append函數類似的功能。其中在appendInt函數參數中的最後的“...”省略號表示接收變長的參數爲slice。我們將在5.7節詳細解釋這個特性。
|
||||
通过下面的小修改,我们可以可以达到append函数类似的功能。其中在appendInt函数参数中的最后的“...”省略号表示接收变长的参数为slice。我们将在5.7节详细解释这个特性。
|
||||
|
||||
```Go
|
||||
func appendInt(x []int, y ...int) []int {
|
||||
@@ -117,4 +117,4 @@ func appendInt(x []int, y ...int) []int {
|
||||
}
|
||||
```
|
||||
|
||||
爲了避免重複,和前面相同的代碼併沒有顯示。
|
||||
为了避免重复,和前面相同的代码并没有显示。
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
### 4.2.2. Slice內存技巧
|
||||
### 4.2.2. Slice内存技巧
|
||||
|
||||
讓我們看看更多的例子,比如镟轉slice、反轉slice或在slice原有內存空間脩改元素。給定一個字符串列表,下面的nonempty函數將在原有slice內存空間之上返迴不包含空字符串的列表:
|
||||
让我们看看更多的例子,比如旋转slice、反转slice或在slice原有内存空间修改元素。给定一个字符串列表,下面的nonempty函数将在原有slice内存空间之上返回不包含空字符串的列表:
|
||||
|
||||
<u><i>gopl.io/ch4/nonempty</i></u>
|
||||
```Go
|
||||
@@ -23,7 +23,7 @@ func nonempty(strings []string) []string {
|
||||
}
|
||||
```
|
||||
|
||||
比較微妙的地方是,輸入的slice和輸出的slice共享一個底層數組。這可以避免分配另一個數組,不過原來的數據將可能會被覆蓋,正如下面兩個打印語句看到的那樣:
|
||||
比较微妙的地方是,输入的slice和输出的slice共享一个底层数组。这可以避免分配另一个数组,不过原来的数据将可能会被覆盖,正如下面两个打印语句看到的那样:
|
||||
|
||||
```Go
|
||||
data := []string{"one", "", "three"}
|
||||
@@ -31,9 +31,9 @@ fmt.Printf("%q\n", nonempty(data)) // `["one" "three"]`
|
||||
fmt.Printf("%q\n", data) // `["one" "three" "three"]`
|
||||
```
|
||||
|
||||
因此我們通常會這樣使用nonempty函數:`data = nonempty(data)`。
|
||||
因此我们通常会这样使用nonempty函数:`data = nonempty(data)`。
|
||||
|
||||
nonempty函數也可以使用append函數實現:
|
||||
nonempty函数也可以使用append函数实现:
|
||||
|
||||
```Go
|
||||
func nonempty2(strings []string) []string {
|
||||
@@ -47,27 +47,27 @@ func nonempty2(strings []string) []string {
|
||||
}
|
||||
```
|
||||
|
||||
無論如何實現,以這種方式重用一個slice一般都要求最多爲每個輸入值産生一個輸出值,事實上很多這類算法都是用來過濾或合併序列中相鄰的元素。這種slice用法是比較複雜的技巧,雖然使用到了slice的一些技巧,但是對於某些場合是比較清晰和有效的。
|
||||
无论如何实现,以这种方式重用一个slice一般都要求最多为每个输入值产生一个输出值,事实上很多这类算法都是用来过滤或合并序列中相邻的元素。这种slice用法是比较复杂的技巧,虽然使用到了slice的一些技巧,但是对于某些场合是比较清晰和有效的。
|
||||
|
||||
一個slice可以用來模擬一個stack。最初給定的空slice對應一個空的stack,然後可以使用append函數將新的值壓入stack:
|
||||
一个slice可以用来模拟一个stack。最初给定的空slice对应一个空的stack,然后可以使用append函数将新的值压入stack:
|
||||
|
||||
```Go
|
||||
stack = append(stack, v) // push v
|
||||
```
|
||||
|
||||
stack的頂部位置對應slice的最後一個元素:
|
||||
stack的顶部位置对应slice的最后一个元素:
|
||||
|
||||
```Go
|
||||
top := stack[len(stack)-1] // top of stack
|
||||
```
|
||||
|
||||
通過收縮stack可以彈出棧頂的元素
|
||||
通过收缩stack可以弹出栈顶的元素
|
||||
|
||||
```Go
|
||||
stack = stack[:len(stack)-1] // pop
|
||||
```
|
||||
|
||||
要刪除slice中間的某個元素併保存原有的元素順序,可以通過內置的copy函數將後面的子slice向前依次移動一位完成:
|
||||
要删除slice中间的某个元素并保存原有的元素顺序,可以通过内置的copy函数将后面的子slice向前依次移动一位完成:
|
||||
|
||||
```Go
|
||||
func remove(slice []int, i int) []int {
|
||||
@@ -81,7 +81,7 @@ func main() {
|
||||
}
|
||||
```
|
||||
|
||||
如果刪除元素後不用保持原來順序的話,我們可以簡單的用最後一個元素覆蓋被刪除的元素:
|
||||
如果删除元素后不用保持原来顺序的话,我们可以简单的用最后一个元素覆盖被删除的元素:
|
||||
|
||||
```Go
|
||||
func remove(slice []int, i int) []int {
|
||||
@@ -95,12 +95,12 @@ func main() {
|
||||
}
|
||||
```
|
||||
|
||||
**練習 4.3:** 重寫reverse函數,使用數組指針代替slice。
|
||||
**练习 4.3:** 重写reverse函数,使用数组指针代替slice。
|
||||
|
||||
**練習 4.4:** 編寫一個rotate函數,通過一次循環完成镟轉。
|
||||
**练习 4.4:** 编写一个rotate函数,通过一次循环完成旋转。
|
||||
|
||||
**練習 4.5:** 寫一個函數在原地完成消除[]string中相鄰重複的字符串的操作。
|
||||
**练习 4.5:** 写一个函数在原地完成消除[]string中相邻重复的字符串的操作。
|
||||
|
||||
**練習 4.6:** 編寫一個函數,原地將一個UTF-8編碼的[]byte類型的slice中相鄰的空格(參考unicode.IsSpace)替換成一個空格返迴
|
||||
**练习 4.6:** 编写一个函数,原地将一个UTF-8编码的[]byte类型的slice中相邻的空格(参考unicode.IsSpace)替换成一个空格返回
|
||||
|
||||
**練習 4.7:** 脩改reverse函數用於原地反轉UTF-8編碼的[]byte。是否可以不用分配額外的內存?
|
||||
**练习 4.7:** 修改reverse函数用于原地反转UTF-8编码的[]byte。是否可以不用分配额外的内存?
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
## 4.2. Slice
|
||||
|
||||
Slice(切片)代表變長的序列,序列中每個元素都有相同的類型。一個slice類型一般寫作[]T,其中T代表slice中元素的類型;slice的語法和數組很像,隻是沒有固定長度而已。
|
||||
Slice(切片)代表变长的序列,序列中每个元素都有相同的类型。一个slice类型一般写作[]T,其中T代表slice中元素的类型;slice的语法和数组很像,只是没有固定长度而已。
|
||||
|
||||
數組和slice之間有着緊密的聯繫。一個slice是一個輕量級的數據結構,提供了訪問數組子序列(或者全部)元素的功能,而且slice的底層確實引用一個數組對象。一個slice由三個部分構成:指針、長度和容量。指針指向第一個slice元素對應的底層數組元素的地址,要註意的是slice的第一個元素併不一定就是數組的第一個元素。長度對應slice中元素的數目;長度不能超過容量,容量一般是從slice的開始位置到底層數據的結尾位置。內置的len和cap函數分别返迴slice的長度和容量。
|
||||
数组和slice之间有着紧密的联系。一个slice是一个轻量级的数据结构,提供了访问数组子序列(或者全部)元素的功能,而且slice的底层确实引用一个数组对象。一个slice由三个部分构成:指针、长度和容量。指针指向第一个slice元素对应的底层数组元素的地址,要注意的是slice的第一个元素并不一定就是数组的第一个元素。长度对应slice中元素的数目;长度不能超过容量,容量一般是从slice的开始位置到底层数据的结尾位置。内置的len和cap函数分别返回slice的长度和容量。
|
||||
|
||||
多個slice之間可以共享底層的數據,併且引用的數組部分區間可能重疊。圖4.1顯示了表示一年中每個月份名字的字符串數組,還有重疊引用了該數組的兩個slice。數組這樣定義
|
||||
多个slice之间可以共享底层的数据,并且引用的数组部分区间可能重叠。图4.1显示了表示一年中每个月份名字的字符串数组,还有重叠引用了该数组的两个slice。数组这样定义
|
||||
|
||||
```Go
|
||||
months := [...]string{1: "January", /* ... */, 12: "December"}
|
||||
```
|
||||
|
||||
因此一月份是months[1],十二月份是months[12]。通常,數組的第一個元素從索引0開始,但是月份一般是從1開始的,因此我們聲明數組時直接跳過第0個元素,第0個元素會被自動初始化爲空字符串。
|
||||
因此一月份是months[1],十二月份是months[12]。通常,数组的第一个元素从索引0开始,但是月份一般是从1开始的,因此我们声明数组时直接跳过第0个元素,第0个元素会被自动初始化为空字符串。
|
||||
|
||||
slice的切片操作s[i:j],其中0 ≤ i≤ j≤ cap(s),用於創建一個新的slice,引用s的從第i個元素開始到第j-1個元素的子序列。新的slice將隻有j-i個元素。如果i位置的索引被省略的話將使用0代替,如果j位置的索引被省略的話將使用len(s)代替。因此,months[1:13]切片操作將引用全部有效的月份,和months[1:]操作等價;months[:]切片操作則是引用整個數組。讓我們分别定義表示第二季度和北方夏天月份的slice,它們有重疊部分:
|
||||
slice的切片操作s[i:j],其中0 ≤ i≤ j≤ cap(s),用于创建一个新的slice,引用s的从第i个元素开始到第j-1个元素的子序列。新的slice将只有j-i个元素。如果i位置的索引被省略的话将使用0代替,如果j位置的索引被省略的话将使用len(s)代替。因此,months[1:13]切片操作将引用全部有效的月份,和months[1:]操作等价;months[:]切片操作则是引用整个数组。让我们分别定义表示第二季度和北方夏天月份的slice,它们有重叠部分:
|
||||
|
||||

|
||||
|
||||
@@ -23,7 +23,7 @@ fmt.Println(Q2) // ["April" "May" "June"]
|
||||
fmt.Println(summer) // ["June" "July" "August"]
|
||||
```
|
||||
|
||||
兩個slice都包含了六月份,下面的代碼是一個包含相同月份的測試(性能較低):
|
||||
两个slice都包含了六月份,下面的代码是一个包含相同月份的测试(性能较低):
|
||||
|
||||
```Go
|
||||
for _, s := range summer {
|
||||
@@ -35,7 +35,7 @@ for _, s := range summer {
|
||||
}
|
||||
```
|
||||
|
||||
如果切片操作超出cap(s)的上限將導致一個panic異常,但是超出len(s)則是意味着擴展了slice,因爲新slice的長度會變大:
|
||||
如果切片操作超出cap(s)的上限将导致一个panic异常,但是超出len(s)则是意味着扩展了slice,因为新slice的长度会变大:
|
||||
|
||||
```Go
|
||||
fmt.Println(summer[:20]) // panic: out of range
|
||||
@@ -44,9 +44,9 @@ endlessSummer := summer[:5] // extend a slice (within capacity)
|
||||
fmt.Println(endlessSummer) // "[June July August September October]"
|
||||
```
|
||||
|
||||
另外,字符串的切片操作和[]byte字節類型切片的切片操作是類似的。它們都寫作x[m:n],併且都是返迴一個原始字節繫列的子序列,底層都是共享之前的底層數組,因此切片操作對應常量時間複雜度。x[m:n]切片操作對於字符串則生成一個新字符串,如果x是[]byte的話則生成一個新的[]byte。
|
||||
另外,字符串的切片操作和[]byte字节类型切片的切片操作是类似的。它们都写作x[m:n],并且都是返回一个原始字节系列的子序列,底层都是共享之前的底层数组,因此切片操作对应常量时间复杂度。x[m:n]切片操作对于字符串则生成一个新字符串,如果x是[]byte的话则生成一个新的[]byte。
|
||||
|
||||
因爲slice值包含指向第一個slice元素的指針,因此向函數傳遞slice將允許在函數內部脩改底層數組的元素。換句話説,複製一個slice隻是對底層的數組創建了一個新的slice别名(§2.3.2)。下面的reverse函數在原內存空間將[]int類型的slice反轉,而且它可以用於任意長度的slice。
|
||||
因为slice值包含指向第一个slice元素的指针,因此向函数传递slice将允许在函数内部修改底层数组的元素。换句话说,复制一个slice只是对底层的数组创建了一个新的slice别名(§2.3.2)。下面的reverse函数在原内存空间将[]int类型的slice反转,而且它可以用于任意长度的slice。
|
||||
|
||||
<u><i>gopl.io/ch4/rev</i></u>
|
||||
```Go
|
||||
@@ -58,7 +58,7 @@ func reverse(s []int) {
|
||||
}
|
||||
```
|
||||
|
||||
這里我們反轉數組的應用:
|
||||
这里我们反转数组的应用:
|
||||
|
||||
```Go
|
||||
a := [...]int{0, 1, 2, 3, 4, 5}
|
||||
@@ -66,7 +66,7 @@ reverse(a[:])
|
||||
fmt.Println(a) // "[5 4 3 2 1 0]"
|
||||
```
|
||||
|
||||
一種將slice元素循環向左镟轉n個元素的方法是三次調用reverse反轉函數,第一次是反轉開頭的n個元素,然後是反轉剩下的元素,最後是反轉整個slice的元素。(如果是向右循環镟轉,則將第三個函數調用移到第一個調用位置就可以了。)
|
||||
一种将slice元素循环向左旋转n个元素的方法是三次调用reverse反转函数,第一次是反转开头的n个元素,然后是反转剩下的元素,最后是反转整个slice的元素。(如果是向右循环旋转,则将第三个函数调用移到第一个调用位置就可以了。)
|
||||
|
||||
```Go
|
||||
s := []int{0, 1, 2, 3, 4, 5}
|
||||
@@ -77,9 +77,9 @@ reverse(s)
|
||||
fmt.Println(s) // "[2 3 4 5 0 1]"
|
||||
```
|
||||
|
||||
要註意的是slice類型的變量s和數組類型的變量a的初始化語法的差異。slice和數組的字面值語法很類似,它們都是用花括弧包含一繫列的初始化元素,但是對於slice併沒有指明序列的長度。這會隱式地創建一個合適大小的數組,然後slice的指針指向底層的數組。就像數組字面值一樣,slice的字面值也可以按順序指定初始化值序列,或者是通過索引和元素值指定,或者的兩種風格的混合語法初始化。
|
||||
要注意的是slice类型的变量s和数组类型的变量a的初始化语法的差异。slice和数组的字面值语法很类似,它们都是用花括弧包含一系列的初始化元素,但是对于slice并没有指明序列的长度。这会隐式地创建一个合适大小的数组,然后slice的指针指向底层的数组。就像数组字面值一样,slice的字面值也可以按顺序指定初始化值序列,或者是通过索引和元素值指定,或者的两种风格的混合语法初始化。
|
||||
|
||||
和數組不同的是,slice之間不能比較,因此我們不能使用==操作符來判斷兩個slice是否含有全部相等元素。不過標準庫提供了高度優化的bytes.Equal函數來判斷兩個字節型slice是否相等([]byte),但是對於其他類型的slice,我們必須自己展開每個元素進行比較:
|
||||
和数组不同的是,slice之间不能比较,因此我们不能使用==操作符来判断两个slice是否含有全部相等元素。不过标准库提供了高度优化的bytes.Equal函数来判断两个字节型slice是否相等([]byte),但是对于其他类型的slice,我们必须自己展开每个元素进行比较:
|
||||
|
||||
```Go
|
||||
func equal(x, y []string) bool {
|
||||
@@ -95,17 +95,17 @@ func equal(x, y []string) bool {
|
||||
}
|
||||
```
|
||||
|
||||
上面關於兩個slice的深度相等測試,運行的時間併不比支持==操作的數組或字符串更多,但是爲何slice不直接支持比較運算符呢?這方面有兩個原因。第一個原因,一個slice的元素是間接引用的,一個slice甚至可以包含自身。雖然有很多辦法處理這種情形,但是沒有一個是簡單有效的。
|
||||
上面关于两个slice的深度相等测试,运行的时间并不比支持==操作的数组或字符串更多,但是为何slice不直接支持比较运算符呢?这方面有两个原因。第一个原因,一个slice的元素是间接引用的,一个slice甚至可以包含自身。虽然有很多办法处理这种情形,但是没有一个是简单有效的。
|
||||
|
||||
第二個原因,因爲slice的元素是間接引用的,一個固定值的slice在不同的時間可能包含不同的元素,因爲底層數組的元素可能會被脩改。併且Go語言中map等哈希表之類的數據結構的key隻做簡單的淺拷貝,它要求在整個聲明週期中相等的key必須對相同的元素。對於像指針或chan之類的引用類型,==相等測試可以判斷兩個是否是引用相同的對象。一個針對slice的淺相等測試的==操作符可能是有一定用處的,也能臨時解決map類型的key問題,但是slice和數組不同的相等測試行爲會讓人睏惑。因此,安全的做法是直接禁止slice之間的比較操作。
|
||||
第二个原因,因为slice的元素是间接引用的,一个固定值的slice在不同的时间可能包含不同的元素,因为底层数组的元素可能会被修改。并且Go语言中map等哈希表之类的数据结构的key只做简单的浅拷贝,它要求在整个声明周期中相等的key必须对相同的元素。对于像指针或chan之类的引用类型,==相等测试可以判断两个是否是引用相同的对象。一个针对slice的浅相等测试的==操作符可能是有一定用处的,也能临时解决map类型的key问题,但是slice和数组不同的相等测试行为会让人困惑。因此,安全的做法是直接禁止slice之间的比较操作。
|
||||
|
||||
slice唯一合法的比較操作是和nil比較,例如:
|
||||
slice唯一合法的比较操作是和nil比较,例如:
|
||||
|
||||
```Go
|
||||
if summer == nil { /* ... */ }
|
||||
```
|
||||
|
||||
一個零值的slice等於nil。一個nil值的slice併沒有底層數組。一個nil值的slice的長度和容量都是0,但是也有非nil值的slice的長度和容量也是0的,例如[]int{}或make([]int, 3)[3:]。與任意類型的nil值一樣,我們可以用[]int(nil)類型轉換表達式來生成一個對應類型slice的nil值。
|
||||
一个零值的slice等于nil。一个nil值的slice并没有底层数组。一个nil值的slice的长度和容量都是0,但是也有非nil值的slice的长度和容量也是0的,例如[]int{}或make([]int, 3)[3:]。与任意类型的nil值一样,我们可以用[]int(nil)类型转换表达式来生成一个对应类型slice的nil值。
|
||||
|
||||
```Go
|
||||
var s []int // len(s) == 0, s == nil
|
||||
@@ -114,16 +114,16 @@ s = []int(nil) // len(s) == 0, s == nil
|
||||
s = []int{} // len(s) == 0, s != nil
|
||||
```
|
||||
|
||||
如果你需要測試一個slice是否是空的,使用len(s) == 0來判斷,而不應該用s == nil來判斷。除了和nil相等比較外,一個nil值的slice的行爲和其它任意0長度的slice一樣;例如reverse(nil)也是安全的。除了文檔已經明確説明的地方,所有的Go語言函數應該以相同的方式對待nil值的slice和0長度的slice。
|
||||
如果你需要测试一个slice是否是空的,使用len(s) == 0来判断,而不应该用s == nil来判断。除了和nil相等比较外,一个nil值的slice的行为和其它任意0长度的slice一样;例如reverse(nil)也是安全的。除了文档已经明确说明的地方,所有的Go语言函数应该以相同的方式对待nil值的slice和0长度的slice。
|
||||
|
||||
內置的make函數創建一個指定元素類型、長度和容量的slice。容量部分可以省略,在這種情況下,容量將等於長度。
|
||||
内置的make函数创建一个指定元素类型、长度和容量的slice。容量部分可以省略,在这种情况下,容量将等于长度。
|
||||
|
||||
```Go
|
||||
make([]T, len)
|
||||
make([]T, len, cap) // same as make([]T, cap)[:len]
|
||||
```
|
||||
|
||||
在底層,make創建了一個匿名的數組變量,然後返迴一個slice;隻有通過返迴的slice才能引用底層匿名的數組變量。在第一種語句中,slice是整個數組的view。在第二個語句中,slice隻引用了底層數組的前len個元素,但是容量將包含整個的數組。額外的元素是留給未來的增長用的。
|
||||
在底层,make创建了一个匿名的数组变量,然后返回一个slice;只有通过返回的slice才能引用底层匿名的数组变量。在第一种语句中,slice是整个数组的view。在第二个语句中,slice只引用了底层数组的前len个元素,但是容量将包含整个的数组。额外的元素是留给未来的增长用的。
|
||||
|
||||
{% include "./ch4-02-1.md" %}
|
||||
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
## 4.3. Map
|
||||
|
||||
哈希表是一種巧妙併且實用的數據結構。它是一個無序的key/value對的集合,其中所有的key都是不同的,然後通過給定的key可以在常數時間複雜度內檢索、更新或刪除對應的value。
|
||||
哈希表是一种巧妙并且实用的数据结构。它是一个无序的key/value对的集合,其中所有的key都是不同的,然后通过给定的key可以在常数时间复杂度内检索、更新或删除对应的value。
|
||||
|
||||
在Go語言中,一個map就是一個哈希表的引用,map類型可以寫爲map[K]V,其中K和V分别對應key和value。map中所有的key都有相同的類型,所有的value也有着相同的類型,但是key和value之間可以是不同的數據類型。其中K對應的key必須是支持==比較運算符的數據類型,所以map可以通過測試key是否相等來判斷是否已經存在。雖然浮點數類型也是支持相等運算符比較的,但是將浮點數用做key類型則是一個壞的想法,正如第三章提到的,最壞的情況是可能出現的NaN和任何浮點數都不相等。對於V對應的value數據類型則沒有任何的限製。
|
||||
在Go语言中,一个map就是一个哈希表的引用,map类型可以写为map[K]V,其中K和V分别对应key和value。map中所有的key都有相同的类型,所有的value也有着相同的类型,但是key和value之间可以是不同的数据类型。其中K对应的key必须是支持==比较运算符的数据类型,所以map可以通过测试key是否相等来判断是否已经存在。虽然浮点数类型也是支持相等运算符比较的,但是将浮点数用做key类型则是一个坏的想法,正如第三章提到的,最坏的情况是可能出现的NaN和任何浮点数都不相等。对于V对应的value数据类型则没有任何的限制。
|
||||
|
||||
內置的make函數可以創建一個map:
|
||||
内置的make函数可以创建一个map:
|
||||
|
||||
```Go
|
||||
ages := make(map[string]int) // mapping from strings to ints
|
||||
```
|
||||
|
||||
我們也可以用map字面值的語法創建map,同時還可以指定一些最初的key/value:
|
||||
我们也可以用map字面值的语法创建map,同时还可以指定一些最初的key/value:
|
||||
|
||||
```Go
|
||||
ages := map[string]int{
|
||||
@@ -19,7 +19,7 @@ ages := map[string]int{
|
||||
}
|
||||
```
|
||||
|
||||
這相當於
|
||||
这相当于
|
||||
|
||||
```Go
|
||||
ages := make(map[string]int)
|
||||
@@ -27,48 +27,48 @@ ages["alice"] = 31
|
||||
ages["charlie"] = 34
|
||||
```
|
||||
|
||||
因此,另一種創建空的map的表達式是`map[string]int{}`。
|
||||
因此,另一种创建空的map的表达式是`map[string]int{}`。
|
||||
|
||||
Map中的元素通過key對應的下標語法訪問:
|
||||
Map中的元素通过key对应的下标语法访问:
|
||||
|
||||
```Go
|
||||
ages["alice"] = 32
|
||||
fmt.Println(ages["alice"]) // "32"
|
||||
```
|
||||
|
||||
使用內置的delete函數可以刪除元素:
|
||||
使用内置的delete函数可以删除元素:
|
||||
|
||||
```Go
|
||||
delete(ages, "alice") // remove element ages["alice"]
|
||||
```
|
||||
|
||||
所有這些操作是安全的,卽使這些元素不在map中也沒有關繫;如果一個査找失敗將返迴value類型對應的零值,例如,卽使map中不存在“bob”下面的代碼也可以正常工作,因爲ages["bob"]失敗時將返迴0。
|
||||
所有这些操作是安全的,即使这些元素不在map中也没有关系;如果一个查找失败将返回value类型对应的零值,例如,即使map中不存在“bob”下面的代码也可以正常工作,因为ages["bob"]失败时将返回0。
|
||||
|
||||
```Go
|
||||
ages["bob"] = ages["bob"] + 1 // happy birthday!
|
||||
```
|
||||
|
||||
而且`x += y`和`x++`等簡短賦值語法也可以用在map上,所以上面的代碼可以改寫成
|
||||
而且`x += y`和`x++`等简短赋值语法也可以用在map上,所以上面的代码可以改写成
|
||||
|
||||
```Go
|
||||
ages["bob"] += 1
|
||||
```
|
||||
|
||||
更簡單的寫法
|
||||
更简单的写法
|
||||
|
||||
```Go
|
||||
ages["bob"]++
|
||||
```
|
||||
|
||||
但是map中的元素併不是一個變量,因此我們不能對map的元素進行取址操作:
|
||||
但是map中的元素并不是一个变量,因此我们不能对map的元素进行取址操作:
|
||||
|
||||
```Go
|
||||
_ = &ages["bob"] // compile error: cannot take address of map element
|
||||
```
|
||||
|
||||
禁止對map元素取址的原因是map可能隨着元素數量的增長而重新分配更大的內存空間,從而可能導致之前的地址無效。
|
||||
禁止对map元素取址的原因是map可能随着元素数量的增长而重新分配更大的内存空间,从而可能导致之前的地址无效。
|
||||
|
||||
要想遍歷map中全部的key/value對的話,可以使用range風格的for循環實現,和之前的slice遍歷語法類似。下面的迭代語句將在每次迭代時設置name和age變量,它們對應下一個鍵/值對:
|
||||
要想遍历map中全部的key/value对的话,可以使用range风格的for循环实现,和之前的slice遍历语法类似。下面的迭代语句将在每次迭代时设置name和age变量,它们对应下一个键/值对:
|
||||
|
||||
```Go
|
||||
for name, age := range ages {
|
||||
@@ -76,7 +76,7 @@ for name, age := range ages {
|
||||
}
|
||||
```
|
||||
|
||||
Map的迭代順序是不確定的,併且不同的哈希函數實現可能導致不同的遍歷順序。在實踐中,遍歷的順序是隨機的,每一次遍歷的順序都不相同。這是故意的,每次都使用隨機的遍歷順序可以強製要求程序不會依賴具體的哈希函數實現。如果要按順序遍歷key/value對,我們必須顯式地對key進行排序,可以使用sort包的Strings函數對字符串slice進行排序。下面是常見的處理方式:
|
||||
Map的迭代顺序是不确定的,并且不同的哈希函数实现可能导致不同的遍历顺序。在实践中,遍历的顺序是随机的,每一次遍历的顺序都不相同。这是故意的,每次都使用随机的遍历顺序可以强制要求程序不会依赖具体的哈希函数实现。如果要按顺序遍历key/value对,我们必须显式地对key进行排序,可以使用sort包的Strings函数对字符串slice进行排序。下面是常见的处理方式:
|
||||
|
||||
```Go
|
||||
import "sort"
|
||||
@@ -91,15 +91,15 @@ for _, name := range names {
|
||||
}
|
||||
```
|
||||
|
||||
因爲我們一開始就知道names的最終大小,因此給slice分配一個合適的大小將會更有效。下面的代碼創建了一個空的slice,但是slice的容量剛好可以放下map中全部的key:
|
||||
因为我们一开始就知道names的最终大小,因此给slice分配一个合适的大小将会更有效。下面的代码创建了一个空的slice,但是slice的容量刚好可以放下map中全部的key:
|
||||
|
||||
```Go
|
||||
names := make([]string, 0, len(ages))
|
||||
```
|
||||
|
||||
在上面的第一個range循環中,我們隻關心map中的key,所以我們忽略了第二個循環變量。在第二個循環中,我們隻關心names中的名字,所以我們使用“_”空白標識符來忽略第一個循環變量,也就是迭代slice時的索引。
|
||||
在上面的第一个range循环中,我们只关心map中的key,所以我们忽略了第二个循环变量。在第二个循环中,我们只关心names中的名字,所以我们使用“_”空白标识符来忽略第一个循环变量,也就是迭代slice时的索引。
|
||||
|
||||
map類型的零值是nil,也就是沒有引用任何哈希表。
|
||||
map类型的零值是nil,也就是没有引用任何哈希表。
|
||||
|
||||
```Go
|
||||
var ages map[string]int
|
||||
@@ -107,30 +107,30 @@ fmt.Println(ages == nil) // "true"
|
||||
fmt.Println(len(ages) == 0) // "true"
|
||||
```
|
||||
|
||||
map上的大部分操作,包括査找、刪除、len和range循環都可以安全工作在nil值的map上,它們的行爲和一個空的map類似。但是向一個nil值的map存入元素將導致一個panic異常:
|
||||
map上的大部分操作,包括查找、删除、len和range循环都可以安全工作在nil值的map上,它们的行为和一个空的map类似。但是向一个nil值的map存入元素将导致一个panic异常:
|
||||
|
||||
```Go
|
||||
ages["carol"] = 21 // panic: assignment to entry in nil map
|
||||
```
|
||||
|
||||
在向map存數據前必須先創建map。
|
||||
在向map存数据前必须先创建map。
|
||||
|
||||
通過key作爲索引下標來訪問map將産生一個value。如果key在map中是存在的,那麽將得到與key對應的value;如果key不存在,那麽將得到value對應類型的零值,正如我們前面看到的ages["bob"]那樣。這個規則很實用,但是有時候可能需要知道對應的元素是否眞的是在map之中。例如,如果元素類型是一個數字,你可以需要區分一個已經存在的0,和不存在而返迴零值的0,可以像下面這樣測試:
|
||||
通过key作为索引下标来访问map将产生一个value。如果key在map中是存在的,那么将得到与key对应的value;如果key不存在,那么将得到value对应类型的零值,正如我们前面看到的ages["bob"]那样。这个规则很实用,但是有时候可能需要知道对应的元素是否真的是在map之中。例如,如果元素类型是一个数字,你可以需要区分一个已经存在的0,和不存在而返回零值的0,可以像下面这样测试:
|
||||
|
||||
```Go
|
||||
age, ok := ages["bob"]
|
||||
if !ok { /* "bob" is not a key in this map; age == 0. */ }
|
||||
```
|
||||
|
||||
你會經常看到將這兩個結合起來使用,像這樣:
|
||||
你会经常看到将这两个结合起来使用,像这样:
|
||||
|
||||
```Go
|
||||
if age, ok := ages["bob"]; !ok { /* ... */ }
|
||||
```
|
||||
|
||||
在這種場景下,map的下標語法將産生兩個值;第二個是一個布爾值,用於報告元素是否眞的存在。布爾變量一般命名爲ok,特别適合馬上用於if條件判斷部分。
|
||||
在这种场景下,map的下标语法将产生两个值;第二个是一个布尔值,用于报告元素是否真的存在。布尔变量一般命名为ok,特别适合马上用于if条件判断部分。
|
||||
|
||||
和slice一樣,map之間也不能進行相等比較;唯一的例外是和nil進行比較。要判斷兩個map是否包含相同的key和value,我們必須通過一個循環實現:
|
||||
和slice一样,map之间也不能进行相等比较;唯一的例外是和nil进行比较。要判断两个map是否包含相同的key和value,我们必须通过一个循环实现:
|
||||
|
||||
```Go
|
||||
func equal(x, y map[string]int) bool {
|
||||
@@ -146,14 +146,14 @@ func equal(x, y map[string]int) bool {
|
||||
}
|
||||
```
|
||||
|
||||
要註意我們是如何用!ok來區分元素缺失和元素不同的。我們不能簡單地用xv != y[k]判斷,那樣會導致在判斷下面兩個map時産生錯誤的結果:
|
||||
要注意我们是如何用!ok来区分元素缺失和元素不同的。我们不能简单地用xv != y[k]判断,那样会导致在判断下面两个map时产生错误的结果:
|
||||
|
||||
```Go
|
||||
// True if equal is written incorrectly.
|
||||
equal(map[string]int{"A": 0}, map[string]int{"B": 42})
|
||||
```
|
||||
|
||||
Go語言中併沒有提供一個set類型,但是map中的key也是不相同的,可以用map實現類似set的功能。爲了説明這一點,下面的dedup程序讀取多行輸入,但是隻打印第一次出現的行。(它是1.3節中出現的dup程序的變體。)dedup程序通過map來表示所有的輸入行所對應的set集合,以確保已經在集合存在的行不會被重複打印。
|
||||
Go语言中并没有提供一个set类型,但是map中的key也是不相同的,可以用map实现类似set的功能。为了说明这一点,下面的dedup程序读取多行输入,但是只打印第一次出现的行。(它是1.3节中出现的dup程序的变体。)dedup程序通过map来表示所有的输入行所对应的set集合,以确保已经在集合存在的行不会被重复打印。
|
||||
|
||||
<u><i>gopl.io/ch4/dedup</i></u>
|
||||
```Go
|
||||
@@ -175,11 +175,11 @@ func main() {
|
||||
}
|
||||
```
|
||||
|
||||
Go程序員將這種忽略value的map當作一個字符串集合,併非所有`map[string]bool`類型value都是無關緊要的;有一些則可能會同時包含true和false的值。
|
||||
Go程序员将这种忽略value的map当作一个字符串集合,并非所有`map[string]bool`类型value都是无关紧要的;有一些则可能会同时包含true和false的值。
|
||||
|
||||
有時候我們需要一個map或set的key是slice類型,但是map的key必須是可比較的類型,但是slice併不滿足這個條件。不過,我們可以通過兩個步驟繞過這個限製。第一步,定義一個輔助函數k,將slice轉爲map對應的string類型的key,確保隻有x和y相等時k(x) == k(y)才成立。然後創建一個key爲string類型的map,在每次對map操作時先用k輔助函數將slice轉化爲string類型。
|
||||
有时候我们需要一个map或set的key是slice类型,但是map的key必须是可比较的类型,但是slice并不满足这个条件。不过,我们可以通过两个步骤绕过这个限制。第一步,定义一个辅助函数k,将slice转为map对应的string类型的key,确保只有x和y相等时k(x) == k(y)才成立。然后创建一个key为string类型的map,在每次对map操作时先用k辅助函数将slice转化为string类型。
|
||||
|
||||
下面的例子演示了如何使用map來記録提交相同的字符串列表的次數。它使用了fmt.Sprintf函數將字符串列表轉換爲一個字符串以用於map的key,通過%q參數忠實地記録每個字符串元素的信息:
|
||||
下面的例子演示了如何使用map来记录提交相同的字符串列表的次数。它使用了fmt.Sprintf函数将字符串列表转换为一个字符串以用于map的key,通过%q参数忠实地记录每个字符串元素的信息:
|
||||
|
||||
```Go
|
||||
var m = make(map[string]int)
|
||||
@@ -190,9 +190,9 @@ func Add(list []string) { m[k(list)]++ }
|
||||
func Count(list []string) int { return m[k(list)] }
|
||||
```
|
||||
|
||||
使用同樣的技術可以處理任何不可比較的key類型,而不僅僅是slice類型。這種技術對於想使用自定義key比較函數的時候也很有用,例如在比較字符串的時候忽略大小寫。同時,輔助函數k(x)也不一定是字符串類型,它可以返迴任何可比較的類型,例如整數、數組或結構體等。
|
||||
使用同样的技术可以处理任何不可比较的key类型,而不仅仅是slice类型。这种技术对于想使用自定义key比较函数的时候也很有用,例如在比较字符串的时候忽略大小写。同时,辅助函数k(x)也不一定是字符串类型,它可以返回任何可比较的类型,例如整数、数组或结构体等。
|
||||
|
||||
這是map的另一個例子,下面的程序用於統計輸入中每個Unicode碼點出現的次數。雖然Unicode全部碼點的數量鉅大,但是出現在特定文檔中的字符種類併沒有多少,使用map可以用比較自然的方式來跟蹤那些出現過字符的次數。
|
||||
这是map的另一个例子,下面的程序用于统计输入中每个Unicode码点出现的次数。虽然Unicode全部码点的数量巨大,但是出现在特定文档中的字符种类并没有多少,使用map可以用比较自然的方式来跟踪那些出现过字符的次数。
|
||||
|
||||
<u><i>gopl.io/ch4/charcount</i></u>
|
||||
```Go
|
||||
@@ -246,15 +246,15 @@ func main() {
|
||||
}
|
||||
```
|
||||
|
||||
ReadRune方法執行UTF-8解碼併返迴三個值:解碼的rune字符的值,字符UTF-8編碼後的長度,和一個錯誤值。我們可預期的錯誤值隻有對應文件結尾的io.EOF。如果輸入的是無效的UTF-8編碼的字符,返迴的將是unicode.ReplacementChar表示無效字符,併且編碼長度是1。
|
||||
ReadRune方法执行UTF-8解码并返回三个值:解码的rune字符的值,字符UTF-8编码后的长度,和一个错误值。我们可预期的错误值只有对应文件结尾的io.EOF。如果输入的是无效的UTF-8编码的字符,返回的将是unicode.ReplacementChar表示无效字符,并且编码长度是1。
|
||||
|
||||
charcount程序同時打印不同UTF-8編碼長度的字符數目。對此,map併不是一個合適的數據結構;因爲UTF-8編碼的長度總是從1到utf8.UTFMax(最大是4個字節),使用數組將更有效。
|
||||
charcount程序同时打印不同UTF-8编码长度的字符数目。对此,map并不是一个合适的数据结构;因为UTF-8编码的长度总是从1到utf8.UTFMax(最大是4个字节),使用数组将更有效。
|
||||
|
||||
作爲一個實驗,我們用charcount程序對英文版原稿的字符進行了統計。雖然大部分是英語,但是也有一些非ASCII字符。下面是排名前10的非ASCII字符:
|
||||
作为一个实验,我们用charcount程序对英文版原稿的字符进行了统计。虽然大部分是英语,但是也有一些非ASCII字符。下面是排名前10的非ASCII字符:
|
||||
|
||||

|
||||
|
||||
下面是不同UTF-8編碼長度的字符的數目:
|
||||
下面是不同UTF-8编码长度的字符的数目:
|
||||
|
||||
```
|
||||
len count
|
||||
@@ -264,7 +264,7 @@ len count
|
||||
4 0
|
||||
```
|
||||
|
||||
Map的value類型也可以是一個聚合類型,比如是一個map或slice。在下面的代碼中,圖graph的key類型是一個字符串,value類型map[string]bool代表一個字符串集合。從概念上將,graph將一個字符串類型的key映射到一組相關的字符串集合,它們指向新的graph的key。
|
||||
Map的value类型也可以是一个聚合类型,比如是一个map或slice。在下面的代码中,图graph的key类型是一个字符串,value类型map[string]bool代表一个字符串集合。从概念上将,graph将一个字符串类型的key映射到一组相关的字符串集合,它们指向新的graph的key。
|
||||
|
||||
<u><i>gopl.io/ch4/graph</i></u>
|
||||
```Go
|
||||
@@ -284,8 +284,8 @@ func hasEdge(from, to string) bool {
|
||||
}
|
||||
```
|
||||
|
||||
其中addEdge函數惰性初始化map是一個慣用方式,也就是説在每個值首次作爲key時才初始化。addEdge函數顯示了如何讓map的零值也能正常工作;卽使from到to的邊不存在,graph[from][to]依然可以返迴一個有意義的結果。
|
||||
其中addEdge函数惰性初始化map是一个惯用方式,也就是说在每个值首次作为key时才初始化。addEdge函数显示了如何让map的零值也能正常工作;即使from到to的边不存在,graph[from][to]依然可以返回一个有意义的结果。
|
||||
|
||||
**練習 4.8:** 脩改charcount程序,使用unicode.IsLetter等相關的函數,統計字母、數字等Unicode中不同的字符類别。
|
||||
**练习 4.8:** 修改charcount程序,使用unicode.IsLetter等相关的函数,统计字母、数字等Unicode中不同的字符类别。
|
||||
|
||||
**練習 4.9:** 編寫一個程序wordfreq程序,報告輸入文本中每個單詞出現的頻率。在第一次調用Scan前先調用input.Split(bufio.ScanWords)函數,這樣可以按單詞而不是按行輸入。
|
||||
**练习 4.9:** 编写一个程序wordfreq程序,报告输入文本中每个单词出现的频率。在第一次调用Scan前先调用input.Split(bufio.ScanWords)函数,这样可以按单词而不是按行输入。
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
### 4.4.1. 結構體面值
|
||||
### 4.4.1. 结构体面值
|
||||
|
||||
結構體值也可以用結構體面值表示,結構體面值可以指定每個成員的值。
|
||||
结构体值也可以用结构体面值表示,结构体面值可以指定每个成员的值。
|
||||
|
||||
```Go
|
||||
type Point struct{ X, Y int }
|
||||
@@ -8,17 +8,17 @@ type Point struct{ X, Y int }
|
||||
p := Point{1, 2}
|
||||
```
|
||||
|
||||
這里有兩種形式的結構體面值語法,上面的是第一種寫法,要求以結構體成員定義的順序爲每個結構體成員指定一個面值。它要求寫代碼和讀代碼的人要記住結構體的每個成員的類型和順序,不過結構體成員有細微的調整就可能導致上述代碼不能編譯。因此,上述的語法一般隻在定義結構體的包內部使用,或者是在較小的結構體中使用,這些結構體的成員排列比較規則,比如image.Point{x, y}或color.RGBA{red, green, blue, alpha}。
|
||||
这里有两种形式的结构体面值语法,上面的是第一种写法,要求以结构体成员定义的顺序为每个结构体成员指定一个面值。它要求写代码和读代码的人要记住结构体的每个成员的类型和顺序,不过结构体成员有细微的调整就可能导致上述代码不能编译。因此,上述的语法一般只在定义结构体的包内部使用,或者是在较小的结构体中使用,这些结构体的成员排列比较规则,比如image.Point{x, y}或color.RGBA{red, green, blue, alpha}。
|
||||
|
||||
其實更常用的是第二種寫法,以成員名字和相應的值來初始化,可以包含部分或全部的成員,如1.4節的Lissajous程序的寫法:
|
||||
其实更常用的是第二种写法,以成员名字和相应的值来初始化,可以包含部分或全部的成员,如1.4节的Lissajous程序的写法:
|
||||
|
||||
```Go
|
||||
anim := gif.GIF{LoopCount: nframes}
|
||||
```
|
||||
|
||||
在這種形式的結構體面值寫法中,如果成員被忽略的話將默認用零值。因爲,提供了成員的名字,所有成員出現的順序併不重要。
|
||||
在这种形式的结构体面值写法中,如果成员被忽略的话将默认用零值。因为,提供了成员的名字,所有成员出现的顺序并不重要。
|
||||
|
||||
兩種不同形式的寫法不能混合使用。而且,你不能企圖在外部包中用第一種順序賦值的技巧來偷偷地初始化結構體中未導出的成員。
|
||||
两种不同形式的写法不能混合使用。而且,你不能企图在外部包中用第一种顺序赋值的技巧来偷偷地初始化结构体中未导出的成员。
|
||||
|
||||
```Go
|
||||
package p
|
||||
@@ -30,9 +30,9 @@ var _ = p.T{a: 1, b: 2} // compile error: can't reference a, b
|
||||
var _ = p.T{1, 2} // compile error: can't reference a, b
|
||||
```
|
||||
|
||||
雖然上面最後一行代碼的編譯錯誤信息中併沒有顯式提到未導出的成員,但是這樣企圖隱式使用未導出成員的行爲也是不允許的。
|
||||
虽然上面最后一行代码的编译错误信息中并没有显式提到未导出的成员,但是这样企图隐式使用未导出成员的行为也是不允许的。
|
||||
|
||||
結構體可以作爲函數的參數和返迴值。例如,這個Scale函數將Point類型的值縮放後返迴:
|
||||
结构体可以作为函数的参数和返回值。例如,这个Scale函数将Point类型的值缩放后返回:
|
||||
|
||||
```Go
|
||||
func Scale(p Point, factor int) Point {
|
||||
@@ -42,7 +42,7 @@ func Scale(p Point, factor int) Point {
|
||||
fmt.Println(Scale(Point{1, 2}, 5)) // "{5 10}"
|
||||
```
|
||||
|
||||
如果考慮效率的話,較大的結構體通常會用指針的方式傳入和返迴,
|
||||
如果考虑效率的话,较大的结构体通常会用指针的方式传入和返回,
|
||||
|
||||
```Go
|
||||
func Bonus(e *Employee, percent int) int {
|
||||
@@ -50,7 +50,7 @@ func Bonus(e *Employee, percent int) int {
|
||||
}
|
||||
```
|
||||
|
||||
如果要在函數內部脩改結構體成員的話,用指針傳入是必須的;因爲在Go語言中,所有的函數參數都是值拷貝傳入的,函數參數將不再是函數調用時的原始變量。
|
||||
如果要在函数内部修改结构体成员的话,用指针传入是必须的;因为在Go语言中,所有的函数参数都是值拷贝传入的,函数参数将不再是函数调用时的原始变量。
|
||||
|
||||
```Go
|
||||
func AwardAnnualRaise(e *Employee) {
|
||||
@@ -58,17 +58,17 @@ func AwardAnnualRaise(e *Employee) {
|
||||
}
|
||||
```
|
||||
|
||||
因爲結構體通常通過指針處理,可以用下面的寫法來創建併初始化一個結構體變量,併返迴結構體的地址:
|
||||
因为结构体通常通过指针处理,可以用下面的写法来创建并初始化一个结构体变量,并返回结构体的地址:
|
||||
|
||||
```Go
|
||||
pp := &Point{1, 2}
|
||||
```
|
||||
|
||||
它是下面的語句是等價的
|
||||
它是下面的语句是等价的
|
||||
|
||||
```Go
|
||||
pp := new(Point)
|
||||
*pp = Point{1, 2}
|
||||
```
|
||||
|
||||
不過&Point{1, 2}寫法可以直接在表達式中使用,比如一個函數調用。
|
||||
不过&Point{1, 2}写法可以直接在表达式中使用,比如一个函数调用。
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
### 4.4.2. 結構體比較
|
||||
### 4.4.2. 结构体比较
|
||||
|
||||
如果結構體的全部成員都是可以比較的,那麽結構體也是可以比較的,那樣的話兩個結構體將可以使用==或!=運算符進行比較。相等比較運算符==將比較兩個結構體的每個成員,因此下面兩個比較的表達式是等價的:
|
||||
如果结构体的全部成员都是可以比较的,那么结构体也是可以比较的,那样的话两个结构体将可以使用==或!=运算符进行比较。相等比较运算符==将比较两个结构体的每个成员,因此下面两个比较的表达式是等价的:
|
||||
|
||||
```Go
|
||||
type Point struct{ X, Y int }
|
||||
@@ -11,7 +11,7 @@ fmt.Println(p.X == q.X && p.Y == q.Y) // "false"
|
||||
fmt.Println(p == q) // "false"
|
||||
```
|
||||
|
||||
可比較的結構體類型和其他可比較的類型一樣,可以用於map的key類型。
|
||||
可比较的结构体类型和其他可比较的类型一样,可以用于map的key类型。
|
||||
|
||||
```Go
|
||||
type address struct {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
### 4.4.3. 結構體嵌入和匿名成員
|
||||
### 4.4.3. 结构体嵌入和匿名成员
|
||||
|
||||
在本節中,我們將看到如何使用Go語言提供的不同尋常的結構體嵌入機製讓一個命名的結構體包含另一個結構體類型的匿名成員,這樣就可以通過簡單的點運算符x.f來訪問匿名成員鏈中嵌套的x.d.e.f成員。
|
||||
在本节中,我们将看到如何使用Go语言提供的不同寻常的结构体嵌入机制让一个命名的结构体包含另一个结构体类型的匿名成员,这样就可以通过简单的点运算符x.f来访问匿名成员链中嵌套的x.d.e.f成员。
|
||||
|
||||
考慮一個二維的繪圖程序,提供了一個各種圖形的庫,例如矩形、橢圓形、星形和輪形等幾何形狀。這里是其中兩個的定義:
|
||||
考虑一个二维的绘图程序,提供了一个各种图形的库,例如矩形、椭圆形、星形和轮形等几何形状。这里是其中两个的定义:
|
||||
|
||||
```Go
|
||||
type Circle struct {
|
||||
@@ -14,7 +14,7 @@ type Wheel struct {
|
||||
}
|
||||
```
|
||||
|
||||
一個Circle代表的圓形類型包含了標準圓心的X和Y坐標信息,和一個Radius表示的半徑信息。一個Wheel輪形除了包含Circle類型所有的全部成員外,還增加了Spokes表示徑向輻條的數量。我們可以這樣創建一個wheel變量:
|
||||
一个Circle代表的圆形类型包含了标准圆心的X和Y坐标信息,和一个Radius表示的半径信息。一个Wheel轮形除了包含Circle类型所有的全部成员外,还增加了Spokes表示径向辐条的数量。我们可以这样创建一个wheel变量:
|
||||
|
||||
```Go
|
||||
var w Wheel
|
||||
@@ -24,7 +24,7 @@ w.Radius = 5
|
||||
w.Spokes = 20
|
||||
```
|
||||
|
||||
隨着庫中幾何形狀數量的增多,我們一定會註意到它們之間的相似和重複之處,所以我們可能爲了便於維護而將相同的屬性獨立出來:
|
||||
随着库中几何形状数量的增多,我们一定会注意到它们之间的相似和重复之处,所以我们可能为了便于维护而将相同的属性独立出来:
|
||||
|
||||
```Go
|
||||
type Point struct {
|
||||
@@ -42,7 +42,7 @@ type Wheel struct {
|
||||
}
|
||||
```
|
||||
|
||||
這樣改動之後結構體類型變的清晰了,但是這種脩改同時也導致了訪問每個成員變得繁瑣:
|
||||
这样改动之后结构体类型变的清晰了,但是这种修改同时也导致了访问每个成员变得繁琐:
|
||||
|
||||
```Go
|
||||
var w Wheel
|
||||
@@ -52,7 +52,7 @@ w.Circle.Radius = 5
|
||||
w.Spokes = 20
|
||||
```
|
||||
|
||||
Go語言有一個特性讓我們隻聲明一個成員對應的數據類型而不指名成員的名字;這類成員就叫匿名成員。匿名成員的數據類型必須是命名的類型或指向一個命名的類型的指針。下面的代碼中,Circle和Wheel各自都有一個匿名成員。我們可以説Point類型被嵌入到了Circle結構體,同時Circle類型被嵌入到了Wheel結構體。
|
||||
Go语言有一个特性让我们只声明一个成员对应的数据类型而不指名成员的名字;这类成员就叫匿名成员。匿名成员的数据类型必须是命名的类型或指向一个命名的类型的指针。下面的代码中,Circle和Wheel各自都有一个匿名成员。我们可以说Point类型被嵌入到了Circle结构体,同时Circle类型被嵌入到了Wheel结构体。
|
||||
|
||||
```Go
|
||||
type Circle struct {
|
||||
@@ -66,7 +66,7 @@ type Wheel struct {
|
||||
}
|
||||
```
|
||||
|
||||
得意於匿名嵌入的特性,我們可以直接訪問葉子屬性而不需要給出完整的路徑:
|
||||
得意于匿名嵌入的特性,我们可以直接访问叶子属性而不需要给出完整的路径:
|
||||
|
||||
```Go
|
||||
var w Wheel
|
||||
@@ -76,16 +76,16 @@ w.Radius = 5 // equivalent to w.Circle.Radius = 5
|
||||
w.Spokes = 20
|
||||
```
|
||||
|
||||
在右邊的註釋中給出的顯式形式訪問這些葉子成員的語法依然有效,因此匿名成員併不是眞的無法訪問了。其中匿名成員Circle和Point都有自己的名字——就是命名的類型名字——但是這些名字在點操作符中是可選的。我們在訪問子成員的時候可以忽略任何匿名成員部分。
|
||||
在右边的注释中给出的显式形式访问这些叶子成员的语法依然有效,因此匿名成员并不是真的无法访问了。其中匿名成员Circle和Point都有自己的名字——就是命名的类型名字——但是这些名字在点操作符中是可选的。我们在访问子成员的时候可以忽略任何匿名成员部分。
|
||||
|
||||
不幸的是,結構體字面值併沒有簡短表示匿名成員的語法, 因此下面的語句都不能編譯通過:
|
||||
不幸的是,结构体字面值并没有简短表示匿名成员的语法, 因此下面的语句都不能编译通过:
|
||||
|
||||
```Go
|
||||
w = Wheel{8, 8, 5, 20} // compile error: unknown fields
|
||||
w = Wheel{X: 8, Y: 8, Radius: 5, Spokes: 20} // compile error: unknown fields
|
||||
```
|
||||
|
||||
結構體字面值必須遵循形狀類型聲明時的結構,所以我們隻能用下面的兩種語法,它們彼此是等價的:
|
||||
结构体字面值必须遵循形状类型声明时的结构,所以我们只能用下面的两种语法,它们彼此是等价的:
|
||||
|
||||
<u><i>gopl.io/ch4/embed</i></u>
|
||||
```Go
|
||||
@@ -110,16 +110,16 @@ fmt.Printf("%#v\n", w)
|
||||
// Wheel{Circle:Circle{Point:Point{X:42, Y:8}, Radius:5}, Spokes:20}
|
||||
```
|
||||
|
||||
需要註意的是Printf函數中%v參數包含的#副詞,它表示用和Go語言類似的語法打印值。對於結構體類型來説,將包含每個成員的名字。
|
||||
需要注意的是Printf函数中%v参数包含的#副词,它表示用和Go语言类似的语法打印值。对于结构体类型来说,将包含每个成员的名字。
|
||||
|
||||
因爲匿名成員也有一個隱式的名字,因此不能同時包含兩個類型相同的匿名成員,這會導致名字衝突。同時,因爲成員的名字是由其類型隱式地決定的,所有匿名成員也有可見性的規則約束。在上面的例子中,Point和Circle匿名成員都是導出的。卽使它們不導出(比如改成小寫字母開頭的point和circle),我們依然可以用簡短形式訪問匿名成員嵌套的成員
|
||||
因为匿名成员也有一个隐式的名字,因此不能同时包含两个类型相同的匿名成员,这会导致名字冲突。同时,因为成员的名字是由其类型隐式地决定的,所有匿名成员也有可见性的规则约束。在上面的例子中,Point和Circle匿名成员都是导出的。即使它们不导出(比如改成小写字母开头的point和circle),我们依然可以用简短形式访问匿名成员嵌套的成员
|
||||
|
||||
```Go
|
||||
w.X = 8 // equivalent to w.circle.point.X = 8
|
||||
```
|
||||
|
||||
但是在包外部,因爲circle和point沒有導出不能訪問它們的成員,因此簡短的匿名成員訪問語法也是禁止的。
|
||||
但是在包外部,因为circle和point没有导出不能访问它们的成员,因此简短的匿名成员访问语法也是禁止的。
|
||||
|
||||
到目前爲止,我們看到匿名成員特性隻是對訪問嵌套成員的點運算符提供了簡短的語法糖。稍後,我們將會看到匿名成員併不要求是結構體類型;其實任何命名的類型都可以作爲結構體的匿名成員。但是爲什麽要嵌入一個沒有任何子成員類型的匿名成員類型呢?
|
||||
到目前为止,我们看到匿名成员特性只是对访问嵌套成员的点运算符提供了简短的语法糖。稍后,我们将会看到匿名成员并不要求是结构体类型;其实任何命名的类型都可以作为结构体的匿名成员。但是为什么要嵌入一个没有任何子成员类型的匿名成员类型呢?
|
||||
|
||||
答案是匿名類型的方法集。簡短的點運算符語法可以用於選擇匿名成員嵌套的成員,也可以用於訪問它們的方法。實際上,外層的結構體不僅僅是獲得了匿名成員類型的所有成員,而且也獲得了該類型導出的全部的方法。這個機製可以用於將一個有簡單行爲的對象組合成有複雜行爲的對象。組合是Go語言中面向對象編程的核心,我們將在6.3節中專門討論。
|
||||
答案是匿名类型的方法集。简短的点运算符语法可以用于选择匿名成员嵌套的成员,也可以用于访问它们的方法。实际上,外层的结构体不仅仅是获得了匿名成员类型的所有成员,而且也获得了该类型导出的全部的方法。这个机制可以用于将一个有简单行为的对象组合成有复杂行为的对象。组合是Go语言中面向对象编程的核心,我们将在6.3节中专门讨论。
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
## 4.4. 結構體
|
||||
## 4.4. 结构体
|
||||
|
||||
結構體是一種聚合的數據類型,是由零個或多個任意類型的值聚合成的實體。每個值稱爲結構體的成員。用結構體的經典案例處理公司的員工信息,每個員工信息包含一個唯一的員工編號、員工的名字、家庭住址、出生日期、工作崗位、薪資、上級領導等等。所有的這些信息都需要綁定到一個實體中,可以作爲一個整體單元被複製,作爲函數的參數或返迴值,或者是被存儲到數組中,等等。
|
||||
结构体是一种聚合的数据类型,是由零个或多个任意类型的值聚合成的实体。每个值称为结构体的成员。用结构体的经典案例处理公司的员工信息,每个员工信息包含一个唯一的员工编号、员工的名字、家庭住址、出生日期、工作岗位、薪资、上级领导等等。所有的这些信息都需要绑定到一个实体中,可以作为一个整体单元被复制,作为函数的参数或返回值,或者是被存储到数组中,等等。
|
||||
|
||||
下面兩個語句聲明了一個叫Employee的命名的結構體類型,併且聲明了一個Employee類型的變量dilbert:
|
||||
下面两个语句声明了一个叫Employee的命名的结构体类型,并且声明了一个Employee类型的变量dilbert:
|
||||
|
||||
```Go
|
||||
type Employee struct {
|
||||
@@ -18,33 +18,33 @@ type Employee struct {
|
||||
var dilbert Employee
|
||||
```
|
||||
|
||||
dilbert結構體變量的成員可以通過點操作符訪問,比如dilbert.Name和dilbert.DoB。因爲dilbert是一個變量,它所有的成員也同樣是變量,我們可以直接對每個成員賦值:
|
||||
dilbert结构体变量的成员可以通过点操作符访问,比如dilbert.Name和dilbert.DoB。因为dilbert是一个变量,它所有的成员也同样是变量,我们可以直接对每个成员赋值:
|
||||
|
||||
```Go
|
||||
dilbert.Salary -= 5000 // demoted, for writing too few lines of code
|
||||
```
|
||||
|
||||
或者是對成員取地址,然後通過指針訪問:
|
||||
或者是对成员取地址,然后通过指针访问:
|
||||
|
||||
```Go
|
||||
position := &dilbert.Position
|
||||
*position = "Senior " + *position // promoted, for outsourcing to Elbonia
|
||||
```
|
||||
|
||||
點操作符也可以和指向結構體的指針一起工作:
|
||||
点操作符也可以和指向结构体的指针一起工作:
|
||||
|
||||
```Go
|
||||
var employeeOfTheMonth *Employee = &dilbert
|
||||
employeeOfTheMonth.Position += " (proactive team player)"
|
||||
```
|
||||
|
||||
相當於下面語句
|
||||
相当于下面语句
|
||||
|
||||
```Go
|
||||
(*employeeOfTheMonth).Position += " (proactive team player)"
|
||||
```
|
||||
|
||||
下面的EmployeeByID函數將根據給定的員工ID返迴對應的員工信息結構體的指針。我們可以使用點操作符來訪問它里面的成員:
|
||||
下面的EmployeeByID函数将根据给定的员工ID返回对应的员工信息结构体的指针。我们可以使用点操作符来访问它里面的成员:
|
||||
|
||||
```Go
|
||||
func EmployeeByID(id int) *Employee { /* ... */ }
|
||||
@@ -55,9 +55,9 @@ id := dilbert.ID
|
||||
EmployeeByID(id).Salary = 0 // fired for... no real reason
|
||||
```
|
||||
|
||||
後面的語句通過EmployeeByID返迴的結構體指針更新了Employee結構體的成員。如果將EmployeeByID函數的返迴值從`*Employee`指針類型改爲Employee值類型,那麽更新語句將不能編譯通過,因爲在賦值語句的左邊併不確定是一個變量(譯註:調用函數返迴的是值,併不是一個可取地址的變量)。
|
||||
后面的语句通过EmployeeByID返回的结构体指针更新了Employee结构体的成员。如果将EmployeeByID函数的返回值从`*Employee`指针类型改为Employee值类型,那么更新语句将不能编译通过,因为在赋值语句的左边并不确定是一个变量(译注:调用函数返回的是值,并不是一个可取地址的变量)。
|
||||
|
||||
通常一行對應一個結構體成員,成員的名字在前類型在後,不過如果相鄰的成員類型如果相同的話可以被合併到一行,就像下面的Name和Address成員那樣:
|
||||
通常一行对应一个结构体成员,成员的名字在前类型在后,不过如果相邻的成员类型如果相同的话可以被合并到一行,就像下面的Name和Address成员那样:
|
||||
|
||||
```Go
|
||||
type Employee struct {
|
||||
@@ -70,13 +70,13 @@ type Employee struct {
|
||||
}
|
||||
```
|
||||
|
||||
結構體成員的輸入順序也有重要的意義。我們也可以將Position成員合併(因爲也是字符串類型),或者是交換Name和Address出現的先後順序,那樣的話就是定義了不同的結構體類型。通常,我們隻是將相關的成員寫到一起。
|
||||
结构体成员的输入顺序也有重要的意义。我们也可以将Position成员合并(因为也是字符串类型),或者是交换Name和Address出现的先后顺序,那样的话就是定义了不同的结构体类型。通常,我们只是将相关的成员写到一起。
|
||||
|
||||
如果結構體成員名字是以大寫字母開頭的,那麽該成員就是導出的;這是Go語言導出規則決定的。一個結構體可能同時包含導出和未導出的成員。
|
||||
如果结构体成员名字是以大写字母开头的,那么该成员就是导出的;这是Go语言导出规则决定的。一个结构体可能同时包含导出和未导出的成员。
|
||||
|
||||
結構體類型往往是冗長的,因爲它的每個成員可能都會占一行。雖然我們每次都可以重寫整個結構體成員,但是重複會令人厭煩。因此,完整的結構體寫法通常隻在類型聲明語句的地方出現,就像Employee類型聲明語句那樣。
|
||||
结构体类型往往是冗长的,因为它的每个成员可能都会占一行。虽然我们每次都可以重写整个结构体成员,但是重复会令人厌烦。因此,完整的结构体写法通常只在类型声明语句的地方出现,就像Employee类型声明语句那样。
|
||||
|
||||
一個命名爲S的結構體類型將不能再包含S類型的成員:因爲一個聚合的值不能包含它自身。(該限製同樣適應於數組。)但是S類型的結構體可以包含`*S`指針類型的成員,這可以讓我們創建遞歸的數據結構,比如鏈表和樹結構等。在下面的代碼中,我們使用一個二叉樹來實現一個插入排序:
|
||||
一个命名为S的结构体类型将不能再包含S类型的成员:因为一个聚合的值不能包含它自身。(该限制同样适应于数组。)但是S类型的结构体可以包含`*S`指针类型的成员,这可以让我们创建递归的数据结构,比如链表和树结构等。在下面的代码中,我们使用一个二叉树来实现一个插入排序:
|
||||
|
||||
<u><i>gopl.io/ch4/treesort</i></u>
|
||||
```Go
|
||||
@@ -121,9 +121,9 @@ func add(t *tree, value int) *tree {
|
||||
}
|
||||
```
|
||||
|
||||
結構體類型的零值是每個成員都對是零值。通常會將零值作爲最合理的默認值。例如,對於bytes.Buffer類型,結構體初始值就是一個隨時可用的空緩存,還有在第9章將會講到的sync.Mutex的零值也是有效的未鎖定狀態。有時候這種零值可用的特性是自然獲得的,但是也有些類型需要一些額外的工作。
|
||||
结构体类型的零值是每个成员都对是零值。通常会将零值作为最合理的默认值。例如,对于bytes.Buffer类型,结构体初始值就是一个随时可用的空缓存,还有在第9章将会讲到的sync.Mutex的零值也是有效的未锁定状态。有时候这种零值可用的特性是自然获得的,但是也有些类型需要一些额外的工作。
|
||||
|
||||
如果結構體沒有任何成員的話就是空結構體,寫作struct{}。它的大小爲0,也不包含任何信息,但是有時候依然是有價值的。有些Go語言程序員用map帶模擬set數據結構時,用它來代替map中布爾類型的value,隻是強調key的重要性,但是因爲節約的空間有限,而且語法比較複雜,所有我們通常避免避免這樣的用法。
|
||||
如果结构体没有任何成员的话就是空结构体,写作struct{}。它的大小为0,也不包含任何信息,但是有时候依然是有价值的。有些Go语言程序员用map带模拟set数据结构时,用它来代替map中布尔类型的value,只是强调key的重要性,但是因为节约的空间有限,而且语法比较复杂,所有我们通常避免避免这样的用法。
|
||||
|
||||
```Go
|
||||
seen := make(map[string]struct{}) // set of strings
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
## 4.5. JSON
|
||||
|
||||
JavaScript對象表示法(JSON)是一種用於發送和接收結構化信息的標準協議。在類似的協議中,JSON併不是唯一的一個標準協議。 XML(§7.14)、ASN.1和Google的Protocol Buffers都是類似的協議,併且有各自的特色,但是由於簡潔性、可讀性和流行程度等原因,JSON是應用最廣泛的一個。
|
||||
JavaScript对象表示法(JSON)是一种用于发送和接收结构化信息的标准协议。在类似的协议中,JSON并不是唯一的一个标准协议。 XML(§7.14)、ASN.1和Google的Protocol Buffers都是类似的协议,并且有各自的特色,但是由于简洁性、可读性和流行程度等原因,JSON是应用最广泛的一个。
|
||||
|
||||
Go語言對於這些標準格式的編碼和解碼都有良好的支持,由標準庫中的encoding/json、encoding/xml、encoding/asn1等包提供支持(譯註:Protocol Buffers的支持由 github.com/golang/protobuf 包提供),併且這類包都有着相似的API接口。本節,我們將對重要的encoding/json包的用法做個概述。
|
||||
Go语言对于这些标准格式的编码和解码都有良好的支持,由标准库中的encoding/json、encoding/xml、encoding/asn1等包提供支持(译注:Protocol Buffers的支持由 github.com/golang/protobuf 包提供),并且这类包都有着相似的API接口。本节,我们将对重要的encoding/json包的用法做个概述。
|
||||
|
||||
JSON是對JavaScript中各種類型的值——字符串、數字、布爾值和對象——Unicode本文編碼。它可以用有效可讀的方式表示第三章的基礎數據類型和本章的數組、slice、結構體和map等聚合數據類型。
|
||||
JSON是对JavaScript中各种类型的值——字符串、数字、布尔值和对象——Unicode本文编码。它可以用有效可读的方式表示第三章的基础数据类型和本章的数组、slice、结构体和map等聚合数据类型。
|
||||
|
||||
基本的JSON類型有數字(十進製或科學記數法)、布爾值(true或false)、字符串,其中字符串是以雙引號包含的Unicode字符序列,支持和Go語言類似的反斜槓轉義特性,不過JSON使用的是\Uhhhh轉義數字來表示一個UTF-16編碼(譯註:UTF-16和UTF-8一樣是一種變長的編碼,有些Unicode碼點較大的字符需要用4個字節表示;而且UTF-16還有大端和小端的問題),而不是Go語言的rune類型。
|
||||
基本的JSON类型有数字(十进制或科学记数法)、布尔值(true或false)、字符串,其中字符串是以双引号包含的Unicode字符序列,支持和Go语言类似的反斜杠转义特性,不过JSON使用的是\Uhhhh转义数字来表示一个UTF-16编码(译注:UTF-16和UTF-8一样是一种变长的编码,有些Unicode码点较大的字符需要用4个字节表示;而且UTF-16还有大端和小端的问题),而不是Go语言的rune类型。
|
||||
|
||||
這些基礎類型可以通過JSON的數組和對象類型進行遞歸組合。一個JSON數組是一個有序的值序列,寫在一個方括號中併以逗號分隔;一個JSON數組可以用於編碼Go語言的數組和slice。一個JSON對象是一個字符串到值的映射,寫成以繫列的name:value對形式,用花括號包含併以逗號分隔;JSON的對象類型可以用於編碼Go語言的map類型(key類型是字符串)和結構體。例如:
|
||||
这些基础类型可以通过JSON的数组和对象类型进行递归组合。一个JSON数组是一个有序的值序列,写在一个方括号中并以逗号分隔;一个JSON数组可以用于编码Go语言的数组和slice。一个JSON对象是一个字符串到值的映射,写成以系列的name:value对形式,用花括号包含并以逗号分隔;JSON的对象类型可以用于编码Go语言的map类型(key类型是字符串)和结构体。例如:
|
||||
|
||||
```
|
||||
boolean true
|
||||
@@ -20,7 +20,7 @@ object {"year": 1980,
|
||||
"medals": ["gold", "silver", "bronze"]}
|
||||
```
|
||||
|
||||
考慮一個應用程序,該程序負責收集各種電影評論併提供反饋功能。它的Movie數據類型和一個典型的表示電影的值列表如下所示。(在結構體聲明中,Year和Color成員後面的字符串面值是結構體成員Tag;我們稍後會解釋它的作用。)
|
||||
考虑一个应用程序,该程序负责收集各种电影评论并提供反馈功能。它的Movie数据类型和一个典型的表示电影的值列表如下所示。(在结构体声明中,Year和Color成员后面的字符串面值是结构体成员Tag;我们稍后会解释它的作用。)
|
||||
|
||||
<u><i>gopl.io/ch4/movie</i></u>
|
||||
```Go
|
||||
@@ -42,7 +42,7 @@ var movies = []Movie{
|
||||
}
|
||||
```
|
||||
|
||||
這樣的數據結構特别適合JSON格式,併且在兩種之間相互轉換也很容易。將一個Go語言中類似movies的結構體slice轉爲JSON的過程叫編組(marshaling)。編組通過調用json.Marshal函數完成:
|
||||
这样的数据结构特别适合JSON格式,并且在两种之间相互转换也很容易。将一个Go语言中类似movies的结构体slice转为JSON的过程叫编组(marshaling)。编组通过调用json.Marshal函数完成:
|
||||
|
||||
```Go
|
||||
data, err := json.Marshal(movies)
|
||||
@@ -52,7 +52,7 @@ if err != nil {
|
||||
fmt.Printf("%s\n", data)
|
||||
```
|
||||
|
||||
Marshal函數返還一個編碼後的字節slice,包含很長的字符串,併且沒有空白縮進;我們將它摺行以便於顯示:
|
||||
Marshal函数返还一个编码后的字节slice,包含很长的字符串,并且没有空白缩进;我们将它折行以便于显示:
|
||||
|
||||
```
|
||||
[{"Title":"Casablanca","released":1942,"Actors":["Humphrey Bogart","Ingr
|
||||
@@ -61,7 +61,7 @@ tors":["Paul Newman"]},{"Title":"Bullitt","released":1968,"color":true,"
|
||||
Actors":["Steve McQueen","Jacqueline Bisset"]}]
|
||||
```
|
||||
|
||||
這種緊湊的表示形式雖然包含了全部的信息,但是很難閲讀。爲了生成便於閲讀的格式,另一個json.MarshalIndent函數將産生整齊縮進的輸出。該函數有兩個額外的字符串參數用於表示每一行輸出的前綴和每一個層級的縮進:
|
||||
这种紧凑的表示形式虽然包含了全部的信息,但是很难阅读。为了生成便于阅读的格式,另一个json.MarshalIndent函数将产生整齐缩进的输出。该函数有两个额外的字符串参数用于表示每一行输出的前缀和每一个层级的缩进:
|
||||
|
||||
```Go
|
||||
data, err := json.MarshalIndent(movies, "", " ")
|
||||
@@ -71,7 +71,7 @@ if err != nil {
|
||||
fmt.Printf("%s\n", data)
|
||||
```
|
||||
|
||||
上面的代碼將産生這樣的輸出(譯註:在最後一個成員或元素後面併沒有逗號分隔符):
|
||||
上面的代码将产生这样的输出(译注:在最后一个成员或元素后面并没有逗号分隔符):
|
||||
|
||||
```Json
|
||||
[
|
||||
@@ -103,18 +103,18 @@ fmt.Printf("%s\n", data)
|
||||
]
|
||||
```
|
||||
|
||||
在編碼時,默認使用Go語言結構體的成員名字作爲JSON的對象(通過reflect反射技術,我們將在12.6節討論)。隻有導出的結構體成員才會被編碼,這也就是我們爲什麽選擇用大寫字母開頭的成員名稱。
|
||||
在编码时,默认使用Go语言结构体的成员名字作为JSON的对象(通过reflect反射技术,我们将在12.6节讨论)。只有导出的结构体成员才会被编码,这也就是我们为什么选择用大写字母开头的成员名称。
|
||||
|
||||
細心的讀者可能已經註意到,其中Year名字的成員在編碼後變成了released,還有Color成員編碼後變成了小寫字母開頭的color。這是因爲構體成員Tag所導致的。一個構體成員Tag是和在編譯階段關聯到該成員的元信息字符串:
|
||||
细心的读者可能已经注意到,其中Year名字的成员在编码后变成了released,还有Color成员编码后变成了小写字母开头的color。这是因为构体成员Tag所导致的。一个构体成员Tag是和在编译阶段关联到该成员的元信息字符串:
|
||||
|
||||
```
|
||||
Year int `json:"released"`
|
||||
Color bool `json:"color,omitempty"`
|
||||
```
|
||||
|
||||
結構體的成員Tag可以是任意的字符串面值,但是通常是一繫列用空格分隔的key:"value"鍵值對序列;因爲值中含義雙引號字符,因此成員Tag一般用原生字符串面值的形式書寫。json開頭鍵名對應的值用於控製encoding/json包的編碼和解碼的行爲,併且encoding/...下面其它的包也遵循這個約定。成員Tag中json對應值的第一部分用於指定JSON對象的名字,比如將Go語言中的TotalCount成員對應到JSON中的total_count對象。Color成員的Tag還帶了一個額外的omitempty選項,表示當Go語言結構體成員爲空或零值時不生成JSON對象(這里false爲零值)。果然,Casablanca是一個黑白電影,併沒有輸出Color成員。
|
||||
结构体的成员Tag可以是任意的字符串面值,但是通常是一系列用空格分隔的key:"value"键值对序列;因为值中含义双引号字符,因此成员Tag一般用原生字符串面值的形式书写。json开头键名对应的值用于控制encoding/json包的编码和解码的行为,并且encoding/...下面其它的包也遵循这个约定。成员Tag中json对应值的第一部分用于指定JSON对象的名字,比如将Go语言中的TotalCount成员对应到JSON中的total_count对象。Color成员的Tag还带了一个额外的omitempty选项,表示当Go语言结构体成员为空或零值时不生成JSON对象(这里false为零值)。果然,Casablanca是一个黑白电影,并没有输出Color成员。
|
||||
|
||||
編碼的逆操作是解碼,對應將JSON數據解碼爲Go語言的數據結構,Go語言中一般叫unmarshaling,通過json.Unmarshal函數完成。下面的代碼將JSON格式的電影數據解碼爲一個結構體slice,結構體中隻有Title成員。通過定義合適的Go語言數據結構,我們可以選擇性地解碼JSON中感興趣的成員。當Unmarshal函數調用返迴,slice將被隻含有Title信息值填充,其它JSON成員將被忽略。
|
||||
编码的逆操作是解码,对应将JSON数据解码为Go语言的数据结构,Go语言中一般叫unmarshaling,通过json.Unmarshal函数完成。下面的代码将JSON格式的电影数据解码为一个结构体slice,结构体中只有Title成员。通过定义合适的Go语言数据结构,我们可以选择性地解码JSON中感兴趣的成员。当Unmarshal函数调用返回,slice将被只含有Title信息值填充,其它JSON成员将被忽略。
|
||||
|
||||
```Go
|
||||
var titles []struct{ Title string }
|
||||
@@ -124,7 +124,7 @@ if err := json.Unmarshal(data, &titles); err != nil {
|
||||
fmt.Println(titles) // "[{Casablanca} {Cool Hand Luke} {Bullitt}]"
|
||||
```
|
||||
|
||||
許多web服務都提供JSON接口,通過HTTP接口發送JSON格式請求併返迴JSON格式的信息。爲了説明這一點,我們通過Github的issue査詢服務來演示類似的用法。首先,我們要定義合適的類型和常量:
|
||||
许多web服务都提供JSON接口,通过HTTP接口发送JSON格式请求并返回JSON格式的信息。为了说明这一点,我们通过Github的issue查询服务来演示类似的用法。首先,我们要定义合适的类型和常量:
|
||||
|
||||
<u><i>gopl.io/ch4/github</i></u>
|
||||
```Go
|
||||
@@ -157,9 +157,9 @@ type User struct {
|
||||
}
|
||||
```
|
||||
|
||||
和前面一樣,卽使對應的JSON對象名是小寫字母,每個結構體的成員名也是聲明爲大小字母開頭的。因爲有些JSON成員名字和Go結構體成員名字併不相同,因此需要Go語言結構體成員Tag來指定對應的JSON名字。同樣,在解碼的時候也需要做同樣的處理,GitHub服務返迴的信息比我們定義的要多很多。
|
||||
和前面一样,即使对应的JSON对象名是小写字母,每个结构体的成员名也是声明为大小字母开头的。因为有些JSON成员名字和Go结构体成员名字并不相同,因此需要Go语言结构体成员Tag来指定对应的JSON名字。同样,在解码的时候也需要做同样的处理,GitHub服务返回的信息比我们定义的要多很多。
|
||||
|
||||
SearchIssues函數發出一個HTTP請求,然後解碼返迴的JSON格式的結果。因爲用戶提供的査詢條件可能包含類似`?`和`&`之類的特殊字符,爲了避免對URL造成衝突,我們用url.QueryEscape來對査詢中的特殊字符進行轉義操作。
|
||||
SearchIssues函数发出一个HTTP请求,然后解码返回的JSON格式的结果。因为用户提供的查询条件可能包含类似`?`和`&`之类的特殊字符,为了避免对URL造成冲突,我们用url.QueryEscape来对查询中的特殊字符进行转义操作。
|
||||
|
||||
<u><i>gopl.io/ch4/github</i></u>
|
||||
```Go
|
||||
@@ -198,9 +198,9 @@ func SearchIssues(terms []string) (*IssuesSearchResult, error) {
|
||||
}
|
||||
```
|
||||
|
||||
在早些的例子中,我們使用了json.Unmarshal函數來將JSON格式的字符串解碼爲字節slice。但是這個例子中,我們使用了基於流式的解碼器json.Decoder,它可以從一個輸入流解碼JSON數據,盡管這不是必須的。如您所料,還有一個針對輸出流的json.Encoder編碼對象。
|
||||
在早些的例子中,我们使用了json.Unmarshal函数来将JSON格式的字符串解码为字节slice。但是这个例子中,我们使用了基于流式的解码器json.Decoder,它可以从一个输入流解码JSON数据,尽管这不是必须的。如您所料,还有一个针对输出流的json.Encoder编码对象。
|
||||
|
||||
我們調用Decode方法來填充變量。這里有多種方法可以格式化結構。下面是最簡單的一種,以一個固定寬度打印每個issue,但是在下一節我們將看到如果利用模闆來輸出複雜的格式。
|
||||
我们调用Decode方法来填充变量。这里有多种方法可以格式化结构。下面是最简单的一种,以一个固定宽度打印每个issue,但是在下一节我们将看到如果利用模板来输出复杂的格式。
|
||||
|
||||
<u><i>gopl.io/ch4/issues</i></u>
|
||||
```Go
|
||||
@@ -228,7 +228,7 @@ func main() {
|
||||
}
|
||||
```
|
||||
|
||||
通過命令行參數指定檢索條件。下面的命令是査詢Go語言項目中和JSON解碼相關的問題,還有査詢返迴的結果:
|
||||
通过命令行参数指定检索条件。下面的命令是查询Go语言项目中和JSON解码相关的问题,还有查询返回的结果:
|
||||
|
||||
```
|
||||
$ go build gopl.io/ch4/issues
|
||||
@@ -249,12 +249,12 @@ $ ./issues repo:golang/go is:open json decoder
|
||||
#4237 gjemiller encoding/base64: URLEncoding padding is optional
|
||||
```
|
||||
|
||||
GitHub的Web服務接口 https://developer.github.com/v3/ 包含了更多的特性。
|
||||
GitHub的Web服务接口 https://developer.github.com/v3/ 包含了更多的特性。
|
||||
|
||||
**練習 4.10:** 脩改issues程序,根據問題的時間進行分類,比如不到一個月的、不到一年的、超過一年。
|
||||
**练习 4.10:** 修改issues程序,根据问题的时间进行分类,比如不到一个月的、不到一年的、超过一年。
|
||||
|
||||
**練習 4.11:** 編寫一個工具,允許用戶在命令行創建、讀取、更新和關閉GitHub上的issue,當必要的時候自動打開用戶默認的編輯器用於輸入文本信息。
|
||||
**练习 4.11:** 编写一个工具,允许用户在命令行创建、读取、更新和关闭GitHub上的issue,当必要的时候自动打开用户默认的编辑器用于输入文本信息。
|
||||
|
||||
**練習 4.12:** 流行的web漫畵服務xkcd也提供了JSON接口。例如,一個 https://xkcd.com/571/info.0.json 請求將返迴一個很多人喜愛的571編號的詳細描述。下載每個鏈接(隻下載一次)然後創建一個離線索引。編寫一個xkcd工具,使用這些離線索引,打印和命令行輸入的檢索詞相匹配的漫畵的URL。
|
||||
**练习 4.12:** 流行的web漫画服务xkcd也提供了JSON接口。例如,一个 https://xkcd.com/571/info.0.json 请求将返回一个很多人喜爱的571编号的详细描述。下载每个链接(只下载一次)然后创建一个离线索引。编写一个xkcd工具,使用这些离线索引,打印和命令行输入的检索词相匹配的漫画的URL。
|
||||
|
||||
**練習 4.13:** 使用開放電影數據庫的JSON服務接口,允許你檢索和下載 https://omdbapi.com/ 上電影的名字和對應的海報圖像。編寫一個poster工具,通過命令行輸入的電影名字,下載對應的海報。
|
||||
**练习 4.13:** 使用开放电影数据库的JSON服务接口,允许你检索和下载 https://omdbapi.com/ 上电影的名字和对应的海报图像。编写一个poster工具,通过命令行输入的电影名字,下载对应的海报。
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
## 4.6. 文本和HTML模闆
|
||||
## 4.6. 文本和HTML模板
|
||||
|
||||
前面的例子,隻是最簡單的格式化,使用Printf是完全足夠的。但是有時候會需要複雜的打印格式,這時候一般需要將格式化代碼分離出來以便更安全地脩改。這寫功能是由text/template和html/template等模闆包提供的,它們提供了一個將變量值填充到一個文本或HTML格式的模闆的機製。
|
||||
前面的例子,只是最简单的格式化,使用Printf是完全足够的。但是有时候会需要复杂的打印格式,这时候一般需要将格式化代码分离出来以便更安全地修改。这写功能是由text/template和html/template等模板包提供的,它们提供了一个将变量值填充到一个文本或HTML格式的模板的机制。
|
||||
|
||||
一個模闆是一個字符串或一個文件,里面包含了一個或多個由雙花括號包含的`{{action}}`對象。大部分的字符串隻是按面值打印,但是對於actions部分將觸發其它的行爲。每個actions都包含了一個用模闆語言書寫的表達式,一個action雖然簡短但是可以輸出複雜的打印值,模闆語言包含通過選擇結構體的成員、調用函數或方法、表達式控製流if-else語句和range循環語句,還有其它實例化模闆等諸多特性。下面是一個簡單的模闆字符串:
|
||||
一个模板是一个字符串或一个文件,里面包含了一个或多个由双花括号包含的`{{action}}`对象。大部分的字符串只是按面值打印,但是对于actions部分将触发其它的行为。每个actions都包含了一个用模板语言书写的表达式,一个action虽然简短但是可以输出复杂的打印值,模板语言包含通过选择结构体的成员、调用函数或方法、表达式控制流if-else语句和range循环语句,还有其它实例化模板等诸多特性。下面是一个简单的模板字符串:
|
||||
|
||||
{% raw %}
|
||||
|
||||
@@ -21,11 +21,11 @@ Age: {{.CreatedAt | daysAgo}} days
|
||||
|
||||
{% raw %}
|
||||
|
||||
這個模闆先打印匹配到的issue總數,然後打印每個issue的編號、創建用戶、標題還有存在的時間。對於每一個action,都有一個當前值的概念,對應點操作符,寫作“.”。當前值“.”最初被初始化爲調用模闆是的參數,在當前例子中對應github.IssuesSearchResult類型的變量。模闆中`{{.TotalCount}}`對應action將展開爲結構體中TotalCount成員以默認的方式打印的值。模闆中`{{range .Items}}`和`{{end}}`對應一個循環action,因此它們直接的內容可能會被展開多次,循環每次迭代的當前值對應當前的Items元素的值。
|
||||
这个模板先打印匹配到的issue总数,然后打印每个issue的编号、创建用户、标题还有存在的时间。对于每一个action,都有一个当前值的概念,对应点操作符,写作“.”。当前值“.”最初被初始化为调用模板是的参数,在当前例子中对应github.IssuesSearchResult类型的变量。模板中`{{.TotalCount}}`对应action将展开为结构体中TotalCount成员以默认的方式打印的值。模板中`{{range .Items}}`和`{{end}}`对应一个循环action,因此它们直接的内容可能会被展开多次,循环每次迭代的当前值对应当前的Items元素的值。
|
||||
|
||||
{% endraw %}
|
||||
|
||||
在一個action中,`|`操作符表示將前一個表達式的結果作爲後一個函數的輸入,類似於UNIX中管道的概念。在Title這一行的action中,第二個操作是一個printf函數,是一個基於fmt.Sprintf實現的內置函數,所有模闆都可以直接使用。對於Age部分,第二個動作是一個叫daysAgo的函數,通過time.Since函數將CreatedAt成員轉換爲過去的時間長度:
|
||||
在一个action中,`|`操作符表示将前一个表达式的结果作为后一个函数的输入,类似于UNIX中管道的概念。在Title这一行的action中,第二个操作是一个printf函数,是一个基于fmt.Sprintf实现的内置函数,所有模板都可以直接使用。对于Age部分,第二个动作是一个叫daysAgo的函数,通过time.Since函数将CreatedAt成员转换为过去的时间长度:
|
||||
|
||||
```Go
|
||||
func daysAgo(t time.Time) int {
|
||||
@@ -33,9 +33,9 @@ func daysAgo(t time.Time) int {
|
||||
}
|
||||
```
|
||||
|
||||
需要註意的是CreatedAt的參數類型是time.Time,併不是字符串。以同樣的方式,我們可以通過定義一些方法來控製字符串的格式化(§2.5),一個類型同樣可以定製自己的JSON編碼和解碼行爲。time.Time類型對應的JSON值是一個標準時間格式的字符串。
|
||||
需要注意的是CreatedAt的参数类型是time.Time,并不是字符串。以同样的方式,我们可以通过定义一些方法来控制字符串的格式化(§2.5),一个类型同样可以定制自己的JSON编码和解码行为。time.Time类型对应的JSON值是一个标准时间格式的字符串。
|
||||
|
||||
生成模闆的輸出需要兩個處理步驟。第一步是要分析模闆併轉爲內部表示,然後基於指定的輸入執行模闆。分析模闆部分一般隻需要執行一次。下面的代碼創建併分析上面定義的模闆templ。註意方法調用鏈的順序:template.New先創建併返迴一個模闆;Funcs方法將daysAgo等自定義函數註冊到模闆中,併返迴模闆;最後調用Parse函數分析模闆。
|
||||
生成模板的输出需要两个处理步骤。第一步是要分析模板并转为内部表示,然后基于指定的输入执行模板。分析模板部分一般只需要执行一次。下面的代码创建并分析上面定义的模板templ。注意方法调用链的顺序:template.New先创建并返回一个模板;Funcs方法将daysAgo等自定义函数注册到模板中,并返回模板;最后调用Parse函数分析模板。
|
||||
|
||||
```Go
|
||||
report, err := template.New("report").
|
||||
@@ -46,9 +46,9 @@ if err != nil {
|
||||
}
|
||||
```
|
||||
|
||||
因爲模闆通常在編譯時就測試好了,如果模闆解析失敗將是一個致命的錯誤。template.Must輔助函數可以簡化這個致命錯誤的處理:它接受一個模闆和一個error類型的參數,檢測error是否爲nil(如果不是nil則發出panic異常),然後返迴傳入的模闆。我們將在5.9節再討論這個話題。
|
||||
因为模板通常在编译时就测试好了,如果模板解析失败将是一个致命的错误。template.Must辅助函数可以简化这个致命错误的处理:它接受一个模板和一个error类型的参数,检测error是否为nil(如果不是nil则发出panic异常),然后返回传入的模板。我们将在5.9节再讨论这个话题。
|
||||
|
||||
一旦模闆已經創建、註冊了daysAgo函數、併通過分析和檢測,我們就可以使用github.IssuesSearchResult作爲輸入源、os.Stdout作爲輸出源來執行模闆:
|
||||
一旦模板已经创建、注册了daysAgo函数、并通过分析和检测,我们就可以使用github.IssuesSearchResult作为输入源、os.Stdout作为输出源来执行模板:
|
||||
|
||||
```Go
|
||||
var report = template.Must(template.New("issuelist").
|
||||
@@ -66,7 +66,7 @@ func main() {
|
||||
}
|
||||
```
|
||||
|
||||
程序輸出一個純文本報告:
|
||||
程序输出一个纯文本报告:
|
||||
|
||||
```
|
||||
$ go build gopl.io/ch4/issuesreport
|
||||
@@ -86,9 +86,9 @@ Age: 695 days
|
||||
...
|
||||
```
|
||||
|
||||
現在讓我們轉到html/template模闆包。它使用和text/template包相同的API和模闆語言,但是增加了一個將字符串自動轉義特性,這可以避免輸入字符串和HTML、JavaScript、CSS或URL語法産生衝突的問題。這個特性還可以避免一些長期存在的安全問題,比如通過生成HTML註入攻擊,通過構造一個含有惡意代碼的問題標題,這些都可能讓模闆輸出錯誤的輸出,從而讓他們控製頁面。
|
||||
现在让我们转到html/template模板包。它使用和text/template包相同的API和模板语言,但是增加了一个将字符串自动转义特性,这可以避免输入字符串和HTML、JavaScript、CSS或URL语法产生冲突的问题。这个特性还可以避免一些长期存在的安全问题,比如通过生成HTML注入攻击,通过构造一个含有恶意代码的问题标题,这些都可能让模板输出错误的输出,从而让他们控制页面。
|
||||
|
||||
下面的模闆以HTML格式輸出issue列表。註意import語句的不同:
|
||||
下面的模板以HTML格式输出issue列表。注意import语句的不同:
|
||||
|
||||
{% raw %}
|
||||
|
||||
@@ -119,26 +119,26 @@ var issueList = template.Must(template.New("issuelist").Parse(`
|
||||
|
||||
{% endraw %}
|
||||
|
||||
下面的命令將在新的模闆上執行一個稍微不同的査詢:
|
||||
下面的命令将在新的模板上执行一个稍微不同的查询:
|
||||
|
||||
```
|
||||
$ go build gopl.io/ch4/issueshtml
|
||||
$ ./issueshtml repo:golang/go commenter:gopherbot json encoder >issues.html
|
||||
```
|
||||
|
||||
圖4.4顯示了在web瀏覽器中的效果圖。每個issue包含到Github對應頁面的鏈接。
|
||||
图4.4显示了在web浏览器中的效果图。每个issue包含到Github对应页面的链接。
|
||||
|
||||

|
||||
|
||||
圖4.4中issue沒有包含會對HTML格式産生衝突的特殊字符,但是我們馬上將看到標題中含有`&`和`<`字符的issue。下面的命令選擇了兩個這樣的issue:
|
||||
图4.4中issue没有包含会对HTML格式产生冲突的特殊字符,但是我们马上将看到标题中含有`&`和`<`字符的issue。下面的命令选择了两个这样的issue:
|
||||
|
||||
```
|
||||
$ ./issueshtml repo:golang/go 3133 10535 >issues2.html
|
||||
```
|
||||
|
||||
圖4.5顯示了該査詢的結果。註意,html/template包已經自動將特殊字符轉義,因此我們依然可以看到正確的字面值。如果我們使用text/template包的話,這2個issue將會産生錯誤,其中“&lt;”四個字符將會被當作小於字符“<”處理,同時“<link>”字符串將會被當作一個鏈接元素處理,它們都會導致HTML文檔結構的改變,從而導致有未知的風險。
|
||||
图4.5显示了该查询的结果。注意,html/template包已经自动将特殊字符转义,因此我们依然可以看到正确的字面值。如果我们使用text/template包的话,这2个issue将会产生错误,其中“&lt;”四个字符将会被当作小于字符“<”处理,同时“<link>”字符串将会被当作一个链接元素处理,它们都会导致HTML文档结构的改变,从而导致有未知的风险。
|
||||
|
||||
我們也可以通過對信任的HTML字符串使用template.HTML類型來抑製這種自動轉義的行爲。還有很多采用類型命名的字符串類型分别對應信任的JavaScript、CSS和URL。下面的程序演示了兩個使用不同類型的相同字符串産生的不同結果:A是一個普通字符串,B是一個信任的template.HTML字符串類型。
|
||||
我们也可以通过对信任的HTML字符串使用template.HTML类型来抑制这种自动转义的行为。还有很多采用类型命名的字符串类型分别对应信任的JavaScript、CSS和URL。下面的程序演示了两个使用不同类型的相同字符串产生的不同结果:A是一个普通字符串,B是一个信任的template.HTML字符串类型。
|
||||
|
||||

|
||||
|
||||
@@ -163,15 +163,15 @@ func main() {
|
||||
|
||||
{% endraw %}
|
||||
|
||||
圖4.6顯示了出現在瀏覽器中的模闆輸出。我們看到A的黑體標記被轉義失效了,但是B沒有。
|
||||
图4.6显示了出现在浏览器中的模板输出。我们看到A的黑体标记被转义失效了,但是B没有。
|
||||
|
||||

|
||||
|
||||
我們這里隻講述了模闆繫統中最基本的特性。一如旣往,如果想了解更多的信息,請自己査看包文檔:
|
||||
我们这里只讲述了模板系统中最基本的特性。一如既往,如果想了解更多的信息,请自己查看包文档:
|
||||
|
||||
```
|
||||
$ go doc text/template
|
||||
$ go doc html/template
|
||||
```
|
||||
|
||||
**練習 4.14:** 創建一個web服務器,査詢一次GitHub,然後生成BUG報告、里程碑和對應的用戶信息。
|
||||
**练习 4.14:** 创建一个web服务器,查询一次GitHub,然后生成BUG报告、里程碑和对应的用户信息。
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# 第四章 複合數據類型
|
||||
# 第四章 复合数据类型
|
||||
|
||||
在第三章我們討論了基本數據類型,它們可以用於構建程序中數據結構,是Go語言的世界的原子。在本章,我們將討論複合數據類型,它是以不同的方式組合基本類型可以構造出來的複合數據類型。我們主要討論四種類型——數組、slice、map和結構體——同時在本章的最後,我們將演示如何使用結構體來解碼和編碼到對應JSON格式的數據,併且通過結合使用模闆來生成HTML頁面。
|
||||
在第三章我们讨论了基本数据类型,它们可以用于构建程序中数据结构,是Go语言的世界的原子。在本章,我们将讨论复合数据类型,它是以不同的方式组合基本类型可以构造出来的复合数据类型。我们主要讨论四种类型——数组、slice、map和结构体——同时在本章的最后,我们将演示如何使用结构体来解码和编码到对应JSON格式的数据,并且通过结合使用模板来生成HTML页面。
|
||||
|
||||
數組和結構體是聚合類型;它們的值由許多元素或成員字段的值組成。數組是由同構的元素組成——每個數組元素都是完全相同的類型——結構體則是由異構的元素組成的。數組和結構體都是有固定內存大小的數據結構。相比之下,slice和map則是動態的數據結構,它們將根據需要動態增長。
|
||||
数组和结构体是聚合类型;它们的值由许多元素或成员字段的值组成。数组是由同构的元素组成——每个数组元素都是完全相同的类型——结构体则是由异构的元素组成的。数组和结构体都是有固定内存大小的数据结构。相比之下,slice和map则是动态的数据结构,它们将根据需要动态增长。
|
||||
|
||||
|
||||
Reference in New Issue
Block a user