1. 程式人生 > 其它 >Go xmas2020 學習筆記 01-14 上篇

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

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 是基於樹形結構的,它又一個隱含的順序,按字母順序排列。

gomap 是無序的,基於雜湊表。不同時間迭代對映會得到不同的順序。如果你需要順序取出,那你要先取出 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\) 無法工作。所以必須給定長度。


不要引用用於迴圈的變數。在迴圈內部宣告新變數將其特殊化。