go語言基礎學習筆記完整版

目錄

背景

基礎

helloworld

變量

常量

數據類型

基本數據類型與複雜數據類型

值類型與引用類型

查看變量類型

字符與字符串

類型轉換

指針

打包

讀取控制檯數據

for-range遍歷

生成隨機數

函數

普通函數

匿名函數

閉包

defer

分配內存

異常捕獲

 數組

切片

映射

面向對象

結構體

方法

工廠模式

繼承

接口

類型斷言

文件操作

打開與關閉

讀取文件

創建與寫入

命令行參數

序列化反序列化

序列化

反序列化

單元測試

併發編程

協程goroutine

MPG模式

全局互斥鎖

管道

異常捕獲

反射

反射基本數據類型

反射結構體

反射改基本數據類型變量的值

獲取結構體所有屬性和標籤

調用結構體方法

修改結構體字段值

反射構造結構體變量

網絡編程

go連接redis

go連接mysql

結語


背景

這裏整理一下上個月學習go語言的筆記,更多請參考go的官方文檔:https://studygolang.com/pkgdoc

使用的文本編輯器:VS code

基礎

項目工程的路徑爲:D:\develop\Go\workspace,要先把這個路徑添加到環境變量GO_PATH裏。

代碼放在項目路徑下的src\go_code\src\project01目錄裏

helloworld

go語言的helloWorld如下

package main  // 必須打main包

import "fmt"

func main()  {
    fmt.Println("szc")
}

main函數必須在main目錄下,包名則必須和上級目錄名一致(main);一個項目必須有且只有一個main目錄(或main包)

go程序既可以直接運行

亦可以先編譯,再運行

變量

三種聲明方式

package main

import "fmt"

func main() {
    var i int  = 10 // var 變量名 類型 = 值
    var j = 1.2 // var 變量名 = 值

    name := "szc" // 變量名 := 值,自動推導類型

    fmt.Println("i = ", i, ", j = " , j , ", name = ", name)
}

一次聲明多個變量,變量名和值一一對應

    var a, sex, b = 1, "male", 7

也可以這樣

    a, sex, b := 2, "male", 4

函數外聲明全局變量

var (
    n1 = 1
    n2 = 2
    n3 = 3
)

var n4 = "n4"

func main() {
    fmt.Println("n1 = ", n1, ", n2 = ", n2, "n3 = ", n3, ", n4 = ", n4)
}

變量聲明後必須使用,而且不能隱式改變類型(int轉float)

常量

常量必須賦初值,而且不可更改

    const tax int = 1
    const name_ = "szc"
    const b = 4 / 2

//    const b_ = getVal() // 編譯期值不確定
//    num := 1
//    const b_ = num / 2 // 編譯期值不確定
//    const tax_0 int // 必須賦初值
//    tax = 2 // 不可修改

常量只能修飾布爾、數值、字符串類型

也可以這麼聲明常量,可以在函數裏面聲明

    const (
        a = iota
        b = iota
        c = iota
    )
    fmt.Println(a, b, c) // 0 1 2

上面b和c可以不寫= iota,但是a必須寫

數據類型

基本數據類型與複雜數據類型

基本數據類型:

數值型:

1、整數類型(int、int8、int16、int32、int64、uint、uint8、uint16、uint32、uint64、byte)

2、浮點類型(float32、float64)

3、複數(complex64、complex128)

字符型:沒有專門的字符型,使用byte保存單個字母字符

布爾型、字符串

數值型中的int32又稱爲rune,可保存一個unicode碼點。int和uint的大小和操作系統位數一樣,32位OS則爲4字節,64位OS則爲8字節。浮點數默認64位,整數默認int。

 

複雜數據類型:

指針、數組、結構體、管道、函數、切片、接口、映射

值類型與引用類型

值類型:基本數據類型、數組、結構體。變量直接存儲值,通常存儲於棧中,函數傳參時使用值傳遞

引用類型:指針、切片、映射、管道、接口等。變量存儲的是值的地址,通常存儲於堆中,會發生GC,函數傳參時使用引用傳遞。

查看變量類型

查看變量類型:

    a, sex:= 2, "male"

    fmt.Printf("a的類型:%T,sex的類型:%T\n", a, sex)

查看變量佔用內存大小時,先導入unsafe和fmt包

import (
    "fmt"
    "unsafe"
)

再調用unsafe.Sizeof函數就行

    fmt.Printf("a佔用內存大小:%d, sex佔用內存大小:%d", unsafe.Sizeof(a), unsafe.Sizeof(sex))

輸出結果:

字符與字符串

輸出字符時,需要格式化輸出,否則會輸出的它的ascii值

    c1 := 's'
    c2 := '0'

    fmt.Println("c1 = ", c1, ", c2 = ", c2)
    fmt.Printf("c1 = %c, c2 = %c\n", c1, c2)

輸出如下

輸出漢字和對應unicode碼值

    c3 := '宋'
    fmt.Printf("c3 = %c, 對應unicode碼值: %d\n", c3, c3)

結果如下

跨行字符串,用`包住

var s = `
拜仁慕尼黑來自德甲。
它在今年歐冠八分之一淘汰賽上首回合客場3:0完勝切爾西。
`

多行拼接字符串,要在+後面換行,而不是字符串後面

    s1 := "abc" +
     " def" + "hij"

類型轉換

不同數據類型之間必須顯式類型轉換

    a1 := 1.2
    a2 := int(a1)

    fmt.Println("a2 = ", a2)

如果範圍大轉換成範圍小的,可能會發生精度損失,以下是例子:

    var i1 int32 = 12
    var i2 int8
    var i3 int8

    i2 = int8(i1) + 127 // 運行時溢出,得不到想要結果
    i3 = int(i1) + 128 // 直接溢出,編譯錯誤

    fmt.Println("i2 = ", i2)

基本數據類型轉string:

    var s0 = fmt.Sprintf("%d", n1)
    fmt.Printf("s type:%T, s = %v\n", s0, s0)
    s0 = fmt.Sprintf("%t", b)
    fmt.Printf("s type:%T, s = %v\n", s0, s0)

%v表示按默認格式輸出,%t表示按布爾值輸出

也可以用strconv包中的函數進行轉換。用之前先導入strconv包

import (
    "fmt"
    "strconv"
)

然後調用函數進行轉換

    s0 = strconv.FormatInt(int64(n1), 10) // 10表示十進制
    fmt.Printf("s type:%T, s = %v\n", s0, s0)
    s0 = strconv.FormatFloat(a1, 'f', 10, 64) // 'f'表示浮點數類型、10表示精度10位,64表示float64
    fmt.Printf("s type:%T, s = %v\n", s0, s0)
    s0 = strconv.FormatBool(b)
    fmt.Printf("s type:%T, s = %v\n", s0, s0)

string轉基本類型:

也是用strconv包中的Parse方法,但Parse方法會返回兩個值:轉換的值,以及轉換錯誤

    var b2, _ = strconv.ParseBool(s0) // _表示接收但忽略
    fmt.Printf("b2 type:%T, b2 = %v\n", b2, b2)
    
    var i0, _ = strconv.ParseInt("1233", 10, 64) // 後兩個參數分別表示進制和轉換成int的位數
    fmt.Printf("i0 type:%T, i0 = %v\n", i0, i0)

    var f0, _ = strconv.ParseFloat("21.291", 64) //後面的參數表示轉換成float的位數
    fmt.Printf("f0 type:%T, f0 = %v\n", f0, f0)

得到的輸出如下

如果待轉換的string不合法,就會轉換成對應類型的默認值(0)

指針

和C裏面的指針類似

    i := 1
    ptr0 := &i

    fmt.Printf("%x, %d, %x", ptr0, *ptr0, &ptr0)

%x表示十六進制,輸出如下

同樣,通過指針改變變量的值也是一樣

    (*ptr0) = (*ptr0) * 10
    fmt.Printf("%v\n", i)

打包

包名和目錄名一致。

文件中變量、函數名首字母大寫,則爲public,小寫則爲包私有

引用自己的包時,先把src目錄的上級目錄加入環境變量GO_PATH中,然後引入包在src目錄下的相對路徑

然後就可以引用model包下首字母大寫的變量或函數了

    fmt.Printf(model.Name)

model包下的test_model.go內容如下所示,文件不用引用。引用目錄就行

package model

var Name = "Jason"
var age = 23

讀取控制檯數據

調用fmt.Scan等方法即可

    var j string
    fmt.Scanln(&j) // Scanln讀取一行
    fmt.Println("j = ", j)

或者指定輸入格式

    var j string
    var m float32
    var n bool

    fmt.Scanf("%d%f%s%t", &i, &m, &j, &n)
    fmt.Println("i = ", i, "j = ", j, "m = ", m, "n = ", n)

輸入時按空格或回車區分即可

for-range遍歷

這是一種同時獲取索引和值或鍵值的遍歷方式

	str := "拜仁慕尼黑來自德甲"
	for index, s := range str {
		fmt.Printf("%d---%c\n", index, s)
	}

輸出如下

生成隨機數

導入math/random和time包

import (
    "fmt"
    "math/rand"
    "time"
)

設置種子,生成隨機數

    rand.Seed(time.Now().Unix())
    n := rand.Intn(100) + 1
    fmt.Println(n)

函數

普通函數

函數定義,func 函數名(參數列表) 返回值類型 {

    函數體

},如下所示

func generateRandom(time int64, _range int) int {
    rand.Seed(time)
    return rand.Intn(_range)
}

調用如下

    fmt.Println(generateRandom(time.Now().Unix(), 100))

init函數,用來初始化源文件

func init()  {
    fmt.Println("init variable_advanced..")
}

源文件執行流程:全局變量定義->init->main,如果此文件還引入了別的文件,就先執行被引用文件的變量定義和init

匿名函數

匿名函數,沒有名字的函數,如下

    res := func (n1 int, n2 int) int  {
        return n1 * n2
    }(2, 8)
    fmt.Println("res = ", res)

}後面的(2, 8)表示調用並傳參

也可以把匿名函數賦給一個變量

    a := func(n1 int, n2 int) (int, int) {
        return n2, n1
    }

    n1 := 10
    n2 := 29
    n1, n2 = a(n1, n2)

然後就可以對a進行多次調用了

也可以把匿名函數定義成全局變量

var (
    fun1 = func(n1 int, n2 int) int {
        return n1 * n2
    }
)

func main() {
    fmt.Println(fun1(42, 44))
}

閉包

函數和引用環境的整體叫做閉包,例如

func AddUpper() func (int) int {
    var n int = 10
    return func(x int) int {
        n = n + x
        return n
    }
}

AddUpper()返回的匿名函數,引用了匿名函數外的n,所以AddUpper內部形成了閉包。AddUpper的調用如下:

    f := AddUpper()
    fmt.Println(f(1)) // 11
    fmt.Println(f(3)) // 14
    fmt.Println(f(3)) // 17

由於形成了匿名函數+外部引用的形式,所以每次調用AddUpper()時,n都會繼承上一次調用的值。

就當n是AddUpper()的屬性,一個對象只會初始化一次。

    f := AddUpper()
    fmt.Println(f(1)) // 11
    fmt.Println(f(3)) // 14
    fmt.Println(f(3)) // 17

    g := AddUpper()
    fmt.Println(g(4)) // 14

defer

defer用來表示一條語句在函數結束後再執行,defer語句會把語句和相應數值的拷貝進行壓棧,先入後出。以如下代碼爲例,這是一個defer + 閉包的例子,makeSuffix的入參爲suffix,而返回值是一個函數,此函數入參類型爲string,返回值類型也是string。

func makeSuffix(suffix string) func(string) string {
    var n = 1
    defer fmt.Println("suffix = ", suffix, ", n = ", n)
    defer fmt.Println("...")
    n = n + 1

    fmt.Println("makeSuffix..")
    return func(file_name string) string {
        if (! strings.HasSuffix(file_name, suffix)) {
            return file_name + suffix
        }


        return file_name
    }
}

func main() {
    f := makeSuffix(".txt")
    fmt.Println(f("szc.txt"))
    fmt.Println(f("szc"))
}

輸出信息如下

可見雖然匿名函數執行了兩次,但閉包函數makeSuffix裏的語句只執行了一次,而且defer語句先定義的後輸出,且都在函數體執行完之後。

分配內存

值類型的用new,返回的是一個指針

    p := new(int)
    fmt.Println("*p = ", *p, ", p = ", p)
    *p = 29
    fmt.Println("*p = ", *p)

輸出如下

引用類型的用make

異常捕獲

defer、recover捕獲異常,相當於try-catch

func test() {
    defer func() {
        err := recover() // 捕獲異常
        if err != nil {
            fmt.Println("err:", err) // 輸出異常
        }
    }()


    n1 := 1
    n2 := 0
    fmt.Println("res:", n1 / n2)
}

func main() {
    test()
}

輸出如下

當我們需要自定義錯誤時,使用errors.New。遇到錯誤終止程序,使用panic()函數,示例如下

func testError(name string) (err error) {
    if name == "szc" {
        return nil
    } else {
        return errors.New("Something wrong with " + name + "...") // 定義新的錯誤信息
    }
}


func test2() {
    err := testError("sss")
    if err != nil {
        panic(err) // 終止程序
    }

    fmt.Println("...")
}


func main() {
    test2()
}

要先導入errors包

import (
    "fmt"
    "errors"
)

輸出如下

 數組

定義和使用如下所示

    var hens [6]float64
    total := 0.0
    
    rand.Seed(time.Now().Unix())
    for i:= 0; i < len(hens); i++ {
        hens[i] = rand.Float64() * 20 + 5
        fmt.Println("第", (i + 1), " 個數是", hens[i])
        
        total += hens[i]
    }

    fmt.Println("均值爲", (total / float64(len(hens))))

數組初始化:元素值默認爲0,也可以用下面的方式初始化

    var nums [4]int = [4]int{1, 2, 3, 4}

    var nums1 = [4]int{1, 2, 3, 4}

    var nums3 = [...]int{1, 2, 3, 4} // 自行判斷長度,中括號裏...一個不能少

    var num4 = [...]int{1:3, 0:4, 2:5} // 指定索引和值

由於函數調用時數組形參的值傳遞,我們可以使用數組指針來實現數組內容在函數裏的實際改變,如下所示

func modify(array *[6]float64) {
    array[0] += 5
}

func main() {
    var hens [6]float64
    total := 0.0
    
    rand.Seed(time.Now().Unix())
    for i:= 0; i < len(hens); i++ {
        hens[i] = rand.Float64() * 20 + 5
        fmt.Println("第", (i + 1), " 個數是", hens[i])
        
        total += hens[i]
    }

    fmt.Println("均值爲", (total / float64(len(hens))))

    modify(&hens)

    total = 0
    for i:= 0; i < len(hens); i++ {
        fmt.Println("第", (i + 1), " 個數是", hens[i])
        total += hens[i]
    }

    fmt.Println("均值爲", (total / float64(len(hens))))
}

輸出如下

切片

切片就是動態數組,是數組的一個引用。

切片內存結構相當於一個結構體,由三部分構成:引用數組部分的首地址、切片長度和切片容量

由於是引用,所以改變切片的值,也會改變原數組的對應值

    array0 := [...]int{1, 2, 3, 4, 5, 6}
    slice := array0[1: 4] // 切片

    slice[0] = 7
    fmt.Println(array0[1]) // 7

除了引用創建好的數組外,也可以通過make函數來創建切片,傳入切片類型、長度和容量

slice0 := make([]int, 4, 10)

顯然,make方法創建切片時,會在底層創建一個數組,只是這個數組是我們不可見的

也可以通過類似創建數組的方式創建切片,只是不用傳入長度

slice2 := []int{1, 2, 4}

slice可以通過append的方式來進行動態追加,append時底層會構建一個新的數組,把所有要裝進去的元素裝進去,然後返回。

slice0 = append(slice0, 4, 5, 7, 1, 0, 4, 8) // slice0後面的參數都是要追加的元素
slice0 = append(slice0, slice...) // 把slice1的值追加到slice0後面

切片的拷貝可以通過copy函數實現

    slice1 := make([]int, 10) // 長度爲10
    copy(slice1, slice) // 參數列表:dest、src
    fmt.Println(slice1)
    slice1[len(slice1) - 1] = -1
    fmt.Println(slice) // 原切片不變

copy時,dest切片的長度並不重要

刪除切片可以通過切片的再切片來得以實現,以下是刪除Employees切片中下標爲target_index的元素

employees.Employees = append(employees.Employees[: target_index], employees.Employees[target_index + 1:]...) // 後面的...不能省略

映射

鍵值映射,要先make申請內存,再使用

    m1 := make(map[string] string) // map[鍵類型] 值類型

    m1["name"] = "Jason" // 鍵值對賦值
    m1["age"] = "23"
    fmt.Println(m1)

按鍵取值

    fmt.Println(m1["name"]) // songzeceng
    fmt.Println(m1["gender"]) // 空字符串
    fmt.Println(m1["gender"] == "") // true

刪除某值

delete(m1, "age") // 如果不存在age鍵,則也不會報錯

如果需要清空映射,直接分配新的內存就行

遍歷映射,使用for-range

    for k, v := range m1 {
        fmt.Println(k, "--", v)
    }

切片同樣適用於映射

    var slice_map []map[string] string
    slice_map = make([]map[string] string, 0)

    slice_map = append(slice_map, m1, m2)

    fmt.Println(slice_map)

而映射在函數傳參時是引用傳遞的

面向對象

結構體

結構體是go面向對象的實現方式,沒有this指針、沒有方法覆寫、沒有extends關鍵字等

其聲明和使用如下所示

type Person struct {
    Name string
    Age int
    Hometown string
}

func main()  {
    person0 := Person{"Jason", 23, "Washington"}

    fmt.Println(person0)
}

結構體是值類型,因此函數傳參是值傳遞,而且拷貝也是淺拷貝

    person1 := person0
    person1.Age = 21;
    fmt.Println(person0) // person0的age依舊是23

結構體指針聲明和使用如下

    person2 := new (Person) // 指針聲明方式1
    (*person2).Name = "Jason"
    (*person2).Age = 24

    fmt.Println(*person2) // 沒有賦值的字段默認爲0值

    person3 := &Person{"Mike", 20, "London"} // 指針聲明方式2
    fmt.Println(*person3)

如果結構體有切片、映射等屬性,也要先分配內存再使用

結構體地址爲首字段地址,且內部字段在內存中的地址連續分配。舉例如下

type Point struct {
    x, y int
}


type Rect struct {
    leftUp, rightDown Point
}

則以下代碼

    rect0 := Rect {Point{1, 2}, Point{3, 4}}
    fmt.Printf("%p ", &rect0)
    fmt.Println(&rect0.leftUp.x, &rect0.leftUp.y, &rect0.rightDown.x, &rect0.rightDown.y)

的輸出如下

0xc00000e460 0xc00000e460 0xc00000e468 0xc00000e470 0xc00000e478

當然,結構體內變量值不一定連續分配,看以下示例

type Rect_ struct {
    leftUp, rightDown *Point
}

則以下代碼

fmt.Printf("%p\t%p\n", rect1.leftUp, rect1.rightDown)
    fmt.Println(&rect1.leftUp.x, &rect1.leftUp.y, &rect1.rightDown.x, &rect1.rightDown.y)

的輸出如下

0xc0000120c0    0xc0000120d0
0xc0000120c0 0xc0000120c8 0xc0000120d0 0xc0000120d8

給結構體取別名,相當於定義新的數據類型,兩者的變量賦值時,必須強轉。

給結構體屬性取標籤,可以方便轉json時轉換大小寫

import (
    "fmt"
    "encoding/json"
)

type Person struct {
    Name string `json:"name"` // 標籤
    Age int `json:"age"`
    Hometown string `json:"homeTown"`
}

func main()  {
    person0 := Person{"szc", 23, "Washington"}

    jsonStr, _ := json.Marshal(person0)
    fmt.Println(string(jsonStr))
}

輸出如下

{"name":"szc","age":23,"homeTown":"Washington"}

方法

go中的方法定義如下

func (p Person) test() { 
    fmt.Println("name:", p.Name, "\tage:", p.Age, "\thometown:", p.Hometown)
}

調用方法如下

    person0 := Person{"Bob", 23, "California"}

    person2 := new (Person)
    (*person2).Name = "Jason"
    (*person2).Age = 24

    person0.test()
    (*person2).test()

輸出如下

name: Bob       age: 23         hometown: California
name: Jason     age: 24         hometown:

綁定方法時的p是實際調用者的副本,方法調用時會發生值拷貝。所以當結構體有引用型成員變量時,在方法裏發生的修改會同步到方法外面

type Person struct {
    Name string
    Age int 
    Hometown string 
    score map[string]int
}


func (p Person) test() {
    p.Age += 1
    p.score["China"] += 1
}

func main()  {
    m0 := make(map[string]int)
    m0["China"] = 80
    person0 := Person{"szc", 23, "Henan Anyang", m0}

    person2 := new (Person)
    (*person2).Name = "Jason"
    (*person2).Age = 24
    m2 := make(map[string]int)
    m2["Math"] = 90
    (*person2).score = m2

    person0.test()
    fmt.Println(person0)
    (*person2).test()
    fmt.Println(*person2)
}

會得到以下輸出,age沒有變,但映射屬性卻發生了改變

{szc 23 Henan Anyang map[China:81]}
{Jason 24  map[China:1 Math:90]}

對應的map變量的值也會發生變化

    fmt.Println(m0, "\n", m2)

輸出如下

map[China:81]
map[China:1 Math:90]

不過,爲了能使方法裏的修改更高效地同步到外面,聲明方法時一般會綁定結構體指針,如下

func (p *Person) test_1(n int) string {
    (*p).score["China"] += n
    (*p).Age -= n

    return "succeed"
}

調用時,還是可以直接使用變量名調用方法,而不必取址

    (&person0).test_1(5)
    fmt.Println(person0)
    person0.test_1(5)
    fmt.Println(person0)

輸出如下

{Jason 18 Washington map[China:86]}
{Jason 13 Washington map[China:91]}

所以,方法裏對結構體變量的成員進行的修改能不能同步到外面,關鍵要看方法綁定時綁定的是不是指針,而不是調用時用什麼調用的。

 

以上的方法定義也適用於系統自帶類型,定義方法如下

type integer int // 要先定義別名

func (i *integer) test(n int) {
    *i += integer(n) // int和integer雖然只是別名關係,但依舊不是同一個類型
}

調用過程如下

    var num integer
    num = 8
    num.test(6)
    fmt.Println(num)

會得到輸出14

如果要實現類似java裏的toString,我們可以對指定數據類型綁定String()方法,返回string

func (p Person) String() string {
    return fmt.Sprintf("name:%v\tage:%v\thometown:%v\tscore:%v", p.Name, p.Age, p.Hometown, p.score)
}

然後使用fmt輸出Person變量

    m0 := make(map[string]int)
    m0["China"] = 80
    person0 := Person{"Mike", 23, "Manchester", m0}

    person2 := new (Person)
    (*person2).Name = "Jason"
    (*person2).Age = 24
    m2 := make(map[string]int)
    m2["Math"] = 90
    (*person2).score = m2

    fmt.Println(person0)
    fmt.Println(*person2)

得到的輸出如下

name:Mike       age:23  hometown:Manchester   score:map[China:80]
name:Jason      age:24  hometown:       score:map[Math:90]

如果String()方法綁定的是結構體指針,那麼輸出時要傳入地址,否則會按照原來的方式輸出

func (p *Person) String() string {
    return fmt.Sprintf("name:%v\tage:%v\thometown:%v\tscore:%v", p.Name, p.Age, p.Hometown, p.score)
}

func main()  {
    m0 := make(map[string]int)
    m0["China"] = 80
    person0 := Person{"Mike", 23, "Manchester", m0}

    person1 := person0
    person1.Age = 21;
    
    person2 := new (Person)
    (*person2).Name = "Jason"
    (*person2).Age = 24
    m2 := make(map[string]int)
    m2["Math"] = 90
    (*person2).score = m2

    fmt.Println(&person0)
    fmt.Println(person0)
    fmt.Println(person2)
    fmt.Println(*person2)
}

會得下面的輸出

name:Mike        age:23  hometown:Manchester   score:map[China:80]
{Mike 23 Manchester map[China:80]}
name:Jason      age:24  hometown:       score:map[Math:90]
{Jason 24  map[Math:90]}

工廠模式

當我們的結構體首字母小寫時,我們可以採取對外暴露一個函數,返回結構體變量指針,來進行結構體變量的構造與訪問

package model

type student struct { // 結構體名首字母小寫,則僅能包內訪問
    Name string
    Age int
}

func CreateStudent(name string, age int) *student {
    // 暴露函數名首字母大寫的函數,重當構造方法
    return &student {name, age}
}

然後在main包裏進行如下調用

package main

import (
    "fmt"
    "go_code/project01/model" // 導入model包
)

func main()  {
    student0 := model.CreateStudent("Jason", 23) // 調用公有方法,獲得指針對象
    fmt.Println(*student0)
}

會得到以下輸出

{Jason 23}

訪問包私有屬性也是同樣的方法,暴露公有的方法,返回私有的屬性

package model

type student struct {
    Name string
    age int
}

func CreateStudent(name string, age int) *student {
    return &student {name, age}
}

func (student *student) GetAge() int {
    return student.age
}

外部進行如下調用

fmt.Println(student0.GetAge())

輸出爲23

這就是go語言裏的工廠模式

繼承

繼承可以通過嵌套匿名結構體來實現,如下

package model

type Student struct {
    Name string
    Age int
}


type Graduate struct {
    Student // 匿名結構體
    Major string
}

func (student *Student) GetAge() int {
    return student.Age
}

func (graduate *Graduate) GetMajor() string {
    return graduate.Major
}

外部調用如下

package main

import (
    "fmt"
    "go_code/project01/model"
)

func main()  {
    graduate0 := &model.Graduate{}
    graduate0.Name = "szc" 
    graduate0.Age = 23
    graduate0.Major = "software"
    
    fmt.Println(graduate0.GetAge())
    fmt.Println(graduate0.GetMajor())
}

graduate0.Name是graduate0.Student.Name的簡寫,但由於Student在Graduate裏是匿名結構體,所以可以省略。此時匿名結構體就相當於父類,外層結構體相當於子類。所以,如果匿名結構體和外層結構體中有同名字段或方法時,默認使用外層結構體的字段或方法,如果要訪問匿名結構體中的字段或方法,就要顯式調用,如下所示

package main

import (
    "fmt"
    "go_code/project01/model"
)

type A struct {
    n int
}

type B struct {
    A
    n int
}

func (a *A) test() {
    fmt.Println("A...")
}


func (b *B) test() {
    fmt.Println("B...")
}

func main()  {
    var b B
    b.n = 10
    b.A.n = 21

    fmt.Println(b.n)
    fmt.Println(b.A.n) // 顯式調用
    fmt.Println(b)

    b.test()
    b.A.test()
}

得到的輸出如下

10
21
{{21} 10}
B...
A...

當結構體嵌入了多個匿名結構體,並且這些匿名結構體擁有同名字段或方法時,訪問時就必須顯式調用了。

如果把匿名結構體改成有名結構體,那麼這個有名結構體就相當於外層結構體的屬性,訪問其屬性或方法就必須顯式調用。

package main

import (
    "fmt"
    "go_code/project01/model"
)

type C struct {
    n int
}

type A struct {
    n int
}

type B struct {
    A // 匿名結構體,父類
    n int
    c C // 有名結構體,成員變量
}

func (a *A) test() {
    fmt.Println("A...")
}

func (b *B) test() {
    fmt.Println("B...")
}

func (c *C) test() {
    fmt.Println("C...")
}

func main()  {
    var b B
    b.n = 10
    b.A.n = 21
    b.c.n = 31 // 顯式訪問成員變量的屬性

    fmt.Println(b.n)
    fmt.Println(b.A.n)
    fmt.Println(b)
    fmt.Println(b.c.n)

    b.test()
    b.A.test()
    b.c.test() // 顯式調用成員變量的方法
}

會得到如下輸出

10
21
{{21} 10 {31}}
31
B...
A...
C...

接口

go中的接口定義如下

type ICalculate interface { // type 接口名 interface
    add()
    sub()
}

然後定義兩個結構體,來實現ICalculate

type B struct {

}


type D struct {

}

func (b B) add() {
    fmt.Println("B..add")
}

func (b B) sub() {
    fmt.Println("B..sub")
}

func (d D) add() {
    fmt.Println("D..add")
}

func (d D) sub() {
    fmt.Println("D..sub")
}

再定義一個結構體,爲其綁定一個方法,傳入接口對象

type E struct {

}

func (e *E) add(ic ICalculate) { // 接口是引用類型,所以這裏傳遞的是變量的引用
    ic.add()
}

func (e *E) sub(ic ICalculate) {
    ic.sub()
}

最後,調用E中的方法

    b0 := B{}
    d0 := D{}
    e0 := E{}

    e0.add(b0)
    e0.add(d0)
    e0.sub(b0)
    e0.sub(d0)

會得到如下輸出

B..add
D..add
B..sub
D..sub

go中,只要一個結構體實現了接口的全部方法,這個結構體就是這個接口的一個實現。所以go中沒有implement關鍵字

不過,go中接口變量可以指向接口實現結構體的變量,如下所示

    var ic ICalculate
    ic = b0
    ic.add()

不止結構體,自定義類型都可以實現接口

type integer0 int


func (i integer0) add() {
    fmt.Println("integer..add")
}

調用方法也是一樣的

    i0 := integer0(1)
    ic = i0
    ic.add()

空接口裏沒有任何方法,所以任何類型都實現了空接口

使用接口數組,是實現多態的一種方式

    var cals []ICalculate
    cals = append(cals, b0)
    cals = append(cals, d0)

    fmt.Println(cals)

接口應用實例:結構體切片排序,要實現sort包下Interface接口中Len()、Less()、Swap()三個接口

type slice_A []A // 先定義結構體切片的別名

// 分別實現三個方法
func (sa slice_A) Len() int {
   // 返回切片長度
    return len(sa)
}

func (sa slice_A) Less(i, j int) bool {
    return sa[i].n < sa[j].n // 自定義排序標準
}

func (sa slice_A) Swap(i, j int) {
    // 自定義排序邏輯
    temp := sa[i]
    sa[i] = sa[j]
    sa[j] = temp
}

調用時,先導入sort包,再進行調用

import (
    "fmt"
    "sort"
)

func main() {
    var slices slice_A

    slices = append(slices, A{1})
    slices = append(slices, A{5})
    slices = append(slices, A{2})
    slices = append(slices, A{4})
    slices = append(slices, A{3})

    sort.Sort(slices)

    fmt.Println(slices)
}

輸出如下

[{1} {2} {3} {4} {5}]

類型斷言

類型斷言用來判斷某個接口對象是不是某個接口實現的實例

    b1, succeed := cals[0].(B) // 使用方法:待斷言變量.(斷言類型)
    if succeed {
        fmt.Println("convert success")
    } else {
        fmt.Println("convert fail")
    }


    c1, succeed = cals[1].(B)
    if succeed {
        fmt.Println("convert success")
    } else {
        fmt.Println("convert fail")
    }

也可以使用switch語句

    switch cals[0].(type) {
        case B: fmt.Println("type b")
        case D: fmt.Println("type d")
        default: fmt.Println("type unkown")
    }

文件操作

打開與關閉

文件在go中是一個結構體,它的定義和相關函數在os包中,所以要先導包

import (
    "os"
)

打開文件和關閉文件的方法如下

    file, err := os.Open("D:/output.txt")
    if err != nil {
        fmt.Println("open file error = ", err)
        return
    }

    fmt.Println("file = ", *file)
    
    err = file.Close()
    if err != nil {
        fmt.Println("close file error = ", err)
    }

其中file的輸出如下,可以看到file結構體裏存放着一個指針

file =  {0xc000110780}

如果指定文件不存在,那麼打開文件時會返回如下的錯誤

open file error =  open D:/output00.txt: The system cannot find the file specified.

讀取文件

文件讀取方法如下所示

    reader := bufio.NewReader(file) // 默認緩衝4096

    for {
        str, err := reader.ReadString('\n') // 一次讀取一行
        if err == nil {
            fmt.Print(str) // reader會把分隔符\n讀進去,所以不用Println
        } else if err == io.EOF { // 讀到文件尾會返回一個EOF異常
            fmt.Println("文件讀取完畢")
            break
        } else {
            fmt.Println("read error: " , err)
        }
    }

要先導包

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

如果文件不大,就可以使用io/ioutil包下的ReadFile函數一次性讀取

    bytes, err1 := ioutil.ReadFile("D:/output.txt")
    if err1 != nil {
        fmt.Println("open file error = ", err1)
        return
    }

    fmt.Println(string(bytes))

導包如下

import (
    "fmt"
    "io/ioutil"
)

創建與寫入

創建文件並寫入內容的方法如下

    file_path := "D:/out_go.txt"
    file, err := os.OpenFile(file_path, os.O_WRONLY | os.O_CREATE, 0777) // 最後的777在windows下沒有用
    
    if err != nil {
        fmt.Println("Open file error: " , err)
        return
    }

    defer file.Close()

    writer := bufio.NewWriter(file)
    for i := 0; i < 5; i++ {
        writer.WriteString("New content" + fmt.Sprintf("%d", i) + "\n") // 寫入一行數據
    }

    writer.Flush() // 把緩存數據刷入文件中

如果打開已存在的文件,覆寫新內容,就要把模式換成os.O_TRUNC

    file_path := "D:/out_go.txt"
    file, err := os.OpenFile(file_path, os.O_WRONLY | os.O_TRUNC, 0777)
    
    if err != nil {
        fmt.Println("Open file error: " , err)
        return
    }

    defer file.Close()

    writer := bufio.NewWriter(file)
    for i := 0; i < 5; i++ {
        writer.WriteString("New content " + fmt.Sprintf("%d", i) + " ....\n")
    }

    writer.Flush()

如果是在已存在文件中追加內容,就把模式換成os.O_APPEND

    file_path := "D:/out_go.txt"
    file, err := os.OpenFile(file_path, os.O_WRONLY | os.O_APPEND, 0777)
    
    if err != nil {
        fmt.Println("Open file error: " , err)
        return
    }

    defer file.Close()

    writer := bufio.NewWriter(file)
    for i := 0; i < 5; i++ {
        writer.WriteString("New content " + fmt.Sprintf("%d", i) + " ~~~~~~\n")
    }

    writer.Flush()

如果既要讀文件又要寫文件,就要把模式改成os.O_RDWR

    file_path := "D:/out_go.txt"
    file, err := os.OpenFile(file_path, os.O_RDWR | os.O_APPEND, 0777)
    
    if err != nil {
        fmt.Println("Open file error: " , err)
        return
    }

    defer file.Close()

    reader := bufio.NewReader(file)
    for {
        str, err1 := reader.ReadString('\n')
        if err1 == io.EOF {
            break
        }

        fmt.Print(str)
    }

    writer := bufio.NewWriter(file)
    for i := 0; i < 5; i++ {
        writer.WriteString("New content " + fmt.Sprintf("%d", i) + " *********\n")
    }

    writer.Flush()

文件複製可以直接調用io.Copy()函數,傳入新文件的writer和老文件的reader

    src_path := "D:/test.jpg"
    dest_path := "D:/gtest.jpg"

    src_file, src_err := os.OpenFile(src_path, os.O_RDONLY, 0777)
    if src_err != nil {
        fmt.Println("open file error: ", src_err)
        return
    }

    defer src_file.Close()

    reader := bufio.NewReader(src_file)

    dest_file, dest_error := os.OpenFile(dest_path, os.O_WRONLY | os.O_CREATE, 0777)
    if dest_error != nil {
        fmt.Println("open file error: ", dest_error)
        return
    }

    defer dest_file.Close()

    writer := bufio.NewWriter(dest_file)

    copy_sie, err2:= io.Copy(writer, reader) // 拷貝字節數和發生的錯誤
    fmt.Println(copy_sie,err2)

命令行參數

命令行參數保存在os.Args裏,是一個字符串切片

先導入os包

import (
    "fmt"
    "os"
)

然後用for-range遍歷os.Args即可

    for index, arg := range os.Args {
        fmt.Println("第", (index + 1), "個參數是", arg)
    }

會得到如下輸出

PS D:\develop\Microsoft VS Code\workspace\src\go_code\project01\main> go run .\cmd_args.go 123 json
第 1 個參數是 D:\deveop\temp\go-build629970270\b001\exe\cmd_args.exe
第 2 個參數是 123
第 3 個參數是 json

也可以用flag包進行參數解析

    var s0 string
    var s1 string
    var i0 int

    flag.StringVar(&s0, "u", "", "字符串參數1") // 接收字符串參數,參數列表:參數接收地址,參數名,默認值, 參數說明
    flag.StringVar(&s1, "p", "", "字符串參數2")
    flag.IntVar(&i0, "i", 0, "整型參數1")

    flag.Parse() // 開始解析

    fmt.Println("s0 = ", s0, ", s1 = ", s1, ", i0 = ", i0)

導包如下

import (
	"flag"
	"fmt"
)

測試輸出如下

PS D:\develop\Microsoft VS Code\workspace\src\go_code\project01\main> go run .\cmd_args.go -u s -p c -i 98
s0 =  s , s1 =  c , i0 =  98

序列化反序列化

序列化

把結構體序列化成json的方法以前提到過,如下所示

func main()  {
    p0 := Person_ser{Name: "szc", Age: 23,}

    json_bytes, error_ := json.Marshal(&p0)
    if error_ != nil {
        fmt.Println("Json error:", error_)
        return
    }

    fmt.Println(string(json_bytes))
     // {"Name":"szc","Age":23}
}

導包

import (
    "fmt"
    "encoding/json"
)

一定要記得結構體的屬性如果要序列化成json,就必須首字母大寫;包私有的屬性不能被json包序列化成json

如果要把首字母大寫的屬性名序列化首字母小寫的json鍵,就需要使用tag

type Person_ser struct {
    Name string `json:"name"` // 標籤
    Age int `json:"age"`
}

json.Marshal函數也可以對映射進行序列化

    var a map[string] interface{} // 鍵是空接口,表示能接收任意類型
    a = make(map[string] interface{})

    a["name"] = "szc"
    a["age"] = 23

    bytes, error_ := json.Marshal(&a)
    if error_ != nil {
        fmt.Println("Json error:", error_)
        return
    }

    fmt.Println(string(bytes))
    // {"age":23,"name":"szc"}

對切片序列化也是可以的

    var slice []map[string] interface{}
    slice = make([]map[string] interface{}, 0)

    for i := 0; i < 5; i++ {
        var a map[string] interface{}
        a = make(map[string] interface{})

        a["name"] = "szc" + fmt.Sprintf("%d", i)
        a["age"] = 23

        slice = append(slice, a)
    }

    bytes, error_ := json.Marshal(&slice)
    if error_ != nil {
        fmt.Println("Json error:", error_)
        return
    }

    fmt.Println(string(bytes))
    // [{"age":23,"name":"szc0"},{"age":23,"name":"szc1"},{"age":23,"name":"szc2"},{"age":23,"name":"szc3"},{"age":23,"name":"szc4"}]

甚至對普通數據類型也能序列化,只是只有值,沒有鍵

    i := 1

    bytes, error_ := json.Marshal(&i)
    if error_ != nil {
        fmt.Println("Json error:", error_)
        return
    }

    fmt.Println(string(bytes))
    // 1

反序列化

反序列化時,要調用Unmarshal函數,傳入待解析字符串的bytes,以及接收結果的對象指針

    str := "{\"Name\":\"szc\",\"Age\":23}"
    var p0 Person_ser

    err := json.Unmarshal([]byte(str), &p0)

    if err != nil {
        fmt.Println("Json error:", err)
        return
    }

    fmt.Println(p0)
    // {szc 23}

同樣可以解析成映射、切片

    str := "{\"Name\":\"szc\",\"Age\":23}"
    var m0 map[string]interface{}

    err := json.Unmarshal([]byte(str), &m0)

    if err != nil {
        fmt.Println("Json error:", err)
        return
    }

    slice := make([]map[string] interface{}, 0)
    err = json.Unmarshal([]byte("[{\"age\":23,\"name\":\"szc0\"},{\"age\":23,\"name\":\"szc1\"},{\"age\":23,\"name\":\"szc2\"}]"), &slice)
    if err != nil {
        fmt.Println("Json error:", err)
        return
    }

    fmt.Println(m0) // map[Age:23 Name:szc]
    fmt.Println(slice) // [map[age:23 name:szc0] map[age:23 name:szc1] map[age:23 name:szc2]]

單元測試

單元測試用來檢測代碼錯誤、邏輯錯誤和性能高低

首先有待測試文件first.go,內有函數addUpper()

package main

func addUpper(n int) int {
    ret := 0
    for i := 0; i < n; i++ {
        ret += i
    }
    return ret
}

然後添加first_test.go文件,導入testing包,編寫TestAddUpper()函數

package main

import (
    "testing" // 引入testing框架
)

func TestAddUpper(t *testing.T) {
    res := addUpper(10) // 調用目標函數
    if res != 45 {
        t.Fatalf("AddUpper(10)執行錯誤, 期望值%d, 實際值%d\n", 55, res) // 打出錯誤日誌
    }

    t.Logf("AddUpper(10)執行正確") // 打出正常日誌
}

然後在命令行執行go test -v,就會看到結果

PS D:\develop\Microsoft VS Code\workspace\src\go_code\project01\main> go test -v
init variable_advanced..
=== RUN   TestAddUpper
    TestAddUpper: first_test.go:14: AddUpper(10)執行正確
--- PASS: TestAddUpper (0.00s)
PASS
ok      go_code/project01/main  0.323s

go test -v命令會執行這個目錄內所有的測試用例,例如再在這個目錄下添加測試文件map_test.go文件和TestSub()函數

package main

import "testing"

func TestSub(t *testing.T) {
    ret := sub(9, 3)
    if ret != 6 {
        t.Fatalf("sub 執行錯誤, 預期值%d, 實際值%d\n", 6, ret)
    }

    t.Logf("sub執行正確")
}

待檢測函數sub如下

func sub(n1 int, n2 int) int {
    return n1 - n2
}

運行測試用例

PS D:\develop\Microsoft VS Code\workspace\src\go_code\project01\main> go test -v
init variable_advanced..
=== RUN   TestAddUpper
    TestAddUpper: first_test.go:14: AddUpper(10)執行正確
--- PASS: TestAddUpper (0.00s)
=== RUN   TestSub
    TestSub: map_test.go:11: sub執行正確
--- PASS: TestSub (0.00s)
PASS
ok      go_code/project01/main  0.322s

會發現測試累計用時比測試那兩個函數用時的和要大,因爲加載testing框架也要消耗時間

如果要測試單個文件,則要執行命令go test -v xx_test.go xx.go

PS D:\develop\Microsoft VS Code\workspace\src\go_code\project01\main> go test -v .\map_test.go .\map.go
=== RUN   TestSub
    TestSub: map_test.go:11: sub執行正確
--- PASS: TestSub (0.00s)
PASS
ok      command-line-arguments  0.382s

如果測試單個函數的話,使用-test.run TestXxxx選項即可

λ go test -v -test.run TestAddUpper
init variable_advanced..
=== RUN   TestAddUpper
    TestAddUpper: first_test.go:14: AddUpper(10)執行正確
--- PASS: TestAddUpper (0.00s)
PASS
ok      go_code/project01/main  0.410s

如果要在測試前統一進行一些操作,可以覆寫TestMain函數

func TestMain(m *testing.M) {
    fmt.Println("testing start")

    m.Run() // 執行測試
}

如果要在測試時執行另一個測試函數,可以執行t.Run()函數

func TestAddSale(t *testing.T) {
    sale := &Sale{Widget_id: 9, Qty: 80, Street: "Huanghe South Road", City: "Anyang Henan", State: "China", Zip: 455000, Sale_date: "2020-03-24"}
    sale.AddSale()


    t.Run("fun", fun_test)
}

func fun_test(t *testing.T) {
    fmt.Println("fun_test")
}

測試輸出如下

PS D:\develop\Go\workspace\src\go_code\go_web\src\main> go test

testing start
fun_test
PASS
ok      go_code/go_web/src/main 0.987s

併發編程

協程goroutine

協程是輕量級線程,有獨立棧空間,但共享堆空間

go中開啓協程執行函數的方法如下

go test_r()

test_r()函數體如下

func test_r() {
    for i := 0; i < 10; i++ {
        fmt.Println("test_r test.......", strconv.Itoa(i + 1))
        time.Sleep(2 * time.Second) // 休眠2秒
    }
}

寫一個main()函數做測試

func main()  {
    go test_r()
    for i := 0; i < 10; i++ {
        fmt.Println("main test.......", strconv.Itoa(i + 1))
        time.Sleep(time.Second)
    }
}

導入strconv和time包

import (
    "fmt"
    "strconv"
    "time"
)

輸出如下

PS D:\develop\Microsoft VS Code\workspace\src\go_code\project01\main> go run .\go_routine.go
main test....... 1
test_r test....... 1
main test....... 2
main test....... 3
test_r test....... 2
main test....... 4
test_r test....... 3
main test....... 5
main test....... 6
test_r test....... 4
main test....... 7
main test....... 8
test_r test....... 5
main test....... 9
main test....... 10
test_r test....... 6

由於主函數循環每1秒執行一次,子協程的循環每2秒執行一次,所以子協程沒執行完,主線程就執行完了,程序故而退出

獲取並設置使用cpu數量

    num := runtime.NumCPU() // 獲取cpu核數
    fmt.Println("CPU count:", num)
    runtime.GOMAXPROCS(num - 1) // 設置最大併發數

 事先導入runtime包

import (
    "fmt"
    "runtime"
)

 輸出如下

CPU count: 12

MPG模式

MPG模式:

M:操作系統主線程

P:協程執行所需的上下文

G:協程

三者關係如下圖所示

假設主線程M0的G0協程阻塞,如果協程隊列裏有別的協程,那麼就會新啓一個M1主線程,把協程隊列裏的其他協程掛在到M1上執行,這就是MPG模式

全局互斥鎖

當涉及多個協程對同一個引用類型的對象進行讀寫操作時,就需要全局鎖來幫助同步。

先導包

import (
    "math"
    "fmt"
    "strconv"
    "time"
    "runtime"
    "sync" // 同步包
)

然後聲明鎖變量

var (
    result []int = make([]int, 0) // 素數切片
    lock sync.Mutex // 全局鎖
)

編寫is_prime函數,在append素數切片時,進行鎖的請求和釋放

func exits(slice []int, n int) bool {
    for _, value := range slice {
        if value == n {
            return true
        }
    }

    return false
}


func is_prime(n int) {
    is_prime := true
    for i := 2; i < int(math.Sqrt(float64(n))) + 1; i++ {
        if n % i == 0 {
            is_prime = false
            break
        }
    }

    if is_prime && !exits(result, n) {
        lock.Lock() // 請求鎖
        result = append(result, n)
        lock.Unlock() // 釋放鎖
    }
}

主線程開啓2000個協程,進行素數判斷,等待10秒後,讀取素數切片內容。

由於讀取的是全局變量,所以讀的時候也要加鎖和釋放鎖

func main()  {
    num := runtime.NumCPU()
    runtime.GOMAXPROCS(num - 1)


    for i := 2; i < 2000; i++ {
        // 開啓將近2000個協程,判斷素數
        go is_prime(i)
    }

    time.Sleep(10 * time.Second) // 主線程等待10秒

    lock.Lock() // 遍歷的時候依舊要利用鎖進行同步控制
    for _, value := range result {
        fmt.Println(value)
    }
    lock.Unlock()
}

最後的輸出如下

2
3
5
7
11
13
17
19
23
...
1987
1993
1997
1999

管道

管道本質是一個隊列,而且線程安全,有類型

以下爲實例:一個寫協程,一個讀協程,主線程等待兩者完成後退出

先構造兩個協程,一個存儲數據,一個表示是否讀寫完成

var (
    write_chan chan int = make(chan int, 50) // 數據管道,整型管道,容量50(不可擴容)
    exit_chan chan bool = make(chan bool, 1) // 狀態管道,布爾型管道,容量1(不可擴容)
)

然後,構造讀寫函數。寫函數往數據管道里寫入50個數據,並關閉數據管道;讀函數負責從數據管道里讀取數據,如果讀完,則往狀態管道里置入true,表示讀取完成

func write_data() {
    for i := 0; i < 50; i++ {
        write_chan<- i // 往管道里寫數據
        fmt.Println("write data: ", i)
    }

    close(write_chan)
    // 關閉管道不影響讀,隻影響寫
}

func read_data() {
    for {
        v, ok := <-write_chan // 從管道里讀數據,返回具體數據和成功與否。如果管道爲空,就會阻塞
        if !ok { // 如果管道爲空,則ok爲false
            break
        }
        fmt.Println("read data: ", v)
    }

    exit_chan<- true
    close(exit_chan)
}

主線程負責開啓兩個協程,並監視狀態管道

func main()  {
    go write_data()
    go read_data()

    for {
        _, ok := <-exit_chan
        if !ok {
            break
        }
    }

}

最後輸出如下

write data:  0
write data:  1
write data:  2
write data:  3
write data:  4
write data:  5
write data:  6
write data:  7
write data:  8
write data:  9
write data:  10
write data:  11
write data:  12
write data:  13
write data:  14
write data:  15
write data:  16
write data:  17
write data:  18
write data:  19
write data:  20
write data:  21
write data:  22
write data:  23
write data:  24
write data:  25
write data:  26
write data:  27
write data:  28
write data:  29
write data:  30
write data:  31
write data:  32
read data:  0
read data:  1
read data:  2
read data:  3
read data:  4
read data:  5
read data:  6
read data:  7
read data:  8
read data:  9
read data:  10
read data:  11
read data:  12
read data:  13
read data:  14
read data:  15
read data:  16
read data:  17
read data:  18
read data:  19
read data:  20
read data:  21
read data:  22
read data:  23
read data:  24
read data:  25
read data:  26
read data:  27
read data:  28
read data:  29
read data:  30
read data:  31
read data:  32
read data:  33
write data:  33
write data:  34
write data:  35
write data:  36
write data:  37
write data:  38
write data:  39
write data:  40
write data:  41
write data:  42
write data:  43
write data:  44
write data:  45
write data:  46
write data:  47
write data:  48
write data:  49
read data:  34
read data:  35
read data:  36
read data:  37
read data:  38
read data:  39
read data:  40
read data:  41
read data:  42
read data:  43
read data:  44
read data:  45
read data:  46
read data:  47
read data:  48
read data:  49

往管道里寫數據時,如果超出了管道容量,就會阻塞;

但是讀寫頻率不一致,則不會發生阻塞問題。

不使用協程時,從空管道里讀數據會發生死鎖錯誤;

普通for-range遍歷沒有關閉的管道時,也發生死鎖錯誤。

 

如下是channel版的尋找素數。

和上面的讀寫數據類似,這裏有三個管道:原始數據管道、素數結果管道和協程狀態管道

var (
    int_chan chan int = make(chan int, 80000) // 待判斷的數爲2-80001
    prime_chan chan int = make(chan int, 2000) // 素數管道
    over_chan chan bool = make(chan bool, 4) // 狀態管道
)

也有三個函數:寫入數據

func put_num() {
    for i := 2; i < 80002; i++ {
        int_chan<- i
    }

    close(int_chan)
}

由於輸入數據的管道只有一個,所以最後就把原始數據管道關閉了

第二個函數用來判斷數據是否是素數

func check_prime() {
    for {
        is_prime := true
        num, ok := <- int_chan
        if !ok {
            break
        }

        for i := 2; i < int(math.Sqrt(float64(num))) + 1; i++ {
            if num % i == 0 {
                is_prime = false
                break
            }
        }

        if is_prime {
            prime_chan<- num
        }
    }

    fmt.Println("One routine has exit for the lack of data..")

    over_chan<- true
}

由於有多個管道處理素數判斷,所以這裏最後不關閉over_chan和prime_chan

第三個函數作爲一個匿名函數,判斷是否所有判斷素數的協程都已完成

func() {
        over_num := 0
        for {
            if over_num == 4 {
                break
            }
    
            status, ok := <-over_chan
            if !ok {
                break
            }
    
            if status {
                over_num += 1
            }
        }

        close(prime_chan) // 此時所有判斷協程已經結束,關閉prime_chan,主線程遍歷prime_chan處喚醒阻塞
    }

最後在main函數裏,啓動一個輸入協程、四個判斷攜程,最後自己負責從素數管道里拿素數,順便計時

    go put_num()

    start := time.Now().Unix()

    for i := 0; i < 4; i++ {
        go check_prime()
    }

    go func() {
        over_num := 0
        for {
            if over_num == 4 {
                break
            }
    
            status, ok := <-over_chan
            if !ok {
                break
            }
    
            if status {
                over_num += 1
            }
        }

        close(prime_chan)
    }()


    for {
        num, ok:= <-prime_chan
        if !ok {
            break
        }
        fmt.Println(num)
    }

    fmt.Println("Time used:", strconv.Itoa(int(time.Now().Unix()) - int(start)))
    close(over_chan)

最後輸出如下

PS D:\develop\Microsoft VS Code\workspace\src\go_code\project01\main> go run .\go_routine.go
CPU count: 12
2
3
5
7
11
13
17
19
....
79979
79987
79997
79999
One routine has exit for the lack of data..
One routine has exit for the lack of data..
One routine has exit for the lack of data..
One routine has exit for the lack of data..
Time used: 2

可見速度非常快

 

把管道聲明爲只讀或只寫

    int_chan1 chan<- int = make(chan int, 8) // 只寫
    int_chan2 <- chan int  = make(chan int, 8) // 只讀

傳統方法遍歷管道時,如果管道不關閉,就會發生死鎖。如果我們不確定何時關閉管道,就可以使用select,如下所示

    label:
    for {
        select {
            case v := <-int_chan_3:
                // 如果管道一直不關閉,也不會死鎖,而會向下匹配
                fmt.Println("data from int chan: ", v)
            case v := <-string_chan:
                fmt.Println("data from string chan: ", v)
            default:
                break label
        }
    }

向int_chan_3和string_chan中賦值的代碼如下

    int_chan_3 := make(chan int, 10)
    for i := 0; i < 10; i++ {
        int_chan_3 <- i
    }

    string_chan := make(chan string, 5)
    for i := 0; i < 5; i++ {
        string_chan <- "string " + strconv.Itoa(i)
    }

最後輸出如下

PS D:\develop\Microsoft VS Code\workspace\src\go_code\project01\main> go run .\go_routine.go
data from string chan:  string 0
data from int chan:  0
data from int chan:  1
data from string chan:  string 1
data from string chan:  string 2
data from string chan:  string 3
data from string chan:  string 4
data from int chan:  2
data from int chan:  3
data from int chan:  4
data from int chan:  5
data from int chan:  6
data from int chan:  7
data from int chan:  8
data from int chan:  9

異常捕獲

當我們需要在某個協程函數裏捕獲異常時,使用以前的defer-recover即可

func test_r_0() {
    defer func() {
        if err := recover(); err != nil {
            fmt.Println("test_r_0 發生錯誤:", err)
        }
    }()
    var map0 map[int]string
    map0[0] = "szc"
}

同時寫一個正常方法

func test_r() {
    for i := 0; i < 10; i++ {
        fmt.Println("test_r test.......", strconv.Itoa(i + 1))
        time.Sleep(500 * time.Millisecond)
    }
}

最後在main函數裏測試

func main()  {

    go test_r()
    go test_r_0()

    time.Sleep(time.Second * 10)
}

輸出如下

PS D:\develop\Microsoft VS Code\workspace\src\go_code\project01\main> go run .\go_routine.go
test_r test....... 1
test_r_0 發生錯誤: assignment to entry in nil map
test_r test....... 2
test_r test....... 3
test_r test....... 4
test_r test....... 5
test_r test....... 6
test_r test....... 7
test_r test....... 8
test_r test....... 9
test_r test....... 10

反射

反射可以動態獲取變量的類型、結構體的屬性和方法,以及設置屬性、執行方法等

先導入reflect包

import (
    "reflect"
)

反射基本數據類型

下面是對基本數據類型進行反射的方法

func reflect_base(n int) {
    rTyp := reflect.TypeOf(n) // 獲取反射類型
    fmt.Println("rType = ", rTyp) // int
    fmt.Println("rType`s name = ", rTyp.Name()) // int
    
    rVal := reflect.ValueOf(n) // 獲取反射值
    fmt.Printf("rValue = %v, rValue`s type = %T\n", rVal, rVal) // 100, reflect.Value

    n1 := 2 + rVal.Int() // 獲取反射值持有的整型值
    fmt.Println("n1 = ", n1)

    iV := rVal.Interface() // 反射值轉換成空接口
    num := iV.(int) // 類型斷言
    fmt.Println("num = ", num)
}

注意反射值必須轉換成空接口,然後進行類型斷言,才能獲取真正的值,因爲反射是運行時進行的。

反射結構體

以下是對結構體進行反射的方法

func reflect_struct(n interface{}) {
    rType := reflect.TypeOf(n)
    rValue := reflect.ValueOf(n)

    iv := rValue.Interface()

    fmt.Println("rType = ", rType, ", iv = ", iv)
    // rType =  main.Student_rf , iv =  {szc 23}
    fmt.Printf("Type of iv = %T\n", iv)
    // Type of iv = main.Student_rf

    // 類型斷言
    switch iv.(type) {
        case Student_rf:
            student := iv.(Student_rf)
            fmt.Println(student.Name, ", ", student.Age)
        case Student_rf_0:
            student := iv.(Student_rf_0)
            fmt.Println(student.Name, ", ", student.Age)
    }
}

獲取反射種類:

    fmt.Println("rKind = ", rValue.Kind(), ", rKind = ", rType.Kind())
        // rKind =  struct , rKind =  struct

故而反射類型就是變量的類型,反射種類則更寬泛一些。例如對於基本數據類型,反射類型=反射種類;對於結構體,反射類型則是包名.結構體名,反射種類則是struct

反射改基本數據類型變量的值

如果要通過反射改變基本數據類型變量的值,那麼要調用反射值的Elem()方法,再調用setXXX()方法,而且反射的對象應該是指針

func reflect_base(n interface{}) {
    rVal := reflect.ValueOf(n) // 這裏不要傳入空接口的指針
    fmt.Printf("rValue = %v, rValue`s type = %T\n", rVal, rVal) // 100, reflect.Value

    rVal.Elem().SetInt(10)
}

主函數調用時要傳入指針

func main() {
    n := 100
    reflect_base(&n) // 傳入指針
    fmt.Println(n) // 10
}

獲取結構體所有屬性和標籤

獲取結構體所有屬性和json標籤的方法如下

func reflect_struct(n interface{}) {
    rType := reflect.TypeOf(n)
    rValue := reflect.ValueOf(n)

    if rValue.Kind() != reflect.Struct {
        // 如果不是Struct類別,直接結束
        return
    }

    num := rValue.NumField() // 獲取字段數量
    for i := 0; i < num; i++ {
        fmt.Printf("Field %d value = %v\n", i, rValue.Field(i)) // 獲取字段值
        tagVal := rType.Field(i).Tag.Get("json") // 獲取字段的json標籤值
        if tagVal != "" {
            fmt.Printf("Field %d tag = %v\n", i, tagVal)
        }
    }
}

修改結構體,爲其添加標籤

type Student_rf struct {
    Name string `json:"name"`
    Age int `json:"age"`
}

main函數調用測試

func main() {
    reflect_struct(Student_rf{
        Name: "szc",
        Age: 23,
    })
}

輸出如下

Field 0 value = szc
Field 0 tag = name
Field 1 value = 23
Field 1 tag = age

調用結構體方法

調用結構體方法的過程如下

    num = rValue.NumMethod() // 獲取方法數量
    for i := 0; i < num; i++ {
        method := rValue.Method(i)
        fmt.Println(method) // 打印方法地址
    }

    var params []reflect.Value
    params = append(params, reflect.ValueOf("szc"))
    params = append(params, reflect.ValueOf(24))
    rValue.Method(1).Call(params) // 調用方法,傳入參數

    fmt.Println("...")

    res := rValue.Method(0).Call(nil) // 調用方法,接收返回值
    fmt.Println(res[0].Int())

對應的方法如下

func (s Student_rf) Show(name string, age int ) {
    fmt.Println(name, " -- ", age)
}

func (s Student_rf) GetAge() int {
    return s.Age
}

反射中方法的排序按照方法名的ascii碼排序,所以GetAge()在前,Show()在後

main函數中調用測試

func main() {
    s := Student_rf{
        Name: "szc",
        Age: 23,
    }

    reflect_struct(s)
}

輸出如下

szc  --  24
...
23

修改結構體字段值

修改結構體字段的值,就要和修改普通類型變量的值一樣,獲取地址的引用

    s := Student_rf{
        Name: "szc",
        Age: 23,
    }

    rValue := reflect.ValueOf(&s)
    rValue.Elem().Field(0).SetString("szc")
    rValue.Elem().Field(1).SetInt(24)

    fmt.Println(s)

輸出如下

{szc 24}

反射構造結構體變量

利用反射構造結構體變量並賦予屬性值

    var (
        ptr *Student_rf
        rType reflect.Type
        rValue reflect.Value
    )

    ptr = &Student_rf{} // 結構體指針
    rType = reflect.TypeOf(ptr).Elem() // 結構體反射類型

    rValue = reflect.New(rType) // 由結構體反射類型,獲取新結構體指針反射值

    ptr = rValue.Interface().(*Student_rf) // 把指針反射值轉成空接口,並進行類型斷言

    rValue = rValue.Elem() // 由結構體指針反射值獲取結構體反射值

    rValue.FieldByName("Name").SetString("szc") // 根據屬性名,對結構體反射值設置值
    rValue.FieldByName("Age").SetInt(22)

    fmt.Println(*ptr) // 輸出結果

結果如下

{szc 22}

綜上,我們可以發現:如果要通過反射改變變量的值,就要先獲取指針的反射,再通過Elem()方法獲取變量的反射值,然後進行設置;如果只是查看變量的值,就用變量的反射即可

網絡編程

以tcp爲例,服務端建立監聽套接字,然後阻塞等待客戶端連接。客戶端連接後,開啓協程處理客戶端。

package main

import (
    "fmt"
    "net"
)

func process_client(conn net.Conn) {
    for {
        var bytes []byte = make([]byte, 1024)
        n, err := conn.Read(bytes)
         // 從客戶端讀取數據,阻塞。返回讀取的字節數
        if err != nil {
            fmt.Println("Read from client error:", err)
            fmt.Println("Connection with ", conn.RemoteAddr().String(), " down")
            break
        }

        fmt.Println(string(bytes[:n])) // 字節切片轉string
    }
}


func main() {
    fmt.Println("Server on..")
    listen, err := net.Listen("tcp", "localhost:9999")
    // 建立tcp的監聽套接字,監聽本地9999號端口
    if (err != nil) {
        fmt.Println("Server listen error..")
        return
    }

    defer listen.Close()

    for {
        fmt.Println("Waiting for client to connect..")
        conn, err := listen.Accept() // 等待客戶端連接

        if err != nil {
            fmt.Println("Client connect error..")
            continue
        }

        defer conn.Close()

        fmt.Println("Connection established with ip:", conn.RemoteAddr().String()) // 獲取遠程地址
        go process_client(conn)
        
    }
    
}

客戶端方面,直接連接服務端,然後通過連接套接字發送信息即可

package main

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

func main() {
    conn, err := net.Dial("tcp", "localhost:9999") // 和本地9999端口建立tcp連接
    if err != nil {
        fmt.Println("Connect to server failure..")
        return
    }

    fmt.Println("Connected to server whose ip is ", conn.RemoteAddr().String())

    reader := bufio.NewReader(os.Stdin) // 建立控制檯的reader

    for {
        line, err := reader.ReadString('\n') // 讀取控制檯一行信息
        if err != nil {
            fmt.Println("Read String error :", err)
        }
    
        line = strings.Trim(line, "\r\n")

        if line == "quit" {
            break
        }
    
        _, err = conn.Write([]byte(line)) // 向服務端發送信息,返回發送的字節數和錯誤
        if err != nil {
            fmt.Println("Write to server error:", err)
        }
    }
}

go連接redis

首先安裝所需第三方庫

go get github.com/garyburd/redigo/redis

1)、然後導包,並建立和服務器的連接

import (
    "fmt"
    "github.com/garyburd/redigo/redis"
)


func main() {
    conn, err := redis.Dial("tcp", "localhost:6379")
    if err != nil {
        fmt.Println("redis connection failed..")
        return
    }

    defer conn.Close()
}

2)、往redis裏寫入數據

    _, err  = conn.Do("Set", "name", "songzeceng") // 參數列表:指令、鍵、值
    if err != nil {
        fmt.Println("redis set failed..")
        return
    }

3)、從redis讀取數據並轉爲字符串

    r, err := redis.String(conn.Do("Get", "name"))
    fmt.Println("result = ", r)

4)、哈希的插入和讀取

    _, err = conn.Do("HSet", "userhash01", "name", "szc") // 操作、哈希名、鍵、值
    _, err = conn.Do("HSet", "userhash01", "age", 23)

    r, err = redis.String(conn.Do("HGet", "userhash01", "name")) // 從哈希userhash01中讀取name
    fmt.Println("hash name = ", r)
    age, err := redis.Int(conn.Do("HGet", "userhash01", "age")) // 讀取int
    fmt.Println("hash age = ", age)

5)、一次寫入或讀取多個值

    _, err = conn.Do("MSet", "name", "songzeceng", "home", "Henan,Anyang")
    multi_r, err := redis.Strings(conn.Do("MGet", "name", "home")) // 注意String多了個s,而且multi_r是[]string

6)、爲了提高效率,可以使用redis連接池來獲取連接

    var pool *redis.Pool // 連接池指針
    pool = &redis.Pool{
        MaxIdle: 8, // 最大空閒連接數
        MaxActive: 0, // 最大連接數,0表示不限
        IdleTimeout: 100, // 最大空閒時間
        Dial: func() (redis.Conn, error) { // 產生連接的函數
            return redis.Dial("tcp", "localhost:6379")
        },
    }
    conn := pool.Get() // 獲取連接

    defer pool.Close() // 連接池關閉

go連接mysql

首先下載github上的mysql驅動 https://github.com/go-sql-driver/mysql,放入GO_PATH環境變量下

然後導包

import (
    "fmt"
    "database/sql" // 操作數據庫的方法、結構體等
    _ "github.com/go-sql-driver/mysql" // 導入驅動,不用使用
    "os"
)

1)、連接數據庫

var (
    Db *sql.DB
    err error
)

func init() {
    Db, err = sql.Open("mysql", "root:root@tcp(localhost:3306)/test")
    if err != nil {
        fmt.Println("Open mysql error:", err)
        os.Exit(-1)
    }
}

sql.Open()函數的參數列表:數據庫類型(mysql),數據庫url(用戶名:密碼@tcp(url)/數據庫名)

2)、插入數據

先定義結構體(最好)

type Sale struct {
    Widget_id int
    Qty int
    Street string
    City string
    State string
    Zip int
    Sale_date string
}

然後使用佔位符+預編譯的方式進行插入數據

func (sale *Sale) AddSale() (err_ error) {
    sql_str := "insert into sales(widget_id, qty, street, city, state, zip, sale_date) values(?, ?, ?, ?, ?, ?, ?)"

    inStmt, err_ := Db.Prepare(sql_str) // 預編譯

    _, err_ = inStmt.Exec(sale.Widget_id, sale.Qty, sale.Street, sale.City, sale.State, sale.Zip, sale.Sale_date) // 執行預編譯語句,傳入參數

    return err_
}


func main() {
    sale := &Sale{Widget_id: 9, Qty: 80, Street: "Huanghe South Road", City: "Anyang Henan", State: "China", Zip: 455000, Sale_date: "2020-03-24"}

    err_ := sale.AddSale()
    if err_ != nil {
        fmt.Println("sql execute err:", err_)
    }
}

或者使用單元測試,新建first_test.go文件,寫入以下內容

package main

import (
    "testing"
)

func TestAddSale(t *testing.T) {
    sale := &Sale{Widget_id: 9, Qty: 80, Street: "Huanghe South Road", City: "Anyang Henan", State: "China", Zip: 455000, Sale_date: "2020-03-24"}

    sale.AddSale()
}

然後在此目錄下運行命令

PS D:\develop\Go\workspace\src\go_code\go_web\src\main> go test

PASS
ok      go_code/go_web/src/main 0.799s

3)、查詢單條數據

func (sale *Sale) GetRecordById() (ret *Sale, err_ error) {
    sql_str := "select * from sales where widget_id = ?"
    in_stmt, _ := Db.Prepare(sql_str)

    row := in_stmt.QueryRow(sale.Widget_id)

    if row == nil {
        fmt.Println("No such record with id = ", sale.Widget_id)
        return nil, errors.New("No such record with id = " + fmt.Sprintf("%d", sale.Widget_id))
    }

    ret = &Sale{}

    err_ = row.Scan(&ret.Widget_id, &ret.Qty, &ret.Street, &ret.City, &ret.State, &ret.Zip, &ret.Sale_date)

    return ret, err_
}

QueryRow()最多隻接收一行查詢結果,main函數中測試如下

func main() {
    sale := &Sale{Widget_id: 9, Qty: 80, Street: "Huanghe South Road", City: "Anyang Henan", State: "China", Zip: 455000, Sale_date: "2020-03-24"}

    ret, _ := sale.GetRecordById()
     // {9 80 Huanghe South Road Anyang Henan China 455000 2020-03-24}
    if ret != nil {
        fmt.Println(*ret)
    }
}

4)、查詢所有數據

func (sale *Sale) GetAllRecord() (ret []*Sale, err_ error) {
    sql_str := "select * from sales"
    in_stmt, _ := Db.Prepare(sql_str)

    rows, err_ := in_stmt.Query()

    if err_ != nil {
        fmt.Println("Error get all: ", err_)
        return nil, err_
    }

    ret = make([]*Sale, 0)

    for rows.Next() {
        record := &Sale{}

        err_ = rows.Scan(&record.Widget_id, &record.Qty, &record.Street, &record.City, &record.State, &record.Zip, &record.Sale_date)

        if err_ != nil {
            fmt.Println("Error get record: ", err_)
            continue
        }

        ret = append(ret, record)
    }

    return ret, nil
}

Query()接收多行查詢結果,main函數中測試如下

func main() {
    sale := &Sale{}

    ret2, _ := sale.GetAllRecord()
    if ret2 != nil {
        for _, record := range ret2 {
            fmt.Println(*record)
        }
    }

/*    {1 20 Huasha Road Anyang Henan China 455000 2019-11-03}
...
{8 28 Dongfeng Road Anyang Henan China 455000 2019-11-10}
{9 80 Huanghe South Road Anyang Henan China 455000 2020-03-24}
*/
}

結語

go語言的學習筆記到這兒就結束了,一些太簡單的內容(分支語句、判斷語句等)沒有記錄下來,因爲很容易掌握。

最後,列一下go語言的特點:

1、繼承了C的指針

2、每個文件都屬於一個包

3、垃圾回收

4、天然併發,goroutine,基於CPS併發模型實現

5、管道通信,解決goroutine之間的通信

6、函數返回多個值(Python)

7、切片、延遲執行defer等

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