Go学习笔记4

十三、对象

9.挎包创建结构体实例

【1】创建不同的包:

image-20230908174309371

【2】student.go:

image-20230908174316139

【3】main.go:

image-20230908174320728

发现:如果结构体首字母大写的话,在其它包下可以访问
但是:如果结构体的首字母小写?

image-20230908174328206

解决:结构体首字母小写,跨包访问没问题:---》工厂模式

image-20230908174332174 image-20230908174336015

10.封装

【1】什么是封装:
封装(encapsulation)就是把抽象出的字段和对字段的操作封装在一起,数据被保护在内部,程序的其它包只有通过被授权的操作方法,才能对字段进行操作。

【2】封装的好处:

  1. 隐藏实现细节
  2. 提可以对数据进行验证,保证安全合理

【3】Golang中如何实现封装:

  1. 建议将结构体、字段(属性)的首字母小写(其它包不能使用,类似private,实际开发不小写也可能,因为封装没有那么严格)
  2. 给结构体所在包提供一个工厂模式的函数,首字母大写(类似一个构造函数)
  3. 提供一个首字母大写的Set方法(类似其它语言的public),用于对属性判断并赋值
    func (var 结构体类型名)SetXxx(参数列表){
    //加入数据验证的业务逻辑
    var.Age =参数
    }
  4. 提供一个首字母大写的Get方法(类似其它语言的public),用于获取属性的值
    func (var结构体类型名) GetXxx() (返回值列表){
    return var.字段;
    }
    【4】代码实现:
image-20230908174353631 image-20230908174356625 image-20230908174402616

11.继承

【1】继承的引入:
当多个结构体存在相同的属性(字段)和方法时,可以从这些结构体中抽象出结构体,在该结构体中定义这些相同的属性和方法,其它的结构体不需要重新定义这些属性和方法,只需嵌套一个匿名结构体即可。也就是说:在Golang中,如果一个struct嵌套了另一个匿名结构体,那么这个结构体可以直接访问匿名结构体的字段和方法,从而实现了继承特性。

image-20230908174440091

【2】代码引入:

package main
import (
        "fmt"
)
//定义动物结构体:
type Animal struct{
        Age int
        Weight float32
}
//给Animal绑定方法:喊叫:
func (an *Animal) Shout(){
        fmt.Println("我可以大声喊叫")
}
////给Animal绑定方法:自我展示:
func (an *Animal) ShowInfo(){
        fmt.Printf("动物的年龄是:%v,动物的体重是:%v",an.Age,an.Weight)
}
//定义结构体:Cat
type Cat struct{
        //为了复用性,体现继承思维,嵌入匿名结构体:——》将Animal中的字段和方法都达到复用
        Animal
}
//对Cat绑定特有的方法:
func (c *Cat) scratch(){
        fmt.Println("我是小猫,我可以挠人")
}
func main(){
        //创建Cat结构体示例:
        cat := &Cat{}
        cat.Animal.Age = 3
        cat.Animal.Weight = 10.6
        cat.Animal.Shout()
        cat.Animal.ShowInfo()
        cat.scratch()
}


【3】继承的优点:
提高代码的复用性、扩展性

12.继承的注意事项

【1】结构体可以使用嵌套匿名结构体所有的字段和方法,即:首字母大写或者小写的字段、方法,都可以使用。

 package main
import (
        "fmt"
)
//定义动物结构体:
type Animal struct{
        Age int
        weight float32
}
//给Animal绑定方法:喊叫:
func (an *Animal) Shout(){
        fmt.Println("我可以大声喊叫")
}
////给Animal绑定方法:自我展示:
func (an *Animal) showInfo(){
        fmt.Printf("动物的年龄是:%v,动物的体重是:%v",an.Age,an.weight)
}
//定义结构体:Cat
type Cat struct{
        //为了复用性,体现继承思维,嵌入匿名结构体:——》将Animal中的字段和方法都达到复用
        Animal
}
//对Cat绑定特有的方法:
func (c *Cat) scratch(){
        fmt.Println("我是小猫,我可以挠人")
}
func main(){
        //创建Cat结构体示例:
        cat := &Cat{}
        cat.Animal.Age = 3
        cat.Animal.weight = 10.6
        cat.Animal.Shout()
        cat.Animal.showInfo()
        cat.scratch()
}

【2】匿名结构体字段访问可以简化。

等价于:

image-20230908174532157

cat.Age --->cat对应的结构体中找是否有Age字段,如果有直接使用,如果没有就去找嵌入的结构体类型中的Age
【3】当结构体和匿名结构体有相同的字段或者方法时,编译器采用就近访问原则访问,如希望访问匿名结构体的字段和方法,可以通过匿名结构体名来区分。

package main
import (
        "fmt"
)
//定义动物结构体:
type Animal struct{
        Age int
        weight float32
}
//给Animal绑定方法:喊叫:
func (an *Animal) Shout(){
        fmt.Println("我可以大声喊叫")
}
////给Animal绑定方法:自我展示:
func (an *Animal) showInfo(){
        fmt.Printf("动物的年龄是:%v,动物的体重是:%v",an.Age,an.weight)
}
//定义结构体:Cat
type Cat struct{
        //为了复用性,体现继承思维,嵌入匿名结构体:——》将Animal中的字段和方法都达到复用
        Animal
        Age int
}
func (c *Cat) showInfo(){
        fmt.Printf("~~~~~~~~动物的年龄是:%v,动物的体重是:%v",c.Age,c.weight)
}
//对Cat绑定特有的方法:
func (c *Cat) scratch(){
        fmt.Println("我是小猫,我可以挠人")
}
func main(){
        //创建Cat结构体示例:
        // cat := &Cat{}
        // cat.Age = 3
        // cat.weight = 10.6
        // cat.Shout()
        // cat.showInfo()
        // cat.scratch()
        cat := &Cat{}
        cat.weight = 9.4
        cat.Age = 10 //就近原则
        cat.Animal.Age = 20
        cat.showInfo()//就近原则
        cat.Animal.showInfo()
}

【4】Golang中支持多继承:如一个结构体嵌套了多个匿名结构体,那么该结构体可以直接访问嵌套的匿名结构体的字段和方法,从而实现了多重继承。为了保证代码的简洁性,建议大家尽量不使用多重继承,很多语言就将多重继承去除了,但是Go中保留了。

package main
import (
        "fmt"
)
type A struct{
        a int
        b string
}
type B struct{
        c int
        d string
}
type C struct{
        A
        B
}
func main(){
        //构建C结构体实例:
        c := C{A{10,"aaa"},B{20,"ccc"}}
        fmt.Println(c)
}

【5】如嵌入的匿名结构体有相同的字段名或者方法名,则在访问时,需要通过匿名结构体类型名来区分。

package main
import (
        "fmt"
)
type A struct{
        a int
        b string
}
type B struct{
        c int
        d string
        a int
}
type C struct{
        A
        B
}
func main(){
        //构建C结构体实例:
        c := C{A{10,"aaa"},B{20,"ccc",50}}
        fmt.Println(c.b)
        fmt.Println(c.d)
        fmt.Println(c.A.a)
        fmt.Println(c.B.a)
}

【6】结构体的匿名字段可以是基本数据类型。

image-20230908174617016

【7】嵌套匿名结构体后,也可以在创建结构体变量(实例)时,直接指定各个匿名结构体字段的值。

image-20230908174621439

【8】嵌入匿名结构体的指针也是可以的。

image-20230908174628111

【9】结构体的字段可以是结构体类型的。(组合模式)

image-20230908174656306

13.接口

【1】代码入门:

package main
import "fmt"
//接口的定义:定义规则、定义规范,定义某种能力:
type SayHello interface{
        //声明没有实现的方法:
        sayHello()
}
//接口的实现:定义一个结构体:
//中国人:
type Chinese struct{
}
//实现接口的方法---》具体的实现:
func (person Chinese) sayHello(){
        fmt.Println("你好")
}
//接口的实现:定义一个结构体:
//美国人:
type American struct{
}
//实现接口的方法---》具体的实现:
func (person American) sayHello(){
        fmt.Println("hi")
}
//定义一个函数:专门用来各国人打招呼的函数,接收具备SayHello接口的能力的变量:
func greet(s SayHello){
        s.sayHello()
}
func main(){
        //创建一个中国人:
        c := Chinese{}
        //创建一个美国人:
        a := American{}
        //美国人打招呼:
        greet(a)
        //中国人打招呼:
        greet(c)
}

【2】总结:
(1)接口中可以定义一组方法,但不需要实现,不需要方法体。并且接口中不能包含任何变量。到某个自定义类型要使用的时候(实现接口的时候),再根据具体情况把这些方法具体实现出来。
(2)实现接口要实现所有的方法才是实现。
(3)Golang中的接口不需要显式的实现接口。Golang中没有implement关键字。
(Golang中实现接口是基于方法的,不是基于接口的)
例如:
A接口 a,b方法
B接口 a,b方法
C结构体 实现了 a,b方法 ,那么C实现了A接口,也可以说实现了B接口 (只要实现全部方法即可,和实际接口耦合性很低,比Java松散得多)
(4)接口目的是为了定义规范,具体由别人来实现即可。

14.接口注意事项

【1】接口本身不能创建实例,但是可以指向一个实现了该接口的自定义类型的变量。

image-20230908174948195

【2】只要是自定义数据类型,就可以实现接口,不仅仅是结构体类型。

image-20230908174952128

【3】一个自定义类型可以实现多个接口

package main
import "fmt"
type AInterface interface{
        a()
}
type BInterface interface{
        b()
}
type Stu struct{
}
func (s Stu) a(){
        fmt.Println("aaaa")
}
func (s Stu) b(){
        fmt.Println("bbbb")
}
func main(){
        var s Stu
        var a AInterface = s
    var b BInterface = s
        a.a()
        b.b()
}

【4】一个接口(比如A接口)可以继承多个别的接口(比如B,C接口),这时如果要实现A接口,也必须将B,C接口的方法也全部实现。

package main
import "fmt"
type CInterface interface{
        c()
}
type BInterface interface{
        b()
}
type AInterface interface{
        BInterface
        CInterface
        a()
}
type Stu struct{
}
func (s Stu) a(){
        fmt.Println("a")
}
func (s Stu) b(){
        fmt.Println("b")
}
func (s Stu) c(){
        fmt.Println("c")
}
func main(){
        var s Stu
        var a AInterface = s
        a.a()
        a.b()
        a.c()
}

【5】interface类型默认是一个指针(引用类型),如果没有对interface初始化就使用,那么会输出nil

【6】空接口没有任何方法,所以可以理解为所有类型都实现了空接口,也可以理解为我们可以把任何一个变量赋给空接口。

image-20230908174938019

15.多态

【1】基本介绍
变量(实例)具有多种形态。面向对象的第三大特征,在Go语言,多态特征是通过接口实现的。可以按照统一的接口来调用不同的实现。这时接口变量就呈现不同的形态。

【2】案例:

image-20230908193450698

【3】接口体现多态特征

  1. 多态参数: s叫多态参数

image-20230908193455554

  1. 多态数组 :
    比如:定义SayHello数组,存放中国人结构体、美国人结构体

image-20230908193459516

16.断言

Go语言里面有一个语法,可以直接判断是否是该类型的变量: value, ok := element.(T).

这里value就是变量的值,ok是一个bool类型,element是interface变量,T是断言的类型。

【2】断言的案例引入:

package main
import "fmt"
//接口的定义:定义规则、定义规范,定义某种能力:
type SayHello interface{
        //声明没有实现的方法:
        sayHello()
}
//接口的实现:定义一个结构体:
//中国人:
type Chinese struct{
        name string
}
//实现接口的方法---》具体的实现:
func (person Chinese) sayHello(){
        fmt.Println("你好")
}
//中国人特有的方法
func (person Chinese) niuYangGe(){
        fmt.Println("东北文化-扭秧歌")
}
//接口的实现:定义一个结构体:
//美国人:
type American struct{
        name string
}
//实现接口的方法---》具体的实现:
func (person American) sayHello(){
        fmt.Println("hi")
}
//定义一个函数:专门用来各国人打招呼的函数,接收具备SayHello接口的能力的变量:
func greet(s SayHello){
        s.sayHello()
        //断言:
        var ch Chinese = s.(Chinese)//看s是否能转成Chinese类型并且赋给ch变量
        ch.niuYangGe()
}
func main(){       

    //创建一个中国人:
    c := Chinese{}
    //创建一个美国人:
    //a := American{}
    //美国人打招呼:
    //greet(a)
    //中国人打招呼:
    greet(c)

}

解决第二个返回值问题:

package main
import "fmt"
//接口的定义:定义规则、定义规范,定义某种能力:
type SayHello interface{
        //声明没有实现的方法:
        sayHello()
}
//接口的实现:定义一个结构体:
//中国人:
type Chinese struct{
        name string
}
//实现接口的方法---》具体的实现:
func (person Chinese) sayHello(){
        fmt.Println("你好")
}
//中国人特有的方法
func (person Chinese) niuYangGe(){
        fmt.Println("东北文化-扭秧歌")
}
//接口的实现:定义一个结构体:
//美国人:
type American struct{
        name string
}
//实现接口的方法---》具体的实现:
func (person American) sayHello(){
        fmt.Println("hi")
}
//定义一个函数:专门用来各国人打招呼的函数,接收具备SayHello接口的能力的变量:
func greet(s SayHello){
        s.sayHello()
        //断言:
        ch,flag := s.(Chinese)//看s是否能转成Chinese类型并且赋给ch变量,flag是判断是否转成功
        if flag == true{
                ch.niuYangGe()
        }else{
                fmt.Println("美国人不会扭秧歌")
        }
        fmt.Println("打招呼。。。")
}
func main(){

        //创建一个中国人:
        //c := Chinese{}
        //创建一个美国人:
        a := American{}
        //美国人打招呼:
        greet(a)
        //中国人打招呼:
        //greet(c)

}

更简略的语法:

image-20230908193746281

【3】Type Switch 的基本用法
Type Switch 是 Go 语言中一种特殊的 switch 语句,它比较的是类型而不是具体的值。它判断某个接口变量的类型,然后根据具体类型再做相应处理。

package main
import "fmt"
//接口的定义:定义规则、定义规范,定义某种能力:
type SayHello interface{
        //声明没有实现的方法:
        sayHello()
}
//接口的实现:定义一个结构体:
//中国人:
type Chinese struct{
        name string
}
//实现接口的方法---》具体的实现:
func (person Chinese) sayHello(){
        fmt.Println("你好")
}
//中国人特有的方法
func (person Chinese) niuYangGe(){
        fmt.Println("东北文化-扭秧歌")
}
//接口的实现:定义一个结构体:
//美国人:
type American struct{
        name string
}
//实现接口的方法---》具体的实现:
func (person American) sayHello(){
        fmt.Println("hi")
}
func (person American) disco(){
        fmt.Println("野狼disco")
}
//定义一个函数:专门用来各国人打招呼的函数,接收具备SayHello接口的能力的变量:
func greet(s SayHello){
        s.sayHello()
        //断言:
        // ch,flag := s.(Chinese)//看s是否能转成Chinese类型并且赋给ch变量,flag是判断是否转成功
        // if flag == true{
        // 	ch.niuYangGe()
        // }else{
        // 	fmt.Println("美国人不会扭秧歌")
        // }
        

        // if ch,flag := s.(Chinese);flag{
        // 	ch.niuYangGe()
        // }else{
        // 	fmt.Println("美国人不会扭秧歌")
        // }
        switch s.(type){//type属于go中的一个关键字,固定写法
                case Chinese:
                        ch := s.(Chinese)
                        ch.niuYangGe()
                case American:
                        am := s.(American)
                        am.disco()
        }
        fmt.Println("打招呼。。。")

}
func main(){
        //创建一个中国人:
        c := Chinese{}
        //创建一个美国人:
        //a := American{}
        //美国人打招呼:
        //greet(a)
        //中国人打招呼:
        greet(c)

}

十四、文件操作

1.文件

【1】文件是什么?
文件是保存数据的地方,是数据源的一种,比如大家经常使用的word文档、txt文件、excel文件、jpg文件...都是文件。文件最主要的作用就是保存数据,它既可以保存一张图片,也可以保持视频,声音...
【2】os包下的File结构体封装了对文件的操作:

image-20230908200611278

【3】File结构体---打开文件和关闭文件:
(1)打开文件,用于读取:(函数)

image-20230908200617936

传入一个字符串(文件的路径),返回的是文件的指针,和是否打开成功

(2)关闭文件:(方法)

使文件不能用于读写。它返回可能出现的错误

【4】案例:

package main
import(
        "fmt"
        "os"
)
func main(){
        //打开文件:
        file,err := os.Open("d:/Test.txt");
        if err != nil {//出错
                fmt.Println("文件打开出错,对应错误为:",err)
        }
        //没有出错,输出文件:
        fmt.Printf("文件=%v",file)
        //.........一系列操作
        //关闭文件:
        err2 := file.Close();
        if err2 != nil {
                fmt.Println("关闭失败")
        }
}

2.IO流的引入

image-20230908200715250

3.读取文件(一次性)

【1】读取文件的内容并显示在终端(使用ioutil一次将整个文件读入到内存中),这种方式适用于文件不大的情况。相关方法和函数(ioutil.ReadFile)

image-20230908200759118

【2】案例:

package main
import(
        "fmt"
        "io/ioutil"
)
func main(){
        //备注:在下面的程序中不需要进行 Open\Close操作,因为文件的打开和关闭操作被封装在ReadFile函数内部了
        //读取文件:
        content,err := ioutil.ReadFile("d:/Test.txt")//返回内容为:[]byte,err
        if err != nil {//读取有误
                fmt.Println("读取出错,错误为:",err)
        }
        //如果读取成功,将内容显示在终端即可:
        //fmt.Printf("%v",content)
        fmt.Printf("%v",string(content))
}

4.读取文件(带缓冲区)

【1】读取文件的内容并显示在终端(带缓冲区的方式-4096字节),适合读取比较大的文件,使用os.Open,file.Close,bufio.NewReader(),reader.ReadString函数和方法

【2】案例:

 package main
import(
        "fmt"
        "os"
        "bufio"
        "io"
)
func main(){
        //打开文件:
        file,err := os.Open("d:/Test.txt")
        if err != nil {//打开失败
                fmt.Println("文件打开失败,err=",err)
        }
        //当函数退出时,让file关闭,防止内存泄露:
        defer file.Close()
        //创建一个流:
        reader := bufio.NewReader(file)
        //读取操作:
        for {
                str,err := reader.ReadString('\n')//读取到一个换行就结束
                if err == io.EOF {//io.EOF 表示已经读取到文件的结尾
                        break
                }
                //如果没有读取到文件结尾的话,就正常输出文件内容即可:
                fmt.Println(str)
        }
        //结束:
        fmt.Println("文件读取成功,并且全部读取完毕")
}

5.写入文件

【1】打开文件操作:

image-20230908200944060

三个参数含义:
(1)要打开的文件的路径

(2)文件打开模式(可以利用"|"符号进行组合)

image-20230908201008260

(3)权限控制(linux/unix系统下才生效,windows下设置无效)- 0666

【2】案例:

 package main
import(
        "fmt"
        "os"
        "bufio"
)
func main(){
        //写入文件操作:
        //打开文件:
        file , err := os.OpenFile("d:/Demo.txt",os.O_RDWR | os.O_APPEND | os.O_CREATE,0666)
        if err != nil {//文件打开失败
                fmt.Printf("打开文件失败",err)
                return
        }
        //及时将文件关闭:
        defer file.Close()
        //写入文件操作:---》IO流---》缓冲输出流(带缓冲区)
        writer := bufio.NewWriter(file)
        for i := 0; i < 10;i++ {
                writer.WriteString("你好 马士兵\n")
        } 
        //流带缓冲区,刷新数据--->真正写入文件中:
        writer.Flush()
        s :=os.FileMode(0666).String()
        fmt.Println(s)
}

6.文件复制操作

package main
import(
        "fmt"
        "io/ioutil"
)
func main(){
        //定义源文件:
        file1Path := "d:/Demo.txt"
        //定义目标文件:
        file2Path := "d:/Demo2.txt"
        //对文件进行读取:
        content,err := ioutil.ReadFile(file1Path)
        if err != nil {
                fmt.Println("读取有问题!")
                return
        }
        //写出文件:
        err = ioutil.WriteFile(file2Path,content,0666)
        if err != nil {
                fmt.Println("写出失败!")
        }
}

十五、协程和管道

1.程序、进程、线程、协程

【1】程序(program)
是为完成特定任务、用某种语言编写的一组指令的集合,是一段静态的代码。 (程序是静态的)

【2】进程(process)
是程序的一次执行过程。正在运行的一个程序,进程作为资源分配的单位,在内存中会为每个进程分配不同的内存区域。 (进程是动态的)是一个动的过程 ,进程的生命周期 : 有它自身的产生、存在和消亡的过程

image-20230908212311094

【3】线程(thread)
进程可进一步细化为线程, 是一个程序内部的一条执行路径。
若一个进程同一时间并行执行多个线程,就是支持多线程的。

image-20230908212328769

【4】协程(goroutine)
又称为微线程,纤程,协程是一种用户态的轻量级线程

作用:在执行A函数的时候,可以随时中断,去执行B函数,然后中断继续执行A函数(可以自动切换),注意这一切换过程并不是函数调用(没有调用语句),过程很像多线程,然而协程中只有一个线程在执行(协程的本质是个单线程)

image-20230908212442016 对于单线程下,我们不可避免程序中出现io操作,但如果我们能在自己的程序中(即用户程序级别,而非操作系统级别)控制**单线程下的多**

个任务能在一个任务遇到io阻塞时就将寄存器上下文和栈保存到某个其他地方,然后切换到另外一个任务去计算。

在任务切回来的时候,恢复先前保存的寄存器上下文和栈,这样就保证了该线程能够最大限度地处于就绪态.

即随时都可以被cpu执行的状态,相当于我们在用户程序级别将自己的io操作最大限度地隐藏起来,从而可以迷惑操作系统,让其看到:该线程好像是一直在计算,io比较少,从而会更多的将cpu的执行权限分配给我们的线程

(注意:线程是CPU控制的,而协程是程序自身控制的,属于程序级别的切换,操作系统完全感知不到,因而更加轻量级)

2.协程入门

【1】案例:
请编写一个程序,完成如下功能:
(1)在主线程中,开启一个goroutine,该goroutine每隔1秒输出"hello golang"
(2)在主线程中也每隔一秒输出"hello msb",输出10次后,退出程序
(3)要求主线程和goroutine同时执行

代码:

package main
import(
        "fmt"
        "strconv"
        "time"
)
func test(){
        for i := 1;i <= 10;i++ {
                fmt.Println("hello golang + " + strconv.Itoa(i))
                //阻塞一秒:
                time.Sleep(time.Second)
        }
}
func main(){//主线程
        go test() //开启一个协程
        for i := 1;i <= 10;i++ {
                fmt.Println("hello msb + " + strconv.Itoa(i))
                //阻塞一秒:
                time.Sleep(time.Second)
        }
}

代码结果:

image-20230908212654090

主线程和协程执行流程示意图:

image-20230908212658297

3.主死从随

【1】主死从随:

  1. 如果主线程退出了,则协程即使还没有执行完毕,也会退出
  2. 当然协程也可以在主线程没有退出前,就自己结束了,比如完成了自己的任务
package main
import(
        "fmt"
        "strconv"
        "time"
)
func test(){
        for i := 1;i <= 1000;i++ {
                fmt.Println("hello golang + " + strconv.Itoa(i))
                //阻塞一秒:
                time.Sleep(time.Second)
        }
}
func main(){//主线程
        go test() //开启一个协程
        for i := 1;i <= 10;i++ {
                fmt.Println("hello msb + " + strconv.Itoa(i))
                //阻塞一秒:
                time.Sleep(time.Second)
        }
}

4.多个协程

package main
import(
        "fmt"
        "time"
)
func main(){
        //匿名函数+外部变量 = 闭包
        for i := 1;i <= 5;i++ {
                //启动一个协程
                //使用匿名函数,直接调用匿名函数
                go func(n int){
                        fmt.Println(n)
                }(i)
        }
        time.Sleep(time.Second * 2)
}

5.使用WaitGroup控制协程退出

【1】WaitGroup的作用:
WaitGroup用于等待一组线程的结束。父线程调用Add方法来设定应等待的线程的数量。每个被等待的线程在结束时应调用Done方法。同时,主线程里可以调用Wait方法阻塞至所有线程结束。---》解决主线程在子协程结束后自动结束

【2】主要方法:
(1)

image-20230908212952786

(2)

image-20230908213000485

(3)

image-20230908213005299

【3】案例:

(1)Add\Done\Wait:

package main
import(
        "fmt"
        "sync"
)
var wg sync.WaitGroup //只定义无需赋值
func main(){
        //启动五个协程
        for i := 1 ;i <= 5;i++ {
                wg.Add(1) //协程开始的时候加1操作
                go func(n int){
                        fmt.Println(n)
                        wg.Done()  //协程执行完成减1
                }(i)
        }
        //主线程一直在阻塞,什么时候wg减为0了,就停止
        wg.Wait()
}

(2)如果防止忘记计数器减1操作,结合defer关键字使用:

package main
import(
        "fmt"
        "sync"
)
var wg sync.WaitGroup //只定义无需赋值
func main(){
        //启动五个协程
        for i := 1 ;i <= 5;i++ {
                wg.Add(1) //协程开始的时候加1操作
                go func(n int){
                        defer wg.Done()
                        fmt.Println(n)		
                }(i)
        }
        //主线程一直在阻塞,什么时候wg减为0了,就停止
        wg.Wait()
}

(3)可以最开始在知道协程次数的情况下先Add操作:

package main
import(
        "fmt"
        "sync"
)
var wg sync.WaitGroup //只定义无需赋值
func main(){
        wg.Add(5)
        //启动五个协程
        for i := 1 ;i <= 5;i++ {
                go func(n int){
                        defer wg.Done()
                        fmt.Println(n)		
                }(i)
        }
        //主线程一直在阻塞,什么时候wg减为0了,就停止
        wg.Wait()
}

注意:Add中加入的数字和协程的次数一定要保持一致

6.多协同操作同一数据(互斥锁)

【1】案例:多个协程操纵同一数据

package main
import(
        "fmt"
        "sync"
)
//定义一个变量:
var totalNum int
var wg sync.WaitGroup //只定义无需赋值
func add(){
        defer wg.Done()
        for i := 0 ;i < 100000;i++{
                totalNum = totalNum + 1
        }
}
func sub(){
        defer wg.Done()
        for i := 0 ;i < 100000;i++{
                totalNum = totalNum - 1
        }
}
func main(){
        wg.Add(2)
        //启动协程
        go add()
        go sub()
        wg.Wait()
        fmt.Println(totalNum)
}

结果:在理论上,这个totalNum结果应该是0 ,无论协程怎么交替执行,最终想象的结果就是0
但是事实上:不是

image-20230908213237923

问题出现的原因:(图解为其中一种可能性)

image-20230908213242816

解决问题:
有一个机制:确保:一个协程在执行逻辑的时候另外的协程不执行
----》锁的机制---》加入互斥锁

image-20230908213250992

代码:

package main
import(
        "fmt"
        "sync"
)
//定义一个变量:
var totalNum int
var wg sync.WaitGroup //只定义无需赋值
//加入互斥锁:
var lock sync.Mutex
func add(){
        defer wg.Done()
        for i := 0 ;i < 100000;i++{
                //加锁
                lock.Lock()
                totalNum = totalNum + 1
                //解锁:
                lock.Unlock()
        }
}
func sub(){
        defer wg.Done()
        for i := 0 ;i < 100000;i++{
                //加锁
                lock.Lock()
                totalNum = totalNum - 1
                //解锁:
                lock.Unlock()
        }
}
func main(){
        wg.Add(2)
        //启动协程
        go add()
        go sub()
        wg.Wait()
        fmt.Println(totalNum)
}

7.读写锁

golang中sync包实现了两种锁Mutex (互斥锁)和RWMutex(读写锁)
【1】互斥锁
其中Mutex为互斥锁,Lock()加锁,Unlock()解锁,使用Lock()加锁后,便不能再次对其进行加锁,直到利用Unlock()解锁对其解锁后,才能再次加锁.适用于读写不确定场景,即读写次数没有明显的区别
----性能、效率相对来说比较低

【2】读写锁
RWMutex是一个读写锁,其经常用于读次数远远多于写次数的场景.
---在读的时候,数据之间不产生影响, 写和读之间才会产生影响

【3】案例:

package main
import(
        "fmt"
        "sync"
        "time"
)
var wg sync.WaitGroup //只定义无需赋值
//加入读写锁:
var lock sync.RWMutex
func read(){
        defer wg.Done()
        lock.RLock()//如果只是读数据,那么这个锁不产生影响,但是读写同时发生的时候,就会有影响
        fmt.Println("开始读取数据")
        time.Sleep(time.Second)
        fmt.Println("读取数据成功")
        lock.RUnlock()
}
func write(){
        defer wg.Done()
        lock.Lock()
        fmt.Println("开始修改数据")
        time.Sleep(time.Second * 10)
        fmt.Println("修改数据成功")
        lock.Unlock()
}
func main(){
        wg.Add(6)
        //启动协程 ---> 场合:读多写少
        for i := 0;i < 5;i++ {
                go read()
        }
        go write()
        wg.Wait()
}

image-20230908213336482

posted @ 2023-09-09 18:35  橡皮筋儿  阅读(64)  评论(0编辑  收藏  举报