主頁 > 後端開發 > Go xmas2020 學習筆記 01-14 上篇

Go xmas2020 學習筆記 01-14 上篇

2022-05-19 13:02:23 後端開發

課程地址 go-class-slides/xmas-2020 at trunk · matt4biz/go-class-slides (github.com)

主講老師 Matt Holiday

image-20220401081031592

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

變數型別與解釋器

image-20220401054655735

先看右圖,在 python 中,a并不是計算機實際意義上的數字,a是在解釋器中表示或偽裝的數字,使用C撰寫的解釋器將使用底層硬體來做實際的數學運算,把這些東西編成二進制數字,所以在python中雖然a=2,但計算機并不能立刻知道,

來看左圖,a純粹是機器中記憶體位置的地址,沒有解釋器,沒有jvm,這就是go的性能優勢,go編譯器直接生成機器代碼,操作要快很多,

a:=2 在64位系統上,默認 64位 int

不要用內置浮點型別表示金錢

嘗試使用內置浮點型別來表示金錢是一種不好的做法,幾乎所有語言都是如此,浮點數實際上是為了科學計算,

使用內置浮點數表示金錢會有表示錯誤(精確度問題),缺少邏輯(100美分三次分割問題)

變數宣告方式

image-20220401060131381

特殊型別

image-20220401061413369

go 中布林值跟數字雙方是獨立的,不能互相轉換和比較,

變數初始化

image-20220401061757414

如果不希望初始化最好使用 var 宣告變數,如果需要初始化使用短宣告語法

常量定義

image-20220401061846775

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

image-20220402025441130

字串在 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

image-20220402031112990

可以把圖片左邊的 s 理解為一個描述符(描述符不是指標、不是 go 的專業術語),它有指標和額外的資訊(位元組數),

go 字串末尾沒有空位元組,很多編程語言通過回圈字串判斷空位元組獲取長度,效率并不高,在 go 中字串長度直接保存在描述符中,


通過索引字串創建 hello 的時候,hello 的 data 指向的是跟 s 描述符 data 的相同記憶體地址(共享存盤),

因為字串沒有空位元組,而且它們是不可變的,所以共享存盤是完全可行的,world 也是同理,它們重用 s 中的記憶體,


t := s 的結果是 t 將有與 s 一樣的內容,但是 t 跟 s 是不一樣的描述符,

image-20220402032338925

b、c 與 s 共享存盤,

d 開辟了新的記憶體空間,存入了新的字串,

s[5] = 'a' 出錯,字串是不可變的,不能單獨修改字串,

s +="es" 相當于 s = s + "es" ,開辟了新的記憶體空間,復制原有內容,再添加新內容,并使 data 指向新的記憶體地址,

原來的字串并沒有改變、消失,因為 b、c 依舊指向原來的記憶體地址,s 指向了新開辟的記憶體地址,


String functions

image-20220402032835785

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

image-20220402042140132

string、array、slice 在記憶體中是連續存盤的,map不是連續存盤的,


Array

image-20220402042243076

在創建陣列的時候需要指定大小,如果不指定需要使用 ... ,圖中 a、b 將是固定的 24 位元組物件(int在64位作業系統上默認為int64),一旦設定不能改變,

d=b 中,由于陣列只是一塊記憶體,并不是像字串那樣的描述符,我們只是物理地復制了位元組,當陣列大小不一致時,無法進行拷貝復制,


Slice

image-20220402042810564

切片有描述符,指向一個特定的記憶體地址,它的作業方式類似于字串的作業方式,

切片描述符包含 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

image-20220402043131288

柵欄柱錯誤:假設我有三個柵欄部分,我必須有四個柵欄在他們旁邊將它們固定住,(不懂直接看圖)


Compare Array、Slice

image-20220402045248221

切片可以是任意長度,而且大部分 Go 的標準庫使用切片作為引數,

切片是不能進行比較的,想進行比較可以使用陣列,這也導致切片不能作為 Map Key,


陣列可以作為一些演算法必備的陣列,大小固定,值不改變,近似于偽常量,注意,不能添加 const 常量關鍵字,只有數字,字串,布林值可以作為常量,

image-20220402045647614


Example

image-20220402045941511

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

image-20220402050751005

假設要計算一個檔案中不同單詞出現的次數,就可以使用 Maps,是一個 Hash table,

m 是一個描述符,但是整體為空, p 的 data 指標指向一個哈希表,


image-20220402051658526

map 與 map 間不能進行比較,只能進行 nil 比較,

可以查看 map 的長度,不能查看 map 的容量,


image-20220402051820579

可以通過獲取第二個引數判斷鍵值對是否存在,


Built in functions

image-20220402052141191

Make nil useful

image-20220402052630469

由于 len、cap、range 這些內建函式是安全的,我們不需要 if 判斷 nil 就可以直接使用,

range 將會跳過 nil、empty 的回圈物件,


Quote

image-20220402053526148

一種不影響你思考編程的方式的語言是不值得了解的


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

image-20220403143416752

  • 花括號是必須寫的,而且有嚴格的格式
  • if 陳述句內可以寫短宣告

Loop

for

image-20220403151535163


range array

image-20220403151710262

注意第二種方式 v 是被拷貝的,假設 myArray 是個 4K 大小的陣列,那么每次回圈時都會進行復制,這種情況下最好采用第一種方式,第一種更加高效,只用索引的方式直接從陣列中獲取避免了復制,


range map

image-20220403155526840

這兩種情況下,回圈都會進行很長的時間,

c++map 是基于樹形結構的,它又一個隱含的順序,按字母順序排列,

gomap 是無序的,基于哈希表,不同時間迭代映射會得到不同的順序,如果你需要順序取出,那你要先取出 keys 然后對其按字母進行排列,再遍歷從maps 取出值,


infinite loop

image-20220404070856610


common mistake

image-20220404071027405


labels and loops

image-20220404071214722

例子中可能 returnedData 切片很長,所以匹配到第一個之后應該回傳到標簽 outer 的外部回圈,

需要明確指出 continue outer ,即對應 outer 標簽


Switch

switch

image-20220404071451116

  • switch 其實就是 if else 的語法糖,更容易理解,提高可讀性,
  • 可以在 switch 后短宣告,
  • 可以為一個 case 添加空的陳述句段,只判斷不執行,
  • 不需要添加 break
  • 最好添加 default

switch on true

image-20220404072040968

cases 可以有邏輯陳述句,就像一堆 if else,更加方便,


Packages

image-20220404072301382

所有 go 檔案必須以 package 開頭,

短宣告只能在函式中使用,因為在包中應該以關鍵詞開頭,這樣方便編譯器決議,


image-20220404072543682

如果首字母大寫,那么就是匯出的,否則是私有的,


image-20220404072727953

image-20220404090351683

包依賴應該是一個樹形結構,不能回圈依賴


image-20220404090532914

包內的東西將在 main 函式前被初始化,在運行時將會執行包內的 init 函式(在main呼叫前)

回圈依賴會導致不知道優先初始化哪個的問題,


image-20220404090856952

好的包在一個簡單的 api 后面封裝了深層復雜的功能


06-Declarations & Types

Declaration

image-20220404091249750

image-20220404091316023


Short declarations

image-20220404091500794

重點講一下第三條

  • 第一行用短宣告了 err 變數
  • 第二行重復宣告 err 會報錯
  • 第三行會正確運行,因為宣告了新變數 x,而 err 只是重新賦值

image-20220404092029077

image-20220404092231754


Structural typing

image-20220404092757716

duck typing

在程式設計中,鴨子型別(英語:duck typing)是動態型別的一種風格,在這種風格中,一個物件有效的語意,不是由繼承自特定的類或實作特定的介面,而是由"當前方法和屬性的集合"決定,

“當看到一只鳥走起來像鴨子、游泳起來像鴨子、叫起來也像鴨子,那么這只鳥就可以被稱為鴨子,”

在鴨子型別中,關注點在于物件的行為,能作什么;而不是關注物件所屬的型別,例如,在不使用鴨子型別的語言中,我們可以撰寫一個函式,它接受一個型別為"鴨子"的物件,并呼叫它的"走"和"叫"方法,在使用鴨子型別的語言中,這樣的一個函式可以接受一個任意型別的物件,并呼叫它的"走"和"叫"方法,如果這些需要被呼叫的方法不存在,那么將引發一個運行時錯誤,任何擁有這樣的正確的"走"和"叫"方法的物件都可被函式接受的這種行為引出了以上表述,這種決定型別的方式因此得名,


image-20220404092920843

image-20220404093411195

Operator

image-20220404093727823

邏輯運算子只能給布林值用,其他編程語言可能有 0 == false,但是 go 沒有


image-20220404093915246


07-Formatted & File I/O

I/O steams

image-20220404094820605

作業系統具有三個標準 io 流,標準輸入、標準輸出、標準錯誤,它們分別可以重定向,


formatted I/O

image-20220404095050876

Println 將引數默認輸出到標準輸出流,如果會用 Fprintln 可以指定輸出到某個流,比如 os.Stderr


fmt functions

image-20220404095236836

Sprintln 格式化字串并回傳,

image-20220404095458035


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

image-20220404102923944

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 檔案,內容是 標準輸出流 的內容,

image-20220404105436335

image-20220404105445902


Always check the err

image-20220404111400635

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

image-20220405052151396

image-20220405052410972

你可以在函式體內宣告函式,但必須是匿名函式,作為一個變數


function signatures

image-20220405052615027

函式簽名指的是 函式引數型別與排列順序、函式回傳值


parameter

image-20220405052951913

image-20220405053010574

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

image-20220405054837612

go 里只有值傳遞,函式內的變數都是區域變數,它被分配、拷貝實際引數的值,假如傳入的是切片描述符,它也是被復制到區域變數里的,描述符被復制,切片底層資料沒有被復制,


returns

image-20220405055316730


Recursion

image-20220405055450508

遞回運行比迭代慢因為要創建一系列堆疊幀,


08-Defer

image-20220405055759385

image-20220405055925498

defer gotcha #1

image-20220405060115943

image-20220405060316700

Defer is based on function scope

第二個例子中,只有退出函式才會執行 defer 將會打開很多檔案導致程式崩潰,所以直接使用 f.close 關閉檔案,


defer gotcha #2

image-20220405060649324

defer 執行時,以引數實際的值拷貝傳遞進延遲函式并壓入 defer堆疊 中,而不是參考,


image-20220405060931635

當我離開函式時執行延遲堆疊,延遲的匿名函式修改回傳值 a


09-Closures

image-20220405061936164

變數的生命周期可以超過變數宣告背景關系的范圍


image-20220405062143900

image-20220405062649749

左側 f 只是函式指標,右側 f 則是閉包,注意右上角標紅的 &,閉包是參考封閉的,拿到a、b變數的參考而不是單純的值,


image-20220405064035876

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{}

image-20220405134013956

\(t\) 中的 \(addr\) 指向一個起哨兵作用的結構,所以我們知道它是空的而不是 \(nil\).

可以用 append 方法生成元素存盤地址,并回傳一個描述符參考這個存盤給 \(s\) . 即便 \(s\)\(nil\)


Empty vs nil slice

image-20220405134702334

使用 \(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

image-20220405145330189

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="https://img.uj5u.com/2022/05/19/3111311913003477.jpg"  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 =https://www.cnblogs.com/linxiaoxu/p/="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,有幾個常用的實作

io.reader

  • 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


image-20220408081816170

判斷結構體一致的條件

  • 欄位一樣,字段型別也一樣
  • 欄位按順序排列
  • 相同的欄位標簽

image-20220408081956587

紅圈用于包含一些如何以各種方式進行編碼的資訊協議,比如為 \(json\) 創建 \(key\),當我們查看 \(json\) 的作業原理時它將使用反射,


image-20220408082135061

但如果它們是一致的,可以進行強制轉換,


image-20220408082343307

需要注意的是,從 \(go\ 1.8\) 起,不同欄位標簽不阻礙型別轉換,


image-20220408082556037


Make the zero value useful

image-20220408082758958

\(nil\ [\ ]byte\) 可以使用 \(append\),當 \(buffer\) 被創建時就可以直接被使用,不需要做什么前置作業,


Empty structs

image-20220408083215121

\(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\) 靜態分析工具來看會給出一個警告,


image-20220408085123362


正則運算式參考資料

Syntax · google/re2 Wiki (github.com)

13-Regular Expressions

Simple string searches

image-20220411094554046

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

image-20220411105109285

image-20220411105124164

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

image-20220411114347646


URL re

image-20220411115840243

(?::([0-9]+))? 末尾的問號確定圓括號內的內容可以出現零次或一次,
?: 表示不被捕獲,即整個括號內容匹配了也不添加到回傳的切片里,
但內部又有一個捕獲組 ([0-9]+) 匹配出現一次或多次的數字,將被捕獲到字串放入回傳的切片中,
所以 :([0-9]+) 這一整個不會出現在切片中,而 ([0-9]+) 會出現在切片中,

image-20220411120248457

image-20220411120852695

FindStringSubmatch 只會匹配最后一個,使用FindAllStringSubmatch回傳全部匹配,切片的切片,


14-Reference & Value Semantics

Pointers vs Values

image-20220423174451039

image-20220423174951051

如果要共享變數并修改,建議統一用指標傳遞的方式,否則 \(f3\) 回傳的是原來的副本,\(f4\) 作出的修改將無法反映到 \(f1\)\(f2\) 修改的物件上,即針對一個物件的修改卻產生了兩個物件,


image-20220423175515747

Loop Gotcha

image-20220423175628859

回圈內第二個引數拿到的是副本,要在回圈內修改原切片欄位的值不能直接修改副本,需要通過索引進行修改,


image-20220423175938721

在函式內修改切片最好將切片回傳,因為修改切片很可能會導致切片描述符指向的底層陣列地址發生改變,比如 \(grow\) 擴容,


image-20220423180254232

將指標指向切片內的元素是極其危險的,當切片描述符指向的底層陣列擴容時,會導致指標指向已經過時的底層陣列,再通過指標修改元素會導致修改無效,


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\) 無法作業,所以必須給定長度,


image-20220423181837165

不要參考用于回圈的變數,在回圈內部宣告新變數將其特殊化,

image-20220423182027032

轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/477503.html

標籤:Go

上一篇:JavaScriptkeyup事件監聽器在iPhone上不起作用

下一篇:Spring Ioc原始碼分析系列--Ioc容器注冊BeanPostProcessor后置處理器以及事件訊息處理

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 【C++】Microsoft C++、C 和匯編程式檔案

    ......

    uj5u.com 2020-09-10 00:57:23 more
  • 例外宣告

    相比于斷言適用于排除邏輯上不可能存在的狀態,例外通常是用于邏輯上可能發生的錯誤。 例外宣告 Item 1:當函式不可能拋出例外或不能接受拋出例外時,使用noexcept 理由 如果不打算拋出例外的話,程式就會認為無法處理這種錯誤,并且應當盡早終止,如此可以有效地阻止例外的傳播與擴散。 示例 //不可 ......

    uj5u.com 2020-09-10 00:57:27 more
  • Codeforces 1400E Clear the Multiset(貪心 + 分治)

    鏈接:https://codeforces.com/problemset/problem/1400/E 來源:Codeforces 思路:給你一個陣列,現在你可以進行兩種操作,操作1:將一段沒有 0 的區間進行減一的操作,操作2:將 i 位置上的元素歸零。最終問:將這個陣列的全部元素歸零后操作的最少 ......

    uj5u.com 2020-09-10 00:57:30 more
  • UVA11610 【Reverse Prime】

    本人看到此題沒有翻譯,就附帶了一個自己的翻譯版本 思考 這一題,它的第一個要求是找出所有 $7$ 位反向質數及其質因數的個數。 我們應該需要質數篩篩選1~$10^{7}$的所有數,這里就不慢慢介紹了。但是,重讀題,我們突然發現反向質數都是 $7$ 位,而將它反過來后的數字卻是 $6$ 位數,這就說明 ......

    uj5u.com 2020-09-10 00:57:36 more
  • 統計區間素數數量

    1 #pragma GCC optimize(2) 2 #include <bits/stdc++.h> 3 using namespace std; 4 bool isprime[1000000010]; 5 vector<int> prime; 6 inline int getlist(int ......

    uj5u.com 2020-09-10 00:57:47 more
  • C/C++編程筆記:C++中的 const 變數詳解,教你正確認識const用法

    1、C中的const 1、區域const變數存放在堆疊區中,會分配記憶體(也就是說可以通過地址間接修改變數的值)。測驗代碼如下: 運行結果: 2、全域const變數存放在只讀資料段(不能通過地址修改,會發生寫入錯誤), 默認為外部聯編,可以給其他源檔案使用(需要用extern關鍵字修飾) 運行結果: ......

    uj5u.com 2020-09-10 00:58:04 more
  • 【C++犯錯記錄】VS2019 MFC添加資源不懂如何修改資源宏ID

    1. 首先在資源視圖中,添加資源 2. 點擊新添加的資源,復制自動生成的ID 3. 在解決方案資源管理器中找到Resource.h檔案,編輯,使用整個專案搜索和替換的方式快速替換 宏宣告 4. Ctrl+Shift+F 全域搜索,點擊查找全部,然后逐個替換 5. 為什么使用搜索替換而不使用屬性視窗直 ......

    uj5u.com 2020-09-10 00:59:11 more
  • 【C++犯錯記錄】VS2019 MFC不懂的批量添加資源

    1. 打開資源頭檔案Resource.h,在其中預先定義好宏 ID(不清楚其實ID值應該設定多少,可以先新建一個相同的資源項,再在這個資源的ID值的基礎上遞增即可) 2. 在資源視圖中選中專案資源,按F7編輯資源檔案,按 ID 型別 相對路徑的形式添加 資源。(別忘了先把檔案拷貝到專案中的res檔案 ......

    uj5u.com 2020-09-10 01:00:19 more
  • C/C++編程筆記:關于C++的參考型別,專供新手入門使用

    今天要講的是C++中我最喜歡的一個用法——參考,也叫別名。 參考就是給一個變數名取一個變數名,方便我們間接地使用這個變數。我們可以給一個變數創建N個參考,這N + 1個變數共享了同一塊記憶體區域。(參考型別的變數會占用記憶體空間,占用的記憶體空間的大小和指標型別的大小是相同的。雖然參考是一個物件的別名,但 ......

    uj5u.com 2020-09-10 01:00:22 more
  • 【C/C++編程筆記】從頭開始學習C ++:初學者完整指南

    眾所周知,C ++的學習曲線陡峭,但是花時間學習這種語言將為您的職業帶來奇跡,并使您與其他開發人員區分開。您會更輕松地學習新語言,形成真正的解決問題的技能,并在編程的基礎上打下堅實的基礎。 C ++將幫助您養成良好的編程習慣(即清晰一致的編碼風格,在撰寫代碼時注釋代碼,并限制類內部的可見性),并且由 ......

    uj5u.com 2020-09-10 01:00:41 more
最新发布
  • Rust中的智能指標:Box<T> Rc<T> Arc<T> Cell<T> RefCell<T> Weak

    Rust中的智能指標是什么 智能指標(smart pointers)是一類資料結構,是擁有資料所有權和額外功能的指標。是指標的進一步發展 指標(pointer)是一個包含記憶體地址的變數的通用概念。這個地址參考,或 ” 指向”(points at)一些其 他資料 。參考以 & 符號為標志并借用了他們所 ......

    uj5u.com 2023-04-20 07:24:10 more
  • Java的值傳遞和參考傳遞

    值傳遞不會改變本身,參考傳遞(如果傳遞的值需要實體化到堆里)如果發生修改了會改變本身。 1.基本資料型別都是值傳遞 package com.example.basic; public class Test { public static void main(String[] args) { int ......

    uj5u.com 2023-04-20 07:24:04 more
  • [2]SpinalHDL教程——Scala簡單入門

    第一個 Scala 程式 shell里面輸入 $ scala scala> 1 + 1 res0: Int = 2 scala> println("Hello World!") Hello World! 檔案形式 object HelloWorld { /* 這是我的第一個 Scala 程式 * 以 ......

    uj5u.com 2023-04-20 07:23:58 more
  • 理解函式指標和回呼函式

    理解 函式指標 指向函式的指標。比如: 理解函式指標的偽代碼 void (*p)(int type, char *data); // 定義一個函式指標p void func(int type, char *data); // 宣告一個函式func p = func; // 將指標p指向函式func ......

    uj5u.com 2023-04-20 07:23:52 more
  • Django筆記二十五之資料庫函式之日期函式

    本文首發于公眾號:Hunter后端 原文鏈接:Django筆記二十五之資料庫函式之日期函式 日期函式主要介紹兩個大類,Extract() 和 Trunc() Extract() 函式作用是提取日期,比如我們可以提取一個日期欄位的年份,月份,日等資料 Trunc() 的作用則是截取,比如 2022-0 ......

    uj5u.com 2023-04-20 07:23:45 more
  • 一天吃透JVM面試八股文

    什么是JVM? JVM,全稱Java Virtual Machine(Java虛擬機),是通過在實際的計算機上仿真模擬各種計算機功能來實作的。由一套位元組碼指令集、一組暫存器、一個堆疊、一個垃圾回收堆和一個存盤方法域等組成。JVM屏蔽了與作業系統平臺相關的資訊,使得Java程式只需要生成在Java虛擬機 ......

    uj5u.com 2023-04-20 07:23:31 more
  • 使用Java接入小程式訂閱訊息!

    更新完微信服務號的模板訊息之后,我又趕緊把微信小程式的訂閱訊息給實作了!之前我一直以為微信小程式也是要企業才能申請,沒想到小程式個人就能申請。 訊息推送平臺🔥推送下發【郵件】【短信】【微信服務號】【微信小程式】【企業微信】【釘釘】等訊息型別。 https://gitee.com/zhongfuch ......

    uj5u.com 2023-04-20 07:22:59 more
  • java -- 緩沖流、轉換流、序列化流

    緩沖流 緩沖流, 也叫高效流, 按照資料型別分類: 位元組緩沖流:BufferedInputStream,BufferedOutputStream 字符緩沖流:BufferedReader,BufferedWriter 緩沖流的基本原理,是在創建流物件時,會創建一個內置的默認大小的緩沖區陣列,通過緩沖 ......

    uj5u.com 2023-04-20 07:22:49 more
  • Java-SpringBoot-Range請求頭設定實作視頻分段傳輸

    老實說,人太懶了,現在基本都不喜歡寫筆記了,但是網上有關Range請求頭的文章都太水了 下面是抄的一段StackOverflow的代碼...自己大修改過的,寫的注釋挺全的,應該直接看得懂,就不解釋了 寫的不好...只是希望能給視頻網站開發的新手一點點幫助吧. 業務場景:視頻分段傳輸、視頻多段傳輸(理 ......

    uj5u.com 2023-04-20 07:22:42 more
  • Windows 10開發教程_編程入門自學教程_菜鳥教程-免費教程分享

    教程簡介 Windows 10開發入門教程 - 從簡單的步驟了解Windows 10開發,從基本到高級概念,包括簡介,UWP,第一個應用程式,商店,XAML控制元件,資料系結,XAML性能,自適應設計,自適應UI,自適應代碼,檔案管理,SQLite資料庫,應用程式到應用程式通信,應用程式本地化,應用程式 ......

    uj5u.com 2023-04-20 07:22:35 more