Go xmas2020 學習筆記 01-14 上篇
課程地址 go-class-slides/xmas-2020 at trunk · matt4biz/go-class-slides (github.com)
主講老師 Matt Holiday
00-02-Hello Example
目錄結構
L:.
│ main.go
│
└───hello
hello.go
hello_test.go
- main.go 是主程式入口
- hello.go 是 hello 模組
- hello_test.go 用於單元測試 hello 模組
不一樣的Hello World
hello.go
package hello import ( "strings" ) func Say(names []string) string { if len(names) == 0 { names = []string{"world"} } return "Hello, " + strings.Join(names, ", ") + "!" }
傳入引數是一個字串切片,當切片長度為 0 時,自動給定一個長度為 1 的切片。
然後呼叫 strings.Join
方法將字串切片各個元素根據間隔符 ,
合併,在進行 + 運算子後返回完整字串。
巧妙的單元測試
hello_test.go
package hello import "testing" func TestSayHello(t *testing.T) { subtests := []struct { items []string result string }{ { result: "Hello, world!", }, { items: []string{"Matt"}, result: "Hello, Matt!", }, { items: []string{"Matt", "Anne"}, result: "Hello, Matt, Anne!", }, } for _, st := range subtests { if s := Say(st.items); s != st.result { t.Errorf("wanted %s (%v) | got %s", st.result, st.items, s) } } }
subtests
是一個匿名結構體的切片,我們可以在第二個花括號定義切片元素。
將引數與結果放在一起,for迴圈 subtests
進行多個單元測試,如果與預期不符就通過 t.Errorf
報錯
因為 Say
方法在 Hello.go
中首字母大寫宣告,所以為 Public
公開,外界可用
傳入os.Args切片
main.go
package main import ( "Work/Work/Study/Matt/2_cmd/hello" "fmt" "os" ) func main() { fmt.Println("Hello,", hello.Say(os.Args[1:])) }
匯入了hello包,通過 包名.方法名 呼叫(可以給包名起別名),因為 Say
函式需要傳入一個字串切片,我們不能直接傳入 os.Args[1]
否則是一個字串變數,巧妙的是可以用切片擷取 os.Args[1:]
的方式獲取一整個切片。
os.Args[n]
通常是執行go程式時新增的多餘引數,os.Args[0]
是程式的絕對路徑。
go run main.go cat dog
os.Args[0] 輸出 C:/xxx/xxx/xxx/main.go
os.Args[1] 輸出 cat
os.Args[2] 輸出 dog
go mod init
go mod init hello
用於在當前根目錄下生成 go.mod 檔案,可以 ignore GOPATH,可用於在任何目錄下的程式碼編寫。go 會自動做處理。
03-Basic Types
變數型別與直譯器
先看右圖,在 python 中,a並不是計算機實際意義上的數字,a是在直譯器中表示或偽裝的數字,使用C編寫的直譯器將使用底層硬體來做實際的數學運算,把這些東西編成二進位制數字。所以在python中雖然a=2,但計算機並不能立刻知道。
來看左圖,a純粹是機器中記憶體位置的地址,沒有直譯器,沒有jvm。這就是go的效能優勢,go編譯器直接生成機器程式碼,操作要快很多。
a:=2 在64位系統上,預設 64位 int
不要用內建浮點型別表示金錢
嘗試使用內建浮點型別來表示金錢是一種不好的做法,幾乎所有語言都是如此。浮點數實際上是為了科學計算。
使用內建浮點數表示金錢會有表示錯誤(精確度問題),缺少邏輯(100美分三次分割問題)
變數宣告方式
特殊型別
go 中布林值跟數字雙方是獨立的,不能互相轉換和比較。
變數初始化
如果不希望初始化最好使用 var 宣告變數,如果需要初始化使用短宣告語法
常量定義
go 限制常量為 數字,字串,布林值 型別。
型別轉換
package main
import "fmt"
func main() {
a := 2
b := 3.1
fmt.Printf("a: %8T %v\n", a, a)
fmt.Printf("b: %8T %[1]v\n", b) // ^ [1] 是Printf方法第二個引數
a = int(b) // ^ go 是一門嚴格的語言,需要進行顯式型別轉換
fmt.Printf("a: %8T %[1]v\n", a)
b = float64(a)
fmt.Printf("b: %8T %[1]v\n", b)
}
求平均值、標準流
package main
import (
"fmt"
"os"
)
func main() {
var sum float64
var n int
for {
var val float64
if _, err := fmt.Fscanln(os.Stdin, &val); err != nil {
fmt.Println(err)
break
} else {
sum += val
n++
}
}
if n == 0 {
fmt.Fprintln(os.Stderr, "no values") // ^ 需要告訴在哪個輸出流上列印
os.Exit(-1)
}
fmt.Println("The average is", sum/float64(n)) // ^ go沒有自動轉換,需要強制轉換
}
_, err := fmt.Fscanln(os.Stdin, &val)
用於從標準輸入流獲取輸入的行資料,並進行轉換,轉換失敗會將錯誤返回給 err,否則 err 為 nil
fmt.Fprintln(os.Stderr, "no values")
與 Println
差不多,只是需要告訴在哪個輸出流上列印
04-Strings
Strings
字串在 go 中都是 unicode ,unicode 是一種特殊的技術用於表示國際通用字元。
rune 相當於 wide character,是 int32 的同義詞,四個位元組足夠大,任何 unicode、字元,邏輯字元 可以指向它。
但是為了讓程式更高效,我們不想一直用 4 個位元組表示每個字元,因為很多程式使用 ascii 字元。
因此有一種稱為 utf-8 編碼的 unicode 技術,以位元組 byte 表示 unicode 的簡便方法。
從物理角度上看,strings 就是 unicode 字元的 utf-8 編碼。
ascii characters 適合 0-127 的範圍
func main() {
s := "élite"
fmt.Printf("%8T %[1]v %d\n", s, len(s))
fmt.Printf("%8T %[1]v\n", []rune(s))
b := []byte(s)
fmt.Printf("%8T %[1]v %d\n", b, len(b))
}
string élite 6
[]int32 [233 108 105 116 101]
[]uint8 [195 169 108 105 116 101] 6
é 為 233 超出了 ascii 的表示範圍,由 2 個位元組表示,而不是為每個字元使用 4 個位元組,這是 utf8 編碼的效果。中文字經常為 20000 的數字,五個中文字會用 15 個位元組表示。
len(s) 顯示 6 的原因,在程式中字串的長度是在 utf-8 中編碼字串所必需的位元組字串的長度
The length of a string in the program is the length of the byte string that's necessary to encode the string in utf-8,not the number of unicode characters
就是說給定一個字串,把它進行 utf-8 編碼需要的位元組數量就是它的長度,而不是 unicode 字元的數量。
String structure
可以把圖片左邊的 s 理解為一個描述符(描述符不是指標、不是 go 的專業術語),它有指標和額外的資訊(位元組數)。
go 字串末尾沒有空位元組,很多程式語言通過迴圈字串判斷空位元組獲取長度,效率並不高。在 go 中字串長度直接儲存在描述符中。
通過索引字串建立 hello 的時候,hello 的 data 指向的是跟 s 描述符 data 的相同記憶體地址(共享儲存)。
因為字串沒有空位元組,而且它們是不可變的,所以共享儲存是完全可行的。world 也是同理。它們重用 s 中的記憶體。
t := s
的結果是 t 將有與 s 一樣的內容,但是 t 跟 s 是不一樣的描述符。
b、c 與 s 共享儲存。
d 開闢了新的記憶體空間,存入了新的字串。
s[5] = 'a' 出錯,字串是不可變的,不能單獨修改字串。
s +="es" 相當於 s = s + "es" ,開闢了新的記憶體空間,複製原有內容,再新增新內容,並使 data 指向新的記憶體地址。
原來的字串並沒有改變、消失,因為 b、c 依舊指向原來的記憶體地址,s 指向了新開闢的記憶體地址。
String functions
s = strings.ToUpper(s)
字串不允許被更改,所以會建立新字串進行舊字串的拷貝並大寫。由於開闢了新的記憶體空間,將返回值給 s 也就很好理解了。
如果沒有變數引用字串,它會自動被垃圾回收。
Practice
做一個替換句子中指定單詞的程式
main.go
package main
import (
"bufio"
"fmt"
"os"
"strings"
)
func main() {
if len(os.Args) < 3 {
fmt.Fprintln(os.Stderr, "not enough args")
os.Exit(-1)
}
old, new := os.Args[1], os.Args[2]
scan := bufio.NewScanner(os.Stdin)
for scan.Scan() {
s := strings.Split(scan.Text(), old)
t := strings.Join(s, new)
fmt.Println(t)
}
}
os.Args
執行 go 程式時附加的引數,具體可以看前幾節的內容。
buffio.NewScanner(os.Stdin)
掃描器是一個緩衝io工具,預設以行分割輸入的內容。舉個例子,如果輸入特別大,就可以把它以一系列行的形式讀取。
scan.Scan()
將迴圈讀取行,如果有可用的行讀取將會返回true。
scan.Text()
獲取讀取的行。
for 迴圈中使用 strings
標準庫的 Split
方法根據舊單詞 變數 old
(大小寫敏感)分割字串獲得字串切片。
再將切片傳入 strings
標準庫的 Join
方法,通過新單詞 變數 new
合併字串。
test.txt
matt went to greece
where did matt go
alan went to rome
matt didn't go there
第一行留空行,因為會讀取 BOM 頭,具體請看這篇文章
重定向管道流讀取TXT文字第一次讀取為""空字串 - 小能日記 - 部落格園 (cnblogs.com)
result
cat test.txt | go run . matt ed
ed went to greece
where did ed go
alan went to rome
ed didn't go there
這裡我們使用了重定向管道,讀取 test.txt 的內容當做 main.go 的程式輸入,指令在 linux 是 go run . matt ed < test.txt。
old, new := os.Args[1], os.Args[2]
old, new = new, old
值得注意的一點是初始化變數的方式,使用一行初始化兩個變數。巧妙的是可以用這種方式進行兩個變數值的交換。
05-Arrays, Slices, and Maps
In memory
string、array、slice 在記憶體中是連續儲存的,map不是連續儲存的。
Array
在建立陣列的時候需要指定大小,如果不指定需要使用 ... ,圖中 a、b 將是固定的 24 位元組物件(int在64位作業系統上預設為int64),一旦設定不能改變。
d=b
中,由於陣列只是一塊記憶體,並不是像字串那樣的描述符,我們只是物理地複製了位元組。當陣列大小不一致時,無法進行拷貝複製。
Slice
切片有描述符,指向一個特定的記憶體地址。它的工作方式類似於字串的工作方式。
切片描述符包含 data、len、capacity。
append
方法需要把返回值重新賦給 a,假設 a 指向的記憶體區域已經滿了,再新增元素就要開闢新的更大的記憶體區域存放。
a=b
表示 b 描述符的內容被拷貝到 a 描述符中。
e:=a
新建一個描述符,內容與 a 描述符內的一致。
切片可以被切片(擷取)操作,就像從字串(前面的os.Args[1:])中取出切片,從切片陣列切片等。
package main
import "fmt"
func main() {
t := []byte("string")
fmt.Println(len(t), t)
fmt.Println(t[2])
fmt.Println(t[:2])
fmt.Println(t[2:])
fmt.Println(t[3:5], len(t[3:5]))
}
6 [115 116 114 105 110 103]
114
[115 116]
[114 105 110 103]
[105 110] 2
fence post error
柵欄柱錯誤:假設我有三個柵欄部分,我必須有四個柵欄在他們旁邊將它們固定住。(不懂直接看圖)
Compare Array、Slice
切片可以是任意長度,而且大部分 Go 的標準庫使用切片作為引數。
切片是不能進行比較的,想進行比較可以使用陣列。這也導致切片不能作為 Map Key。
陣列可以作為一些演算法必備的陣列。大小固定,值不改變。近似於偽常量。注意,不能新增 const 常量關鍵字,只有數字,字串,布林值可以作為常量。
Example
a[0]=4
因為 a 只是 w 的值拷貝(陣列),所以修改後 w 並沒有被修改。
b[0]=3
將會使 x 修改,因為兩者 data 都指向同一個記憶體地址。(但是要注意,這是值拷貝,如果新增元素過多,會導致 b 的 data 指標使用新的記憶體地址而 x 還是指向原來的)
copy(c, b)
函式不會因為切片大小不同出錯,會盡可能把 b 切片中的元素拷貝到 c 中。
我們可以對陣列切片如 z := a[0:2]
z 將是一個切片,指向 a 的前兩個元素,go 會自動提供陣列來儲存。
Map
假設要計算一個檔案中不同單詞出現的次數,就可以使用 Maps。是一個 Hash table。
m 是一個描述符,但是整體為空。 p 的 data 指標指向一個雜湊表。
map 與 map 間不能進行比較,只能進行 nil 比較。
可以檢視 map 的長度,不能檢視 map 的容量。
可以通過獲取第二個引數判斷鍵值對是否存在。
Built in functions
Make nil useful
由於 len、cap、range 這些內建函式是安全的,我們不需要 if 判斷 nil 就可以直接使用。
range 將會跳過 nil、empty 的迴圈物件。
Quote
一種不影響你思考程式設計的方式的語言是不值得了解的
Practice
編寫一個段落單詞計數器,輸出前三個出現次數最多的單詞。
main.go
package main
import (
"bufio"
"fmt"
"os"
"sort"
)
func main() {
scan := bufio.NewScanner(os.Stdin)
words := make(map[string]int)
// ^ 預設是按行讀取,所以手動指定按單詞讀取
scan.Split(bufio.ScanWords)
for scan.Scan() {
words[scan.Text()]++
}
fmt.Println(len(words), "unique words")
type kv struct {
key string
val int
}
var ss []kv
for k, v := range words {
ss = append(ss, kv{k, v})
}
// ^ 直接修改原切片
sort.Slice(ss, func(i, j int) bool {
return ss[i].val > ss[j].val
})
for _, s := range ss[:3] {
fmt.Println(s.key, "appears", s.val, "times")
}
}
scan.Split(bufio.ScanWords)
Scanner 預設是按行讀取,所以手動指定按單詞讀取。
kv{k, v}
結構體的初始化
sort.Slice
函式直接修改原切片,傳入的函式在 return
前面的元素排在切片的前面。如左>右,則大的元素在切片最前面,屬於降序排序。
test.txt
matt went to greece
where did matt go
alan went to rome
matt didn't go there
第一行是空行是有原因的,這是 BOM頭(Byte Order Mark) 導致的,具體請看另一篇文章
重定向管道流讀取TXT文字第一次讀取為""空字串 - 小能日記 - 部落格園 (cnblogs.com)
result
cat test.txt | go run .
12 unique words
matt appears 3 times
to appears 2 times
go appears 2 times
06-Control Statements
If-then-else
- 花括號是必須寫的,而且有嚴格的格式
- if 語句內可以寫短宣告
Loop
for
range array
注意第二種方式 v 是被拷貝的,假設 myArray
是個 4K 大小的陣列,那麼每次迴圈時都會進行復制,這種情況下最好採用第一種方式。第一種更加高效,只用索引的方式直接從陣列中獲取避免了複製。
range map
這兩種情況下,迴圈都會進行很長的時間。
在 c++
中 map
是基於樹形結構的,它又一個隱含的順序,按字母順序排列。
在 go
中 map
是無序的,基於雜湊表。不同時間迭代對映會得到不同的順序。如果你需要順序取出,那你要先取出 keys
然後對其按字母進行排列,再遍歷從maps
取出值。
infinite loop
common mistake
labels and loops
例子中可能 returnedData
切片很長,所以匹配到第一個之後應該返回到標籤 outer
的外部迴圈。
需要明確指出 continue outer
,即對應 outer
標籤
Switch
switch
-
switch
其實就是 if else 的語法糖。更容易理解,提高可讀性。 - 可以在
switch
後短宣告。 - 可以為一個
case
新增空的語句段,只判斷不執行。 - 不需要新增
break
。 - 最好新增
default
。
switch on true
cases
可以有邏輯語句,就像一堆 if else,更加方便。
Packages
所有 go
檔案必須以 package
開頭。
短宣告只能在函式中使用,因為在包中應該以關鍵詞開頭,這樣方便編譯器解析。
如果首字母大寫,那麼就是匯出的,否則是私有的。
包依賴應該是一個樹形結構,不能迴圈依賴。
包內的東西將在 main
函式前被初始化,在執行時將會執行包內的 init
函式(在main呼叫前)。
迴圈依賴會導致不知道優先初始化哪個的問題。
好的包在一個簡單的 api 後面封裝了深層複雜的功能
06-Declarations & Types
Declaration
Short declarations
重點講一下第三條
- 第一行用短聲明瞭
err
變數 - 第二行重複宣告
err
會報錯 - 第三行會正確執行,因為聲明瞭新變數
x
,而err
只是重新賦值
Structural typing
duck typing
在程式設計中,鴨子型別(英語:duck typing)是動態型別的一種風格。在這種風格中,一個物件有效的語義,不是由繼承自特定的類或實現特定的介面,而是由"當前方法和屬性的集合"決定。
“當看到一隻鳥走起來像鴨子、游泳起來像鴨子、叫起來也像鴨子,那麼這隻鳥就可以被稱為鴨子。”
在鴨子型別中,關注點在於物件的行為,能作什麼;而不是關注物件所屬的型別。例如,在不使用鴨子型別的語言中,我們可以編寫一個函式,它接受一個型別為"鴨子"的物件,並呼叫它的"走"和"叫"方法。在使用鴨子型別的語言中,這樣的一個函式可以接受一個任意型別的物件,並呼叫它的"走"和"叫"方法。如果這些需要被呼叫的方法不存在,那麼將引發一個執行時錯誤。任何擁有這樣的正確的"走"和"叫"方法的物件都可被函式接受的這種行為引出了以上表述,這種決定型別的方式因此得名。
Operator
邏輯運算子只能給布林值用,其他程式語言可能有 0 == false,但是 go 沒有
07-Formatted & File I/O
I/O steams
作業系統具有三個標準 io 流,標準輸入、標準輸出、標準錯誤。它們分別可以重定向。
formatted I/O
Println
將引數預設輸出到標準輸出流,如果會用 Fprintln
可以指定輸出到某個流,比如 os.Stderr
fmt functions
Sprintln
格式化字串並返回。
package main
import "fmt"
func main() {
a, b := 12, 345
c, d := 1.2, 3.45
fmt.Printf("%d %d\n", a, b)
fmt.Printf("%x %x\n", a, b)
fmt.Printf("%#x %#x\n", a, b)
fmt.Printf("%f %.2f", c, d)
fmt.Println()
fmt.Printf("|%6d|%6d|\n", a, b)
fmt.Printf("|%-6d|%-6d|\n", a, b)
fmt.Printf("|%06d|%06d|\n", a, b)
fmt.Printf("|%9f|%9.2f|\n", c, d) // ^ 當數字過大時也會超出
}
12 345
c 159
0xc 0x159
1.200000 3.45
| 12| 345|
|12 |345 |
|000012|000345|
| 1.200000| 3.45|
package main
import (
"fmt"
)
func main() {
s := []int{1, 2, 3}
a := [3]rune{'a', 'b', 'c'}
m := map[string]int{"and": 1, "or": 2}
ss := "a string"
b := []byte(ss)
fmt.Printf("%T\n", s)
fmt.Printf("%v\n", s)
fmt.Printf("%#v\n", s) // ^ %#v 更符合初始化時輸入的形式
fmt.Println()
fmt.Printf("%T\n", a)
fmt.Printf("%v\n", a)
fmt.Printf("%q\n", a) // ^ 注意這個%q將rune從int32轉化成了字串
fmt.Printf("%#v\n", a)
fmt.Println()
fmt.Printf("%T\n", m)
fmt.Printf("%v\n", m)
fmt.Printf("%#v\n", m)
fmt.Println()
fmt.Printf("%T\n", ss)
fmt.Printf("%v\n", ss)
fmt.Printf("%q\n", ss)
fmt.Printf("%#v\n", ss)
fmt.Printf("%v\n", b)
fmt.Printf("%v\n", string(b)) // ^ 將位元組切片轉換為字串
}
[]int
[1 2 3]
[]int{1, 2, 3}
[3]int32
[97 98 99]
['a' 'b' 'c']
[3]int32{97, 98, 99}
map[string]int
map[and:1 or:2]
map[string]int{"and":1, "or":2}
string
a string
"a string"
"a string"
[97 32 115 116 114 105 110 103]
a string
file I/O
Practice ① I/O
編寫一個類似 Unix cat 的程式,將多個檔案輸出到標準輸出流中,並輸出為一個檔案。
package main
import (
"fmt"
"io"
"os"
)
func main() {
for _, fname := range os.Args[1:] {
file, err := os.Open(fname)
if err != nil {
fmt.Fprintln(os.Stderr, err)
continue
}
if _, err := io.Copy(os.Stdout, file); err != nil {
fmt.Fprint(os.Stderr, err)
continue
}
fmt.Fprint(os.Stdout, "\n") // ^ 每個檔案內容末尾新增換行符
file.Close()
}
}
io.copy
是一個很棒的功能,它知道如何緩衝、如何以塊的形式讀入並寫會,它不會嘗試把整個檔案讀取到記憶體中也不會一次讀取一個字元。
file.Close
大多數作業系統對程式中開啟多少個檔案有限制,所以檔案使用完成後需要進行關閉。
在當前目錄新建 txt
檔案,寫入內容。執行下面三條命令。
go run . a.txt
go run . a.txt b.txt c.txt
go run . a.txt b.txt c.txt > new.txt
第二條指令結果
[]int{1, 2, 3}
go go go
people car
cat
apple
banana
第三條指令在當前目錄生成了 new.txt 檔案,內容是 標準輸出流 的內容。
Always check the err
Practice ② I/O
編寫一個簡短的程式計算檔案大小。一次性讀取(小檔案情況下)
我們前面知道, io/ioutil
包可以對整個檔案進行讀取,存入記憶體中。我們可以使用它計算檔案大小。
原先的 io.Copy
返回的是複製的位元組數,而 ReadAll
將返回整個 data
,位元組切片和一個err。
package main
import (
"fmt"
"io/ioutil"
"os"
)
func main() {
for _, fname := range os.Args[1:] {
file, err := os.Open(fname)
if err != nil {
fmt.Fprintln(os.Stderr, err)
continue
}
data, err := ioutil.ReadAll(file)
if err != nil {
fmt.Fprint(os.Stderr, err)
continue
}
fmt.Println("The file has", len(data), "bytes")
file.Close()
}
}
go run . a.txt b.txt c.txt
The file has 30 bytes
The file has 20 bytes
The file has 18 bytes
data, err := ioutil.ReadAll(file)
從 if
中取出單獨成行,是因為需要 data
這個變數。如果放在 if
短聲明裡會導致作用域只在 if
語句塊內。
Practice ③ I/O
編寫一個 wc 程式(word counter),輸出lines、words、characters數量。使用緩衝 buffio(大檔案情況下)
package main
import (
"bufio"
"fmt"
"os"
"strings"
)
func main() {
for _, fname := range os.Args[1:] {
var lc, wc, cc int
file, err := os.Open(fname)
if err != nil {
fmt.Fprintln(os.Stderr, err)
continue
}
scan := bufio.NewScanner(file)
for scan.Scan() {
s := scan.Text()
wc += len(strings.Fields(s)) // ^ 根據空格、製表符分割 a slice of words
cc += len(s)
lc++
}
fmt.Printf("%7d %7d %7d %s\n", lc, wc, cc, fname)
file.Close()
}
}
go run . a.txt b.txt c.txt
3 7 26 a.txt
2 5 18 b.txt
3 3 14 c.txt
bufio.NewScanner(file)
建立一個掃描器按行掃描。考慮到多行需要用 for
迴圈 scan.Scan
。
strings.Fields(s)
根據空格、製表符分割,拿到的是字串切片。
08-Functions, Parameters
functions
first class
你可以在函式體內宣告函式,但必須是匿名函式,作為一個變數。
function signatures
函式簽名指的是 函式引數型別與排列順序、函式返回值
parameter
pass by value
func do(b [3]int) int {
b[0] = 0
return b[1]
}
func main() {
a := [3]int{1, 2, 3}
v := do(a) // ^ 陣列被複制到函式的區域性變數
fmt.Println(a, v)
}
[1 2 3] 2
pass by reference
func do(b []int) int {
b[0] = 0
fmt.Printf("b2 @ %p\n", b)
b = append(b, 100)
b = append(b, 100)
fmt.Printf("b3 @ %p\n", b)
return b[1]
}
func main() {
a := []int{1, 2, 3}
fmt.Printf("b1 @ %p\n", a)
v := do(a) // ^ 切片被複制到函式的區域性變數
fmt.Println(a, v)
}
b1 @ 0xc00012c078
b2 @ 0xc00012c078
b3 @ 0xc00013e060
[0 2 3] 2
func do(m1 map[int]int) {
m1[3] = 0 // ^ 兩個描述符和相同的雜湊表,且雜湊表有三個鍵,因此修改m1,m被修改
m1 = make(map[int]int) // ^ 分配了新對映,但m不會被改變
m1[4] = 4
fmt.Println("m1", m1)
}
func main() {
m := map[int]int{4: 1, 7: 2, 8: 3}
do(m)
fmt.Println(m)
}
m1 map[4:4]
map[3:0 4:1 7:2 8:3]
the ultimate truth
go 裡只有值傳遞,函式內的變數都是區域性變數,它被分配、拷貝實際引數的值,假如傳入的是切片描述符,它也是被複制到區域性變數裡的。描述符被複制,切片底層資料沒有被複制。
returns
Recursion
遞迴執行比迭代慢因為要建立一系列堆疊幀。
08-Defer
defer gotcha #1
Defer is based on function scope
第二個例子中,只有退出函式才會執行 defer
將會開啟很多檔案導致程式崩潰。所以直接使用 f.close
關閉檔案。
defer gotcha #2
defer
執行時,以引數實際的值拷貝傳遞進延遲函式並壓入 defer棧 中,而不是引用。
當我離開函式時執行延遲堆疊,延遲的匿名函式修改返回值 a
09-Closures
變數的生命週期可以超過變數宣告上下文的範圍
左側 f 只是函式指標,右側 f 則是閉包。注意右上角標紅的 &,閉包是引用封閉的,拿到a、b變數的引用而不是單純的值。
Slice
需要一個特定的閉包簽名函式。在閉包的上下文中,我唯一傳遞給我的閉包是 i、j 他們是整數,ss 也是這個函式的一部分雖然沒有被明確傳入。
package main
import "fmt"
func do(d func()) {
d()
}
func main() {
for i := 0; i < 4; i++ {
v := func() {
fmt.Printf("%d @ %p\n", i, &i)
}
do(v)
}
}
0 @ 0xc000016088
1 @ 0xc000016088
2 @ 0xc000016088
3 @ 0xc000016088
package main
import "fmt"
func main() {
s := make([]func(), 4)
for i := 0; i < 4; i++ {
s[i] = func() {
fmt.Printf("%d @ %p\n", i, &i)
}
}
for i := 0; i < 4; i++ {
s[i]()
}
}
4 @ 0xc000016088
4 @ 0xc000016088
4 @ 0xc000016088
4 @ 0xc000016088
當封閉 i
變數時,每個閉包需要一個引用。四個匿名函式引用的都是同一個 i
,在第一個迴圈退出後,i
值為 4。i
並沒有被垃圾回收,因為它仍被 4
個匿名閉包函式所引用。每次列印都是 4
比如傳入一個閉包函式作為回撥函式的時候,所引用的值在回撥執行前會發生改變,那會出現大問題。
在第一個迴圈內建立一個新變數,每次迴圈宣告初始化一個新變數,每個閉包函式會引用這個新變數,每個 i2
地址不一樣。
for i := 0; i < 4; i++ {
i2 := i // closure capture
s[i] = func() {
fmt.Printf("%d @ %p\n", i, &i)
}
閉包是一種函式,呼叫具有來自函式外部的附加資料。例如資料來自另一個函式的範圍,並且它通過引用封閉(封蓋)。被封閉引數有點像引數,但它並不是,它允許我們函式使用那些不能用引數傳遞的額外資料,例如有些被其他型別固定的資料而無法被傳遞的資料。我們需要注意 gotcha
,因為閉包通過引用封閉,如果閉包是非同步執行的,那麼我封閉(封蓋)的變數可能會發生改變。正如前面的例子,修復方法就是建立一個對應的本地副本,讓閉包函式關閉(封蓋)本地副本,這樣副本的值就固定了。
10-Slices in Detail
Slice
package main
import "fmt"
func main() {
var s []int
t := []int{}
u := make([]int, 5)
v := make([]int, 0, 5)
fmt.Printf("%d, %d, %T, %5t %#[3]v\n", len(s), cap(s), s, s == nil)
fmt.Printf("%d, %d, %T, %5t %#[3]v\n", len(t), cap(t), t, t == nil)
fmt.Printf("%d, %d, %T, %5t %#[3]v\n", len(u), cap(u), u, u == nil)
fmt.Printf("%d, %d, %T, %5t %#[3]v\n", len(v), cap(v), v, v == nil)
}
0, 0, []int, true []int(nil)
0, 0, []int, false []int{}
5, 5, []int, false []int{0, 0, 0, 0, 0}
0, 5, []int, false []int{}
\(t\) 中的 \(addr\) 指向一個起哨兵作用的結構,所以我們知道它是空的而不是 \(nil\).
可以用 append
方法生成元素儲存地址,並返回一個描述符引用這個儲存給 \(s\) . 即便 \(s\) 為 \(nil\)
Empty vs nil slice
使用 \(nil\) 、\(empty\) 對映替換切片在這個例子中分別是 null、{ } 。
判斷切片是否為空不能使用 a == nil
,因為有 \(nil\)、\(empty\) 兩種情況,應該用 len(a)
進行判斷。
最好make
切片的時候給定 length
,否則新建同長度容量的切片用append
會將元素追加在一堆0的後面。
Important
package main
import "fmt"
func main() {
a := [3]int{1, 2, 3}
b := a[:1]
fmt.Println("a = ", a)
fmt.Println("b = ", b)
c := b[0:2]
fmt.Println("c = ", c)
fmt.Println(len(b))
fmt.Println(cap(b))
fmt.Println(len(c))
fmt.Println(cap(c))
d := a[0:1:1]
// e := d[0:2]
fmt.Println("d = ", d)
// fmt.Println("e = ", e) Error
fmt.Println(len(d))
fmt.Println(cap(d))
}
a = [1 2 3]
b = [1]
c = [1 2]
1
3
2
3
d = [1]
1
2
對擷取的切片再次進行切片是根據原先的底層陣列來的。
如果你使用兩個索引切片符,你得到的切片的容量等於底層陣列的容量。
package main
import "fmt"
func main() {
a := [3]int{1, 2, 3}
b := a[:1]
// c := b[0:2]
c := b[0:2:2]
fmt.Printf("a[%p] = %v\n", &a, a)
fmt.Printf("b[%p] = %v\n", b, b)
fmt.Printf("c[%p] = %v\n", c, c)
c = append(c, 5)
fmt.Printf("a[%p] = %v\n", &a, a)
fmt.Printf("c[%p] = %v\n", c, c)
c[0] = 9
fmt.Printf("a[%p] = %v\n", &a, a)
fmt.Printf("c[%p] = %v\n", c, c)
}
a[0xc000010150] = [1 2 3]
b[0xc000010150] = [1]
c[0xc000010150] = [1 2]
a[0xc000010150] = [1 2 3]
c[0xc00000e2a0] = [1 2 5]
a[0xc000010150] = [1 2 3]
c[0xc00000e2a0] = [9 2 5]
\(a\) 是一個數組,\(b、c\) 是兩個切片,它們指向 \(a\)。
-
對 \(c\) 新增元素,會發現 \(a、c\) 被改變。\(c\) 的容量為 \(3\),長度為 \(2\),對 \(c\) 新增元素的時候把 \(a\) 修改了,覆蓋 \(a\) 的第三個值。
-
對 \(c\) 限制容量數量,再新增元素會導致沒有地方放置,所以會重新分配一塊容量更大的記憶體區域,拷貝原先的元素,再把新加的元素放進去,底層陣列地址發生改變。
去掉 \(a\),將 \(b\) 宣告為切片並初始化,\(b\) 描述符指向無命名的底層陣列。用 \(c\) 對其切片,並新增元素,結果和上面是一樣的。切片實際上是一些底層陣列的別名。
11-Homework #2
package main
import (
"bytes"
"fmt"
"os"
"strings"
"golang.org/x/net/html"
)
var raw = `
<!DOCTYPE html>
<html>
<body>
<h1>My First Heading</h1>
<p>My first paragraph.</p>
<p>HTML <a href="https://www.w3schools.com/html/html_images.asp">images</a> are defined with the img tag:</p>
<img src="xxx.jpg" width="104" height="142">
</body>
</html>
`
func visit(n *html.Node, words, pics *int) {
if n.Type == html.TextNode {
*words += len(strings.Fields(n.Data))
} else if n.Type == html.ElementNode && n.Data == "img" {
*pics++
}
for c := n.FirstChild; c != nil; c = c.NextSibling {
visit(c, words, pics)
}
}
func countWordsAndImages(doc *html.Node) (int, int) {
var words, pics int
visit(doc, &words, &pics)
return words, pics
}
func main() {
doc, err := html.Parse(bytes.NewReader([]byte(raw)))
if err != nil {
fmt.Fprintf(os.Stderr, "parse failed:%s\n", err)
os.Exit(-1)
}
words, pics := countWordsAndImages(doc)
fmt.Printf("%d words and %d images\n", words, pics)
}
14 words and 1 images
假如我去訪問一個網站,我會得到一個位元組的片段,將它放到閱讀器中。
doc, err := html.Parse(bytes.NewReader([]byte(raw)))
返回的\(doc\)是樹節點,我們可以用 \(for\) 迴圈通過節點的 \(FirstChild、NextSibling\) 屬性遍歷整棵樹。
11-Reader
Reader interface
上文出現了閱讀器這個概念,我感到很模糊,於是查詢相關資料進行學習。
type Reader interface {
Read(p []byte) (n int ,err error)
}
官方文件中關於該介面方法的說明
Read 將 len(p) 個位元組讀取到 p 中。它返回讀取的位元組數 n(0 <= n <= len(p)) 以及任何遇到的錯誤。即使 Read 返回的 n < len(p),它也會在呼叫過程中使用 p 的全部作為暫存空間。若一些資料可用但不到 len(p) 個位元組,Read 會照例返回可用的資料,而不是等待更多資料。
Read 在成功讀取 n > 0 個位元組後遇到一個錯誤或
EOF (end-of-file)
,它就會返回讀取的位元組數。它會從相同的呼叫中返回(非nil的)錯誤或從隨後的呼叫中返回錯誤(同時 n == 0)。 一般情況的一個例子就是 Reader 在輸入流結束時會返回一個非零的位元組數,同時返回的err
不是EOF
就是nil
。無論如何,下一個 Read 都應當返回0, EOF
。
呼叫者應當總在考慮到錯誤 err 前處理 n > 0 的位元組。這樣做可以在讀取一些位元組,以及允許的 EOF 行為後正確地處理 I/O 錯誤
PS: 當Read
方法返回錯誤時,不代表沒有讀取到任何資料,可能是資料被讀完了時返回的io.EOF
。
Reader 介面的方法集(Method_sets)只包含一個 Read 方法,因此,所有實現了 Read
方法的型別都實現了io.Reader
介面,也就是說,在所有需要 io.Reader
的地方,可以傳遞實現了 Read()
方法的型別的例項。
NewReader func
Reader Struct
NewReader建立一個從s讀取資料的Reader
type Reader struct {
s string //對應的字串
i int64 // 當前讀取到的位置
prevRune int
}
Len 、Size,Read func
Len作用: 返回未讀的字串長度
Size的作用:返回字串的長度
read的作用: 讀取字串資訊
r := strings.NewReader("abcdefghijklmn")
fmt.Println(r.Len()) // 輸出14 初始時,未讀長度等於字串長度
var buf []byte
buf = make([]byte, 5)
readLen, err := r.Read(buf)
fmt.Println("讀取到的長度:", readLen) //讀取到的長度5
if err != nil {
fmt.Println("錯誤:", err)
}
fmt.Println(buf) //adcde
fmt.Println(r.Len()) //9 讀取到了5個 剩餘未讀是14-5
fmt.Println(r.Size()) //14 字串的長度
Practice
任何實現了 Read()
函式的物件都可以作為 Reader
來使用。
圍繞io.Reader/Writer
,有幾個常用的實現
-
net.Conn
,os.Stdin
,os.File
: 網路、標準輸入輸出、檔案的流讀取 -
strings.Reader
: 把字串抽象成Reader -
bytes.Reader
: 把[]byte抽象成Reader -
bytes.Buffer
: 把[]byte抽象成Reader和Writer -
bufio.Reader/Writer
: 抽象成帶緩衝的流讀取(比如按行讀寫)
我們編寫一個通用的閱讀器至標準輸出流方法,並分別傳入物件 \(os.File、net.Conn、strings.Reader\)
func readerToStdout(r io.Reader, bufSize int) {
buf := make([]byte, bufSize)
for {
n, err := r.Read(buf)
if err == io.EOF {
break
}
if err != nil {
fmt.Println(err)
break
}
if n > 0 {
fmt.Println(string(buf[:n]))
}
}
}
在\(readerToStdout\) 方法中,我們傳入實現了 \(io.Reader\) 介面的物件,並規定一個每次讀取資料的緩衝位元組切片的大小。
需要注意的是,由於是分段讀取,需要使用 \(for\) 迴圈,通過判斷 \(io.EOF\) 退出迴圈,同時還需要考慮其他錯誤。輸出至 \(os.Stdin\) 標準流時需要對位元組切片進行字串型別轉換,同時位元組切片應該被索引擷取。\(n\)是本次讀取到的位元組數。
如果輸出時切片不被索引擷取會出現什麼情況。
func fileReader() {
f, err := os.Open("book.txt")
if err != nil {
panic(err)
}
defer f.Close()
buf := make([]byte, 3)
for {
n, err := f.Read(buf)
if err == io.EOF {
break
}
if err != nil {
fmt.Println(err)
break
}
if n > 0 {
fmt.Println(buf)
}
}
}
book.txt 內容為 abcd
[97 98 99]
[100 98 99]
第一次迴圈緩衝切片被正常填滿,而第二次由於還剩一個位元組,便將這一個位元組讀入緩衝切片中,而後面元素未被改變。假定檔案位元組數很小,緩衝切片很大,那麼第一次就可以讀取完成,這會導致輸出位元組陣列後面的 \(0\) 或一些奇怪的內容。
func connReader() {
conn, err := net.Dial("tcp", "example.com:80")
if err != nil {
panic(err)
}
defer conn.Close()
fmt.Fprint(conn, "GET /index.html HTTP/1.0\r\n\r\n")
readerToStdout(conn, 20)
}
這裡我們通過 \(net.Dial\) 方法建立一個 \(tcp\) 連線,同時我們需要使用 \(fmt.Fprint\) 方法給特定連線傳送請求。\(conn\) 實現了 \(io.Reader\) 介面,可以傳入 \(readerToStdout\) 方法。
func stringsReader() {
s := strings.NewReader("very short but interesting string")
readerToStdout(s, 5)
}
func fileReader() {
f, err := os.Open("book.txt")
if err != nil {
panic(err)
}
defer f.Close()
readerToStdout(f, 3)
}
我們給定 \(string\) 物件來構造 \(strings.Reader\),並傳入 \(readerToStdout\) 方法。我們使用 \(os.Open\) 開啟檔案,所得到的 \(File\) 物件也實現了 \(os.Reader\) 介面。
12-Structs, Struct tags & JSON
Struct
結構通常是不同型別的聚合,所以有不同型別的欄位,通過欄位查詢值。
type Employee struct {
Name string
Number int
Boss *Employee
Hired time.Time
}
func main() {
var e Employee
fmt.Printf("%T %+[1]v", e)
}
main.Employee {Name: Number:0 Boss:<nil> Hired:0001-01-01 00:00:00 +0000 UTC}
通過 \(\%+v\) 顯示結構體的欄位。通過點表示法插入值。另外的宣告方法
var e2 = Employee{
"Matt",
1,
nil,
time.Now(),
}
這種需要按順序填寫所有欄位。我們可以指定欄位名就可以只寫部分
var e2 = Employee{
Name: "Matt",
Number: 1,
Hired: time.Now(),
}
boss := Employee{"Lamine", 2, nil, time.Now()}
e2.Boss = &boss
fmt.Printf("%T %+[1]v\n", e2)
main.Employee {Name:Matt Number:1 Boss:0xc00005e100 Hired:2022-04-08 07:40:49.042803 +0800 CST m=+0.006431301}
由於 \(Boss\) 是指標,在 \(e2\) 的輸出中顯示的是指標。上方程式碼也可以寫成
boss := &Employee{"Lamine", 2, nil, time.Now()}
e2.Boss = boss
使 \(boss\) 指向結構體指標,在某種意義上建立結構體,匿名獲取指標。
使用 \(map\) 管理所有 \(Employee\) 物件
c := map[string]*Employee{}
// c := make(map[string]*Employee)
c["Lamine"] = &Employee{"Lamine", 2, nil, time.Now()}
c["Matt"] = &Employee{
Name: "Matt",
Number: 1,
Boss: c["Lamine"],
Hired: time.Now(),
}
fmt.Printf("%T %+[1]v\n", c["Lamine"])
fmt.Printf("%T %+[1]v\n", c["Matt"])
*main.Employee &{Name:Lamine Number:2 Boss:<nil> Hired:2022-04-08 07:51:11.8676147 +0800 CST m=+0.004987001}
*main.Employee &{Name:Matt Number:1 Boss:0xc00005e040 Hired:2022-04-08 07:51:11.8676147 +0800 CST m=+0.004987001}
Struct Gotcha
c := map[string]Employee{}
c["Lamine"] = Employee{"Lamine", 2, nil, time.Now()}
c["Matt"] = Employee{
Name: "Matt",
Number: 1,
Boss: &c["Lamine"],
Hired: time.Now(),
}
fmt.Printf("%T %+[1]v\n", c["Lamine"])
fmt.Printf("%T %+[1]v\n", c["Matt"])
修改 \(map\) 儲存物件,從結構體指標變為結構體,而 \(Employee\) 內的 \(Boss\) 欄位需要一個指標,在這種情況下,假設我們從對映中獲取物件,並得到其指標,那麼 \(IDE\) 會報錯。
invalid operation: cannot take address of c["Lamine"]
對映有限制,你不能獲取對映內實體的地址。原因在於每當操作地圖的時候,如果我將某些內容插入地圖或從地圖中刪除某些內容,地圖可以在內部重新排列,因為雜湊表資料結構是動態的,那樣獲得的地址是非常不安全的,可能會變成過時的指標。
c["Lamine"] = Employee{"Lamine", 2, nil, time.Now()}
c["Lamine"].Number++
cannot assign to struct field c["Lamine"].Number in map
如果有一張結構體的對映,對對映中一個該結構體中的值進行修改是不可能的。必須要將結構體的對映修改為結構體指標的對映。
Anonymous Struct Type
func main() {
var album = struct {
title string
artist string
year int
copies int
}{
"The White Album",
"The Beatles",
1968,
1000000000,
}
var pAlbum *struct {
title string
artist string
year int
copies int
}
fmt.Println(album, pAlbum)
}
基於匿名結構型別,並用結構文字初始化,但並不是特別方便。比如建立一個空的匿名結構體指標的時候。
var album1 = struct {
title string
}{
"The White Album",
}
var album2 = struct {
title string
}{
"The Black Album",
}
album1 = album2
fmt.Println(album1, album2)
可以執行這種賦值操作,將拷貝 \(album2\) 的副本複製給 \(album1\) ,兩個匿名結構體具有相同的結構和行為(有相同的欄位和欄位型別)
type album1 struct {
title string
}
type album2 struct {
title string
}
func main() {
var a1 = album1{
"The White Album",
}
var a2 = album2{
"The Black Album",
}
a1 = a2
// a1 = album1(a2)
fmt.Println(a1, a2)
}
而在這種情況下會報錯,因為他們不是同一個型別名,但是他們是可以互相轉換的。
cannot use a2 (variable of type album2) as album1 value in assignment
判斷結構體一致的條件
- 欄位一樣,欄位型別也一樣
- 欄位按順序排列
- 相同的欄位標籤
紅圈用於包含一些如何以各種方式進行編碼的資訊協議。比如為 \(json\) 建立 \(key\),當我們檢視 \(json\) 的工作原理時它將使用反射。
但如果它們是一致的,可以進行強制轉換。
需要注意的是,從 \(go\ 1.8\) 起,不同欄位標籤不阻礙型別轉換。
Make the zero value useful
\(nil\ [\ ]byte\) 可以使用 \(append\),當 \(buffer\) 被建立時就可以直接被使用,不需要做什麼前置工作。
Empty structs
\(struct\{\}\) 在記憶體中作為單例物件存在,構建空結構體集合比布林值集合更省空間。
JSON
type Response struct {
Page int `json:"page"`
Words []string `json:"words,omitempty"`
}
func main() {
r := Response{
Page: 1,
Words: []string{"up", "in", "out"},
}
j, _ := json.Marshal(r)
fmt.Println(string(j))
fmt.Printf("%#v\n", r)
var r2 Response
_ = json.Unmarshal(j, &r2)
fmt.Printf("%#v\n", r2)
r3 := Response{
Page: 100,
}
j3, _ := json.Marshal(r3)
fmt.Println(string(j3))
fmt.Printf("%#v\n", r3)
}
\(json.Marshal()\) 返回位元組切片,輸出到控制檯需要轉換成 \(string\)。\(json.Unmarshal\) 需要提供一個結構體指標用於存放解析的資料。\(omitempty\) 關鍵詞用於判空,如果為空就省去。否則轉換為 \(json\) 的時候會給該欄位預設加 \(null\) 值。
欄位都以大寫開頭,這樣它們可以被匯出。如果欄位名以小寫開頭,\(json\) 不會對它進行編碼。
struct field words has json tag but is not exported
從編譯器來看程式是正確的,而從 \(linting\ tool\) 靜態分析工具來看會給出一個警告。
正則表示式參考資料
Syntax · google/re2 Wiki (github.com)
13-Regular Expressions
Simple string searches
func main() {
test := "Here is $1 which is $2!"
test = strings.ReplaceAll(test, "$1", "honey")
test = strings.ReplaceAll(test, "$2", "tasty")
fmt.Println(test)
}
Here is honey which is tasty!
使用 \(strings\) 包進行簡單搜尋,對於複雜搜尋和驗證,謹慎使用 \(regexp\) 。
Location by regex
func main() {
te := "aba abba abbba"
re := regexp.MustCompile(`b+`)
mm := re.FindAllString(te, -1)
id := re.FindAllStringIndex(te, -1)
fmt.Println(mm)
fmt.Println(id)
for _, d := range id {
fmt.Println(te[d[0]:d[1]])
}
up := re.ReplaceAllStringFunc(te, strings.ToUpper)
fmt.Println(up)
}
[b bb bbb]
[[1 2] [5 7] [10 13]]
b
bb
bbb
aBa aBBa aBBBa
FindAllString(te, -1)
返回匹配的字串切片。
FindAllStringIndex(te, -1)
返回匹配的字串位置,是切片的切片。
UUID validation
var uu = regexp.MustCompile(`^[[:xdigit:]]{8}-[[:xdigit:]]{4}-[1-5][[:xdigit:]]{3}-[89abAB][[:xdigit:]]{3}-[[:xdigit:]]{12}$`)
var test = []string{
"072664ee-a034-4cc3-a2e8-9f1822c43bbb",
"072664ee-a034-4cc3-a2e8-9f1822c43bbbb", // ^ 如果不加 ^ $ 匹配了前面的且忽略了後面的b
"072664ee-a034-6cc3-a2e8-9f1822c43bbbb",
"072664ee-a034-4cc3-C2e8-9f1822c43bbb",
}
func main() {
for i, t := range test {
if !uu.MatchString(t) {
fmt.Println(i, t, "\tfails")
}
}
}
1 072664ee-a034-4cc3-a2e8-9f1822c43bbbb fails
2 072664ee-a034-6cc3-a2e8-9f1822c43bbbb fails
3 072664ee-a034-4cc3-C2e8-9f1822c43bbb fails
Capture groups
var ph = regexp.MustCompile(`\(([[:digit:]]{3})\) ([[:digit:]]{3})-([[:digit:]]{4})`)
func main() {
orig := "(214) 514-9548"
match := ph.FindStringSubmatch(orig)
fmt.Printf("%q\n", match)
if len(match) > 3 {
fmt.Printf("+1 %s-%s-%s\n", match[1], match[2], match[3])
}
}
["(214) 514-9548" "214" "514" "9548"]
+1 214-514-9548
URL re
(?::([0-9]+))? 末尾的問號確定圓括號內的內容可以出現零次或一次。
?: 表示不被捕獲,即整個括號內容匹配了也不新增到返回的切片裡。
但內部又有一個捕獲組 ([0-9]+) 匹配出現一次或多次的數字,將被捕獲到字串放入返回的切片中。
所以 :([0-9]+) 這一整個不會出現在切片中,而 ([0-9]+) 會出現在切片中。
FindStringSubmatch
只會匹配最後一個,使用FindAllStringSubmatch
返回全部匹配,切片的切片。
14-Reference & Value Semantics
Pointers vs Values
如果要共享變數並修改,建議統一用指標傳遞的方式,否則 \(f3\) 返回的是原來的副本,\(f4\) 作出的修改將無法反映到 \(f1\)、\(f2\) 修改的物件上。即針對一個物件的修改卻產生了兩個物件。
Loop Gotcha
迴圈內第二個引數拿到的是副本,要在迴圈內修改原切片欄位的值不能直接修改副本,需要通過索引進行修改。
在函式內修改切片最好將切片返回,因為修改切片很可能會導致切片描述符指向的底層陣列地址發生改變,比如 \(grow\) 擴容。
將指標指向切片內的元素是極其危險的。當切片描述符指向的底層陣列擴容時,會導致指標指向已經過時的底層陣列。再通過指標修改元素會導致修改無效。
package main
import "fmt"
func main() {
items := [][2]byte{{1, 2}, {3, 4}, {5, 6}}
a := [][]byte{}
for _, item := range items {
a = append(a, item[:])
}
fmt.Println(items)
fmt.Println(a)
}
[[1 2] [3 4] [5 6]]
[[5 6] [5 6] [5 6]]
因為 \(item\) 是切片元素的副本,所以是兩位元組陣列,在記憶體中有特定位置,每次迴圈獲得到的副本都在記憶體的同一個地方。當迴圈結束後,最後兩個位元組陣列是 \(5、6\) ,而向 \(a\) 新增的是三個相同的 \(item\) 引用,所以都將引用 \(item\) 的最終值。修復這種方法的方法是在每次迴圈內部宣告一個新變數。
func main() {
items := [][2]byte{{1, 2}, {3, 4}, {5, 6}}
a := [][]byte{}
for _, item := range items {
i := make([]byte, len(item))
copy(i, item[:])
a = append(a, i)
}
fmt.Println(items)
fmt.Println(a)
}
[[1 2] [3 4] [5 6]]
[[1 2] [3 4] [5 6]]
如果給定 \(i\) 的 \(length\) 為 \(0\),會導致 \(copy\) 無法工作。所以必須給定長度。
不要引用用於迴圈的變數。在迴圈內部宣告新變數將其特殊化。