《golong入門教學📚》,從零開始入門❤️(建議收藏⭐️)

2023-03-18 18:00:36

Go語言學習筆記

本菜鳥的Go語言學習筆記,歷時1個月,包含了Go語言大部分的基本語法(不敢說全部),學習期間參考了各種視訊,閱讀了各種文章,主要參考名單如下:

點選跳轉到參考名單<( ̄︶ ̄)>

在這裡小聲說兩句:Go語言對於並行的支援非常nice,在現在這個卷的時代,多學習一門程式語言,就多一點競爭力,Go語言還是我比較推薦學習的一門程式語言

文章目錄

  1. 基本語法
  2. 流程控制
  3. 函數
  4. 容器
  5. 物件導向
  6. 並行
  7. 反射
  8. 檔案處理

基本語法

Go 語言在很多特性上和C語言非常相近。如果各位看官有C語言基礎(或者其他程式語言基礎),那麼本章的內容閱讀起來將會非常輕鬆,但如果讀者沒有程式語言基礎也沒關係,因為本章的內容非常簡單易懂。

註釋

註釋就是為了增強程式碼的可讀性,註釋起來的程式碼或者文字不會被程式執行,程式會自動略過註釋的部分

寫註釋是一個非常好的習慣,十分建議多寫註釋,否則以後你會想打死不寫註釋的自己╮( ̄▽ ̄)╭

  1. 單行註釋

    package main
    
    // 單行註釋,這一行都不會被程式執行 (~ ̄▽ ̄~)
    
    func main() {
    	println("Hello World")
    }
    
  2. 多行註釋

    package main
    
    /**
     不會被執行(~ ̄▽ ̄~)
     多行註釋  <( ̄︶ ̄)>
     不會被執行(~ ̄▽ ̄~)
     */
     
     func main() {
    	println("Hello World")
    }
    

變數

Go語言是靜態型別語言,因此變數(variable)是有明確型別的,編譯器也會檢查變數型別的正確性。

  • 字面上來理解,變數就是會變化的量。

  • 在數學概念中,變數表示沒有固定值且可改變的數。

  • 從計算機系統實現角度來看,變數是一段或多段用來儲存資料的記憶體。

這裡的name就是變數,右邊的「玄德」就是給這個變數賦值,所以這個變數name的值就是「玄德」

var name string = "玄德"

不用寫分號真不錯啊<( ̄︶ ̄)>

變數的宣告

在G語言中,宣告一個變數一般使用var關鍵字

var name type

這裡的type表示變數的型別,就如上面的字串型別:string

// 定義一個字元型變數 name
var name string
// 定義一個整形變數 age
var age int

Go語言和許多程式語言不同,它在宣告變數時將變數的型別放在變數的名稱之後。

這樣做的好處就是可以避免像C語言中那樣含糊不清的宣告形式,例如:int* a, b;, 其中只有 a 是指標而 b 不是,如果你想要這兩個變數都是指標,則需要將它們分開書寫。

在 Go 語言中,可以輕鬆地將它們都宣告為指標型別:

var a, b *int

變數的命名遵循駝峰命名法,即首個字母小寫,每個新單詞的首字母大寫,例如:userName、email

標準格式

Go語言變數宣告的標準格式

var 變數名 變數型別

變數宣告以關鍵字 var 開頭,後置變數型別,行尾無須分號。

批次格式

如果覺得單獨定義比較麻煩,那麼就可以使用批次定義變數,使用關鍵字 var 和括號,可以將一組變數定義放在一起。

初始值

當一個變數被宣告後,如果沒有給這個變數賦值,那麼系統將自動賦予它們預設值:

  • 整型和浮點型的預設值是 0 和0.0
  • 字串變數的預設值是空字串
  • 布林型變數預設值是 false
  • 切面、函數、指標變數的預設值是 nil。
    var (
        name string
        age int
        c [] float32
        d func() bool
        e struct {
            x int
        }
    )

Go語言的基本型別

  • bool
  • string
  • int、int8、int16、int32、int64
  • uint、uint8、uint16、uint32、uint64、uintptr
  • byte // uint8 的別名
  • rune // int32 的別名 代表一個 Unicode 碼
  • float32、float64
  • complex64、complex128

變數的初始化

標準格式

var 變數名 型別 = 表示式

比如,定義一個玄德的資訊,可以這麼表示

var name string = "玄德"		// 定義字元型變數name
var age int = 20			 // 定義整形變數 age

println(name, age)	

在標準格式的基礎上,如果將型別省去,編譯器會嘗試根據等號右邊的表示式推導變數的型別。

var name = "玄德"

等號右邊的部分在編譯原理裡被稱做右值。

簡短變數

除 var 關鍵字外,還可使用更加簡短的變數定義和初始化語法。

名字 := 表示式

編譯器會根據右值型別推斷出左值對應的型別

需要注意的是,簡短模式(short variable declaration)有以下限制:

  • 定義變數,同時顯式初始化。
  • 不能提供資料型別。
  • 只能用在函數內部。

因為簡潔和靈活的特點,簡短變數宣告被廣泛用於大部分的區域性變數宣告和初始化,var 形式的宣告語句往往是用於需要顯式指定變數型別地方,或者因為變數稍後會被重新賦值而初始值無關緊要的地方

注意:由於使用了:=,而不是=,因此推導宣告寫法的左值變數必須是沒有定義過的變數,若定義過,將會發生編譯錯誤

func main() {
   x:=100
   a,s:=1, "abc"
}

變數的交換

程式設計最簡單的演演算法之一,莫過於變數交換。交換變數的常見演演算法需要一箇中間變數進行變數的臨時儲存。用傳統方法編寫變數交換程式碼如下:

var a int = 100
var b int = 200
var t int
t = a
a = b
b = t
Println(a, b)

而在go語言,如今記憶體已經不再是緊缺資源,所以可以更簡單的實現:

var a int = 100
var b int = 200
b, a = a, b
Println(a, b)

匿名變數

匿名變數的特點是一個下畫線 __ 本身就是一個特殊的識別符號,被稱為空白識別符號。它可以像其他識別符號那樣用於變數的宣告或賦值(任何型別都可以賦值給它),但任何賦給這個識別符號的值都將被拋棄,因此這些值不能在後續的程式碼中使用,也不可以使用這個識別符號作為變數對其它變數進行賦值或運算。使用匿名變數時,只需要在變數宣告的地方使用下畫線替換即可。例如:

package main

func test() (int, int) {
	return 100, 200
}

func main() {
	a, _ := test()		
	_, b := test()		
	println(a, b)		//100, 200
}

在編碼過程中,可能會遇到沒有名稱的變數、型別或方法。雖然這不是必須的,但有時候這樣做可以極大地增強程式碼的靈活性,這些變數被統稱為匿名變數。

匿名變數不佔用記憶體空間,不會分配記憶體。匿名變數與匿名變數之間也不會因為多次宣告而無法使用。

變數的作用域

一個變數(常數、型別或函數)在程式中都有一定的作用範圍,稱之為作用域。

瞭解變數的作用域對我們學習Go語言來說是比較重要的,因為Go語言會在編譯時檢查每個變數是否使用過,一旦出現未使用的變數,就會報編譯錯誤。如果不能理解變數的作用域,就有可能會帶來一些不明所以的編譯錯誤。

根據變數定義位置的不同,可以分為以下三個型別:

  • 函數內定義的變數稱為區域性變數
  • 函數外定義的變數稱為全域性變數
  • 函數定義中的變數稱為形式引數

區域性變數

在函數體內宣告的變數稱之為區域性變數,它們的作用域只在函數體內,函數的引數和返回值變數都屬於區域性變數。

func main() {
	//宣告區域性變數 a 和 b 並賦值
	var a int = 3
	var b int = 4
	//宣告區域性變數 c 並計算 a 和 b 的和
	c := a + b
	fmt.Printf("a = %d, b = %d, c = %d\n", a, b, c)
}

全域性變數

在函數體外宣告的變數稱之為全域性變數,全域性變數只需要在一個原始檔中定義,就可以在所有原始檔中使用,當然,不包含這個全域性變數的原始檔需要使用「import」關鍵字引入全域性變數所在的原始檔之後才能使用這個全域性變數。

全域性變數宣告必須以 var 關鍵字開頭,如果想要在外部包中使用全域性變數的首字母必須大寫。

Go語言程式中全域性變數與區域性變數名稱可以相同,但是函數體內的區域性變數會被優先考慮。

// 宣告全域性變數
var a float32 = 3.14

func main() {
	//宣告區域性變數
	var a int = 3
	fmt.Printf("a = %d\n", a)
}

常數

常數是一個簡單的識別符號,在程式執行時,不會被修改的量

常數的資料只能是布林型、數位型(整數型、浮點型和複數)和字串型。

const name [type] = value

在Go語言中,你可以省略型別說明符 [type],因為編譯器可以根據變數的值來推斷其型別。

  • 顯式型別定義: const b string = "abc"
  • 隱式型別定義: const b = "abc"

多個相同型別的宣告可以簡寫為:

const c_name1, c_name2 = value1, value2

iota 常數計數器

常數宣告可以使用 iota 常數生成器初始化,它用於生成一組以相似規則初始化的常數,但是不用每行都寫一遍初始化表示式。

在一個 const 宣告語句中,第一個宣告的常數所在的行,iota 將會被置為 0,然後在每一個有常數宣告的行加一。

在其它程式語言中,這種型別一般被稱為列舉型別。

func main() {
	// iota 預設為0
	const (
		// 一組常數中,如果某個常數沒有初始值,預設和上一行一致
		a = iota // 0
		b        // 1
		c        // 2
	)
	// 下一個const語句重新計數
	const (
		// 常數被賦值不會打斷 iota 計數
		d = iota    // 0
		e = "hello" // hello
		f = iota    // 2
	)
	fmt.Println(a, b, c, d, e, f)
}

基本資料型別

Go語言是靜態型別語言,在 Go 程式語言中,資料型別用於宣告函數和變數,資料型別就是告訴編譯器要向系統申請一塊多大的記憶體空間,並知道這塊空間表示什麼

布林型別

一個布林型別的值只有兩種:true 或 false。if 和 for 語句的條件部分都是布林型別的值,並且==<等比較操作也會產生布林型的值。

func main() {

	// 預設值為 false
	var flag bool
	var flag1 bool = false
	var flag2 bool = true

	println(flag)
	println(flag1)
	println(flag2)
	
}

數位型別

整型 int 和浮點型 float32、float64,Go語言支援整型和浮點型數位,並且支援複數,其中位的運算採用二補數

Go也有基於架構的型別,例如:uint 無符號、 int有符號

  • 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)
func main() {

	// 定義一個整形
	var age int = 20
	fmt.Printf("%d \n", age)	// 20

	// 定義一個浮點型
	// 預設6位小數列印 3.140000
	var test float64 = 3.14
	fmt.Printf("%f", test)	// 3.140000

}

其他數位型別

  • byte 類似 uint8
  • rune 類似 int32
  • uint 32 或 64 位
  • int 與 uint 一樣大小
  • uintptr 無符號整形,用於存放指標

字元型別

字元有兩種形式,分別是字元和字串('' 和 「」)

字串是一種值型別,且值不可變,即建立某個文字後將無法再次修改這個文字的內容,更深入地講,字串是位元組的定長陣列。

一個字串是一個不可改變的位元組序列,字串可以包含任意的資料,但是通常是用來包含可讀的文字,字串是 UTF-8 字元的一個序列(當字元為 ASCII 碼錶上的字元時則佔用 1 個位元組,其它字元根據需要佔用 2-4 個位元組)。

與C++、Java、Python不同(Java 始終使用 2 個位元組),Go語言中字串也可能根據需要佔用 1 至 4 個位元組,這樣做不僅減少了記憶體和硬碟空間佔用,同時也不用像其它語言那樣需要對使用 UTF-8 字元集的文字進行編碼和解碼。

func main() {

	// \n 為跳脫字元,表示換行
	var str string
	str = "Hello,World"
	fmt.Printf("%T,%s\n", str, str)
    
    // 獲取指定的位元組
    fmt.Printf("第四個位元組是:" + "%c\n", str[4])

	// 單引號和雙引號區別,單引號 字元,ASCII字元碼
	v1 := 'A'
	v2 := "A"
	fmt.Printf("%T,%d\n", v1, v1)
	fmt.Printf("%T,%s\n", v2, v2)

	// 中國的編碼表:GBK
	// Unicode編碼表:號稱相容了全世界的文字
	v3 := '中'
	fmt.Printf("%T,%d\n", v3, v3)

	// 字串拼接
	println("hello" + ",xuande")
}

常用跳脫字元:

  • \n:換行符
  • \r:回車符
  • \t:tab 鍵
  • \u 或 \U:Unicode 字元
  • \:反斜槓自身

定義多行字串

在Go語言中,使用雙引號書寫字串的方式是字串常見表達方式之一,這種雙引號字面量不能跨行,如果想要在原始碼中嵌入一個多行字串時,就必須使用反引號(鍵盤上 1 鍵左邊的鍵,長這樣" ` ")

    const str = `第一行
    第二行
    第三行
    \r\n
    `
    fmt.Println(str)

UTF-8 和 Unicode 有何區別?

Unicode 與 ASCII 類似,都是一種字元集。

字元集為每個字元分配一個唯一的 ID,我們使用到的所有字元在 Unicode 字元集中都有一個唯一的 ID,例如上面例子中的 a 在 Unicode 與 ASCII 中的編碼都是 97。漢字「你」在 Unicode 中的編碼為 20320,在不同國家的字元集中,字元所對應的 ID 也會不同。而無論任何情況下,Unicode 中的字元的 ID 都是不會變化的。

UTF-8 是編碼規則,將 Unicode 中字元的 ID 以某種方式進行編碼,UTF-8 的是一種變長編碼規則,從 1 到 4 個位元組不等。編碼規則如下:

  • 0xxxxxx 表示文字元號 0~127,相容 ASCII 字元集。
  • 從 128 到 0x10ffff 表示其他字元。

根據這個規則,拉丁文語系的字元編碼一般情況下每個字元佔用一個位元組,而中文每個字元佔用 3 個位元組。

廣義的 Unicode 指的是一個標準,它定義了字元集及編碼規則,即 Unicode 字元集和 UTF-8、UTF-16 編碼等。

指標型別

資料型別轉換

在必要以及可行的情況下,一個型別的值可以被轉換成另一種型別的值。由於Go語言不存在隱式型別轉換,因此所有的型別轉換都必須顯式的宣告:

valueOfTypeB = typeB(valueOfTypeA)

型別 B 的值 = 型別 B(型別 A 的值)

func main() {

	a := 5.0    // float
	b := int(a) // 強轉為int

	fmt.Printf("%T,%f\n", a, a)
	fmt.Printf("%T,%d\n", b, b)

}

型轉換隻能在定義正確的情況下轉換成功,例如從一個取值範圍較小的型別轉換到一個取值範圍較大的型別(將 int16 轉換為 int32)。當從一個取值範圍較大的型別轉換到取值範圍較小的型別時(將 int32 轉換為 int16 或將 float32 轉換為 int),會發生精度丟失(截斷)的情況。

運運算元

  • 算術運運算元
  • 關係運算子
  • 邏輯運運算元
  • 位運運算元
  • 賦值運運算元
  • 其他運運算元

Go語言運運算元優先順序

先計算乘法後計算加法,說明乘法運運算元的優先順序比加法運運算元的優先順序高。所謂優先順序,就是當多個運運算元出現在同一個表示式中時,先執行哪個運運算元。

優先順序 分類 運運算元 結合性
1 逗號運運算元 , 從左到右
2 賦值運運算元 =、+=、-=、*=、/=、 %=、 >=、 <<=、&=、^=、|= 從右到左
3 邏輯或 || 從左到右
4 邏輯與 && 從左到右
5 按位元或 | 從左到右
6 按位元互斥或 ^ 從左到右
7 按位元與 & 從左到右
8 相等/不等 ==、!= 從左到右
9 關係運算子 <、<=、>、>= 從左到右
10 位移運運算元 <<、>> 從左到右
11 加法/減法 +、- 從左到右
12 乘法/除法/取餘 *(乘號)、/、% 從左到右
13 單目運運算元 !、*(指標)、& 、++、--、+(正號)、-(負號) 從右到左
14 字尾運運算元 ( )、[ ]、-> 從左到右

注意:優先順序值越大,表示優先順序越高

算術運運算元

簡單的加減乘除,取餘就是相除之後的餘數,++ 代表自增、-- 代表自減

func main() {

	var a int = 10
	var b int = 3

	// + - * / % ++ --
	println(a + b) // 13
	println(a - b) // 7
	println(a * b) // 30
	println(a / b) // 3
	println(a % b) // 1
	a++            // a = a + 1
	println(a)     // 11
	b--            // a = a - 1
	println(b)     // 12
	println(a, b)  // 11 2

}

關係運算子

關係運算子的結果都是布林值,它嚐嚐會出現在判斷語句當中

func main() {

	var a int = 11
	var b int = 10

	// == 等於 = 賦值
	// 關係運算子,結果都是bool(布林值)
	println(a == b) // 等於
	println(a != b) // 不等於
	println(a > b)
	println(a < b)
	println(a >= b)
	println(a <= b)

}

邏輯運運算元

邏輯運運算元等同於高中數學裡面的與、或、非

  • 邏輯與(&&):兩邊都是真(True),則條件為真(True),否則為假(False)
  • 邏輯或(||):兩邊有一個為 True,怎該條件為 True,否則為 False
  • 邏輯非( ! ):如果為 True,則變為 False,反之亦然
func main() {

	var a bool = true
	var b bool = false
	var c bool
	// 邏輯與(&&):兩邊都為真,結果為真,反之為假
	c = a && b
	println(c) // false
	// 邏輯或(||):兩邊都為假,結果為假,反之為真
	c = a || b
	println(c) // true
	// 邏輯非(!):改變布林值,真變假、假變真
	c = a
	println(c) // true
	c = !a
	println(c) // false

}

位運運算元

乍一看與邏輯運運算元很相似,但它倆可是天差地別,位運運算元是對二進位制進行操作

二進位制:通常用兩個不同的符號0(代表零)和1(代表一)來表示,我們平常使用的是十進位制,逢十進一;二進位制是逢二進一

十進位制 二進位制
0 0
1 1
2 10
3 11
...... .....
10 1010
11 1011

所有的位運算都是建立在二進位制上的

  • 按位元與( & ):都為1結果為1,否則為0
  • 按位元或( | ):都為0結果為0,否則為1
  • 按位元互斥或( ^ ):不同為1,相同為0
  • 左移運運算元( << ):二進位制位全部左移若干位
  • 右移運運算元( >> ):二進位制位全部右移若干位
func main() {
	/**

	  位運算案例
	60	0011 1100	|	60 0011 1100
	13	0000 1101	|	------------
	-------------	|	<< 0111 1000
	&	0000 1100	|	>> 0001 1110
	|	0011 1101	|
	^	0011 0001	|

	*/
	var a uint = 60
	var b uint = 13
	var c uint = 0

	c = a & b
	fmt.Printf("十進位制:%d, 二進位制:%b\n", c, c) // 十進位制:12, 二進位制:1100
	c = a | b
	fmt.Printf("十進位制:%d, 二進位制:%b\n", c, c) // 十進位制:61, 二進位制:111101
	c = a ^ b
	fmt.Printf("十進位制:%d, 二進位制:%b\n", c, c) // 十進位制:49, 二進位制:110001

	c = a << 2
	fmt.Printf("十進位制:%d, 二進位制:%b\n", c, c) // 十進位制:240, 二進位制:11110000
	c = a >> 2
	fmt.Printf("十進位制:%d, 二進位制:%b\n", c, c) // 十進位制:15, 二進位制:1111
}

賦值運運算元

簡化操作,這裡演示一下+=和-=,其他同理

func main() {

	var a int = 66
	var b int

	// = 賦值
	b = a
	println(b) // 66
	// += 等效於 b = a + b
	b += a
	println(b) // 132
	// -= 等效於 b = a - b
	b -= a
	println(b) // 66
	
}

其他運運算元

  • &:返回變數儲存的地址
  • *:指標變數

指標


流程控制

程控制是每種程式語言控制邏輯走向和執行次序的重要部分,流程控制可以說是一門語言的「經脈」

Go 語言的常用流程控制有 if 和 for,而 switch 和 goto 主要是為了簡化程式碼、降低重複程式碼而生的結構,屬於擴充套件類的流程控制。

程式的流程結構分為三種:順序結構、選擇結構、迴圈結構

順序結構:從上到下,逐行執行

選擇結構:條件滿足才會繼續執行

  • if
  • switch
  • select

迴圈結構:條件滿足會被反覆執行0~n次

  • for

選擇結構

  • if
  • switch
  • select

if語句

在Go語言中,關鍵字 if 是用於測試某個條件(布林型或邏輯型)的語句,如果該條件成立,則會執行 if 後由大括號{}括起來的程式碼塊,否則就忽略該程式碼塊繼續執行後續的程式碼。

func main() {

	var score int
	fmt.Println("請輸入成績:")
	fmt.Scanln(&score) // 對score的地址進行賦值
	fmt.Printf("你輸入的成績為:%d\n", score)
	if score >= 90 && score <= 100 {
		fmt.Println("評級為:A")
	} else if score >= 80 && score < 90 {
		fmt.Println("評級為:B")
	} else if score >= 70 && score < 80 {
		fmt.Println("評級為:C")
	} else if score >= 60 && score < 70 {
		fmt.Println("評級為:D")
	} else {
		fmt.Println("評級為:不及格")
	}
	
}

if巢狀語句

有時一個判斷條件是無法滿足需求的,這時就需要if語句巢狀了

if 布林表示式1 {
	// 布林表示式1為true時執行
	if 布林表示式2 {
		//布林表示式2為true時執行
	}
}

switch語句

Go語言 switch 語句的表示式不需要為常數,甚至不需要為整數,case 按照從上到下的順序進行求值,直到找到匹配的項,如果 switch 沒有表示式,則對 true 進行匹配,因此,可以將 if else-if else 改寫成一個 switch。

Go語言改進了 switch 的語法設計,每一個case分支都是唯一的,從上到下依次測試,因此不需要通過 break 語句跳出當前 case 程式碼塊以避免執行到下一行。

func main() {

	var a = "hello"
	switch a {
	case "hello":
		fmt.Println(1)		// 匹配成功,輸出1,終止switch
	case "world":
		fmt.Println(2)		// 不會執行
	default:
		fmt.Println(0)		// case分支全部匹配失敗時,執行該語句
	}

}

Go語言的 switch 語句可以一分支多值,也可以case後面新增表示式

  • 一分支多值

        var a = "mum"
        switch a {
        case "mum", "daddy":	// 注意逗號
            fmt.Println("family")
        }
    
  • 分支表示式

        var r int = 11
        switch {				// 預設匹配itrue
        case r > 10 && r < 20:
            fmt.Println(r)
        }
    

fallthrough——相容C語言的 case 設計

在Go語言中 case 是一個獨立的程式碼塊,執行完畢後不會像C語言那樣緊接著執行下一個 case,但是為了相容一些移植程式碼,依然加入了 fallthrough 關鍵字來實現這一功能。

    var s = "hello"
    switch {
    case s == "hello":
        fmt.Println("hello")
        fallthrough				// case穿透,不管下個條件是否滿足,都會執行
    case s != "world":
        fmt.Println("world")
    }

迴圈結構

與多數語言不同的是,Go語言中的迴圈語句只支援 for 關鍵字,而不支援 while 和 do-while 結構,關鍵字 for 的基本使用方法與C語言和C++中非常接近

  • for
  • break

for語句

for迴圈是一個迴圈控制結構,可以執行指定次數的迴圈。

func main() {
	sum := 0
    // for 條件起始值; 迴圈條件; 操作控制變數
	for i := 0; i <= 6; i++ {
		sum += i
	}
	fmt.Println(sum)			// 21
}

可以看到比較大的一個不同在於 for 後面的條件表示式不需要用圓括號()括起來,Go語言還進一步考慮到無限迴圈的場景,讓開發者不用寫無聊的 for(;;){}do{} while(1);

func main() {
	sum := 0
	for {
		sum++
		fmt.Println(sum) 		// 無限迴圈(記得停止,否則會卡死)
	}
}

continue 結束本次迴圈,開啟下一次迴圈

Go語言中 continue 語句可以結束當前迴圈,開始下一次的迴圈迭代過程,僅限在 for 迴圈內使用,在 continue 語句後新增標籤時,表示開始標籤對應的迴圈

func main() {

	for i := 1; i <= 6; i++ {
		if i == 5 {
			continue
		}
		fmt.Println(i)
	}

	fmt.Println("==============我是分界線(~ ̄▽ ̄~)=================")

OuterLoop:
	for i := 0; i < 2; i++ {
		for j := 0; j < 5; j++ {
			switch j {
			case 2:
				fmt.Println(i, j)
				continue OuterLoop // 跳轉到標籤
			}
		}
	}

}

break

Go語言中 break 語句可以結束 for、switch 和 select 的程式碼塊,另外 break 語句還可以在語句後面新增標籤,表示退出某個標籤對應的程式碼塊,標籤要求必須定義在對應的 for、switch 和 select 的程式碼塊上。

func main() {
	sum := 0
	for {
		sum++
		if sum > 100 {
			break // 跳出迴圈
		}
	}
	fmt.Println(sum)
}

鍵值迴圈

for range 結構是Go語言特有的一種的迭代結構,在許多情況下都非常有用,for range 可以遍歷陣列、切片、字串、map 及通道(channel),for range 語法上類似於其它語言中的 foreach 語句。

func main() {
	var str string = "hello,xuande"
    // 返回下標和對應的值
	for key, val := range str {
		fmt.Printf("key:%d val:%c\n", key, val)
	}
}

案例

  • 九九乘法表

    func main() {
    	// 遍歷, 決定處理第幾行
    	for y := 1; y <= 9; y++ {
    		// 遍歷, 決定這一行有多少列
    		for x := 1; x <= y; x++ {
    			fmt.Printf("%d*%d=%d ", x, y, x*y)
    		}
    		// 手動生成回車
    		fmt.Println()
    	}
    }
    
  • 列印6x6方陣

    func main() {
    	for j := 0; j < 5; j++ {
    		for i := 0; i <= 5; i++ {
    			fmt.Print("* ")
    		}
    		fmt.Println()
    	}
    }
    
  • 列印松樹

    func main() {
    	// 上半部分
    	for i := 0; i < 5; i++ {
    		for j := 5; j >= i; j-- {
    			print(" ")
    		}
    		for k := 0; k < 2*i+1; k++ {
    			print("*")
    		}
    		println()
    	}
    	// 下半部分
    	for i := 0; i < 2; i++ {
    		for j := 0; j < 5; j++ {
    			print(" ")
    		}
    		for k := 0; k < 3; k++ {
    			print("*")
    		}
    		println()
    	}
    }
    
  • 氣泡排序

    package main
    
    import "fmt"
    
    func main() {
    	// 定義切片,進行參照傳遞
    	arr := []int{5, 1, 4, 2, 8}
    	// 使用協程加快速度
    	go bubbleSort(arr)
    	fmt.Println(arr)
    }
    
    func bubbleSort(arr []int) {
    	var flag = true
    	var n = len(arr)
    	for i := 0; i < n-1; i++ {
    		for j := 0; j < n-i-1; j++ {
    			if arr[j] > arr[j+1] {
    				temp := arr[j]
    				arr[j] = arr[j+1]
    				arr[j+1] = temp
    				flag = false
    			}
    		}
    		if flag {
    			continue
    		}
    	}
    }
    
    

函數

函數是組織好的、可重複使用的、用來實現單一或相關聯功能的程式碼段,其可以提高應用的模組性和程式碼的重複利用率。

Go 語言支援普通函數、匿名函數和閉包,從設計上對函數進行了優化和改進,讓函數使用起來更加方便。

Go 語言函數:

  • 函數是基本的程式碼塊,用於執行一個任務
  • Go語言最少有個main()函數
  • 函數本身可以作為值進行傳遞
  • 支援匿名函數和閉包(closure)
  • 函數可以滿足介面
  • 可以通過函數來劃分不同的功能,邏輯上每個函數執行的是指定的任務
  • 函數宣告告訴了編譯器函數的名稱、返回型別和引數

函數的宣告

函數構成了程式碼執行的邏輯結構,在Go語言中,函數的基本組成為:關鍵字 func、函數名、參數列、返回值、函數體和返回語句,每一個程式都包含很多的函數,函數是基本的程式碼塊。

Go語言裡面擁三種型別的函數:

  • 普通的帶有名字的函數
  • 匿名函數或者 lambda 函數
  • 方法

Go語言函數定義格式如下:

func 函數名(形參列表)(返回值列表){
    函數體
}

形式參數列描述了函數的引數名以及引數型別,這些引數作為區域性變數,其值由引數呼叫者提供,形式引數簡單來說就是用來接收外部資料的引數。

返回值列表描述了函數返回值的變數名以及型別,如果函數返回一個無名變數或者沒有返回值,返回值列表的括號是可以省略的。

func main() {
    // 實際引數:4,2
    // 形參和實參必須對應
    result := add(4, 2)
	print(result)		// 列印結果:6
}

// 定義一個相加函數add(函數名)
// 形式引數:a,b;返回值列表:int;
func add(a, b int) int {
	c := a + b
    // 如果函數有返回值,那麼就必須使用return語句
	return c
}

函數存取規則

函數名首字母小寫,只能在本包存取,函數名首字母大寫,可以在本包和其他包存取

內建函數

Golang設計者為了程式設計方便,提供了一些函數,這些函數由於不用導包就可以直接使用,所以這些函數被稱為內建函數/內建函數

內建函數位置:builtin包下

常用函數

  • len函數:統計字串的長度,按位元組進行統計
  • new函數:分配記憶體,主要用來分配值型別(int、float、string、array、struct)
  • make函數:分配記憶體,主要用來分配參照型別(指標、切片、管道、介面等)

可變引數

可變引數是指函數傳入的引數個數是可變的(型別確定,個數不確定)

func main() {
	fmt.Println("傳入2個引數:")
	getSum(1, 2)
	fmt.Println("傳入3個引數:")
	getSum(1, 2, 66)
}

// ... 可變引數
func getSum(args ...int) {
	sum := 0
	for _, arg := range args {
		sum += arg
		fmt.Println(arg)
	}
	fmt.Println("sum:", sum)
}

注意:

  • 可變引數只能放在參數列的最後
  • 一個函數最多隻有一個可變引數

任意型別的可變引數

之前的例子中將可變引數型別約束為 int,如果你希望傳任意型別,可以指定型別為 interface{},下面是Go語言標準庫中 fmt.Printf() 的函數原型:

func Printf(format string, args ...interface{}) {
    // ...
}

用 interface{} 傳遞任意型別資料是Go語言的慣例用法,使用 interface{} 仍然是型別安全的,這和 C/C++ 不太一樣

引數傳遞

資料的儲存特點分為:

  • 值傳遞:操作的是資料的本身,int、string、bool、float64、array。。。
  • 參照傳遞:操作的是資料的地址,slice、map、chan。。。

值傳遞

使用普通變數作為函數引數的時候,在傳遞引數時只是對變數值得拷貝,即將實參的值複製給變參,當函數對變參進行處理時,並不會影響原來實參的值。

func main() {
    // 定義陣列
	arr1 := [4]int{1, 2, 3, 4}
	fmt.Println("預設資料arr1", arr1) 				// 預設資料arr1 [1 2 3 4]
	update(arr1)
	fmt.Println("呼叫函數後資料arr1", arr1) 		 // 呼叫函數後資料arr1 [1 2 3 4]
}

func update(arr2 [4]int) {
	fmt.Println("接受資料arr2:", arr2) 		 // 接受資料arr2: [1 2 3 4]
	arr2[0] = 66
	fmt.Println("修改後資料arr2:", arr2) 	// 修改後資料arr2: [66 2 3 4]
}

解析:

  • arr2的資料是從arr1複製的,所以它倆處在不同的記憶體空間
  • 由於是拷貝的副本,所以修改arr2並不會影響arr1
  • 值型別的資料,預設都是值傳遞:基礎型別、array、struct

參照傳遞

函數的變數不僅可以使用普通變數,還可以使用指標變數,使用指標變數作為函數的引數時,在進行引數傳遞時將是一個地址,即將實參的記憶體地址複製給變參,這時對變參的修改也將會影響到實參的值。

func main() {
    // 定義切片
	s1 := []int{1, 2, 3, 4}
	fmt.Println("預設資料s1", s1) // 預設資料s1 [1 2 3 4]
	update(s1)
	fmt.Println("呼叫函數後資料s1", s1) // 呼叫函數後資料s1 [66 2 3 4]
}

func update(s2 []int) {
	fmt.Println("接受資料s2:", s2) // 接受資料s2: [1 2 3 4]
	s2[0] = 66
	fmt.Println("修改後資料s2:", s2) // 修改後資料s2: [66 2 3 4]
}

解析:

  • 參照傳遞傳遞的是記憶體地址
  • 由於傳遞的是地址,所以是同一片記憶體空間
  • 修改變參s2會影響到實參s1

遞迴函數

很對程式語言都支援遞迴函數,Go語言也不例外,所謂遞迴函數指的是在函數內部呼叫函數自身的函數,從數學解題思路來說,遞迴就是把一個大問題拆分成多個小問題,再各個擊破,在實際開發過程中,遞迴函數可以解決許多數學問題,如計算給定數位階乘、產生斐波系列等。

構成遞迴需要具備以下條件:

  • 一個問題可以被拆分成多個子問題;
  • 拆分前的原問題與拆分後的子問題除了資料規模不同,但處理問題的思路是一樣的;
  • 不能無限制的呼叫本身,子問題需要有退出遞迴狀態的條件。

注意:編寫遞迴函數時,一定要有終止條件,否則就會無限呼叫下去,直到記憶體溢位。

斐波那鍥數列

func main() {
    result := 0
    for i := 1; i <= 10; i++ {
        result = fibonacci(i)
        fmt.Printf("fibonacci(%d) is: %d\n", i, result)
    }
}
func fibonacci(n int) (res int) {
    if n <= 2 {
        res = 1
    } else {
        res = fibonacci(n-1) + fibonacci(n-2)
    }
    return
}

函數變數

在Go語言中,函數也是一種型別,可以和其他型別一樣儲存在變數中,下面的程式碼定義了一個函數變數 f,並將一個函數名為 fire() 的函數賦給函數變數 f,這樣呼叫函數變數 f 時,實際呼叫的就是 fire() 函數

func main() {
   var f func()
   f = fire
   f()
}

func fire() {
   fmt.Println("fire")
}

匿名函數

Go語言支援匿名函數,即在需要使用函數時再定義函數,匿名函數沒有函數名只有函數體,函數可以作為一種型別被賦值給函數型別的變數,匿名函數也往往以變數方式傳遞,Go語言支援隨時在程式碼裡定義匿名函數。

匿名函數是指不需要定義函數名的一種函數實現方式,由一個不帶函數名的函數宣告和函數體組成。

匿名函數的定義格式如下:

func(參數列)(返回參數列){
    函數體
}

匿名函數的定義就是沒有名字的普通函數定義。

func main() {
   // 匿名函數賦值給變數
   f := func() {
      fmt.Println("我是匿名函數")
   }
   // 呼叫匿名函數
   f()

   // 定義時呼叫
   func(data int) {
      fmt.Println("xuande", data)
   }(666)
}

回撥函數

根據Go語言資料型別的特點,可以將一個函數作為另外一個函數的引數

func main() {
	// 使用匿名函數列印切片內容
	visit([]int{1, 2, 3, 4}, func(v int) {
		fmt.Println(v)
	})
}

// 遍歷切片的每個元素, 通過給定函數進行元素存取
func visit(list []int, f func(int)) {
	for _, v := range list {
		f(v)
	}
}

defer

Go語言的 defer 語句會將其後面跟隨的語句進行延遲處理,在 defer 歸屬的函數即將返回時,將延遲處理的語句按 defer 的逆序進行執行,也就是說,先被 defer 的語句最後被執行,最後被 defer 的語句,最先被執行。

func main() {
	f("1")
	fmt.Println("2")
	defer f("3")			// 會被延遲到最後執行
	fmt.Println("4")
}

func f(s string) {
	fmt.Println(s)
}

多個defer處理順序

當有多個 defer 行為被註冊時,它們會以逆序執行(類似棧,即後進先出)

func main() {
    fmt.Println("defer begin")
    // 將defer放入延遲呼叫棧
    defer fmt.Println(1)
    defer fmt.Println(2)
    // 最後一個放入, 位於棧頂, 最先呼叫
    defer fmt.Println(3)
    fmt.Println("defer end")
}

解析:

  • 程式碼的延遲順序與最終的執行順序是反向的。
  • 延遲呼叫是在 defer 所在函數結束時進行,函數結束可以是正常返回時,也可以是發生宕機時。

案例:對檔案關閉的操作時使用defer

    func fileSize(filename string) int64 {
        f, err := os.Open(filename)
        if err != nil {
            return 0
        }
        // 延遲呼叫Close, 此時Close不會被呼叫
        defer f.Close()
        info, err := f.Stat()
        if err != nil {
            // defer機制觸發, 呼叫Close關閉檔案
            return 0
        }
        size := info.Size()
        // defer機制觸發, 呼叫Close關閉檔案
        return size
    }

recove

Go語言的recover函數用於捕獲和處理執行時異常,可以在程式中捕獲到panic異常,並執行相應的處理操作,從而避免程式崩潰。

package main

import (
	"fmt"
)

func main() {
	division()
	fmt.Println("上面錯誤已捕獲,我依舊能執行")
}

func division() {
	// 捕獲錯誤並處理
	defer func() {
		err := recover()
		if err != nil {
			fmt.Println("division函數出現錯誤", err)
		}
	}()
	num1 := 10
	num2 := 0
	result := num1 / num2
	fmt.Println(result)
}

閉包

Go語言中閉包是參照了自由變數的函數,被參照的自由變數和函數一同存在,即使已經離開了自由變數的環境也不會被釋放或者刪除,在閉包中可以繼續使用這個自由變數,因此,簡單的說:

函數 + 參照環境 = 閉包

一個函數型別就像結構體一樣,可以被範例化,函數本身不儲存任何資訊,只有與參照環境結合後形成的閉包才具有「記憶性」,函數是編譯期靜態的概念,而閉包是執行期動態的概念。

閉包(Closure)在某些程式語言中也被稱為 Lambda 表示式。

閉包結構中的外層函數的區域性變數不會隨著外層函數的結束而銷燬,因為內層函數還在繼續使用

func main() {
	r1 := inc()		
	fmt.Println(r1())	// 1
	fmt.Println(r1())	// 2
	fmt.Println(r1())	// 3
	r2 := inc()
	fmt.Println(r2())	// 1
	fmt.Println(r2())	// 2
	fmt.Println(r1())	// 4
}

// 自增函數
func inc() func() int {
	// 區域性變數
	i := 0
	// 匿名函數
	fun := func() int { // 內層函數
		i++
		return i
	}
	return fun
}

閉包實現生成器

func main() {
   // 建立一個玩家生成器
   generator := playerGen("xuande")
   // 返回玩家的名字和血量
   name, hp := generator()
   // 列印值
   fmt.Println(name, hp)
}

// 建立一個玩家生成器, 輸入名稱, 輸出生成器
func playerGen(name string) func() (string, int) {
   // 血量一直為150
   hp := 150
   // 返回建立的閉包
   return func() (string, int) {
      // 將變數參照到閉包中
      return name, hp
   }
}

容器

變數在一定程度上能滿足函數及程式碼要求。如果編寫一些複雜演演算法、結構和邏輯,就需要更復雜的型別來實現。這類複雜型別一般情況下具有各種形式的儲存和處理資料的功能,將它們稱為「容器(container)」。

在很多語言裡,容器是以標準庫的方式提供,你可以隨時檢視這些標準庫的程式碼,瞭解如何建立,刪除,維護記憶體。

陣列(array)

陣列是一個由固定長度的特定型別元素組成的序列,一個陣列可以由零個或多個元素組成。因為陣列的長度是固定的,所以在Go語言中很少直接使用陣列(陣列是值傳遞)。

陣列的定義

陣列定義格式如下

var 陣列變數名 [元素數量]Type

語法說明如下所示:

  • 陣列變數名:陣列宣告及使用時的變數名。
  • 元素數量:陣列的元素數量,可以是一個表示式,但最終通過編譯期計算的結果必須是整型數值,元素數量不能含有到執行時才能確認大小的數值。
  • Type:可以是任意基本型別,包括陣列本身,型別為陣列本身時,可以實現多維陣列。

陣列的每個元素都可以通過索引下標來存取,索引下標的範圍是從 0 開始到陣列長度減 1 的位置,內建函數 len() 可以返回陣列中元素的個數。

預設情況下,陣列的每個元素都會被初始化為元素型別對應的零值,對於數位型別來說就是 0

func main() {
	// 定義陣列,預設為對應元素的零值
	var a [3]int
    // 僅列印元素
	for _, v := range a {
		fmt.Printf("未賦值,元素:%d\n", v)
	}
	// 陣列賦值
	a = [3]int{1, 2, 3}
	// 列印索引和元素
	for i, v := range a {
		fmt.Printf("索引:%d,元素:%d\n", i, v)
	}
	// 僅列印元素
	for _, v := range a {
		fmt.Printf("元素:%d\n", v)
	}
}

使用」...」省略號,表示陣列的長度是根據初始化值的個數來計算

func main() {
	q := [...]int{1, 2, 3}
	fmt.Printf("%T\n", q) // "[3]int"
}

多維陣列

Go語言中允許使用多維陣列,因為陣列屬於值型別,所以多維陣列的所有維度都會在建立時自動初始化零值,多維陣列尤其適合管理具有父子關係或者與座標系相關聯的資料。

宣告多維陣列的語法如下所示:

var 多維陣列變數名 [每一維度的長度][每一維度的長度]...[每一維度的長度] 陣列型別
func main() {
	// 宣告一個二維整型陣列,兩個維度的長度分別是 4 和 2
	var array [4][2]int
	// 使用陣列字面量來宣告並初始化一個二維整型陣列
	array = [4][2]int{{10, 11}, {20, 21}, {30, 31}, {40, 41}}
	// 列印二維陣列
	for i := range array {
		for _, j := range array[i] {
			println(j)
		}
	}
}

切片(slice)

陣列雖然有特定的用處,但因為陣列的長度固定不變,這就顯得有些呆,所以Go語言就有一種特殊的資料型別切片(slice)

切片預設指向一段連續記憶體區域,可以是陣列,也可以是切片本身,另外切片是可以動態增長的。

切片是對陣列的一個連續片段的參照,所以切片是一個參照型別,這個片段可以是整個陣列,也可以是由起始和終止索引標識的一些項的子集,需要注意的是,終止索引標識的項不包括在切片內。切片提供了一個相關陣列的動態視窗

由於切片是對陣列的一個連續片段的參照,所以切片有3個欄位的資料結構:

  • 指向底層陣列的指標
  • 切片的長度
  • 切片的容量

切片的定義

從陣列或切片生成新的切片

從連續記憶體區域生成切片是常見的操作

slice [開始位置 : 結束位置]

語法說明如下:

  • slice:表示目標切片物件;
  • 開始位置:對應目標切片物件的索引;
  • 結束位置:對應目標切片的結束索引。
func main() {
	// 定義陣列
	var arr = [6]int{1, 2, 3, 4, 5, 6}
	// 從陣列切片(左閉右開)
	slice := arr[1:3]
	// 列印陣列
	fmt.Println("arr:", arr) 						  // arr: [1 2 3 4 5 6]
	// 列印切片
	fmt.Println("slice:", slice) 					  // slice: [2 3]
	// 切片的元素個數
	fmt.Println("切片的元素個數:", len(slice)) 		  // 2
	// 獲取切片的容量,容量可以動態變化
	fmt.Println("切片的容量:", cap(slice)) 		 	   // 5
    fmt.Printf("陣列中下標為1的地址:%p:\n", &arr[1])		// 0xc0000c8038
	fmt.Printf("切片中下標為0的地址:%p:\n", &slice[0])	// 0xc0000c8038
	slice[1] = 66
	fmt.Println("arr:", arr)     					  // arr: [1 2 66 4 5 6]
	fmt.Println("slice:", slice) 					  // slice: [2 66]
}

make()函數構造切片

如果需要動態地建立一個切片,可以使用 make() 內建函數,格式如下:

make( []Type, size, cap )

語法說明如下:

  • Type:切片型別
  • size: 切片長度
  • cap: 切片容量,不影響 size,主要目的是降低多次分配空間而造成的效能問題。
func main() {
	slice := make([]int, 6, 10)
	fmt.Println(slice)               	// [0 0 0 0 0 0]
	fmt.Println("切片長度:", len(slice)) // 切片長度: 6
	fmt.Println("切片容量:", cap(slice)) // 切片容量: 10
}

注:make底層維護一個陣列,但是這個陣列對外不可見,無法操作這個陣列

直接宣告新的切片

定義一個切片,直接指定具體的陣列,原理類似make()

func main() {
	slice := []int{1, 2, 3, 4, 5, 6}
	fmt.Println(slice)               	// [1 2 3 4 5 6]
	fmt.Println("切片長度:", len(slice)) // 切片長度: 6
	fmt.Println("切片容量:", cap(slice)) // 切片容量: 6
}

追加函數

Go語言的內建函數 append() 可以為切片動態新增元素

func main() {
	var slice []int
	slice = append(slice, 1) // 追加1個元素
	fmt.Println(slice)	// [1]
	slice = append(slice, 1, 2, 3) // 追加多個元素, 手寫解包方式
	fmt.Println(slice)	// [1 1 2 3]
	slice = append(slice, []int{1, 2, 3}...) // 追加一個切片, 切片需要解包
	fmt.Println(slice)  // [1 1 2 3 1 2 3]
}

切片的擴容機制

在新增元素時,如果空間不足,切片就會進行擴容,此時切片長度將會發生改變,擴容的規律是按容量的 2 倍數進行擴容

擴容原理:

  • 建立一個新陣列,將老陣列的值複製到新陣列中
  • 在新陣列中追加值
  • 底層指標由指向老陣列改為指向新陣列
  • 底層的新陣列不能直接維護,需要通過切片間接維護

列表(list)

列表是一種非連續的儲存容器,由多個節點組成,節點通過一些變數記錄彼此之間的關係,列表有多種實現方法,如單連結串列、雙連結串列等。

在Go語言中,列表使用 container/list 包來實現,內部的實現原理是雙連結串列,列表能夠高效地進行任意位置的元素插入和刪除操作。

列表的定義

列表定義格式如下:

  1. 通過 container/list 包的 New() 函數初始化 list

    變數名 := list.New()
    
  2. 通過 var 關鍵字宣告初始化 list

    var 變數名 list.List
    

列表沒有具體元素型別的限制,因此,列表的元素可以是任意型別,但這既帶來了便利,也引來一些問題,例如轉換某種特定的型別時將會發生宕機。

func main() {
   // 建立一個列表
   l := list.New()
   // 插入列表尾部
   l.PushBack("first")
   l.PushBack("second")
   // 插入列表頭部
   l.PushFront("head")
   // 遍歷列表
   for i := l.Front(); i != nil; i = i.Next() {
      fmt.Println(i.Value)			// head first second
   }
}

從列表中刪除元素

列表插入函數的返回值會提供一個 *list.Element 結構,這個結構記錄著列表元素的值以及與其他節點之間的關係等資訊,從列表中刪除元素時,需要用到這個結構進行快速刪除。

func main() {
	// 建立一個列表
	l := list.New()
	// 尾部新增後儲存元素控制程式碼
	element := l.PushBack("first")
	// 在fist之後新增second
	l.InsertAfter("second", element)
	// 在fist之前新增head
	l.InsertBefore("head", element)
	println("刪除前:")
	// 遍歷列表
	for i := l.Front(); i != nil; i = i.Next() {
		fmt.Println(i.Value)		// head first second
	}
	// 刪除first元素
	l.Remove(element)
	println("刪除後:")
	// 遍歷列表
	for i := l.Front(); i != nil; i = i.Next() {
		fmt.Println(i.Value)		// head second
	}
}

對映(map)

對映(map)是一種特殊的資料結構,它內建在Go語言中 ,它有一個 key(索引)和一個 value(值),我們將它成為鍵值對,所以這個結構也稱為關聯陣列或字典,這是一種能夠快速尋找值的理想結構,給定 key,就可以迅速找到對應的 value。

map 這種資料結構在其他程式語言中也稱為字典(Python)、hashmap(Java)

對映的定義

map 是參照型別,它的宣告方式如下:

var mapname map[keytype]valuetype

語法說明如下:

  • mapname 為 map 的變數名。
  • keytype 為鍵型別。
  • valuetype 是鍵對應的值型別。

在宣告的時候不需要知道 map 的長度,因為 map 是可以動態增長的,未初始化的 map 的值是 nil,使用函數 len() 可以獲取 map 中 pair 的數目。

func main() {
	// 定義map變數
	var m map[string]string
	// 只宣告map記憶體是沒有分配空間的
	// 必須通過make函數進行初始化,才會分配空間,map的長度可以動態增長
	m = make(map[string]string, 10)
	// 鍵值對存入map
	m["key1"] = "張三"
	m["key2"] = "李四"
	m["key3"] = "王五"
	// 輸出集合
	fmt.Println(m) 		// map[key1:張三 key2:李四 key3:王五]
	m["key1"] = "阿巴阿巴阿巴"
	// 重複的key將會被覆蓋
	fmt.Println(m) 		// map[key1:阿巴阿巴阿巴 key2:李四 key3:王五]
	// value可以重複
	m["key4"] = "李四"
	fmt.Println(m) 		// map[key1:阿巴阿巴阿巴 key2:李四 key3:王五 key4:李四]
}

使用map的注意事項:

  • map使用前一定要make
  • map的key-value是無序的
  • key不可以重複,重複會被最後一個value覆蓋
  • value是可以重複的

map的容量

與陣列不同,map 可以根據新增的 key-value 動態的擴容,因此它不存在固定長度或者最大限制,但是也可以選擇標明 map 的初始容量 capacity,如果不標明cap,它將預設分配一個記憶體空間

make(map[keytype]valuetype, cap)

當 map 增長到容量上限的時候,如果再增加新的 key-value,map 的大小會自動加 1,所以出於效能的考慮,對於大的 map 或者會快速擴張的 map,即使只是大概知道容量,也最好先標明。

操作map

func main() {
	// 定義map
	m := make(map[string]string)
	// 增
	m["k1"] = "v1"
	m["k2"] = "v2"
	m["k3"] = "v3"
	fmt.Println("增:", m)
	// 刪
	delete(m, "k3")
	fmt.Println("刪:", m)
	// 改
	m["k2"] = "vv2"
	fmt.Println("改:", m)
	// 查
	value, flag := m["k2"]
	fmt.Println("查:", m)
	fmt.Println(value)
	fmt.Println(flag)
}

遍歷map

map的遍歷只能用for-range迴圈

func main() {
	scene := make(map[string]int)
	scene["route"] = 66
	scene["brazil"] = 4
	scene["china"] = 960
	for k, v := range scene {
		fmt.Println(k, v)
	}
}

map的清空機制

Go語言中並沒有為 map 提供任何清空所有元素的函數、方法,清空 map 的唯一辦法就是重新 make 一個新的 map,不過不用擔心垃圾回收的效率,Go語言中的並行垃圾回收效率比寫一個清空函數要高效的多。


物件導向

Go語言也支援物件導向程式設計(OOP),但它和傳統的物件導向程式設計有些許區別,並不是純粹的物件導向語言。

Go 語言中的型別可以被範例化,使用new&構造的型別範例的型別是型別的指標。

Go語言中並沒有類(class),但它的結構體(struct)和其他程式語言的類有同等的地位,結構體的內嵌配合介面比物件導向具有更高的擴充套件性和靈活性,結構體不僅能擁有方法,而且每種自定義型別也可以擁有自己的方法。

結構體

Go語言可以通過自定義的方式形成新的型別,結構體就是這些型別中的一種複合型別,結構體是由零個或多個任意型別的值聚合成的實體,每個值都可以稱為結構體的成員。

Go語言的結構體和Java中的類有相同的地位,甚至比java中的類更靈活

結構體定義

結構體成員也可以稱為「欄位」,這些欄位有以下特性:

  • 欄位擁有自己的型別和值;
  • 欄位名必須唯一;
  • 欄位的型別也可以是結構體,甚至是欄位所在結構體的型別。

使用關鍵字 type 可以將各種基本型別定義為自定義型別,基本型別包括整型、字串、布林等。結構體是一種複合的基本型別,通過 type 定義為自定義型別後,使結構體更便於使用。

結構體的宣告方式如下:

type 型別名 struct {
    欄位1 欄位1型別
    欄位2 欄位2型別
    …
}

語法說明如下:

  • 型別名:標識自定義結構體的名稱,在同一個包內不能重複。
  • struct{}:表示結構體型別,type 型別名 struct{}可以理解為將 struct{} 結構體定義為型別名的型別。
  • 欄位1、欄位2……:表示結構體欄位名,結構體中的欄位名必須唯一。
  • 欄位1型別、欄位2型別……:表示結構體各個欄位的型別。

結構體的定義只是一種記憶體佈局的描述,只有當結構體範例化時,才會真正地分配記憶體

結構體範例

結構體必須在定義並範例化後才能使用結構體的欄位。

範例化就是根據結構體定義的格式建立一份與格式一致的記憶體區域,結構體各個範例間的記憶體是完全獨立的。

基本的範例化方式

基本範例化格式如下:

var 範例 結構體型別

這是最常見的範例化方式,直接建立

// Student 定義學生結構體,將學生的各個屬性統一放到結構體中管理
type Student struct {
	Name   string
	Age    int
	School string
}

func main() {
	// 建立學生結構體範例
	var s Student
	// 結構體賦值,使用點(.)來存取結構體的成員變數
	s.Name = "玄德"
	s.Age = 20
	s.School = "清北幼兒園"
	fmt.Println(s)		// {玄德 20 清北幼兒園}
}

建立指標結構體

Go語言中,還可以使用 new 關鍵字對型別進行範例化,結構體在範例化後會形成指標型別的結構體。

建立格式如下:

var 範例 結構體指標 = new(結構體)
// 也可以簡寫
範例 := new(結構體)

Go語言讓我們可以像存取普通結構體一樣使用.來存取結構體指標的成員,這是因為Go語言為了方便開發者存取結構體指標的成員變數,使用了語法糖,將 s.Name 形式轉換為 (*s).Name。

// Student 定義學生結構體,將學生的各個屬性統一放到結構體中管理
type Student struct {
	Name   string
	Age    int
	School string
}

func main() {
	// 建立學生結構體範例
	s := new(Student)
	// 範例s 其實是指標 *s,s指向的是地址
	(*s).Name = "玄德"
	// 因為go語言的語法糖,簡化了賦值方式,可以直接使用.
	s.Age = 20
	s.School = "清華幼兒園"
	fmt.Println(s) // &{玄德 20 清華幼兒園}
}

結構體的地址範例化

在Go語言中,對結構體進行&取地址操作時,視為對該型別進行一次 new 的範例化操作

取地址範例化是最廣泛的一種結構體範例化方式,可以使用函數封裝上面的初始化過程

// Student 定義學生結構體,將學生的各個屬性統一放到結構體中管理
type Student struct {
   Name   string
   Age    int
   School string
}

func main() {
   // 建立學生結構體範例
   s := &Student{School: "清華幼兒園"}
   (*s).Name = "玄德"
   s.Age = 20
   fmt.Println(s) // &{玄德 20 清華幼兒園}
}

結構體之間的轉換

結構體是單獨定義的型別,和其他型別轉換時需要有完全相同的欄位,轉換時還需加上結構體型別

type Student struct {
	Name string
}

type Person struct {
	Name string
}

func main() {
	s := &Student{""}
	p := &Person{Name: "玄德"}
	s = (*Student)(p)
	fmt.Println(s) // &{玄德}
	fmt.Println(p) // &{玄德}
}

結構體進行type重新定義(相當於取別名),Golang認為這是新的資料型別,但是相互間可以強轉

type Student struct {
	Name string
}

type Stu Student

func main() {
	s1 := &Student{""}
	s2 := &Stu{Name: "玄德"}
	s1 = (*Student)(s2)
	fmt.Println(s1) // &{玄德}
	fmt.Println(s2) // &{玄德}
}

方法

Go語言的方法是作用在指定的資料型別上,和指定的資料型別相互繫結,因此自定義型別,都可以有方法,而不僅僅是struct

方法的定義

方法的宣告方式如下:

type A struct {
    欄位 欄位型別
}

func (a A) 方法名() {
	方法體
}

方法和函數的定義類似,但方法需要傳遞結構體的型別

func (a A) 方法名() 相當於A結構體有一個test方法,方法和結構體需要有繫結關係

由於結構體型別是值傳遞,所以方法的傳遞也是值傳遞,因此方法遵守值型別的傳遞機制,值拷貝傳遞

// 定義Person結構體
type Person struct {
	Name string
}

// 給Person結構體系結test方法
func (p Person) test() {
	fmt.Println(p.Name)
}

func main() {
	// 建立結構體物件
	var p Person
	p.Name = "玄德"
	p.test()
}

如果希望在方法中,改變結構體變數的值,可以通過結構體指標的方式來處理

// 定義Person結構體
type Person struct {
	Name string
}

// 給Person結構體系結test方法
func (p *Person) test() {
    p.Name = "阿巴阿巴"		// 已簡化,實際為(*p)
	fmt.Println(p.Name)
}

func main() {
	// 建立結構體物件
	var p Person
	p.Name = "玄德"		 // 已簡化,實際為(&p)
	p.test()
	fmt.Println(p.Name)
}

方法存取規則

方法名首字母小寫,只能在本包存取,方法名首字母大寫,可以在本包和其他包存取

自定義型別方法

Go語言中的方法作用在指定的資料型別上,和指定的資料型別繫結,因此自定義型別,都可以有方法,不僅僅是結構體

type integer int

func (i integer) print() {
	i = 60
	fmt.Println("i = ", i)
}

func (i *integer) change() {
	*i = 60
	fmt.Println("i = ", *i)
}

func main() {
	var i integer = 30
	i.print()
	fmt.Println(i)
	i.change()
	fmt.Println(i)
}

方法和函數的區別

方法:

  • 需要繫結指定的資料型別
  • 方法的呼叫是變數.方法名(實參列表)
  • 因為編譯器做了處理,所以指標型別和值型別都可以傳入和接收

函數:

  • 不需要繫結資料型別
  • 函數的呼叫是函數名(實參列表)
  • 引數型別是什麼就要傳入什麼
type Student struct {
   Name string
}

// 定義方法
func (s *Student) test01() {
   fmt.Println(s.Name)
}
func (s *Student) test02() {
   fmt.Println(s.Name)
}

// 定義函數
func method01(s Student) {
   fmt.Println(s.Name)
}
func method02(s *Student) {
   fmt.Println(s.Name)
}

func main() {
   var s = Student{"玄德"}
   // 呼叫函數
   method01(s)  // method01(&s)報錯
   method02(&s) // method02(s)報錯
   println("----------------分界線------------------")
   // 呼叫方法
   s.test01()
   (&s).test01() // 雖然是指標型別呼叫,但傳遞還是值傳遞
   s.test02()    // 編譯器做了處理,所以等價下面的語句
   (&s).test02()
}

封裝

封裝的定義

封裝(encapsulation)是把抽象出來的的欄位和對欄位的操作封裝在一起,資料被保護在內部,程式的其他包只有通過被授權的操作方法,才能對欄位進行操作

在程式設計的過程中要追求「高內聚,低耦合」。

高內聚:類的內部資料操作細節自己來完成,不允許外部干涉

低耦合:僅暴露少量的方法給外部使用

而封裝,就是禁止直接存取一個物件中的資料,而是應該通過操作介面來存取,大白話就是該露的露,該藏的藏,你要存取必須經過我設定的規矩(方法),否則你就動不了它(欄位)

實現封裝

  • 將結構體、 欄位(屬性)的首字母小寫(讓其它包不能使用,類似private,但不小寫也有可能,因為go語言封裝不那麼嚴格)
  • 給結構體所在包提供一個工廠模式的函數,首字母大寫(類似一個建構函式)
  • 提供一個首字母大寫的Set方法(類似其它語言的public),用於對屬性判斷並賦值
  • 提供一個首字母大寫的Get方法(類似其它語言的public),用於獲取屬性的值

程式碼實現

目錄結構:

├─test                       
│  └─src               
│      └─main             
│			└─go
│				└─model
│					└─person.go
│				test.go

person.go

package model

import "fmt"

type person struct {
	Name string
	age  int // 首字母小寫,其他包不能直接存取
}

// 定義工廠模式的函數,相當於其他語言的構造器
func NewPerson(name string) *person {
	return &person{
		Name: name,
	}
}

// 定義set和get方法,對age欄位進行封裝,因為在方法中可以定義一系列的限制操作,確保封裝欄位的安全合理性
func (p *person) SetAge(age int) {
	if age > 0 && age < 150 {
		p.age = age
	} else {
		fmt.Println("請輸入正確的年齡")
	}
}
func (p *person) GetAge() int {
	return p.age
}

main.go

package main

import (
	"Test/src/main/go/model"
	"fmt"
)

func main() {
	// 建立person結構體
	p := model.NewPerson("玄德")
	p.SetAge(20)

	fmt.Println(p.Name)     // 玄德
	fmt.Println(p.GetAge()) // 20
	fmt.Println(*p)         // 玄德
}

繼承

繼承的定義

Go語言中的繼承是通過內嵌或組合來實現的,當多個結構體存在相同的屬性(欄位)和方法時,可以從這些結構體中抽象出相同的部分重新定義一個新的結構體:匿名結構體,在這個匿名結構體中包含它們重合的屬性和方法,其它的結構體不需要重新定義這些屬性和方法,只需要巢狀一個匿名結構體即可。

繼承的實現

在Go語言中,如果一個struct巢狀了另一個匿名結構體,那麼這個結構體可以直接存取匿名結構體的欄位和方法,從而實現繼承的特性

  • 要實現繼承,必須內嵌匿名結構體
  • 結構體內嵌後可以使用匿名結構體的所有欄位和方法,並且沒有大小寫限制
  • 匿名結構體欄位和方法的存取可以簡化
  • 結構體的匿名欄位可以是基本資料型別
// 定義動物結構體
type Animal struct {
	Age    int
	Weight float64
}

// 給Animal繫結方法
func (a *Animal) Shout() {
	fmt.Print("現在我要說話了:")
}
func (a *Animal) ShowInfo() {
	fmt.Printf("我的年齡是:%v歲,體重是:%vkg\n", a.Age, a.Weight)
}

// 定義結構體Cat
type Cat struct {
	// 為了複用性,體現繼承思維,加入匿名結構體
	Animal
}

// Cat繫結特有方法
func (c *Cat) catShout() {
	fmt.Println("喵~")
}

func main() {
	// 建立Cat結構體範例
	cat := &Cat{}
	cat.Age = 3				// 原為cat.Animal.Age,已簡化
	cat.Weight = 10.6
	cat.ShowInfo()			// 我的年齡是:3歲,體重是:10.6kg
	cat.Shout()
	cat.catShout()			// 現在我要說話了:喵~
}

繼承的特性

就近存取原則

當結構體和匿名結構體有相同的欄位或者方法時,編譯器採用就近存取原則,如果想要存取匿名結構體的欄位或方法時,可以通過匿名結構體名來區分

type Animal struct {
	Age int
}

func (a *Animal) f() {
	fmt.Printf("我是父類別的方法~,Age:%v\n", a.Age)
}

type Cat struct {
	Animal
	Age int
}

func (c *Cat) f() {
	fmt.Printf("我是子類的方法~,Age:%v\n", c.Age)
}

func main() {
	// 建立Cat結構體範例
	cat := &Cat{}
	cat.Age = 3 // 就近原則
	cat.Animal.Age = 6
	cat.f() // 就近原則
	cat.Animal.f()
}

多重繼承

Go語言中支援多繼承,但為了保證程式碼的間接性,建議儘量不使用多重繼承

type A struct {
	a int
	b string
}

type B struct {
	c int
	d string
}

type C struct {
	A
	B
}

func main() {
	c := C{A{10, "aaa"}, B{20, "bbb"}}
	fmt.Println(c)
}

介面

Go 語言的介面設計是非侵入式的,介面編寫者無須知道介面被哪些型別實現,而介面的實現者只需知道實現的是什麼樣子的介面,但無須指明實現哪一個介面,因為編譯器知道最終編譯時使用哪個型別實現哪個介面,或者介面應該由誰來實現。

Go語言並不是一種 「傳統」 的物件導向程式語言,它並沒有類的概念,繼承也是通過內嵌或組合來實現的,介面本身是呼叫方和實現方均需要遵守的一種協定,大家按照統一的方法命名引數型別和數量來協調邏輯處理的過程。

Go語言裡有非常靈活的介面概念,通過它可以實現很多物件導向的特性,這種設計可以讓你建立一個新的介面型別滿足已經存在的具體型別卻不會去改變這些型別的定義,當我們使用的型別來自於不受我們控制的包時這種設計尤其有用。

介面的宣告

介面是雙方約定的一種合作協定。介面實現者不需要關心介面會被怎樣使用,呼叫者也不需要關心介面的實現細節。介面是一種型別,也是一種抽象結構,不會暴露所含資料的格式、型別及結構。

  • 介面中可以定義一組方法,但不需要實現,不需要方法體。並且介面中不能包含任何變數。直到某個自定義型別要使用的時候,再根據具體情況把這些方法具體實現出來
  • 實現介面要實現所有的方法才算是實現
  • Go語言中的介面,不需要顯式的實現介面
  • 介面的目的是為了定義規範,具體由別人來實現即可

介面宣告格式如下

    type 介面型別名 interface{
        方法名1( 參數列1 ) 返回值列表1
        方法名2( 參數列2 ) 返回值列表2
        …
    }

部分引數講解

  • 介面型別名:使用 type 將介面定義為自定義的型別名。Go語言的介面在命名時,一般會在單詞後面新增 er,如有寫操作的介面叫 Writer,有字串功能的介面叫 Stringer,有關閉功能的介面叫 Closer 等。
  • 方法名:當方法名首字母是大寫時,且這個介面型別名首字母也是大寫時,這個方法可以被介面所在的包(package)之外的程式碼存取。
  • 參數列、返回值列表:參數列和返回值列表中的引數變數名可以被忽略

程式碼範例:

// 介面的定義:定義規則、定義規範,定義某種能力
type AnimalSay interface {
	// 實現某種沒有實現的方法
	say()
}

// 介面的實現
type Cat struct {
}
type Dog struct {
}

// 實現介面的方法
func (c Cat) say() {
	fmt.Println("喵~")
}
func (d Dog) say() {
	fmt.Println("汪~")
}

// 定義一個函數
func shout(s AnimalSay) {
	s.say()
}

func main() {
	c := Cat{}
	d := Dog{}
	// 貓叫
	shout(c)
	// 狗叫
	shout(d)
}

介面的特性

  • 介面本身不能建立範例,但可以指向一個實現了該介面自定義型別的變數
  • 只要是自定義資料型別,就可以實現介面,不僅僅是結構體型別
  • 一個自定義型別可以實現很多介面
  • 一個介面可以繼承多個介面,如果這時要實現介面,那麼必須將繼承的幾個介面方法全部實現
  • interface型別預設是一個指標(參照型別),如果沒有對interfce初始化就使用,那麼將會輸出空
  • 空介面沒有任何方法,所以可以理解為所有型別都實現了空介面,也可以理解為我們可以把如何一個變數賦給空介面

多型

Go語言中的物件導向是抽象的,因此在Go語言中多型特徵是通過介面實現的,可以按照統一的介面來呼叫不同的實現,這時介面變數就呈現出不同的形態

上方程式碼的這個函數,其中的s通過上下文來識別具體是什麼型別的範例就完美體現了多型的表現

func shout(s AnimalSay) {
   s.say()
}

介面體現多型特徵

  1. 多型引數:其中的s就是多型引數

    func shout(s AnimalSay) {
       s.say()
    }
    
  2. 多型陣列:定義一個介面陣列,裡面存放各個結構體變數即可

    var arr [3]AnimalSay
    arr[0] = Dog{"小狗1號"}
    arr[1] = Cat{"小貓1號"}
    arr[2] = Cat{"小貓2號"}
    

如果有過其他語言中物件導向的基礎應該很好理解,但對於純小白來說還是推薦去看更詳細的資料

斷言

型別斷言(Type Assertion)是一個使用在介面值上的操作,用於檢查介面型別變數所持有的值是否實現了期望的介面或者具體的型別,也就是直接判斷是否是該型別的變數

Go語言中型別斷言的語法格式如下:

value, ok := x.(T)

其中,x 表示一個介面的型別,T 表示一個具體的型別(也可為介面型別)。

該斷言表示式會返回 x 的值(也就是 value)和一個布林值(也就是 ok),可根據該布林值判斷 x 是否為 T 型別:

  • 如果 T 是具體某個型別,型別斷言會檢查 x 的動態型別是否等於具體型別 T。如果檢查成功,型別斷言返回的結果是 x 的動態值,其型別是 T。
  • 如果 T 是介面型別,型別斷言會檢查 x 的動態型別是否滿足 T。如果檢查成功,x 的動態值不會被提取,返回值是一個型別為 T 的介面值。
  • 無論 T 是什麼型別,如果 x 是 nil 介面值,型別斷言都會失敗。

程式碼範例:

package main

import (
	"fmt"
)

func main() {
	// 定義介面
	var x interface{}
	x = 10
	// 斷言:x是否能轉成int型別並賦值給value,flag判斷是否成功
	value, flag := x.(int)
	fmt.Println(value, ",", flag)	// 10 , true
}

搭配switch使用

型別斷言還可以配合 switch 使用,範例程式碼如下:

func main() {
    var a int
    a = 10
    getType(a)
}
func getType(a interface{}) {
    switch a.(type) {
    case int:
        fmt.Println("the type of a is int")
    case string:
        fmt.Println("the type of a is string")
    case float64:
        fmt.Println("the type of a is float")
    default:
        fmt.Println("unknown type")
    }
}

並行

程式(program):為了完成某種特定任務而編寫的靜態程式碼,程式是靜態的,程式執行才產生了程序

程序(process):程式的一次執行過程,每個程序都會在記憶體中有自己的記憶體區域,程序是動態的,它有自己的生命週期,有產生、存在、消亡的過程

執行緒(thread):程序可以有多條執行緒,執行緒只是程式內部的一條執行路徑

協程(goroutine ):協程是一種使用者態的輕量級執行緒,這裡的協程和其他語言的協程(coroutine)不一樣

管道(channel):協程之間通訊的橋樑,管道是雙向的

並行(Concurrent):多個執行緒交替操作同一個資源類

並行(Paralled):多個執行緒同時操作多個資源類

死掉的程式只是記憶體上的資料,活過來的程式就是程序,沒錯,程序是有生命的,他有自己的生命週期。

協程

Go主執行緒也可稱為執行緒,也可以理解為程序,一個Go主執行緒上可以起多個協程,可以理解協程為輕量級的執行緒,資源消耗較小

協程的特點:有獨立的棧空間、共用程度堆空間、排程由使用者控制,是輕量級的執行緒(協程的本質是單執行緒)

Go語言高並行的的特性就是基於協程

認識協程

以下全部為知識點,有些枯燥,但看完後會更全面的的瞭解go語言的協程

協程為什麼比執行緒快

協程是一種使用者態的輕量級執行緒,協程的排程完全由使用者控制。從技術的角度來說,「協程就是你可以暫停執行的函數」。

在協程的排程切換時,可以將暫存器上下文和棧儲存到其他地方,在切回來的時候,恢復先前儲存的暫存器上下文和棧,直接操作棧則基本沒有核心切換的開銷,可以不加鎖的存取全域性變數,所以上下文的切換非常快。

協程和執行緒的區別

一個執行緒可以多個協程,一個程序也可以單獨擁有多個協程,執行緒程序都是同步機制,而協程則是非同步。

協程能保留上一次呼叫時的狀態,每次過程重入時,就相當於進入上一次呼叫的狀態,執行緒是搶佔式,而協程是非搶佔式的,所以需要使用者自己釋放使用權來切換到其他協程,因此同一時間其實只有一個協程擁有執行權,相當於單執行緒的能力。

協程並不是取代執行緒,而且抽象於執行緒之上,執行緒是協程的資源,協程通過執行器(Interceptor) 來間接使用執行緒這個資源。

goroutine 和 coroutine

C#、python、Lua等語言都支援 coroutine 特性,兩者雖然都可以將函數或者語句在獨立的環境中執行,但go語言的goroutine支援並行執行,而coroutine始終順序執行,goroutines 是通過通道來通訊;而coroutines 通過讓出和恢復操作來通訊,goroutines 比 coroutines 更強大,也很容易從 coroutines 的邏輯複用到 goroutines。

狹義地說,goroutine 可能發生在多執行緒環境下,goroutine 無法控制自己獲取高優先度支援;coroutine 始終發生在單執行緒,coroutine 程式需要主動交出控制權,宿主才能獲得控制權並將控制權交給其他 coroutine。

goroutine 間使用 channel 通訊,coroutine 使用 yield 和 resume 操作。

goroutine 屬於搶佔式任務處理,作業系統如果發現一個應用程式長時間大量地佔用 CPU,那麼使用者有權終止這個任務。

協程的開啟

協程的開啟十分簡單,只需要在函數前面加上go關鍵字即可

協程是和主執行緒一同執行的

範例程式碼如下:

package main

import (
	"fmt"
	"time"
)

// 主執行緒
func main() {
	// 開啟協程
	go test()
	for i := 0; i < 10; i++ {
		fmt.Printf("我是主執行緒,執行了%v次\n", i+1)
		// 阻塞一秒
		time.Sleep(time.Second)
	}
	fmt.Printf("我是主執行緒,我要結束執行了")
}

func test() {
	for i := 0; i < 10; i++ {
		fmt.Printf("我是一個協程,執行了%v次\n", i+1)
		// 阻塞一秒
		time.Sleep(time.Second)
	}
}

主死從隨

如果主執行緒結束了,即使協程還沒有執行完畢,那麼協程也會跟著退出,但協程結束了並不會影響主程序

程式碼範例:

上方程式碼的test函數更改為無限迴圈即可

func test() {
	for i := 0; i < 10; i++ {
		fmt.Printf("我是一個協程,執行了%v次\n", i+1)
		// 阻塞一秒
		time.Sleep(time.Second)
	}
}

匿名函數建立多個協程

func main() {
	// 匿名函數+外部變數 = 閉包
	for i := 0; i < 6; i++ {
		// 啟動一個協程
		// 使用匿名函數,直接呼叫匿名函數
		go func(n int) {
			fmt.Printf("我是第%v個協程\n", n+1)
		}(i)
	}
	// 阻塞一秒
	time.Sleep(time.Second)
	fmt.Printf("我是主執行緒,我要結束執行了")
}

WaitGroup

Go語言的WaitGroup是一種用於管理多個goroutine的工具,它可以幫助開發者確保所有goroutine都完成了任務,然後再繼續執行下一步操作。簡單來說就是控制協程的主死從隨

WaitGroup的更多詳情請檢視Go語言中文標準庫:Go語言標準庫

程式碼範例:

package main

import (
	"fmt"
	"sync"
)

// 定義WaitGroup
var wg sync.WaitGroup

func main() {
	// 啟動6個協程
	for i := 1; i < 7; i++ {
		wg.Add(1) // 協程開始時加1
		go func(n int) {
			defer wg.Done() // 協程執行完減1
			fmt.Printf("你好,我是第%v個協程\n", n)
		}(i)
	}
	// 阻塞主執行緒,當wg減為0時,停止阻塞
	wg.Wait()
}

互斥鎖

當協程操作同一個資料的時候會發生搶佔資源的行為,導致資料結果不準確,這時,我們就需要互斥鎖(Mutex)來解決這個問題(試試不加鎖會出現什麼(~ ̄▽ ̄~)),注:互斥鎖效能、效率較低

Go語言的mutex(互斥鎖)是一種用於在多個goroutine之間同步存取共用資源的機制。它可以防止多個goroutine同時存取共用資源,從而避免資料競爭。

package main

import (
	"fmt"
	"sync"
)

var wg sync.WaitGroup
var totalNum int

// 加入互斥鎖
var lock sync.Mutex

func main() {
	wg.Add(2)
	go add()
	go sub()
	wg.Wait()
	fmt.Println(totalNum)
}

func sub() {
	defer wg.Done()
	for i := 0; i < 1000; i++ {
		// 加鎖
		lock.Lock()
		totalNum -= 1
		// 解鎖
		lock.Unlock()
	}
}

func add() {
	defer wg.Done()
	for i := 0; i < 1000; i++ {
		lock.Lock()
		totalNum += 1
		lock.Unlock()
	}
}

讀寫鎖

當我們遇到讀多寫少的場景時,由於讀對資料不產生影響,所以推薦使用讀寫鎖(RWMutex)

Go語言的RWMutex(讀寫鎖)是一種同步機制,它可以同時允許多個讀取操作,但只允許一個寫入操作。它可以幫助程式設計師控制對共用資源的存取,以避免競爭條件和資料不一致的問題。

package main

import (
	"fmt"
	"sync"
	"time"
)

var wg sync.WaitGroup

// 加入讀寫鎖
var lock sync.RWMutex

func main() {
	wg.Add(6)
	// 模擬讀多寫少
	for i := 0; i < 5; i++ {
		go read()
	}
	go write()
	wg.Wait()
}

func read() {
	defer wg.Done()
	lock.RLock() // 如果只是讀資料,這個鎖不發揮作用,但讀寫同時發生時,鎖就會發揮作用
	fmt.Println("開始讀取資料")
	time.Sleep(time.Second)
	fmt.Println("讀取資料成功")
	lock.RUnlock()
}

func write() {
	defer wg.Done()
	lock.Lock()
	fmt.Println("開始修改資料")
	time.Sleep(time.Second * 6)
	fmt.Println("修改資料成功")
	lock.Unlock()
}

捕獲錯誤

package main

import (
	"fmt"
	"time"
)

func main() {
	go printNum()
	go division()
	time.Sleep(time.Second)
}

func division() {
	// 捕獲錯誤並處理
	defer func() {
		err := recover()
		if err != nil {
			fmt.Println("division函數出現錯誤", err)
		}
	}()
	num1 := 10
	num2 := 0
	result := num1 / num2
	fmt.Println(result)
}

func printNum() {
	for i := 0; i < 6; i++ {
		fmt.Println(i + 1)
	}
}

管道

簡但來說管道(channel)是協程(goroutine )之間的通訊機制

管道的特性:

  • 管道可以實現多個goroutine之間的通訊

  • 管道可以實現資料的流動、緩衝、同步

  • 管道可以實現資料的安全傳輸

管道的性質:

  • 管道的本質就是一個基於佇列的資料結構,因此它的資料是先進先出

  • 管道自身執行緒安全,多協程存取時,不需要加鎖,因為它本身就是執行緒安全的

  • 管道有型別,一個固定型別的管道只能存放固定型別的資料

Go語言的管道是一種程式設計模式,它可以讓程式設計師將多個函數連線起來,每個函數處理輸入資料,並將處理後的結果傳遞給下一個函數。管道可以讓程式設計師更容易地處理複雜的資料處理任務,並且可以更快地完成任務。

管道入門

通道本身需要一個型別進行修飾,就像切片型別需要標識元素型別,通道的元素型別就是在其內部傳輸的資料型別

管道的宣告方式如下:

var 管道變數 chan 管道型別

管道是參照型別,它在記憶體裡的值是一個地址,所以需要使用 make 進行建立,格式如下:

管道範例 := make(chan 資料型別)

管道建立後,就可以使用<-對通道進行傳送和接收操作

管道變數 <- 值

程式碼範例:

package main

import "fmt"

func main() {
	// 宣告一個int型別的管道
	var intChan chan int
	// make進行初始化:管道可以存放3個int型別的資料
	intChan = make(chan int, 3)
	// 存放資料
	intChan <- 6
	intChan <- 66
	intChan <- 666
	// 從管道中讀取資料
	data1 := <-intChan
	data2 := <-intChan
	data3 := <-intChan
	fmt.Println("data1:", data1)
	fmt.Println("data2:", data2)
	fmt.Println("data3:", data3)
	// 輸出管道
	fmt.Printf("管道的實際長度:%v,管道的容量是:%v", len(intChan), cap(intChan))
}

注意:

  • 管道不能存放大於容量的資料
  • 如果接收方一直沒有接收,那麼傳送操作將持續阻塞
  • 在不使用協程的情況下,如果管道的資料已經全被取出,那麼再取就會報錯

管道的關閉

內建函數close可以關閉管道,關閉後只能讀取資料,但不能在寫入資料

package main

func main() {
	intChan := make(chan int, 6)
	intChan <- 6
	intChan <- 66
	// 關閉管道
	close(intChan)
	// 寫入管道
	// intChan <- 666 // 報錯:send on closed channel
	// 讀取管道
	data := <-intChan
	println(data)	// 6
}

管道的遍歷

管道的遍歷使用for-range遍歷

package main

import "fmt"

func main() {
	intChan := make(chan int, 6)
	for i := 0; i < 6; i++ {
		intChan <- i
	}
	close(intChan)
	// 遍歷
	for data := range intChan {
		fmt.Println("data =", data)
	}
}

單向管道

Go語言的型別系統提供了單方向的 channel 型別,顧名思義,單向 channel 就是隻能用於寫入或者只能用於讀取資料。當然 channel 本身必然是同時支援讀寫的,否則根本沒法用。

單向管道的宣告

單向 channel 變數的宣告非常簡單,只能寫入資料的通道型別為chan<-,只能讀取資料的通道型別為<-chan

單向管道的宣告方式如下:

var 管道範例 chan <- 元素型別    // 只能寫入資料的通道
var 管道範例 <- chan 元素型別    // 只能讀取資料的通道

程式碼範例:

package main

import "fmt"

func main() {
	// 宣告只寫管道
	var ch1 chan<- int
	ch1 = make(chan int, 6)
	ch1 <- 66
	//data1 := <-ch	// 報錯:cannot receive from send-only channel
	fmt.Println("ch1地址:", ch1)
	// 宣告唯讀管道
	var ch2 <-chan int
	// ch2 <- 66	// 報錯:cannot send to receive-only channel
	if ch2 != nil {
		data2 := <-ch2
		fmt.Println("data2:", data2)
	} else {
		fmt.Println("ch2值為空")
	}
}

select

Go語言的select關鍵字用於多路複用,它可以同時監聽多個通道的資料流動,當某個通道有資料流動時,就會進行處理。

select 的用法與 switch 語言非常類似,由 select 開始一個新的選擇塊,每個選擇條件由 case 語句來描述,與 switch 語句相比,select 有比較多的限制,其中最大的一條限制就是每個 case 語句裡必須是一個 IO 操作

package main

import (
	"fmt"
	"time"
)

func main() {
	// 定義int管道
	intChan := make(chan int, 1)
	go func() {
		time.Sleep(time.Second * 6)
		intChan <- 6
	}()
	// 定義string管道
	strChan := make(chan string, 1)
	go func() {
		time.Sleep(time.Second)
		strChan <- "玄德"
	}()
	select {
	case data := <-intChan:
		fmt.Println("intChan:", data)
	case data := <-strChan:
		fmt.Println("strChan:", data)
	default:
		fmt.Println("防止select被阻塞")
	}
}

並行程式設計

並行可以讓多個任務同時執行,從而提高程式的效率,Go語言利用協程和管道可以輕鬆做到百萬並行量,至於是不是真的百萬並行我也不知道(~ ̄▽ ̄~)

協程與管道

Go語言通過協程與管道可以實現複雜的並行程式設計,複習一下協程和管道吧

操作同一個管道

利用WaitGroup來阻塞

package main

import (
	"fmt"
	"sync"
	"time"
)

var wg sync.WaitGroup

func main() {
	wg.Add(2)
	intChan := make(chan int, 66)
	// 開啟讀和寫的協程,共同操作一個管道
	go writeData(intChan)
	go redaData(intChan)
	// 等待協程
	wg.Wait()
}

// 寫
func writeData(intChan chan int) {
    // 在函數退出時呼叫Done 來通知main 函數工作已經完成
	defer wg.Done()
	for i := 0; i < 66; i++ {
		intChan <- i + 1
		fmt.Println("寫入的資料為:", i)
		time.Sleep(time.Second)
	}
	close(intChan)
}

// 讀
func redaData(intChan chan int) {
	defer wg.Done()
	for data := range intChan {
		fmt.Println("讀取的資料為:", data)
	}
}

利用管道來阻塞

package main

import (
	"fmt"
	"time"
)

func main() {
	// 宣告被操作的管道
	intChan := make(chan int, 66)
	// 宣告阻塞管道
	exitChan := make(chan bool, 1)
	// 開啟讀和寫的協程,共同操作一個管道
	go writeData(intChan)
	go redaData(intChan, exitChan)
	time.Sleep(time.Second)
	fmt.Printf("執行完畢")
}

// 寫
func writeData(intChan chan int) {
	for i := 0; i < 66; i++ {
		intChan <- i + 1
		fmt.Println("寫入的資料為:", i+1)
	}
	close(intChan)
}

// 讀
func redaData(intChan chan int, exitChan chan bool) {
	for data := range intChan {
		fmt.Println("讀取的資料為:", data)
	}
	// 讀取完畢
	exitChan <- true
	close(exitChan)
}

求素數

求10000以內的素數,利用協程試一試,在不用協程試一試,看看哪個更快<( ̄︶ ̄)>

package main

import "fmt"

var intChan = make(chan int, 10000)

func main() {
	go initChan(10000)
	var primeChan = make(chan int, 10000)
	var exitChan = make(chan bool, 8)
	for i := 0; i <= 8; i++ {
		go isPrime(intChan, primeChan, exitChan)
	}
	go func() {
		for i := 0; i < 8; i++ {
			<-exitChan
		}
		close(primeChan)
	}()
	for res := range primeChan {
		fmt.Println("素數:", res)
	}
}

func initChan(num int) {
	for i := 1; i <= num; i++ {
		intChan <- i
	}
	close(intChan)
}

func isPrime(intChan <-chan int, primeChan chan int, exitChan chan<- bool) {
	var flag bool
	for num := range intChan {
		flag = true
		for j := 2; j < num; j++ {
			if num%j == 0 {
				flag = false
				continue
			}
		}
		if flag {
			primeChan <- num
		}
	}
	exitChan <- true
}

生產者消費者

通過協程和管道實現生產者消費者模型╮( ̄▽ ̄)╭

程式碼實現:

package main

import (
	"fmt"
	"strconv"
)

func main() {
	storageChan := make(chan Product, 1000)
	shopChan := make(chan Product, 1000)
	exitChan := make(chan bool, 1)
	// 協程生產
	for i := 0; i < 999; i++ {
		go Producer(storageChan, 1000)
	}
	go Logistics(storageChan, shopChan)
	go Consumer(shopChan, 1000, exitChan)
	if <-exitChan {
		return
	}
}

// Product 商品
type Product struct {
	Name string
}

// Producer 生產者
func Producer(storageChan chan<- Product, count int) {
	for {
		producer := Product{"商品: " + strconv.Itoa(count)}
		storageChan <- producer
		count--
		fmt.Println("生產了", producer)
		if count < 1 {
			return
		}
	}
}

// Logistics 運輸者
func Logistics(storageChan <-chan Product, shopChan chan<- Product) {
	for {
		product := <-storageChan
		shopChan <- product
		fmt.Println("運輸了", product)
	}
}

// Consumer 消費者
func Consumer(shopChan <-chan Product, count int, exitChan chan<- bool) {
	for {
		product := <-shopChan
		fmt.Println("消費了", product)
		count--
		if count < 1 {
			exitChan <- true
			return
		}
	}
}

網路程式設計

Go語言裡面提供了一個完善的 net/http 包,通過 net/http 包我們可以很方便的搭建一個可以執行的 Web 伺服器。同時使用 net/http 包能很簡單地對 Web 的路由,靜態檔案,模版,cookie 等資料進行設定和操作。

簡單的Web伺服器

package main

import (
	"fmt"
	"log"
	"net/http"
)

func main() {
	http.HandleFunc("/", index) // index 為向 url傳送請求時,呼叫的函數
	log.Fatal(http.ListenAndServe("localhost:8080", nil))
}
func index(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintf(w, "玄德的網址:http://xuande-hk.gitee.io")
}

瀏覽器存取localhost:8080,結果如下:

TCP網路協定通訊

首先啟動伺服器端,然後啟動使用者端

使用者端

package main

import (
	"bufio"
	"fmt"
	"net"
	"os"
)

func main() {
	fmt.Println("使用者端啟動中。。。。。。")
	// 選擇tcp協定,指定伺服器端ip和埠號
	conn, err := net.Dial("tcp", "127.0.0.1:8888")
	if err != nil {
		fmt.Println("使用者端連線失敗:", err)
		return
	}
	fmt.Println("連線成功:", conn)
	// 通過使用者端傳送資料
	reader := bufio.NewReader(os.Stdin) // os.Stdin表示終端標準輸入
	// 從終端讀取一行使用者輸入的資訊
	str, err := reader.ReadString('\n')
	if err != nil {
		fmt.Println("終端輸入失敗:", err)
		return
	}
	// 將資料傳送給伺服器
	write, err := conn.Write([]byte(str))
	if err != nil {
		fmt.Println("資料傳送失敗:", err)
		return
	}
	fmt.Printf("資料傳送成功,共傳送%d位元組資料\n", write)
	fmt.Printf("使用者端結束連線。。。。。。")
}

伺服器端

package main

import (
	"bufio"
	"fmt"
	"net"
	"os"
)

func main() {
	fmt.Println("伺服器端啟動中。。。。。。")
	// 監聽使用者端,同樣需要選擇tcp協定,指定伺服器端ip和埠號
	listen, err := net.Listen("tcp", "127.0.0.1:8888")
	if err != nil {
		fmt.Println("監聽失敗:", err)
		return
	}
	// 監聽成功,等待使用者端連線
	fmt.Println("啟動成功,等待使用者端連線")
	for {
		conn, err2 := listen.Accept()
		if err2 != nil {
			fmt.Println("使用者端連線失敗:", err)
			return
		} else {
			fmt.Println("使用者端連線成功:", conn)
			fmt.Println("使用者端資訊為:", conn.RemoteAddr().String())
		}
		// 用協程處理使用者端服務請求
		go process(conn)
	}
}

func process(conn net.Conn) {
	// 關閉連線
	defer conn.Close()
	for {
		// 讀取資料的切片
		buf := make([]byte, 1024)
		// 從conn連線中讀取資料
		read, err := conn.Read(buf)
		if err != nil {
			return
		}
		// 伺服器端輸出
		fmt.Println("接收到使用者端資料:" + string(buf[0:read]))
	}
}

反射

Go語言中的反射是一種動態的程式設計技術,它允許程式在執行時獲取有關自身結構和行為的資訊,並可以根據這些資訊動態地改變自身的行為。反射可以讓程式更加靈活,可以更好地處理複雜的問題。

大多數現代的高階語言都以各種形式支援反射功能,反射是把雙刃劍,功能強大但程式碼可讀性並不理想,若非必要並不推薦使用反射。

瞭解反射

反射是指在程式執行期對程式本身進行存取和修改的能力。程式在編譯時,變數被轉換為記憶體地址,變數名不會被編譯器寫入到可執行部分。在執行程式時,程式無法獲取自身的資訊。

C/C++ 語言沒有支援反射功能,Lua、JavaScript 類動態語言因為其本身的語法特性並不需要反射,Java、C# 、Go等語言都支援完整的反射功能。

Go程式在執行期使用reflect包存取程式的反射資訊。

  • reflect.TypeOf(變數名),獲取變數的型別,返回reflect.Type型別
  • reflect.ValueOf(變數名),獲取變數的值,返回reflect.Value型別(結構體型別裡面包含關於變數的資訊)

反射可以做什麼?

  • 反射可以在執行時動態獲取變數的各種資訊,比如變數的型別,類別等資訊
  • 如果是結構體變數,還可以獲取到結構體本身的資訊(包括結構體的欄位、方法)
  • 通過反射,可以修改變數的值,可以呼叫關聯的方法

反射的型別和種類

在使用反射時,需要首先理解型別(Type)和種類(Kind)的區別。程式設計中,使用最多的是型別,但在反射中,當需要區分一個大品種的型別時,就會用到種類(Kind)。例如需要統一判斷型別中的指標時,使用種類(Kind)資訊就較為方便。

反射具體的方法請檢視:Go語言中文標準庫的reflect包

反射的型別

Go語言程式中的型別(Type)指的是系統原生資料型別,如 int、string、bool、float32 等型別,以及使用 type 關鍵字定義的型別,這些型別的名稱就是其型別本身的名稱

基本型別的反射

package main

import (
	"fmt"
	"reflect"
)

func main() {
	var num = 66
	testReflect(num)
}

// 定義一個空介面的函數
func testReflect(i interface{}) {
	// 呼叫TypeOf函數,返回reflect.Type型別的資料
	reType := reflect.TypeOf(i)
	fmt.Println("reType:", reType)
	fmt.Printf("reType的具體型別是:%T \n", reType)
	// 呼叫ValueOf函數,返回reflect.Value型別的資料
	reValue := reflect.ValueOf(i)
	fmt.Println("reValue:", reValue)
	fmt.Printf("reValue的具體型別是:%T", reValue)
}

結構體的反射

package main

import (
	"fmt"
	"reflect"
)

// Student 學生結構體
type Student struct {
	Name string
	Age  int
}

func main() {
	stu := Student{
		Name: "玄德",
		Age:  20,
	}
	testReflect(stu)
}

// 定義一個空介面的函數
func testReflect(i interface{}) {
	// 呼叫TypeOf函數,返回reflect.Type型別的資料
	reType := reflect.TypeOf(i)
	fmt.Println("reType:", reType)
	fmt.Printf("reType的具體型別是:%T \n", reType)
	// 呼叫ValueOf函數,返回reflect.Value型別的資料
	reValue := reflect.ValueOf(i)
	fmt.Println("reValue:", reValue)
	fmt.Printf("reValue的具體型別是:%T \n", reValue)

	// reValue轉為空介面
	i2 := reValue.Interface()
	// 型別斷言
	n, flag := i2.(Student)
	if flag {
		fmt.Printf("學生的名字: %v\n學生的年齡:%v\n", n.Name, n.Age)
	}
}

反射的種類

當需要區分一個大品種的型別時,就會用到種類(Kind)

Kind用於檢查反射物件的型別,可以用來判斷反射物件是否是指定的型別,以及反射物件的型別是什麼。

獲取變數種類的兩種方式

  • reflect.Type.Kind()
  • reflect.Value.Kind()

種類(Kind)指的是物件歸屬的品種,在 reflect 包中有如下定義:

type Kind uint

const (
    Invalid Kind = iota  // 非法型別
    Bool                 // 布林型
    Int                  // 有符號整型
    Int8                 // 有符號8位元整型
    Int16                // 有符號16位元整型
    Int32                // 有符號32位元整型
    Int64                // 有符號64位元整型
    Uint                 // 無符號整型
    Uint8                // 無符號8位元整型
    Uint16               // 無符號16位元整型
    Uint32               // 無符號32位元整型
    Uint64               // 無符號64位元整型
    Uintptr              // 指標
    Float32              // 單精度浮點數
    Float64              // 雙精度浮點數
    Complex64            // 64位元複數型別
    Complex128           // 128位元複數型別
    Array                // 陣列
    Chan                 // 通道
    Func                 // 函數
    Interface            // 介面
    Map                  // 對映
    Ptr                  // 指標
    Slice                // 切片
    String               // 字串
    Struct               // 結構體
    UnsafePointer        // 底層指標
)

程式碼範例:

package main

import (
	"fmt"
	"reflect"
)

// Student 學生結構體
type Student struct {
	Name string
	Age  int
}

func main() {
	stu := Student{
		Name: "玄德",
		Age:  20,
	}
	testReflect(stu)
}

// 定義一個空介面的函數
func testReflect(i interface{}) {
	// 呼叫TypeOf函數,返回reflect.Type型別的資料
	reType := reflect.TypeOf(i)
	fmt.Println("reType:", reType)
	fmt.Printf("reType的具體型別是:%T \n", reType)
	// 呼叫ValueOf函數,返回reflect.Value型別的資料
	reValue := reflect.ValueOf(i)
	fmt.Println("reValue:", reValue)
	fmt.Printf("reValue的具體型別是:%T \n", reValue)
	// 獲取變數的類別
	k1 := reType.Kind()
	fmt.Println(k1)
	k2 := reValue.Kind()
	fmt.Println(k2)
}

反射的操作

反射操作時用到的函數

  • Elem(),值指向的元素值,類似於語言層*操作。當值型別不是指標或介面時發生宕 機,空指標時返回 nil 的 Value
  • Setlnt(x int64),使用 int64 設定值。當值的型別不是 int、int8、int16、 int32、int64 時會發生宕機

通過反射修改變數

package main

import (
	"fmt"
	"reflect"
)

func main() {
    // 宣告整型變數a並賦初值
    var a int = 66
    // 獲取變數a的反射值物件(a的地址)
    valueOfA := reflect.ValueOf(&a)
    // 取出a地址的元素(a的值)
    valueOfA = valueOfA.Elem()
    // 修改a的值為1
    valueOfA.SetInt(666)
    // 列印a的值
    fmt.Println(valueOfA.Int())
}

通過反射修改結構體的值

注:結構體成員中,如果欄位沒有被匯出,即便不使用反射也可以被存取,但不能通過反射修改,因此為了能修改結構體的值,需要將該欄位匯出。

package main

import (
	"fmt"
	"reflect"
)

type Cat struct {
	Name string
	Age  int
}

func main() {
	// 定義結構體範例
	cat := Cat{Name: "貓貓", Age: 0}
	fmt.Printf("修改前的名字: %v \n", cat.Name)
	fmt.Printf("修改前的年齡: %v \n", cat.Age)
	// 獲取Cat範例地址的反射值物件
	valueOfCat := reflect.ValueOf(&cat)
	// 取出cat範例地址的元素
	valueOfCat = valueOfCat.Elem()
	// 獲取並修改Name欄位的值
	valueOfCat.FieldByName("Name").SetString("小貓")
	// 獲取Age欄位的值
	catAge := valueOfCat.FieldByName("Age")
	// 嘗試設定age的值(如果欄位沒有被匯出,這裡會發生崩潰)
	catAge.SetInt(1)
	fmt.Printf("修改後的名字: %v \n", cat.Name)
	fmt.Printf("修改後的年齡: %v \n", cat.Age)
}

通過反射操作結構體屬性和方法

package main

import (
	"fmt"
	"reflect"
)

type Cat struct {
	Name string
	Age  int
}

func (cat Cat) Test1() {
	fmt.Println("這是第一個方法")
}

func (cat Cat) Test2() {
	fmt.Println("這是第二個方法")
}

func (cat Cat) Test3(a int, b int) {
	fmt.Println("這是第三個方法,這是一個求和方法")
	fmt.Printf("%v + %v = %v", a, b, a+b)
}

func main() {
	// 定義結構體範例
	cat := Cat{Name: "貓貓", Age: 0}
	// 獲取Cat範例的反射值物件
	valueOfCat := reflect.ValueOf(cat)
	fmt.Printf("結構體範例反射值物件:%v \n", valueOfCat)
	// 獲取結構體內部的欄位數量
	field := valueOfCat.NumField()
	fmt.Printf("結構體內部欄位數量:%v \n", field)
	// 獲取具體欄位
	for i := 0; i < field; i++ {
		fmt.Printf("第%d個欄位的值是:%v \n", i+1, valueOfCat.Field(i))
	}
	// 獲取結構體方法數量
	method := valueOfCat.NumMethod()
	fmt.Printf("結構體內部方法數量:%v \n", method)
	// 呼叫Test2方法,方法首字母必須大寫
	valueOfCat.MethodByName("Test2").Call(nil)
	// 呼叫Test3方法,傳入引數
	var params []reflect.Value
	params = append(params, reflect.ValueOf(3))
	params = append(params, reflect.ValueOf(3))
	valueOfCat.MethodByName("Test3").Call(params)
}

結果如下:

結構體範例反射值物件:{貓貓 0}
結構體內部欄位數量:2           
第1個欄位的值是:貓貓           
第2個欄位的值是:0              
結構體內部方法數量:3           
這是第二個方法                  
這是第三個方法,這是一個求和方法
3 + 3 = 6  

檔案處理

Go語言可以讀寫標準格式(如 XML 和 JSON 格式)的檔案以及自定義的純文字和二進位制格式檔案。現在我們可以靈活地使用 Go語言提供的所有工具,並利用閉包來避免重複性的程式碼,同時在某些情況下充分利用 Go語言對物件導向的支援,特別是對為函數新增方法的支援。

檔案是儲存資料的地方,是資料來源的一種,比如txt檔案、word、Excel、jpg等都是檔案。檔案最主要的作用就是儲存資料。

其中Go語言內建的OS包下的File結構體封裝了對檔案的操作

Go語言中文標準庫:Go語言標準庫

OpenFile檔案開啟模式Constants

    O_RDONLY int = syscall.O_RDONLY // 唯讀模式開啟檔案
    O_WRONLY int = syscall.O_WRONLY // 只寫模式開啟檔案
    O_RDWR   int = syscall.O_RDWR   // 讀寫模式開啟檔案
    O_APPEND int = syscall.O_APPEND // 寫操作時將資料附加到檔案尾部
    O_CREATE int = syscall.O_CREAT  // 如果不存在將建立一個新檔案
    O_EXCL   int = syscall.O_EXCL   // 和O_CREATE配合使用,檔案必須不存在
    O_SYNC   int = syscall.O_SYNC   // 開啟檔案用於同步I/O
    O_TRUNC  int = syscall.O_TRUNC  // 如果可能,開啟時清空檔案

許可權控制(linux/unix系統生效,windows下設定無效,windows放入0666即可)

文字檔案

對文字檔案操作,離不開IO流,IO流是程式和資料來源之間溝通的橋樑,可以比喻為程式和資料來源之間的一條水管,一點一點的流過去

寫純文字檔案

由於Go語言的 fmt 包中列印函數強大而靈活,寫純文字資料非常簡單直接,範例程式碼如下所示:

package main

import (
	"bufio"
	"fmt"
	"os"
)

func main() {
	//建立一個新檔案,寫入內容
	filePath := "./output.txt"
    // 更改OpenFile引數可以調整唯讀、只寫、讀寫、追加模式
	file, err := os.OpenFile(filePath, os.O_WRONLY|os.O_CREATE, 0666)
	if err != nil {
		fmt.Printf("開啟檔案錯誤= %v \n", err)
		return
	}
	//及時關閉
	defer file.Close()
	//寫入內容
	str := "你好,我是玄德\n" // \n\r表示換行  txt檔案要看到換行效果要用 \r\n
	//寫入時,使用帶快取的 *Writer
	writer := bufio.NewWriter(file)
	for i := 0; i < 3; i++ {
		writer.WriteString(str)
	}
	//因為 writer 是帶快取的,因此在呼叫 WriterString 方法時,內容是先寫入快取的
	//所以要呼叫 flush方法,將快取的資料真正寫入到檔案中。
	writer.Flush()
}

讀純文字檔案

開啟並讀取一個純文字格式的資料跟寫入純文字格式資料一樣簡單。要解析文字來重建原始資料可能稍微複雜,這需根據格式的複雜性而定。

package main

import (
	"bufio"
	"fmt"
	"io"
	"os"
)

func main() {
   //開啟檔案
   file, err := os.Open("./output.txt")
   if err != nil {
      fmt.Println("檔案開啟失敗 = ", err)
   }
   //及時關閉 file 控制程式碼,否則會有記憶體漏失
   defer file.Close()
   //建立一個 *Reader , 是帶緩衝的,緩衝區:4096位元組
   reader := bufio.NewReader(file)
   for {
      str, err := reader.ReadString('\n') //讀到一個換行就結束
      if err == io.EOF {                  //io.EOF 表示檔案的末尾
         break
      }
      fmt.Print(str)
   }
   fmt.Println("檔案讀取結束...")
}

複製檔案

package main

import (
	"fmt"
	"io/ioutil"
)

func main() {
	file1Path := "./output.txt"
	file2Path := "./output2.txt"
	data, err := ioutil.ReadFile(file1Path)
	if err != nil {
		fmt.Printf("檔案開啟失敗=%v\n", err)
		return
	}
	err = ioutil.WriteFile(file2Path, data, 0666)
	if err != nil {
		fmt.Printf("檔案開啟失敗=%v\n", err)
	}
}

寫在最後

我們都是站在巨人的肩膀上,感謝所有願意分享知識的人

參考名單:

顏文字:

<( ̄︶ ̄)>	<( ̄︶ ̄)/ \( ̄︶ ̄)/	╰( ̄▽ ̄)╭	(╯-_-)╯╧╧	
╮( ̄▽ ̄)╭	(~ ̄▽ ̄~)╮( ̄▽ ̄")╭	(  ̄^ ̄)︵θ︵θ︵θ︵θ︵☆(>口<-)

點選回到頂部(~ ̄▽ ̄~)