Go重新學習記錄

T

視頻連接:2020年最新 Go語言線上VIP就業班全套(價值5K)/golang開發工程師
視頻配套博客:Go語言學習之路
文檔:Golang標準庫文檔

第1天

P03~P07

安裝GO語言

下載地址:GO
下載安裝完成後,使用cmd ,輸入go version查看版本號測試是否安裝完成。
在這裏插入圖片描述

配置GO環境變量

GOROOTGOPATH都是環境變量,其中GOROOT是我們安裝go開發包的路徑,而從Go 1.8版本開始,Go開發包在安裝完成後會爲GOPATH設置一個默認目錄,
你也可以修改自己想要的目錄。配置如下
在這裏插入圖片描述
cmd輸入go env可以查看go的一些配置
在這裏插入圖片描述

GO的目錄結構

GO項目一般的目錄結構如圖
在這裏插入圖片描述
根據圖中創建bin/pkg/src/目錄。

GO的IDE

Go採用的是UTF-8編碼的文本文件存放源代碼,理論上使用任何一款文本編輯器都可以做Go語言開發,這裏推薦使用VS Code和Goland。 VS Code是微軟開源的編輯器,而Goland是jetbrains出品的付費IDE。
我們這裏使用VS Code 加插件做爲go語言的開發工具。

VSCode安裝GO語言擴展如圖:
在這裏插入圖片描述
VSCode安裝Go語言開發工具包:
首先修改Go的代理GOPROXY,使用國內代理可以快速下載,打開終端執行以下命令:

go env -w GOPROXY=https://goproxy.cn,direct

然後在VSCode中使用快捷鍵Ctrl+Shift+P,輸入go Install/Update Tools,然後全選,安裝。

執行第一個Go程序

在src目錄中創建test.com域名文件,再創建一個main.go文件,輸入以下代碼:

package main  // 聲明 main 包,表明當前是一個可執行程序
import "fmt"  // 導入內置 fmt 包

func main(){  // main函數,是程序執行的入口
	fmt.Println("Hello World!")  // 在終端打印 Hello World!
}

在終端中執行go run ./src/test.com/main.go
在這裏插入圖片描述

第2天

P08~P18

變量

變量必須聲明之後才能使用,關鍵字var
Go語言中非全局變量聲明後必須使用(不包括初始化),否則編譯失敗。
Go語言的變量聲明格式如下:

var 變量名 變量類型
var 變量名 類型 = 表達式
eg:
var name string
var isOk bool
var age int = 18//聲明的時候初始化
var age1 = 18//類型推導的方式
var name1, age2 = "Q1mi", 20//一次初始化多個變量
var (//批量聲明的方法,只能全局下用。
    a string
    b int
    c bool
)
m := 200//簡短變量聲明,只能在函數裏面使用

在使用多重賦值時,如果想要忽略某個值,可以使用匿名變量_。匿名變量用一個下劃線_表示。匿名變量不佔用命名空間,不會分配內存,所以匿名變量之間不存在重複聲明。 (在Lua等編程語言裏,匿名變量也被叫做啞元變量。)

常量

關鍵字const

const pi = 3.1415
const e = 2.7182
const (//const同時聲明多個常量時,如果省略了值則表示和上面一行的值相同。
    n1 = 100
    n2
    n3
)//常量n1、n2、n3的值都是100。

iota

iota是go語言的常量計數器,只能在常量的表達式中使用。
iota在const關鍵字出現時將被重置爲0。const中每新增一行常量聲明將使iota計數加一次(iota可理解爲const語句塊中的行索引)。 使用iota能簡化定義,在定義枚舉時很有用。

//例子1
const (
		n1 = iota //0
		n2        //1
		n3        //2
		n4        //3
)
const n5 = iota //0
//例子2,使用_跳過某些值
const (
		n1 = iota //0
		n2        //1
		_
		n4        //3
)
//例子3,iota聲明中間插隊
const (
		n1 = iota //0
		n2 = 100  //100,iota=1
		n3 = iota //2
		n4        //3
)
//例子4,<<表示左移操作。
const (
		_  = iota
		KB = 1 << (10 * iota)//1<<10表示將1的二進制表示向左移10位,也就是由1變成了10000000000,也就是十進制的1024。
		MB = 1 << (10 * iota)
		GB = 1 << (10 * iota)
		TB = 1 << (10 * iota)
		PB = 1 << (10 * iota)
)
//例子5,多個iota定義在一行
const (
		a, b = iota + 1, iota + 2 //1,2
		c, d                      //2,3
		e, f                      //3,4
)

基本數據類型

整型

整型分爲以下兩個大類: 按長度分爲:int8、int16、int32、int64 對應的無符號整型:uint8、uint16、uint32、uint64
其中,uint8就是我們熟知的byte型,int16對應C語言中的short型,int64對應C語言中的long型。

類型 描述
uint8 無符號 8位整型 (0 到 255)
uint16 無符號 16位整型 (0 到 65535)
uint32 無符號 32位整型 (0 到 4294967295)
uint64 無符號 64位整型 (0 到 18446744073709551615)
int8 有符號 8位整型 (-128 到 127)
int16 有符號 16位整型 (-32768 到 32767)
int32 有符號 32位整型 (-2147483648 到 2147483647)
int64 有符號 64位整型 (-9223372036854775808 到 9223372036854775807)

特殊整型:

類型 描述
uint 32位操作系統上就是uint32,64位操作系統上就是uint64
int 32位操作系統上就是uint32,64位操作系統上就是uint64
uintptr 無符號整型,用於存放一個指針

注意事項:
獲取對象的長度的內建len()函數返回的長度可以根據不同平臺的字節長度進行變化。實際使用中,切片或 map 的元素數量等都可以用int來表示。在涉及到二進制傳輸、讀寫文件的結構描述時,爲了保持文件的結構不會受到不同編譯目標平臺字節長度的影響,不要使用intuint

//類型推導一般是int類型。
i := 11
fmt.Printf("%T", i)//int

浮點型

Go語言支持兩種浮點型數:float32float64

複數

兩種類型:complex64complex128
複數有實部和虛部,complex64的實部和虛部爲32位,complex128的實部和虛部爲64位。

布爾值

bool

字符串

Go 語言裏的字符串的內部實現使用UTF-8編碼。
1個字符‘A’=1個字節
1個utf-8編碼的漢字‘我’=3個字節。

//定義多行的字符串,內容不轉義
s1 := `第一行
第二行
第三行
`
fmt.Println(s1)

字符串常用的方法:

方法 描述
len(str) 求長度
+或fmt.Sprintf 拼接字符串
strings.Split 分割
strings.contains 判斷是否包含
strings.HasPrefix,strings.HasSuffix 前綴/後綴判斷
strings.Index(),strings.LastIndex() 子串出現的位置
strings.Join(a[]string, sep string) join操作

fmt使用總結:

func main() { // main函數,是程序執行的入口
	i := 11
	fmt.Printf("%T\n", i) //類型int
	fmt.Printf("%v\n", i) //11
	fmt.Printf("%b\n", i) //二進制1011
	fmt.Printf("%d\n", i) //十進制11
	fmt.Printf("%o\n", i) //八進制13
	fmt.Printf("%x\n", i) //十六進制b
	s := "hello"
	fmt.Printf("%s\n", s) //輸出字符串。
}

byte和rune類型

Go 語言的字符有以下兩種類型:
byte 型,或者叫uint8類型 ,代表了ASCII碼的一個字符。
rune類型,代表一個 UTF-8字符。
當需要處理中文、日文或者其他複合字符時,則需要用到rune類型。rune類型實際是一個int32。

// 遍歷字符串
func main() {
	s := "hello沙河"
	for i := 0; i < len(s); i++ { //byte
		fmt.Printf("%v(%c) ", s[i], s[i])
	}
	fmt.Println()
	for _, r := range s { //rune
		fmt.Printf("%v(%c) ", r, r)
	}
	fmt.Println()
}
//輸出結果,所以有中文的字符串用rune類型切割纔不會亂碼。
//104(h) 101(e) 108(l) 108(l) 111(o) 230(æ) 178(²) 153() 230(æ) 178(²) 179(³)
//104(h) 101(e) 108(l) 108(l) 111(o) 27801(沙) 27827(河)

修改字符串
要修改字符串,需要先將其轉換成[]rune或[]byte,完成後再轉換爲string。無論哪種轉換,都會重新分配內存,並複製字節數組。

func changeString() {
	s1 := "big"
	// 強制類型轉換
	byteS1 := []byte(s1)
	byteS1[0] = 'p'
	fmt.Println(string(byteS1))

	s2 := "白蘿蔔"
	runeS2 := []rune(s2)
	runeS2[0] = '紅'
	fmt.Println(string(runeS2))
}

流程控制

if

if格式如下:

if 表達式1 {
    分支1
} else if 表達式2 {
    分支2
} else{
    分支3
}

for

for格式如下:

for 初始語句;條件表達式;結束語句{
    循環體語句
}
for {//死循環
    循環體語句
}

for range

for range也叫鍵值循環
Go語言中可以使用for range遍歷數組、切片、字符串、map 及通道(channel)。 通過for range遍歷的返回值有以下規律:

  • 數組、切片、字符串返回索引和值。
  • map返回鍵和值。
  • 通道(channel)只返回通道內的值。

switch case

//例子1
func testSwitch3() {
	switch n := 7; n {
	case 1, 3, 5, 7, 9:
		fmt.Println("奇數")
	case 2, 4, 6, 8:
		fmt.Println("偶數")
	default:
		fmt.Println(n)
	}
}
//例子2
func switchDemo4() {
	age := 30
	switch {
	case age < 25:
		fmt.Println("好好學習吧")
	case age > 25 && age < 35:
		fmt.Println("好好工作吧")
	case age > 60:
		fmt.Println("好好享受吧")
	default:
		fmt.Println("活着真好")
	}
}
//例子3
func switchDemo5() {
	s := "a"
	switch {
	case s == "a":
		fmt.Println("a")
		fallthrough//fallthrough語法可以執行滿足條件的case的下一個case,是爲了兼容C語言中的case設計的。不推薦使用
	case s == "b":
		fmt.Println("b")
	case s == "c":
		fmt.Println("c")
	default:
		fmt.Println("...")
	}
}
//輸出結果:
//a
//b

goto

goto語句通過標籤進行代碼間的無條件跳轉。goto語句可以在快速跳出循環、避免重複退出上有一定的幫助。(不推薦使用,瞭解就行了)

func gotoDemo2() {
	for i := 0; i < 10; i++ {
		for j := 0; j < 10; j++ {
			if j == 2 {
				// 設置退出標籤
				goto breakTag//跳出兩層循環。
			}
			fmt.Printf("%v-%v\n", i, j)
		}
	}
	return
	// 標籤
breakTag:
	fmt.Println("結束for循環")
}

break

常規

continue

常規

第3天

P19~P23

數組

語法:

// 定義一個長度爲3元素類型爲int的數組a
var 數組變量名 [元素數量]T
//例子
var a [3]int
var numArray = [3]int{1, 2}//使用指定的初始值完成初始化
var cityArray = [...]string{"北京", "上海", "深圳"} //讓編譯器根據初始值的個數自行推斷數組的長度
a := [...]int{1: 1, 3: 5}//指定索引值的方式來初始化數組,結果:[0 1 0 5]
a := [...][2]string{//二維數組的遍歷,多維數組只有第一層可以使用...來讓編譯器推導數組長度
		{"北京", "上海"},
		{"廣州", "深圳"},
		{"成都", "重慶"},
	}

數組的遍歷

func main() {
	var a = [...]string{"北京", "上海", "深圳"}
	// 方法1:for循環遍歷
	for i := 0; i < len(a); i++ {
		fmt.Println(a[i])
	}
	// 方法2:for range遍歷
	for index, value := range a {
		fmt.Println(index, value)
	}
}

切片

切片(Slice)是一個擁有相同類型元素的可變長度的序列。
切片是一個引用類型,它的內部結構包含地址長度容量底層數組的值發生改變,那麼這個切片也隨之改變。
語法如下:

var name []T
make([]T, size, cap)//make函數創建切片
//例子
var a []string              //聲明一個字符串切片
var b = []int{}             //聲明一個整型切片並初始化
var c = []bool{false, true} //聲明一個布爾切片並初始化
var d = make([]int, 5, 10)  //聲明一個長度5,容量10的切片。
var e = make([]int, 5)      //聲明一個長度5,容量5的切片。

切片的本質就是對底層數組的封裝,所以我們可以切割數組來得到切片。切片包含了三個信息:底層數組的指針、切片的長度(len)和切片的容量(cap)。內置的len()函數求長度。內置的cap()函數求切片的容量。
切片的長度就是它元素的個數。
切片的容量就是底層數組從切片的第一個元素到最後一個元素的數量。
例子圖解:
在這裏插入圖片描述

a := [...]int{1, 3, 5, 7, 9, 11, 13}
a1 := a[2:4]//[5,7]基於數組切割,左閉右開。
a2 := a[:4]//=[0:4] [1,3,5,7],長度是4,容量是7
a3 := a[3:]//=[3:len(a)] [7,9,11,13],長度是4,容量是4,
a4 := a[:]//=[0,len(a)]

第4天

P24~P41

指針

區別於C/C++中的指針,Go語言中的指針不能進行偏移和運算,是安全指針。
記住兩個符號:&(取地址)*(根據地址取值)

func main() {
	//指針取值
	a := 10
	b := &a // 取變量a的地址,將指針保存到b中
	fmt.Printf("type of b:%T\n", b)
	c := *b // 指針取值(根據指針去內存取值)
	fmt.Printf("type of c:%T\n", c)
	fmt.Printf("value of c:%v\n", c)
}
輸出:
type of b:*int
type of c:int
value of c:10

map

有點類似C#的字典。
Go語言中提供的映射關係容器爲map,其內部使用散列表(hash)實現。
map是一種無序的基於key-value的數據結構,Go語言中的map是引用類型,必須初始化才能使用。
語法:

map[KeyType]ValueType
make(map[KeyType]ValueType, [cap])//map類型的變量默認初始值爲nil,需要使用make()函數來分配內存。預估好cap,避免再次擴容。
例子:
func main() {
	scoreMap := make(map[string]int, 8)
	scoreMap["張三"] = 90
	scoreMap["小明"] = 100
	fmt.Println(scoreMap)
	fmt.Println(scoreMap["小明"])
	fmt.Printf("type of a:%T\n", scoreMap)
	
	//map也支持在聲明的時候填充元素,例如:
	userInfo := map[string]string{
		"username": "沙河小王子",
		"password": "123456",
	}
}
輸出:
map[小明:100 張三:90]
100
type of a:map[string]int

判斷map中鍵是否存在,語法例子如下:

value, ok := map[key]
例子:
func main() {
	scoreMap := make(map[string]int)
	scoreMap["張三"] = 90
	scoreMap["小明"] = 100
	// 如果key存在ok爲true,v爲對應的值;不存在ok爲false,v爲值類型的零值
	v, ok := scoreMap["張三"]
	if ok {
		fmt.Println(v)
	} else {
		fmt.Println("查無此人")
	}
}

map的遍歷
for range 遍歷map。

func main() {
	scoreMap := make(map[string]int)
	scoreMap["張三"] = 90
	scoreMap["小明"] = 100
	scoreMap["娜扎"] = 60
	for k, v := range scoreMap {
		fmt.Println(k, v)
	}
}

刪除map
使用delete()內建函數從map中刪除一組鍵值對,delete()函數的格式如下:

delete(map, key)
例子:
func main(){
	scoreMap := make(map[string]int)
	scoreMap["張三"] = 90
	scoreMap["小明"] = 100
	scoreMap["娜扎"] = 60
	delete(scoreMap, "小明")//將小明:100從map中刪除
	for k,v := range scoreMap{
		fmt.Println(k, v)
	}
}

函數

語法:

func 函數名(參數 類型)(返回值){
    函數體
}
例子:
//有參數有返回值
//命名的返回值就相當於在函數中聲明瞭一個變量。
func sum(x int,y int)(ret int){
	ret = x + y
	return//使用命名返回值可以省略。
}
//沒有返回值
func f1(x int,y int){
	fmt.Println(x + y)
}
//沒有參數但有返回值
func f2() int{
	return 3
}
//沒有參數沒有返回值
func f3(){
}
//參數類型簡寫
//當參數中連續多個參數的類型一致時,前面一致的都可以忽略。
func f4(x, y int, m, n string){
}
//可變長度參數,必須在參數最後
func f5(x string, y ...int)//y類型是切片 []int
{
}
//調用
f5("xxx", 1, 2, 3)

函數作爲參數

func add(x, y int) int {
	return x + y
}
func calc(x, y int, op func(int, int) int) int {
	return op(x, y)
}
func main() {
	ret2 := calc(10, 20, add)
	fmt.Println(ret2) //30
}

函數作爲返回值

func do(s string) (func(int, int) int, error) {
	switch s {
	case "+":
		return add, nil
	case "-":
		return sub, nil
	default:
		err := errors.New("無法識別的操作符")
		return nil, err
	}
}

匿名函數

函數內部不能定義函數但可以定義匿名函數。匿名函數因爲沒有函數名,所以沒辦法像普通函數那樣調用,所以匿名函數需要保存到某個變量或者作爲立即執行函數

func(參數)(返回值){
    函數體
}
例子
func main() {
	// 將匿名函數保存到變量
	add := func(x, y int) {
		fmt.Println(x + y)
	}
	add(10, 20) // 通過變量調用匿名函數

	//自執行函數:匿名函數定義完加()直接執行
	func(x, y int) {
		fmt.Println(x + y)
	}(10, 20)
}

閉包

閉包指的是一個函數和與其相關的引用環境組合而成的實體。簡單來說,閉包=函數+引用環境

//例子1:
func adder2(x int) func(int) int {
	return func(y int) int {
		x += y
		return x
	}
}
func main() {
	//變量f是一個函數並且它引用了其外部作用域中的x變量,此時f就是一個閉包。 在f的生命週期內,變量x也一直有效。
	var f = adder2(10)
	fmt.Println(f(10)) //20
	fmt.Println(f(20)) //40
	fmt.Println(f(30)) //70

	f1 := adder2(20)
	fmt.Println(f1(40)) //60
	fmt.Println(f1(50)) //110
}
//例子2:
func makeSuffixFunc(suffix string) func(string) string {
	return func(name string) string {
		if !strings.HasSuffix(name, suffix) {
			return name + suffix
		}
		return name
	}
}
func main() {
	jpgFunc := makeSuffixFunc(".jpg")
	txtFunc := makeSuffixFunc(".txt")
	fmt.Println(jpgFunc("test")) //test.jpg
	fmt.Println(txtFunc("test")) //test.txt
}
//例子3:
func calc(base int) (func(int) int, func(int) int) {
	add := func(i int) int {
		base += i
		return base
	}
	sub := func(i int) int {
		base -= i
		return base
	}
	return add, sub
}
func main() {
	f1, f2 := calc(10)
	fmt.Println(f1(1), f2(2)) //11 9
	fmt.Println(f1(3), f2(4)) //12 8
	fmt.Println(f1(5), f2(6)) //13 7
}

defer

Go語言中的defer語句會將其後面跟隨的語句進行延遲處理。在defer歸屬的函數即將返回時,將延遲處理的語句按defer定義的逆序進行執行,也就是說,先被defer的語句最後被執行,最後被defer的語句,最先被執行
Go語言中函數的return不是原子操作,底層分兩步執行。第一步將結果賦值給返回值,第二步真正的return返回值。而defer的執行在這兩者之間

func f1() int {
	x := 5
	defer func() {
		x++  //修改的是x而不是返回值
	}()
	return x
}
結果:5
func f2() (x int) {//x就是返回值
	defer func() {
		x++
	}()
	return 5
}
結果:6

內置函數

一些go自帶的函數

append()

用來追加元素到數組、slice中。
Go語言的內建函數append()可以爲切片動態添加元素。 可以一次添加一個元素,可以添加多個元素,也可以添加另一個切片中的元素(後面加…)。
append()函數將元素追加到切片的最後並返回該切片。當追加後的長度大於原來底層數組的容量時,則該切片會進行擴容,擴容的規則如下

  • 首先判斷,如果新申請容量(cap)大於2倍的舊容量(old.cap),最終容量(newcap)就是新申請的容量(cap)。
  • 否則判斷,如果舊切片的長度小於1024,則最終容量(newcap)就是舊容量(old.cap)的兩倍,即(newcap=doublecap),
  • 否則判斷,如果舊切片長度大於等於1024,則最終容量(newcap)從舊容量(old.cap)開始循環增加原來的1/4,即(newcap=old.cap,for {newcap += newcap/4})直到最終容量(newcap)大於等於新申請的容量(cap),即(newcap >= cap)
  • 如果最終容量(cap)計算值溢出,則最終容量(cap)就是新申請容量(cap)。
func main(){
	var s []int
	s = append(s, 1)        // [1]
	s = append(s, 2, 3, 4)  // [1 2 3 4]
	s2 := []int{5, 6, 7}  
	s = append(s, s2...)    // [1 2 3 4 5 6 7](...表示將s2拆開)
}

copy()

Go語言內建的copy()函數可以迅速地將一個切片的數據複製到另外一個切片空間中。

func main() {
	// copy()複製切片
	a := []int{1, 2, 3, 4, 5}
	c := make([]int, 5, 5)
	copy(c, a)     //使用copy()函數將切片a中的元素複製到切片c
	fmt.Println(a) //[1 2 3 4 5]
	fmt.Println(c) //[1 2 3 4 5]
	c[0] = 1000
	fmt.Println(a) //[1 2 3 4 5]
	fmt.Println(c) //[1000 2 3 4 5]
}

new()

用來分配內存,主要用來分配值類型,比如int、struct。返回的是指針。
項目中用的比較少

func main() {
	a := new(int)
	b := new(bool)
	fmt.Printf("%T\n", a) // *int
	fmt.Printf("%T\n", b) // *bool
	fmt.Println(*a)       // 0
	fmt.Println(*b)       // false
}

make()

用來分配內存,主要用來分配引用類型,比如chan、map、slice。
new和make的區別:

  • 二者都是用來做內存分配的。
  • make只用於slice、map以及channel的初始化,返回的還是這三個引用類型本身;
  • 而new用於類型的內存分配,並且內存對應的值爲類型零值,返回的是指向類型的指針。

panic和recover

用來做錯誤處理。
recover()必須搭配defer使用。
defer一定要在可能引發panic的語句之前定義。

func funcA() {
	fmt.Println("func A")
}

func funcB() {
	defer func() {
		err := recover()
		//如果程序出出現了panic錯誤,可以通過recover恢復過來
		if err != nil {
			fmt.Println("recover in B")
		}
	}()
	panic("panic in B")
}
func funcC() {
	fmt.Println("func C")
}
func main() {
	funcA()
	funcB()
	funcC()
}

第5天

P42~60

標準庫

fmt

標準庫fmt提供了以下幾種輸出相關函數。
Print

func Print(a ...interface{}) (n int, err error)//普通輸出
func Printf(format string, a ...interface{}) (n int, err error)//佔位符
func Println(a ...interface{}) (n int, err error)//換行

Scan
輸入,跟Print相反

func Scan(a ...interface{}) (n int, err error)
func Scanf(format string, a ...interface{}) (n int, err error)
func Scanln(a ...interface{}) (n int, err error)

Sprint
傳入的數據生成並返回一個字符串。

func Sprint(a ...interface{}) string
func Sprintf(format string, a ...interface{}) string
func Sprintln(a ...interface{}) string
例子:
s2 := fmt.Sprintf("name:%s,age:%d", name, age)
s3 := fmt.Sprintln("沙河小王子")

Errorf
根據format參數生成格式化字符串並返回一個包含該字符串的錯誤。

func Errorf(format string, a ...interface{}) error
例子:
e := errors.New("原始錯誤e")
w := fmt.Errorf("Wrap了一個錯誤%w", e)

os

主要用於文件操作
打開文件的模式有以下幾種:

模式 含義
os.O_WRONLY 只寫
os.O_CREATE 創建文件
os.O_RDONLY 只讀
os.O_RDWR 讀寫
os.O_TRUNC 清空
os.O_APPEND 追加
//引入
import (
	"os"
	"io"
	"io/ioutil"
)
//讀取操作
file, err := os.Open("./main.go")//打開文件,只能讀
var tmp = make([]byte, 128)
n, err := file.Read(tmp)
//寫入操作
file, err := os.OpenFile("xx.txt", os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0666) //打開文件,能讀能寫
file.Write([]byte(str))       //寫入字節切片數據
file.WriteString("hello 小王子") //直接寫入字符串數據

//bufio是在file的基礎上封裝了一層API,支持更多的功能。
//bufio讀取操作
file, err := os.Open("./xx.txt")
reader := bufio.NewReader(file)
//bufio寫入操作
file, err := os.OpenFile("xx.txt", os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0666)
writer := bufio.NewWriter(file)
writer.WriteString("hello沙河\n") //將數據先寫入緩存
writer.Flush() //將緩存中的內容寫入文件

//io/ioutil包的ReadFile方法能夠讀取完整的文件,只需要將文件名作爲參數傳入。
//ioutil讀取操作
content, err := ioutil.ReadFile("./main.go")
//ioutil寫入操作
str := "hello 沙河"
err := ioutil.WriteFile("./xx.txt", []byte(str), 0666)

//文件操作完成後記得關閉文件。通常使用defer註冊文件關閉語句。
defer file.Close()

time

time.Duration是time包定義的一個類型,它代表兩個時間點之間經過的時間,以納秒爲單位。time.Duration表示一段時間間隔,可表示的最長時間段大約290年。
//時間內置的一些常量。
const (
    Nanosecond  Duration = 1
    Microsecond          = 1000 * Nanosecond
    Millisecond          = 1000 * Microsecond
    Second               = 1000 * Millisecond
    Minute               = 60 * Second
    Hour                 = 60 * Minute
)
例如:
time.Second表示1s
time.Duration表示1納秒
引入
import (
	"time"
)
now := time.Now() //獲取當前時間
year := now.Year()     //年
month := now.Month()   //月
day := now.Day()       //日
hour := now.Hour()     //小時
minute := now.Minute() //分鐘
second := now.Second() //秒
timestamp1 := now.Unix()     //時間戳
timestamp2 := now.UnixNano() //納秒時間戳
timeObj := time.Unix(timestamp1, 0) //將時間戳轉爲時間格式
later := now.Add(24*time.Hour) // 當前時間加24小時後的時間
d := now.Sub(later)//now減later的時間間隔,返回的是Duration類型。
now.Format("2006-01-02 15:04:05.000 Mon Jan")//24小時制格式化。格式化的模板爲Go的出生時間2006年1月2號15點04分 Mon Jan
now.Format("2006-01-02 03:04:05.000 PM Mon Jan")//指定PM表示12小時制格式化。
newTime,err := time.Parse("2006-01-02","2020-08-03")//字符串轉時間。
// 加載時區
loc, err := time.LoadLocation("Asia/Shanghai")//上海時區
// 按照指定時區和指定格式解析字符串時間
timeObj, err := time.ParseInLocation("2006/01/02 15:04:05", "2019/08/04 14:15:20", loc)

//定時器time.Tick(時間間隔)
func tickDemo() {
	ticker := time.Tick(time.Second) //定義一個1秒間隔的定時器
	for i := range ticker {
		fmt.Println(i)//每秒都會執行的任務
	}
}

strconv

strconv包實現了基本數據類型和其字符串表示的相互轉換。

//Atoi()函數用於將字符串類型的整數轉換爲int類型
s1 := "100"
i1, err := strconv.Atoi(s1)
if err != nil {
	fmt.Println("can't convert to int")
} else {
	fmt.Printf("type:%T value:%#v\n", i1, i1) //type:int value:100
}
//Itoa()函數用於將int類型數據轉換爲對應的字符串表示
i2 := 200
s2 := strconv.Itoa(i2)
fmt.Printf("type:%T value:%#v\n", s2, s2) //type:string value:"200"


//以下方法將字符串轉化爲bool,float,int,uint類型
//這些函數都有兩個返回值,第一個返回值是轉換後的值,第二個返回值爲轉化失敗的錯誤信息。
b, err := strconv.ParseBool("true")
f, err := strconv.ParseFloat("3.1415", 64)
i, err := strconv.ParseInt("-2", 10, 64)//後兩個參數表示10進制64位
u, err := strconv.ParseUint("2", 10, 64)

類型別名和自定義類型

Go語言中可以使用type關鍵字來定義自定義類型。

//將MyInt定義爲int類型
type MyInt int
//類型別名
type TypeAlias = Type

結構體

Go語言中沒有“類”的概念,也不支持“類”的繼承等面向對象的概念。Go語言中通過結構體的內嵌再配合接口比面向對象具有更高的擴展性和靈活性。
結構體中字段大寫開頭表示可公開訪問,小寫表示私有(僅在定義當前結構體的包中可訪問)。
類似C#的結構體,也是值類型。

type 類型名 struct {
    字段名 字段類型
    字段名 字段類型
    …
}
例子:
type person struct {
	name string
	city string
	age  int8
}
func main() {
	var p1 person
	p1.name = "沙河娜扎"
	p1.city = "北京"
	p1.age = 18
	fmt.Printf("p1=%v\n", p1)  //p1={沙河娜扎 北京 18}
	fmt.Printf("p1=%#v\n", p1) //p1=main.person{name:"沙河娜扎", city:"北京", age:18}
}

匿名結構體

package main
     
import (
    "fmt"
)  
func main() {
    var user struct{Name string; Age int}
    user.Name = "小王子"
    user.Age = 18
    fmt.Printf("%#v\n", user)
}

new初始化結構體

var p2 = new(person)
p2.name = "小王子"
p2.age = 28
p2.city = "上海"
fmt.Printf("p2=%#v\n", p2) //p2=&main.person{name:"小王子", city:"上海", age:28}

取結構體的地址實例化

使用&對結構體進行取地址操作相當於對該結構體類型進行了一次new實例化操作。

p3 := &person{}
fmt.Printf("%T\n", p3)     //*main.person
fmt.Printf("p3=%#v\n", p3) //p3=&main.person{name:"", city:"", age:0}
p3.name = "七米"
p3.age = 30
p3.city = "成都"
fmt.Printf("p3=%#v\n", p3) //p3=&main.person{name:"七米", city:"成都", age:30}

使用鍵值對初始化

p5 := person{
	name: "小王子",
	city: "北京",
	age:  18,
}
fmt.Printf("p5=%#v\n", p5) //p5=main.person{name:"小王子", city:"北京", age:18}
p6 := &person{
	name: "小王子",
	city: "北京",
	age:  18,
}
fmt.Printf("p6=%#v\n", p6) //p6=&main.person{name:"小王子", city:"北京", age:18}

構造函數

type person struct {
	name string
	city string
	age  int8
}
//構造函數:約定俗成用new開頭
//但結構體比較大的時候最好使用結構體指針,減少程序內存開銷
func newPerson(name, city string, age int8) *person {
	return &person{
		name: name,
		city: city,
		age:  age,
	}
}

方法和接收者

Go語言中的方法(Method)是一種作用於特定類型變量的函數。這種特定類型變量叫做接收者(Receiver)。接收者的概念就類似於其他語言中的this或者 self。

func (接收者變量 接收者類型) 方法名(參數列表) (返回參數) {
    函數體
}
例子:
//Person 結構體
type Person struct {
	name string
	age  int8
}

//NewPerson 構造函數
func NewPerson(name string, age int8) *Person {
	return &Person{
		name: name,
		age:  age,
	}
}

//只有Person類型能調用。值接受者:傳拷貝進去
func (p Person) Dream() {
	fmt.Printf("%s的夢想是學好Go語言!\n", p.name)
}
// SetAge 設置p的年齡。指針接受者:傳內存進去
func (p *Person) SetAge(newAge int8) {
	p.age = newAge
}

func main() {
	p1 := NewPerson("小王子", 25)
	p1.Dream()
	p1.SetAge(22)
}

結構體的匿名字段

不常用,瞭解下

//Person 結構體Person類型
type Person struct {
	string//匿名字段,唯一
	int
}

func main() {
	p1 := Person{
		"小王子",
		18,
	}
	fmt.Printf("%#v\n", p1)        //main.Person{string:"北京", int:18}
	fmt.Println(p1.string, p1.int) //北京 18
}

匿名嵌套結構體

//Address 地址結構體
type Address struct {
	Province string
	City     string
}
//User 用戶結構體
type User struct {
	Name    string
	Gender  string
	Address //匿名結構體
}
func main() {
	var user2 User
	user2.Name = "小王子"
	user2.Gender = "男"
	user2.Address.Province = "山東"    //通過匿名結構體.字段名訪問
	user2.City = "威海"                //直接訪問匿名結構體的字段名
	fmt.Printf("user2=%#v\n", user2) //user2=main.User{Name:"小王子", Gender:"男", Address:main.Address{Province:"山東", City:"威海"}}
}

結構體的“繼承”

利用匿名嵌套結構體實現繼承

//Animal 動物
type Animal struct {
	name string
}
func (a *Animal) move() {
	fmt.Printf("%s會動!\n", a.name)
}

//Dog 狗
type Dog struct {
	Feet    int8
	*Animal //通過嵌套匿名結構體實現繼承
}
func (d *Dog) wang() {
	fmt.Printf("%s會汪汪汪~\n", d.name)
}

func main() {
	d1 := &Dog{
		Feet: 4,
		Animal: &Animal{ //注意嵌套的是結構體指針
			name: "樂樂",
		},
	}
	d1.wang() //樂樂會汪汪汪~
	d1.move() //樂樂會動!
}

結構體與JSON序列化

//JSON序列化:結構體-->JSON格式的字符串
data, err := json.Marshal(c)//data是序列化後的json,err是報錯信息,c是結構體
//JSON反序列化:JSON格式的字符串-->結構體
err = json.Unmarshal([]byte(str), c)//err是報錯信息,str是json,c是結構體。要一一對應

結構體標籤

反引號包裹起來

//Student 學生
type Student struct {
	ID     int    `json:"id"` //通過指定tag實現json序列化該字段時的key
	Gender string //json序列化是默認使用字段名作爲key
	name   string //私有不能被json包訪問
}

第6天

P61~P80

接口

在Go語言中接口(interface)是一種類型,一種抽象的類型。
如果一個變量實現了接口中的所有方法,那麼這個變量就可以稱爲這個接口的變量。

type 接口類型名 interface{
    方法名1( 參數列表1 ) 返回值列表1
    方法名2( 參數列表2 ) 返回值列表2}
例子:多個類型實現同一接口,一個類型實現多個接口。
// Sayer 接口
type Sayer interface {
	say()
}
type Mover interface{
	move()
}
//定義兩個結構體
type dog struct {}
type cat struct {}
//兩個結構體實現Sayer接口的方法(多個類型實現同一接口)
// cat實現了Sayer接口
func (c cat) say() {
	fmt.Println("喵喵喵")
}
// dog實現了Sayer接口
func (d dog) say() {
	fmt.Println("汪汪汪")
}
// dog還實現Mover接口(一個類型實現多個接口)
func (d dog) move() {
	fmt.Printf("會動")
}
//調用
func main() {
	var x Sayer // 聲明一個Sayer類型的變量x
	var y Mover // 聲明一個Mover類型的變量y
	a := cat{}  // 實例化一個cat
	b := dog{}  // 實例化一個dog
	x = a       // 可以把cat實例直接賦值給x
	x.say()     // 喵喵喵,此時x的類型爲cat
	x = b       // 可以把dog實例直接賦值給x
	x.say()     // 汪汪汪,此時x的類型爲dog
	y = b       // 可以把dog實例直接賦值給y
	y.move()    // 會動,此時y的類型爲dog
}

使用值接收者實現接口與使用指針接收者實現接口的區別?

  • 前者,結構體類型和結構體指針類型的變量都能接收。
  • 後者,只能接收結構體指針類型的變量。

接口嵌套

// Sayer 接口
type Sayer interface {
	say()
}
// Mover 接口
type Mover interface {
	move()
}
// 接口嵌套,嵌套了Sayer和Mover的接口
type animal interface {
	Sayer
	Mover
}
type cat struct {
	name string
}
func (c cat) say() {
	fmt.Println("喵喵喵")
}
func (c cat) move() {
	fmt.Println("貓會動")
}
func main() {
	var x animal
	x = cat{name: "花花"}
	x.move()
	x.say()
}

空接口

空接口是指沒有定義任何方法的接口。因此任何類型都實現了空接口。
空接口類型的變量可以存儲任意類型的變量。

func main() {
	// 定義一個空接口x
	var x interface{}
	s := "Hello 沙河"
	x = s
	fmt.Printf("type:%T value:%v\n", x, x)
	i := 100
	x = i
	fmt.Printf("type:%T value:%v\n", x, x)
	b := true
	x = b
	fmt.Printf("type:%T value:%v\n", x, x)
}
// 空接口作爲函數參數
func justifyType(x interface{}) {
	switch v := x.(type) {//類型斷言,可以得到空接口接收的值的具體類型。
	case string:
		fmt.Printf("x is a string,value is %v\n", v)
	case int:
		fmt.Printf("x is a int is %v\n", v)
	case bool:
		fmt.Printf("x is a bool is %v\n", v)
	default:
		fmt.Println("unsupport type!")
	}
}
// 空接口作爲map值
func main() {
	var studentInfo = make(map[string]interface{})
	studentInfo["name"] = "沙河娜扎"
	studentInfo["age"] = 18
	studentInfo["married"] = false
	fmt.Println(studentInfo)
}

在Go語言中只需要將標識符的首字母大寫就可以讓標識符對外可見了。
語法:

//定義包
package 包名
//包的導入
import "包的路徑"
//或
import 別名 "包的路徑"

在Go語言程序執行時導入包語句會自動觸發包內部init()函數的調用。需要注意的是:init()函數沒有參數也沒有返回值。 init()函數在程序運行時自動被調用執行,不能在代碼中主動調用它。
在運行時,被最後導入的包會最先初始化並調用其init()函數

第7天

P81~P110

反射

reflect包提供了reflect.TypeOfreflect.ValueOf兩個函數來獲取任意對象的ValueType

通過放射獲取類型

package main
import (
	"fmt"
	"reflect"
)
func reflectType(x interface{}) {
	v := reflect.TypeOf(x)
	fmt.Printf("type:%v\n", v)
}
func main() {
	var a float32 = 3.14
	reflectType(a) // type:float32
	var b int64 = 100
	reflectType(b) // type:int64
}

在反射中關於類型還劃分爲兩種:類型(Type)種類(Kind)。種類(Kind)是指底層的類型。

//例子:我們定義了兩個指針類型和兩個結構體類型,通過反射查看它們的類型和種類。
package main
import (
	"fmt"
	"reflect"
)
type myInt int64
func reflectType(x interface{}) {
	t := reflect.TypeOf(x)
	//Go語言的反射中像數組、切片、Map、指針等類型的變量,它們的.Name()都是返回空。
	fmt.Printf("type:%v kind:%v\n", t.Name(), t.Kind())
}
func main() {
	var a *float32 // 指針
	var b myInt    // 自定義類型
	var c rune     // 類型別名
	reflectType(a) // type: kind:ptr
	reflectType(b) // type:myInt kind:int64
	reflectType(c) // type:int32 kind:int32
	type person struct {
		name string
		age  int
	}
	type book struct{ title string }
	var d = person{
		name: "沙河小王子",
		age:  18,
	}
	var e = book{title: "《跟小王子學Go語言》"}
	reflectType(d) // type:person kind:struct
	reflectType(e) // type:book kind:struct
}

通過放射獲取值

func reflectValue(x interface{}) {
	v := reflect.ValueOf(x)
	k := v.Kind()
	switch k {
	case reflect.Int64:
		// v.Int()從反射中獲取整型的原始值,然後通過int64()強制類型轉換
		fmt.Printf("type is int64, value is %d\n", int64(v.Int()))
	case reflect.Float32:
		// v.Float()從反射中獲取浮點型的原始值,然後通過float32()強制類型轉換
		fmt.Printf("type is float32, value is %f\n", float32(v.Float()))
	case reflect.Float64:
		// v.Float()從反射中獲取浮點型的原始值,然後通過float64()強制類型轉換
		fmt.Printf("type is float64, value is %f\n", float64(v.Float()))
	}
}
func main() {
	var a float32 = 3.14
	var b int64 = 100
	reflectValue(a) // type is float32, value is 3.140000
	reflectValue(b) // type is int64, value is 100
	// 將int類型的原始值轉換爲reflect.Value類型
	c := reflect.ValueOf(10)
	fmt.Printf("type c :%T\n", c) // type c :reflect.Value
}

通過反射設置變量的值

反射中使用專有的Elem()方法來獲取指針對應的值。SetInt()來設置值。

package main
import (
	"fmt"
	"reflect"
)
func reflectSetValue1(x interface{}) {
	v := reflect.ValueOf(x)
	if v.Kind() == reflect.Int64 {
		v.SetInt(200) //修改的是副本,reflect包會引發panic
	}
}
func reflectSetValue2(x interface{}) {
	v := reflect.ValueOf(x)
	// 反射中使用 Elem()方法獲取指針對應的值
	if v.Elem().Kind() == reflect.Int64 {
		v.Elem().SetInt(200)
	}
}
func main() {
	var a int64 = 100
	// reflectSetValue1(a) //panic: reflect: reflect.Value.SetInt using unaddressable value
	reflectSetValue2(&a)
	fmt.Println(a)
}

isNil()和isValid()

IsNil()常被用於判斷指針是否爲空;IsValid()常被用於判定返回值是否有效。

func main() {
	// *int類型空指針
	var a *int
	fmt.Println("var a *int IsNil:", reflect.ValueOf(a).IsNil())
	// nil值
	fmt.Println("nil IsValid:", reflect.ValueOf(nil).IsValid())
	// 實例化一個匿名結構體
	b := struct{}{}
	// 嘗試從結構體中查找"abc"字段
	fmt.Println("不存在的結構體成員:", reflect.ValueOf(b).FieldByName("abc").IsValid())
	// 嘗試從結構體中查找"abc"方法
	fmt.Println("不存在的結構體方法:", reflect.ValueOf(b).MethodByName("abc").IsValid())
	// map
	c := map[string]int{}
	// 嘗試從map中查找一個不存在的鍵
	fmt.Println("map中不存在的鍵:", reflect.ValueOf(c).MapIndex(reflect.ValueOf("娜扎")).IsValid())
}

結構體反射

任意值通過reflect.TypeOf()獲得反射對象信息後,如果它的類型是結構體,可以通過反射值對象(reflect.Type)的NumField()Field()方法獲得結構體成員的詳細信息。

type student struct {
	Name  string `json:"name"`
	Score int    `json:"score"`
}
func main() {
	stu1 := student{
		Name:  "小王子",
		Score: 90,
	}
	t := reflect.TypeOf(stu1)
	fmt.Println(t.Name(), t.Kind()) // student struct
	// 通過for循環遍歷結構體的所有字段信息
	for i := 0; i < t.NumField(); i++ {
		field := t.Field(i)
		fmt.Printf("name:%s index:%d type:%v json tag:%v\n", field.Name, field.Index, field.Type, field.Tag.Get("json"))
	}
	// 通過字段名獲取指定結構體字段信息
	if scoreField, ok := t.FieldByName("Score"); ok {
		fmt.Printf("name:%s index:%d type:%v json tag:%v\n", scoreField.Name, scoreField.Index, scoreField.Type, scoreField.Tag.Get("json"))
	}
}

併發

併發:同一時間段內執行多個任務。
並行:同一時刻執行多個任務。
Go語言的併發通過goroutine實現。goroutine類似於線程,屬於用戶態的線程,我們可以根據需要創建成千上萬個goroutine併發工作。goroutine是由Go語言的運行時(runtime)調度完成,而線程是由操作系統調度完成。

Go語言還提供channel在多個goroutine間進行通信。goroutinechannel是 Go 語言秉承的 CSP(Communicating Sequential Process)併發模式的重要實現基礎。

Go程序會智能地將 goroutine 中的任務合理地分配給每個CPU。Go語言之所以被稱爲現代化的編程語言,就是因爲它在語言層面已經內置了調度上下文切換的機制。

Go語言中使用goroutine非常簡單,只需要在調用函數的時候在前面加上go關鍵字,就可以爲一個函數創建一個goroutine。一個goroutine必定對應一個函數,可以創建多個goroutine去執行相同的函數。

package main
import {
	"fmt"
}
var wg sync.WaitGroup//sync.WaitGroup來實現goroutine的同步

func hello(i int) {
	defer wg.Done() // goroutine結束就登記-1
	fmt.Println("Hello Goroutine!", i)
}
//程序啓動後會創建一個主goroutine去執行。
func main() {
	for i := 0; i < 10; i++ {
		wg.Add(1) // 啓動一個goroutine就登記+1
		go hello(i)//單獨開啓一個goroutine去執行函數
	}
	wg.Wait() // 等待所有登記的goroutine都結束,確保所有線程執行完後退出。
	//mian函數結果了,由mian函數啓動的線程也就結束了。
}

可增長的棧

OS線程(操作系統線程)一般都有固定的棧內存(通常爲2MB),一個goroutine的棧在其生命週期開始時只有很小的棧(典型情況下2KB),goroutine的棧不是固定的,他可以按需增大和縮小,goroutine的棧大小限制可以達到1GB,雖然極少會用到這個大。所以在Go語言中一次創建十萬左右的goroutine也是可以的。

goroutine調度

GPM是Go語言運行時(runtime)層面的實現,是go語言自己實現的一套調度系統。區別於操作系統調度OS線程。

  • G很好理解,就是個goroutine的,裏面除了存放本goroutine信息外 還有與所在P的綁定等信息。
  • P管理着一組goroutine隊列,P裏面會存儲當前goroutine運行的上下文環境(函數指針,堆棧地址及地址邊界),P會對自己管理的goroutine隊列做一些調度(比如把佔用CPU時間較長的goroutine暫停、運行後續的goroutine等等)當自己的隊列消費完了就去全局隊列裏取,如果全局隊列裏也消費完了會去其他P的隊列裏搶任務。
  • M(machine)是Go運行時(runtime)對操作系統內核線程的虛擬, M與內核線程一般是一一映射的關係, 一個groutine最終是要放到M上執行的;

GOMAXPROCS

Go語言中可以通過runtime.GOMAXPROCS()函數設置當前程序併發時佔用的CPU邏輯核心數。

Go1.5版本之前,默認使用的是單核心執行。Go1.5版本之後,默認使用全部的CPU邏輯核心數。
M:N表示把M個goroutine分配給N個操作系統線程去執行。(GOMAXPROCS是m:n調度中的n)

func a() {
	for i := 1; i < 10; i++ {
		fmt.Println("A:", i)
	}
}
func b() {
	for i := 1; i < 10; i++ {
		fmt.Println("B:", i)
	}
}
func main() {
	runtime.GOMAXPROCS(1)
	go a()
	go b()
	time.Sleep(time.Second)
}

channel

Go語言的併發模型是CSP(Communicating Sequential Processes),提倡通過通信共享內存而不是通過共享內存而實現通信
channel是可以讓一個goroutine發送特定值到另一個goroutine的通信機制。
Go 語言中的通道(channel)是一種特殊的類型,一種引用類型。遵循先入先出(First In First Out)的規則,保證收發數據的順序。每一個通道都是一個具體類型的導管,也就是聲明channel的時候需要爲其指定元素類型。

var 變量 chan 元素類型
//例子:
var ch1 chan int   // 聲明一個傳遞整型的通道
var ch2 chan bool  // 聲明一個傳遞布爾型的通道
var ch3 chan []int // 聲明一個傳遞int切片的通道

//通道的操作
//定義和初始化通道
ch := make(chan int10)//通道必須使用make函數初始化才能使用,否則爲nil。指定容量10。
//有指定容量的稱爲緩衝區通道,沒有制動容量稱爲無緩存區通道。
//使用無緩衝通道進行通信將導致發送和接收的goroutine同步化。因此,無緩衝通道也被稱爲同步通道。

//發送
ch <- 10 // 把10發送到ch中
//接收
x := <- ch // 從ch中接收值並賦值給變量x
<-ch       // 從ch中接收值,忽略結果
//關閉
close(ch)

單向通道

  • chan<- int是一個只寫單向通道(只能對其寫入int類型值),可以對其執行發送操作但是不能執行接收操作;
  • <-chan int是一個只讀單向通道(只能從其讀取int類型值),可以對其執行接收操作但是不能執行發送操作。

select

select即通道多路複用。滿足同時從多個通道接收數據。
select的使用類似於switch語句,它有一系列case分支和一個默認的分支。每個case會對應一個通道的通信(接收或發送)過程。select會一直等待,直到某個case的通信操作完成時,就會執行case分支對應的語句。

func main() {
	ch := make(chan int, 1)
	for i := 0; i < 10; i++ {
		select {
		case x := <-ch:
			fmt.Println(x)
		case ch <- i:
		}
	}
}

使用select語句能提高代碼的可讀性。

  • 可處理一個或多個channel的發送/接收操作。
  • 如果多個case同時滿足,select會隨機選擇一個。
  • 對於沒有case的select{}會一直等待,可用於阻塞main函數。

互斥鎖

sync包的Mutex類型來實現互斥鎖。

var x int64
var wg sync.WaitGroup
var lock sync.Mutex//

func add() {
	for i := 0; i < 5000; i++ {
		lock.Lock() // 加鎖
		x = x + 1
		lock.Unlock() // 解鎖
	}
	wg.Done()
}
func main() {
	wg.Add(2)
	go add()
	go add()
	wg.Wait()
	fmt.Println(x)
}

讀寫互斥鎖

讀寫鎖在Go語言中使用sync包中的RWMutex類型。
讀寫鎖分爲兩種:讀鎖和寫鎖。當一個goroutine獲取讀鎖之後,其他的goroutine如果是獲取讀鎖會繼續獲得鎖,如果是獲取寫鎖就會等待;當一個goroutine獲取寫鎖之後,其他的goroutine無論是獲取讀鎖還是寫鎖都會等待。

var (
	x      int64
	wg     sync.WaitGroup
	lock   sync.Mutex
	rwlock sync.RWMutex
)
func write() {
	// lock.Lock()   // 加互斥鎖
	rwlock.Lock() // 加寫鎖
	x = x + 1
	time.Sleep(10 * time.Millisecond) // 假設讀操作耗時10毫秒
	rwlock.Unlock()                   // 解寫鎖
	// lock.Unlock()                     // 解互斥鎖
	wg.Done()
}
func read() {
	// lock.Lock()                  // 加互斥鎖
	rwlock.RLock()               // 加讀鎖
	time.Sleep(time.Millisecond) // 假設讀操作耗時1毫秒
	rwlock.RUnlock()             // 解讀鎖
	// lock.Unlock()                // 解互斥鎖
	wg.Done()
}
func main() {
	start := time.Now()
	for i := 0; i < 10; i++ {
		wg.Add(1)
		go write()
	}
	for i := 0; i < 1000; i++ {
		wg.Add(1)
		go read()
	}
	wg.Wait()
	end := time.Now()
	fmt.Println(end.Sub(start))
}

WaitGroup

Go語言中可以使用sync.WaitGroup來實現併發任務的同步。

var wg sync.WaitGroup

func hello() {
	defer wg.Done()
	fmt.Println("Hello Goroutine!")
}
func main() {
	wg.Add(1)
	go hello() // 啓動另外一個goroutine去執行hello函數
	fmt.Println("main goroutine done!")
	wg.Wait()
}

Once

Once可以確保多線程場景下某些操作在高併發的場景下只執行一次。
sync.Once只有一個Do方法,其簽名如下:

func (o *Once) Do(f func()) {}
//例子:實現併發安全的單例模式:
package singleton
import (
    "sync"
)
type singleton struct {}
var instance *singleton
var once sync.Once

func GetInstance() *singleton {
    once.Do(func() {
        instance = &singleton{}
    })
    return instance
}

Map

Go語言中內置的map不是併發安全的。
而Go語言的sync包中提供了一個開箱即用的併發安全版map–sync.Map。開箱即用表示不用像內置的map一樣使用make函數初始化就能直接使用。同時sync.Map內置了諸如StoreLoadLoadOrStoreDeleteRange等操作方法。

var m = sync.Map{}//併發安全的

func main() {
	wg := sync.WaitGroup{}
	for i := 0; i < 20; i++ {
		wg.Add(1)
		go func(n int) {
			key := strconv.Itoa(n)
			m.Store(key, n)//必須使用sync.Map內置的Store方法設置鍵值對
			value, _ := m.Load(key)//必須使用sync.Map內置的Load方法根據key取值。
			fmt.Printf("k=:%v,v:=%v\n", key, value)
			wg.Done()
		}(i)
	}
	wg.Wait()
}

atomic

代碼中的加鎖操作因爲涉及內核態的上下文切換會比較耗時、代價比較高。針對基本數據類型我們還可以使用原子操作來保證併發安全。Go語言中原子操作由內置的標準庫sync/atomic提供。
以下是atomic包提供的一些方法:
讀取操作

  • func LoadInt32(addr *int32) (val int32)
    func LoadInt64(addr *int64) (val int64)
    func LoadUint32(addr *uint32) (val uint32)
    func LoadUint64(addr *uint64) (val uint64)
    func LoadUintptr(addr *uintptr) (val uintptr)
    func LoadPointer(addr *unsafe.Pointer) (val unsafe.Pointer)

寫入操作

  • func StoreInt32(addr *int32, val int32)
    func StoreInt64(addr *int64, val int64)
    func StoreUint32(addr *uint32, val uint32)
    func StoreUint64(addr *uint64, val uint64)
    func StoreUintptr(addr *uintptr, val uintptr)
    func StorePointer(addr *unsafe.Pointer, val unsafe.Pointer)

修改操作

  • func AddInt32(addr *int32, delta int32) (new int32)
    func AddInt64(addr *int64, delta int64) (new int64)
    func AddUint32(addr *uint32, delta uint32) (new uint32)
    func AddUint64(addr *uint64, delta uint64) (new uint64)
    func AddUintptr(addr *uintptr, delta uintptr) (new uintptr)

交換操作

  • func SwapInt32(addr *int32, new int32) (old int32)
    func SwapInt64(addr *int64, new int64) (old int64)
    func SwapUint32(addr *uint32, new uint32) (old uint32)
    func SwapUint64(addr *uint64, new uint64) (old uint64)
    func SwapUintptr(addr *uintptr, new uintptr) (old uintptr)
    func SwapPointer(addr *unsafe.Pointer, new unsafe.Pointer) (old unsafe.Pointer)

比較並交換操作

  • func CompareAndSwapInt32(addr *int32, old, new int32) (swapped bool)
    func CompareAndSwapInt64(addr *int64, old, new int64) (swapped bool)
    func CompareAndSwapUint32(addr *uint32, old, new uint32) (swapped bool)
    func CompareAndSwapUint64(addr *uint64, old, new uint64) (swapped bool)
    func CompareAndSwapUintptr(addr *uintptr, old, new uintptr) (swapped bool)
    func CompareAndSwapPointer(addr *unsafe.Pointer, old, new unsafe.Pointer) (swapped bool)
package main
import "sync"
//
var x int64
var wg sync.WaitGroup
var lock sync.Mutex

func add(){
	// lock.Lock()
	// x++
	// lock.Unlock()
	
	atomic.AddInt64(&x,1)//該語句就相當於上面三句註釋的代碼
	wg.Done()
}
func main(){
	wg.Add(1000)
	for i:=0;i<1000;i++{
		go add()
	}
	wg.Wait()
	fmt.Println(x)
}

第8天

P111~P129

TCP通信

服務端

// tcp/server/main.go
// TCP server端
// 處理函數
func process(conn net.Conn) {
	defer conn.Close() // 關閉連接
	for {
		reader := bufio.NewReader(conn)
		var buf [128]byte
		n, err := reader.Read(buf[:]) // 讀取數據
		if err != nil {
			fmt.Println("read from client failed, err:", err)
			break
		}
		recvStr := string(buf[:n])
		fmt.Println("收到client端發來的數據:", recvStr)
		conn.Write([]byte(recvStr)) // 發送數據
	}
}

func main() {
	listen, err := net.Listen("tcp", "127.0.0.1:20000")//監聽
	if err != nil {
		fmt.Println("listen failed, err:", err)
		return
	}
	for {
		conn, err := listen.Accept() // 建立連接
		if err != nil {
			fmt.Println("accept failed, err:", err)
			continue
		}
		go process(conn) // 啓動一個goroutine處理連接
	}
}

客戶端

// tcp/client/main.go
// 客戶端
func main() {
	conn, err := net.Dial("tcp", "127.0.0.1:20000")//撥號
	if err != nil {
		fmt.Println("err :", err)
		return
	}
	defer conn.Close() // 關閉連接
	inputReader := bufio.NewReader(os.Stdin)
	for {
		input, _ := inputReader.ReadString('\n') // 讀取用戶輸入
		inputInfo := strings.Trim(input, "\r\n")
		if strings.ToUpper(inputInfo) == "Q" { // 如果輸入q就退出
			return
		}
		_, err = conn.Write([]byte(inputInfo)) // 發送數據
		if err != nil {
			return
		}
		buf := [512]byte{}
		n, err := conn.Read(buf[:])
		if err != nil {
			fmt.Println("recv failed, err:", err)
			return
		}
		fmt.Println(string(buf[:n]))
	}
}

UDP通信

UDP協議(User Datagram Protocol)中文名稱是用戶數據報協議,是OSI(Open System Interconnection,開放式系統互聯)參考模型中一種無連接的傳輸層協議,不需要建立連接就能直接進行數據發送和接收,屬於不可靠的、沒有時序的通信,但是UDP協議的實時性比較好,通常用於視頻直播相關領域。

服務端

// UDP/server/main.go
// UDP server端
func main() {
	listen, err := net.ListenUDP("udp", &net.UDPAddr{
		IP:   net.IPv4(0, 0, 0, 0),
		Port: 30000,
	})
	if err != nil {
		fmt.Println("listen failed, err:", err)
		return
	}
	defer listen.Close()
	for {
		var data [1024]byte
		n, addr, err := listen.ReadFromUDP(data[:]) // 接收數據
		if err != nil {
			fmt.Println("read udp failed, err:", err)
			continue
		}
		fmt.Printf("data:%v addr:%v count:%v\n", string(data[:n]), addr, n)
		_, err = listen.WriteToUDP(data[:n], addr) // 發送數據
		if err != nil {
			fmt.Println("write to udp failed, err:", err)
			continue
		}
	}
}

客戶端

// UDP 客戶端
func main() {
	socket, err := net.DialUDP("udp", nil, &net.UDPAddr{
		IP:   net.IPv4(0, 0, 0, 0),
		Port: 30000,
	})
	if err != nil {
		fmt.Println("連接服務端失敗,err:", err)
		return
	}
	defer socket.Close()
	sendData := []byte("Hello server")
	_, err = socket.Write(sendData) // 發送數據
	if err != nil {
		fmt.Println("發送數據失敗,err:", err)
		return
	}
	data := make([]byte, 4096)
	n, remoteAddr, err := socket.ReadFromUDP(data) // 接收數據
	if err != nil {
		fmt.Println("接收數據失敗,err:", err)
		return
	}
	fmt.Printf("recv:%v addr:%v count:%v\n", string(data[:n]), remoteAddr, n)
}

HTTP通信

net/http包提供了HTTP客戶端和服務端的實現。
服務端實現:

// http server

func sayHello(w http.ResponseWriter, r *http.Request) {
	str := '<h1>hello</h1>'
	w.Write([]byte(str))//響應
}

func main() {
	//HandleFunc函數可以向DefaultServeMux添加處理器。
	http.HandleFunc("/api/test", sayHello)
	//ListenAndServe使用指定的監聽地址和處理器啓動一個HTTP服務端。處理器參數通常是nil,這表示採用包變量DefaultServeMux作爲處理器。
	err := http.ListenAndServe("127.0.0.1:9090", nil)
	if err != nil {
		fmt.Printf("http server failed, err:%v\n", err)
		return
	}
}

客戶端:GET方式和POST方式
GET方式:

//GET方式
func main() {
	apiUrl := "http://127.0.0.1:9090/get"
	// URL param
	data := url.Values{}
	data.Set("name", "小王子")
	data.Set("age", "18")
	u, err := url.ParseRequestURI(apiUrl)//請求地址
	if err != nil {
		fmt.Printf("parse url requestUrl failed,err:%v\n", err)
	}
	u.RawQuery = data.Encode() // URL encode
	fmt.Println(u.String())
	resp, err := http.Get(u.String())//發送請求
	if err != nil {
		fmt.Println("post failed, err:%v\n", err)
		return
	}
	defer resp.Body.Close()//使用完response後必須關閉回覆的主體。
	b, err := ioutil.ReadAll(resp.Body)//響應內容
	if err != nil {
		fmt.Println("get resp failed,err:%v\n", err)
		return
	}
	fmt.Println(string(b))
}

POST方式

package main

import (
	"fmt"
	"io/ioutil"
	"net/http"
	"strings"
)

// net/http post demo

func main() {
	url := "http://127.0.0.1:9090/post"
	// 表單數據
	//contentType := "application/x-www-form-urlencoded"
	//data := "name=小王子&age=18"
	// json
	contentType := "application/json"
	data := `{"name":"小王子","age":18}`
	resp, err := http.Post(url, contentType, strings.NewReader(data))
	if err != nil {
		fmt.Println("post failed, err:%v\n", err)
		return
	}
	defer resp.Body.Close()
	b, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		fmt.Println("get resp failed,err:%v\n", err)
		return
	}
	fmt.Println(string(b))
}

Server
//對應的Server端HandlerFunc如下:
func postHandler(w http.ResponseWriter, r *http.Request) {
	defer r.Body.Close()
	// 1. 請求類型是application/x-www-form-urlencoded時解析form數據
	r.ParseForm()
	fmt.Println(r.PostForm) // 打印form數據
	fmt.Println(r.PostForm.Get("name"), r.PostForm.Get("age"))
	// 2. 請求類型是application/json時從r.Body讀取數據
	b, err := ioutil.ReadAll(r.Body)
	if err != nil {
		fmt.Println("read request.Body failed, err:%v\n", err)
		return
	}
	fmt.Println(string(b))
	answer := `{"status": "ok"}`
	w.Write([]byte(answer))
}

單元測試

性能測試

單元,性能測試

第9天

P130~P139

MySQL

存儲引擎

mysql支持插件式的存儲引擎
常見的存儲引擎:MyISAM和InnoDB。
MyISAM:

  • 查詢適度快
  • 只支持表鎖
  • 不支持事務
    InnoDB:
  • 整體速度快
  • 支持表鎖和行鎖
  • 支持事務

事務

特點:
通常事務必須滿足4個條件(ACID):原子性(Atomicity,或稱不可分割性)、一致性(Consistency)、隔離性(Isolation,又稱獨立性)、持久性(Durability)

條件 解釋
原子性 一個事務(transaction)中的所有操作,要麼全部完成,要麼全部不完成,不會結束在中間某個環節。事務在執行過程中發生錯誤,會被回滾(Rollback)到事務開始前的狀態,就像這個事務從來沒有執行過一樣。
一致性 在事務開始之前和事務結束以後,數據庫的完整性沒有被破壞。這表示寫入的資料必須完全符合所有的預設規則,這包含資料的精確度、串聯性以及後續數據庫可以自發性地完成預定的工作。
隔離性 數據庫允許多個併發事務同時對其數據進行讀寫和修改的能力,隔離性可以防止多個事務併發執行時由於交叉執行而導致數據的不一致。事務隔離分爲不同級別,包括讀未提交(Read uncommitted)、讀提交(read committed)、可重複讀(repeatable read)和串行化(Serializable)。
持久性 事務處理結束後,對數據的修改就是永久的,即便系統故障也不會丟失。
  • 原子性:要麼成功要麼失敗。
  • 一致性:數據庫的完整性沒有被破壞
  • 隔離性:事務之間是相互隔離的。
  • 持久性:事務操作的結果是不會丟失的。

索引

MySQL索引原理:B樹和B+樹

Go操作MySQL

Go語言中的database/sql包提供了保證SQL或類SQL數據庫的泛用接口,並不提供具體的數據庫驅動。使用database/sql包時必須注入(至少)一個數據庫驅動。

//下載驅動Go-MySQL-Driver
go get -u github.com/go-sql-driver/mysql

操作數據庫

// 定義一個全局對象db
var db *sql.DB

// 定義一個初始化數據庫的函數
func initDB() (err error) {
	// DSN:Data Source Name
	dsn := "user:password@tcp(127.0.0.1:3306)/test"	
	// 注意!!!這裏不要使用:=,我們是給全局變量賦值,然後在main函數中使用全局變量db
	db, err = sql.Open("mysql", dsn)// 不會校驗賬號密碼是否正確,只會校驗連接字符串的格式是否準確。
	if err != nil {
		return err
	}	
	err = db.Ping()// 嘗試與數據庫建立連接(校驗賬號密碼是否正確)
	if err != nil {
		return err
	}
	db.SetMaxOpenConns(10)//設置與數據庫建立連接的最大數目。一般很少設置。
	db.SetMaxIdleConns(10)//設置連接池中的最大閒置連接數。一般很少設置。
	return nil
}

其中sql.DB是一個數據庫(操作)句柄,代表一個具有零到多個底層連接的連接池。它可以安全地被多個goroutine同時使用。database/sql包會自動創建和釋放連接;它也會維護一個閒置連接的連接池。

單行查詢

// 查詢單條數據示例
func queryRowDemo() {
	sqlStr := "select id, name, age from user where id=?"//Go語言中mysql語句的佔位符是"?"
	var u user
	// 確保QueryRow之後調用Scan方法,因爲Scan方法內有個close動作會釋放mysql連接。
	err := db.QueryRow(sqlStr, 2).Scan(&u.id, &u.name, &u.age)//QueryRow參數2是傳遞佔位符的參數
	if err != nil {
		fmt.Printf("scan failed, err:%v\n", err)
		return
	}
	fmt.Printf("id:%d name:%s age:%d\n", u.id, u.name, u.age)
}

多行查詢

// 查詢多條數據示例
func queryMultiRowDemo() {
	sqlStr := "select id, name, age from user where id > ?"
	rows, err := db.Query(sqlStr, 0)
	if err != nil {
		fmt.Printf("query failed, err:%v\n", err)
		return
	}
	// 非常重要:關閉rows釋放持有的數據庫鏈接
	defer rows.Close()
	// 循環讀取結果集中的數據
	for rows.Next() {
		var u user
		err := rows.Scan(&u.id, &u.name, &u.age)
		if err != nil {
			fmt.Printf("scan failed, err:%v\n", err)
			return
		}
		fmt.Printf("id:%d name:%s age:%d\n", u.id, u.name, u.age)
	}
}

插入、更新和刪除操作都使用Exec方法。

插入

// 插入數據
func insertRowDemo() {
	sqlStr := "insert into user(name, age) values (?,?)"
	ret, err := db.Exec(sqlStr, "王五", 38)
	if err != nil {
		fmt.Printf("insert failed, err:%v\n", err)
		return
	}
	theID, err := ret.LastInsertId() // 返回新插入數據的id
	if err != nil {
		fmt.Printf("get lastinsert ID failed, err:%v\n", err)
		return
	}
	fmt.Printf("insert success, the id is %d.\n", theID)
}

更新

// 更新數據
func updateRowDemo() {
	sqlStr := "update user set age=? where id = ?"
	ret, err := db.Exec(sqlStr, 39, 3)
	if err != nil {
		fmt.Printf("update failed, err:%v\n", err)
		return
	}
	n, err := ret.RowsAffected() // 返回操作影響的行數
	if err != nil {
		fmt.Printf("get RowsAffected failed, err:%v\n", err)
		return
	}
	fmt.Printf("update success, affected rows:%d\n", n)
}

刪除

// 刪除數據
func deleteRowDemo() {
	sqlStr := "delete from user where id = ?"
	ret, err := db.Exec(sqlStr, 3)
	if err != nil {
		fmt.Printf("delete failed, err:%v\n", err)
		return
	}
	n, err := ret.RowsAffected() // 操作影響的行數
	if err != nil {
		fmt.Printf("get RowsAffected failed, err:%v\n", err)
		return
	}
	fmt.Printf("delete success, affected rows:%d\n", n)
}

預處理

// 預處理查詢示例
func prepareQueryDemo() {
	sqlStr := "select id, name, age from user where id > ?"
	//Prepare方法會先將sql語句發送給MySQL服務端,返回一個準備好的狀態用於之後的查詢和命令。
	//返回值可以同時執行多個查詢和命令。
	//好處是
	//1.提高性能,服務器編譯一次就可以多次運行
	//2.防止SQL注入
	stmt, err := db.Prepare(sqlStr)
	if err != nil {
		fmt.Printf("prepare failed, err:%v\n", err)
		return
	}
	defer stmt.Close()
	rows, err := stmt.Query(0)
	if err != nil {
		fmt.Printf("query failed, err:%v\n", err)
		return
	}
	defer rows.Close()
	// 循環讀取結果集中的數據
	for rows.Next() {
		var u user
		err := rows.Scan(&u.id, &u.name, &u.age)
		if err != nil {
			fmt.Printf("scan failed, err:%v\n", err)
			return
		}
		fmt.Printf("id:%d name:%s age:%d\n", u.id, u.name, u.age)
	}
}

事務

// 事務操作示例
func transactionDemo() {
	tx, err := db.Begin() // 開啓事務
	if err != nil {
		if tx != nil {
			tx.Rollback() // 回滾
		}
		fmt.Printf("begin trans failed, err:%v\n", err)
		return
	}
	sqlStr1 := "Update user set age=30 where id=?"
	_, err = tx.Exec(sqlStr1, 2)
	if err != nil {
		tx.Rollback() // 回滾
		fmt.Printf("exec sql1 failed, err:%v\n", err)
		return
	}
	sqlStr2 := "Update user set age=40 where id=?"
	_, err = tx.Exec(sqlStr2, 4)
	if err != nil {
		tx.Rollback() // 回滾
		fmt.Printf("exec sql2 failed, err:%v\n", err)
		return
	}
	err = tx.Commit() // 提交事務
	if err != nil {
		tx.Rollback() // 回滾
		fmt.Printf("commit failed, err:%v\n", err)
		return
	}
	fmt.Println("exec trans success!")
}

第10天

P140~P145

Redis

應用場景

  • 緩存系統,減輕主數據庫(MySQL)的壓力。
  • 計數場景,比如微博、抖音中的關注數和粉絲數。
  • 熱門排行榜,需要排序的場景特別適合使用ZSET。
  • 利用LIST可以實現隊列的功能。

go-redis庫操作redis

go-redis支持連接哨兵及集羣模式的Redis。

安裝
go get -u github.com/go-redis/redis
連接

普通連接

// 聲明一個全局的rdb變量
var rdb *redis.Client

// 初始化連接
func initClient() (err error) {
	rdb = redis.NewClient(&redis.Options{
		Addr:     "localhost:6379",
		Password: "", // no password set
		DB:       0,  // use default DB
	})

	_, err = rdb.Ping().Result()
	if err != nil {
		return err
	}
	return nil
}

連接Redis哨兵模式

func initClient()(err error){
	rdb := redis.NewFailoverClient(&redis.FailoverOptions{
		MasterName:    "master",
		SentinelAddrs: []string{"x.x.x.x:26379", "xx.xx.xx.xx:26379", "xxx.xxx.xxx.xxx:26379"},
	})
	_, err = rdb.Ping().Result()
	if err != nil {
		return err
	}
	return nil
}

連接Redis集羣

func initClient()(err error){
	rdb := redis.NewClusterClient(&redis.ClusterOptions{
		Addrs: []string{":7000", ":7001", ":7002", ":7003", ":7004", ":7005"},
	})
	_, err = rdb.Ping().Result()
	if err != nil {
		return err
	}
	return nil
}
基本操作

set/get示例

func redisExample() {
	err := rdb.Set("score", 100, 0).Err()
	if err != nil {
		fmt.Printf("set score failed, err:%v\n", err)
		return
	}

	val, err := rdb.Get("score").Result()
	if err != nil {
		fmt.Printf("get score failed, err:%v\n", err)
		return
	}
	fmt.Println("score", val)

	val2, err := rdb.Get("name").Result()
	if err == redis.Nil {
		fmt.Println("name does not exist")
	} else if err != nil {
		fmt.Printf("get name failed, err:%v\n", err)
		return
	} else {
		fmt.Println("name", val2)
	}
}

zset示例

func redisExample2() {
	zsetKey := "language_rank"
	languages := []*redis.Z{
		&redis.Z{Score: 90.0, Member: "Golang"},
		&redis.Z{Score: 98.0, Member: "Java"},
		&redis.Z{Score: 95.0, Member: "Python"},
		&redis.Z{Score: 97.0, Member: "JavaScript"},
		&redis.Z{Score: 99.0, Member: "C/C++"},
	}
	// ZADD
	num, err := rdb.ZAdd(zsetKey, languages...).Result()
	if err != nil {
		fmt.Printf("zadd failed, err:%v\n", err)
		return
	}
	fmt.Printf("zadd %d succ.\n", num)

	// 把Golang的分數加10
	newScore, err := rdb.ZIncrBy(zsetKey, 10.0, "Golang").Result()
	if err != nil {
		fmt.Printf("zincrby failed, err:%v\n", err)
		return
	}
	fmt.Printf("Golang's score is %f now.\n", newScore)

	// 取分數最高的3個
	ret, err := rdb.ZRevRangeWithScores(zsetKey, 0, 2).Result()
	if err != nil {
		fmt.Printf("zrevrange failed, err:%v\n", err)
		return
	}
	for _, z := range ret {
		fmt.Println(z.Member, z.Score)
	}

	// 取95~100分的
	op := &redis.ZRangeBy{
		Min: "95",
		Max: "100",
	}
	ret, err = rdb.ZRangeByScoreWithScores(zsetKey, op).Result()
	if err != nil {
		fmt.Printf("zrangebyscore failed, err:%v\n", err)
		return
	}
	for _, z := range ret {
		fmt.Println(z.Member, z.Score)
	}
}
//輸出結果如下:
$ ./06redis_demo 
zadd 0 succ.
Golang's score is 100.000000 now.
Golang 100
C/C++ 99
Java 98
JavaScript 97
Java 98
C/C++ 99
Golang 100

NSQ消息隊列

第11天

P146~P159

Go module

go module是Go1.11版本之後官方推出的版本管理工具,並且從Go1.13版本開始,go module將是Go語言默認的依賴管理工具。

GO111MODULE

要啓用go module支持首先要設置環境變量GO111MODULE,通過它可以開啓或關閉模塊支持,它有三個可選值:off、on、auto,默認值是auto。
使用 go module 管理依賴後會在項目根目錄下生成兩個文件go.modgo.sum

go mod

常用的go mod命令如下:

go mod download    下載依賴的module到本地cache(默認爲$GOPATH/pkg/mod目錄)
go mod edit        編輯go.mod文件
go mod graph       打印模塊依賴圖
go mod init        初始化當前文件夾, 創建go.mod文件
go mod tidy        增加缺少的module,刪除無用的module
go mod vendor      將依賴複製到vendor下
go mod verify      校驗依賴
go mod why         解釋爲什麼需要依賴

我們在代碼中刪除依賴代碼後,相關的依賴庫並不會在go.mod文件中自動移除。這種情況下我們可以使用go mod tidy命令更新go.mod中的依賴關係。

//添加依賴項:golang.org/x/text
go mod edit -require=golang.org/x/text
//移除依賴項:golang.org/x/text
go mod edit -droprequire=golang.org/x/text

go.mod文件記錄了項目所有的依賴信息,其結構大致如下:

module github.com/Q1mi/studygo/blogger
go 1.12
require (
	github.com/DeanThompson/ginpprof v0.0.0-20190408063150-3be636683586
	github.com/gin-gonic/gin v1.4.0
	github.com/go-sql-driver/mysql v1.4.1
	github.com/jmoiron/sqlx v1.2.0
	github.com/satori/go.uuid v1.2.0
	google.golang.org/appengine v1.6.1 // indirect
)
其中
module用來定義包名
require用來定義依賴包及版本
indirect表示間接引用

Context

兩個默認值

context.Background()
context.TODO()

四個方法

context.withCancel(context.Background())
context.withDeadline(context.Background(),time.Time)
context.withTimeout(context.Background(),tiem.Duration)
context.withValue(context.Background(),key,value)

kafka

Go語言中連接kafka使用第三方庫sarama:github.com/Shopify/sarama。
下載安裝:

go get github.com/Shopify/sarama

發送消息

package main
import (
	"fmt"

	"github.com/Shopify/sarama"
)
// 基於sarama第三方庫開發的kafka client
func main() {
	config := sarama.NewConfig()
	config.Producer.RequiredAcks = sarama.WaitForAll          // 發送完數據需要leader和follow都確認
	config.Producer.Partitioner = sarama.NewRandomPartitioner // 新選出一個partition
	config.Producer.Return.Successes = true                   // 成功交付的消息將在success channel返回
	// 構造一個消息
	msg := &sarama.ProducerMessage{}
	msg.Topic = "web_log"
	msg.Value = sarama.StringEncoder("this is a test log")
	// 連接kafka
	client, err := sarama.NewSyncProducer([]string{"192.168.1.7:9092"}, config)
	if err != nil {
		fmt.Println("producer closed, err:", err)
		return
	}
	defer client.Close()
	// 發送消息
	pid, offset, err := client.SendMessage(msg)
	if err != nil {
		fmt.Println("send msg failed, err:", err)
		return
	}
	fmt.Printf("pid:%v offset:%v\n", pid, offset)
}

消費消息

package main
import (
	"fmt"

	"github.com/Shopify/sarama"
)
// kafka consumer
func main() {
	consumer, err := sarama.NewConsumer([]string{"127.0.0.1:9092"}, nil)
	if err != nil {
		fmt.Printf("fail to start consumer, err:%v\n", err)
		return
	}
	partitionList, err := consumer.Partitions("web_log") // 根據topic取到所有的分區
	if err != nil {
		fmt.Printf("fail to get list of partition:err%v\n", err)
		return
	}
	fmt.Println(partitionList)
	for partition := range partitionList { // 遍歷所有的分區
		// 針對每個分區創建一個對應的分區消費者
		pc, err := consumer.ConsumePartition("web_log", int32(partition), sarama.OffsetNewest)
		if err != nil {
			fmt.Printf("failed to start consumer for partition %d,err:%v\n", partition, err)
			return
		}
		defer pc.AsyncClose()
		// 異步從每個分區消費信息
		go func(sarama.PartitionConsumer) {
			for msg := range pc.Messages() {
				fmt.Printf("Partition:%d Offset:%d Key:%v Value:%v", msg.Partition, msg.Offset, msg.Key, msg.Value)
			}
		}(pc)
	}
}

第12天

P160~P178

etcd

etcd是使用Go語言開發的一個開源的、高可用的分佈式key-value存儲系統,可以用於配置共享和服務的註冊和發現。
類似項目有zookeeper和consul。
etcd具有以下特點:

  • 完全複製:集羣中的每個節點都可以使用完整的存檔
  • 高可用性:Etcd可用於避免硬件的單點故障或網絡問題
  • 一致性:每次讀取都會返回跨多主機的最新寫入
  • 簡單:包括一個定義良好、面向用戶的API(gRPC)
  • 安全:實現了帶有可選的客戶端證書身份驗證的自動化TLS
  • 快速:每秒10000次寫入的基準速度
  • 可靠:使用Raft算法實現了強一致、高可用的服務存儲目錄

ES

Elasticsearch(ES)是一個基於Lucene構建的開源、分佈式、RESTful接口的全文搜索引擎。Elasticsearch還是一個分佈式文檔數據庫,其中每個字段均可被索引,而且每個字段的數據均可被搜索,ES能夠橫向擴展至數以百計的服務器存儲以及處理PB級的數據。可以在極短的時間內存儲、搜索和分析大量的數據。通常作爲具有複雜搜索場景情況下的核心發動機。

Kibana

第13天

P179~P189

Gin框架

下載安裝

go get -u github.com/gin-gonic/gin

請求方式示例

Get方式示例:

package main
import (
	"github.com/gin-gonic/gin"
)
func main() {
	// 創建一個默認的路由引擎
	r := gin.Default()
	// GET:請求方式;/hello:請求的路徑
	// 當客戶端以GET方法請求/hello路徑時,會執行後面的匿名函數
	r.GET("/hello", func(c *gin.Context) {
		//c.Param:獲取GET方式傳遞的name參數的值
		name := c.Param("name")
		//c.DefaultQuery:獲取GET方式傳遞的name參數的值,第二個參數是默認值
		name := c.DefaultQuery("name","Jack")
		// c.JSON:返回JSON格式的數據
		c.JSON(200, gin.H{
			"message": "Hello world!",
		})
	})
	// 啓動HTTP服務,默認在0.0.0.0:8080啓動服務
	r.Run()
}

POST方式示例:

package main
import (
	"github.com/gin-gonic/gin"
)
func main() {
	// 創建一個默認的路由引擎
	r := gin.Default()
	// Post:請求方式;/hello:請求的路徑
	// 當客戶端以GET方法請求/hello路徑時,會執行後面的匿名函數
	r.POST("/hello", func(c *gin.Context) {
		//c.PostForm:獲取POST方式傳遞的name參數的值
		name := c.PostForm("name")
		//c.DefaultPostForm:獲取POST方式傳遞的name參數的值,第二個參數是默認值
		name := c.DefaultPostForm("name","Jack")
		//c.PostFormArray:數組的形式接收
		key := c.PostFormArray("key")
		//c.FormFile:獲取上傳的文件
		file,_ := c.FormFile("file")
		// c.JSON:返回JSON格式的數據
		c.JSON(200, gin.H{
			"message": "Hello world!",
		})
	})
	// 啓動HTTP服務,默認在0.0.0.0:8080啓動服務
	r.Run()
}

多文件上傳:

func main() {
	router := gin.Default()
	// 處理multipart forms提交文件時默認的內存限制是32 MiB
	// 可以通過下面的方式修改
	// router.MaxMultipartMemory = 8 << 20  // 8 MiB
	router.POST("/upload", func(c *gin.Context) {
		// Multipart form
		form, _ := c.MultipartForm()
		files := form.File["file"]

		for index, file := range files {
			log.Println(file.Filename)
			dst := fmt.Sprintf("C:/tmp/%s_%d", file.Filename, index)
			// 上傳文件到指定的目錄
			c.SaveUploadedFile(file, dst)
		}
		c.JSON(http.StatusOK, gin.H{
			"message": fmt.Sprintf("%d files uploaded!", len(files)),
		})
	})
	router.Run()
}

路由組

Gin框架中的路由使用的是httprouter這個庫。
其基本原理就是構造一個路由地址的前綴樹。

func main() {
	r := gin.Default()
	userGroup := r.Group("/user")
	{
		userGroup.GET("/index", func(c *gin.Context) {...})
		userGroup.GET("/login", func(c *gin.Context) {...})
		userGroup.POST("/login", func(c *gin.Context) {...})

	}
	//路由組也是支持嵌套的
	shopGroup := r.Group("/shop")
	{
		shopGroup.GET("/index", func(c *gin.Context) {...})
		shopGroup.GET("/cart", func(c *gin.Context) {...})
		shopGroup.POST("/checkout", func(c *gin.Context) {...})
		// 嵌套路由組
		xx := shopGroup.Group("xx")
		xx.GET("/oo", func(c *gin.Context) {...})
	}
	r.Run()
}

中間件

Gin中的中間件必須是一個gin.HandlerFunc類型
定義一箇中間件例子:

// StatCost 是一個統計耗時請求耗時的中間件
func StatCost() gin.HandlerFunc {
	return func(c *gin.Context) {
		start := time.Now()
		c.Set("name", "小王子") // 可以通過c.Set在請求上下文中設置值,後續的處理函數能夠取到該值
		// 調用該請求的剩餘處理程序
		//c.Next() 之前的操作是在 Handler 執行之前就執行;
		c.Next()
		//c.Next() 之後的操作是在 Handler 執行之後再執行;
		// 不調用該請求的剩餘處理程序
		// c.Abort()
		// 計算耗時
		cost := time.Since(start)
		log.Println(cost)
	}
}

註冊路由

//爲全局路由註冊
func main() {
	// 新建一個沒有任何默認中間件的路由
	r := gin.New()
	// 註冊一個全局中間件
	r.Use(StatCost())
	
	r.GET("/test", func(c *gin.Context) {
		name := c.MustGet("name").(string) // 從上下文取值
		log.Println(name)
		c.JSON(http.StatusOK, gin.H{
			"message": "Hello world!",
		})
	})
	r.Run()
}
// 給/test2路由單獨註冊中間件(可註冊多個)
r.GET("/test2", StatCost(), func(c *gin.Context) {
	name := c.MustGet("name").(string) // 從上下文取值
	log.Println(name)
	c.JSON(http.StatusOK, gin.H{
		"message": "Hello world!",
	})
})
// 爲路由組註冊中間件
shopGroup := r.Group("/shop", StatCost())
{
    shopGroup.GET("/index", func(c *gin.Context) {...})
    ...
}

gin默認中間件:
gin.Default()默認使用了LoggerRecovery中間件,其中:
Logger中間件將日誌寫入gin.DefaultWriter,即使配置了GIN_MODE=release。
Recovery中間件會recover任何panic。如果有panic的話,會寫入500響應碼。
如果不想使用上面兩個默認的中間件,可以使用gin.New()新建一個沒有任何默認中間件的路由。

gin中間件中使用goroutine:
當在中間件或handler中啓動新的goroutine時,不能使用原始的上下文(c *gin.Context),必須使用其只讀副本(c.Copy())。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章