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等

  • 19
    点赞
  • 118
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值