【Golang第12章:goroutine协程与channel管道】GO语言goroutine协程和channel管道的基本介绍、goroutine协程和channel管道的应用案例

介绍

这个是在B站上看边看视频边做的笔记,这一章是Glang的goroutine协程与channel管道

内容有GO语言goroutine协程和channel管道的基本介绍、goroutine协程和channel管道的应用案例、GO协程和GO主线程、goroutine调度模型、channel的遍历、channel管道阻塞机制

配套视频自己去B站里面搜【go语言】,最高的播放量就是

里面的注释我写的可能不太对,欢迎大佬们指出╰(°▽°)╯

(十二)、goroutine和channel

一、goroutine协程

看一个需求

  • 需求:要求统计1-9000000000 的数字中,哪些是素数?

  • 分析思路:

    1. 传统的方法,就是使用一个循环,循环的判断各个数是不是素数。[很慢]
    2. 使用并发或者并行的方式,将统计素数的任务分配给多个goroutine 去完成,这时就会使用到goroutine.【速度提高4 倍】


1.基本介绍


1)进程和线程介绍

  1. 进程就是程序程序在操作系统中的一次执行过程,是系统进行资源分配和调度的基本单位
  2. 线程是进程的一个执行实例,是程序执行的最小单元,它是比进程更小的能独立运行的基本单位。
  3. 一个进程可以创建核销毁多个线程,同一个进程中的多个线程可以并发执行。
  4. 一个程序至少有一个进程,一个进程至少有一个线程

在这里插入图片描述



2)程序、进程和线程

在这里插入图片描述



3)并发和并行

  1. 多线程程序在单核上运行,就是并发

  2. 多线程程序在多核上运行,就是并行

  3. 示意图:

    在这里插入图片描述

  • 小结

    并发:因为是在一个cpu上,比如有10个线程,每个线程执行10毫秒(进行轮询操作),从人的角度看,好像这10个线程都在运行,但是从微观上看,在某一个时间点看,其实只有一个线程在执行,这就是并发。

    并行: 因为是在多个cpu上(比如有10个cpu),比如有10个线程,每个线程执行10毫秒(各自在不同cpu上执行),从人的角度看,这10个线程都在运行,但是从微观上看,在某一个时间点看,也同时有10个线程在执行,这就是并行



4)Go 协程和Go 主线程

  • Go 主线程(有程序员直接称为线程/也可以理解成进程): 一个Go 线程上,可以起多个协程,你可以这样理解,协程是轻量级的线程[编译器做优化]。

  • Go 协程的特点

    1. 有独立的栈空间
    2. 共享程序堆空间
    3. 调度由程序员控制
    4. 协程是轻量级的线程
  • 示意图

    在这里插入图片描述



2.快速入门

1)案例说明

  • 请编写一个程序,完成如下功能:

    1. 在主线程(可以理解成进程)中,开启一个goroutine, 该协程每隔1 秒输出"hello,world"
    2. 在主线程中也每隔一秒输出"hello,golang", 输出10 次后,退出程序
    3. 要求主线程和goroutine 同时执行.
    4. 画出主线程和协程执行流程图
  • 代码

    package main
    
    import (
    	"fmt"
    	"strconv"
    	"time"
    )
    
    // 1) 在主线程(可以理解成进程)中,开启一个goroutine, 该协程每隔1 秒输出"hello,world"
    // 2) 在主线程中也每隔一秒输出"hello,golang", 输出10 次后,退出程序
    // 3) 要求主线程和goroutine 同时执行.
    
    // 编写一个函数,每隔1秒输出“he1lo,world”
    func test() {
    	for i := 0; i < 10; i++ {
    		fmt.Println("test函数 hello,world" + strconv.Itoa(i)) //strconv转换为字符串
    		time.Sleep(time.Second)                             //休眠1秒
    	}
    }
    
    func main() {
    	go test() //开启了1个协程
    
    	for i := 0; i < 10; i++ {
    		fmt.Println("主函数 hello,world" + strconv.Itoa(i)) //strconv转换为字符串
    		time.Sleep(time.Second)                          //休眠1秒
    	}
    }
    

    输出的效果说明, main 这个主线程和test 协程同时执行

    在这里插入图片描述

  • 主线程和协程执行流程图

    在这里插入图片描述



2)快速入门小结

  1. 主线程是一个物理线程,直接作用在cpu 上的。是重量级的,非常耗费cpu 资源。
  2. 协程从主线程开启的,是轻量级的线程,是逻辑态。对资源消耗相对小。
  3. Golang 的协程机制是重要的特点,可以轻松的开启上万个协程。其它编程语言的并发机制是一般基于线程的,开启过多的线程,资源耗费大,这里就突显Golang 在并发上的优势了


3.goroutine 的调度模型


1)MPG 模式基本介绍

在这里插入图片描述

  1. M: 操作系统的主线程(是物理线程)(Machine)
  2. P:协程执行需要的上下文(Processor)
  3. G:协程(Goroutine)

2)MPG 模式运行的状态1


3)MPG 模式运行的状态2

在这里插入图片描述



4.设置Golang运行的cpu数

  • 介绍:为了充分了利用多cpu 的优势,在Golang 程序中,设置运行的cpu 数目

    package main
    
    import (
    	"fmt"
    	"runtime"
    )
    
    func main() {
    	CpuNum := runtime.NumCPU() //查询电脑有几个CPU
    	fmt.Println("电脑有", CpuNum, "个CPU")
    
    	//可以自己设置多个CPU
    	runtime.GOMAXPROCS(CpuNum - 1) //减一个CPU运行
    }
    
    1. go1.8后,默认让程序运行在多个核上,可以不用设置了
    2. go1.8前,还是要设置一下,可以更高效的利用cpu





二、channel管道

1.引出channel

**需求:**现在要计算1-200 的各个数的阶乘,并且把各个数的阶乘放入到map 中。最后显示出来。要求使用goroutine 完成

  • 分析思路:

    1. 使用goroutine 来完成(看看使用gorotine 并发完成会出现什么问题? 然后我们会去解决)

    2. 在运行某个程序时,如何知道是否存在资源竞争问题。方法很简单,在编译该程序时,增加一个参数-race 即可

    3. 代码:

      package main
      
      import (
      	"fmt"
      	"time"
      )
      
      //需求:现在要计算1-200 的各个数的阶乘,并且把各个数的阶乘放入到map 中。
      //最后显示出来。要求使用goroutine 完成
      
      //思路
      //1.编写一个函数,来计算各个数的阶乘,并放到map中
      //2.我们启动多个协程,将统计的结果放到map中
      //3.map应该做出一个全局的。
      
      var myMap = make(map[int]int, 10) //声明一个全局的map
      
      func test(n int) {
      
      	res := 1
      	for i := 1; i <= n; i++ {
      		res *= i //乘等于
      	}
      
      	//将res放入到myMap中
      	myMap[n] = res
      }
      
      func main() {
      	//开启多个协程完成
      	for i := 1; i <= 20; i++ {
      		go test(i)
      	}
      
      	//休眠10秒
      	time.Sleep(time.Second)
      
      	//打印map
      	for i, v := range myMap {
      		fmt.Printf("map[%d]=%d\n", i, v)
      	}
      
      }
      

      增加一个参数-race查看资源竞争数量

      PS D:\code\Go\src\demo2\09demo\01> go build -race .\main.go
      
      PS D:\code\Go\src\demo2\09demo\01> .\main.exe
      
      ==================
      WARNING: DATA RACE
      Write at 0x00c0000704b0 by goroutine 8:
      runtime.mapassign_fast64()
         D:/apps/Go/src/runtime/map_fast64.go:93 +0x0
      main.test()
         D:/code/Go/src/demo2/09demo/01/main.go:26 +0x70
      main.main.func1()
         D:/code/Go/src/demo2/09demo/01/main.go:32 +0x39
      
      Previous write at 0x00c0000704b0 by goroutine 7:
      runtime.mapassign_fast64()
         D:/apps/Go/src/runtime/map_fast64.go:93 +0x0
      main.test()
         D:/code/Go/src/demo2/09demo/01/main.go:26 +0x70
      main.main.func1()
         D:/code/Go/src/demo2/09demo/01/main.go:32 +0x39
      
      Goroutine 8 (running) created at:
      main.main()
         D:/code/Go/src/demo2/09demo/01/main.go:32 +0x84
      
      Goroutine 7 (finished) created at:
      main.main()
         D:/code/Go/src/demo2/09demo/01/main.go:32 +0x84
      ==================
      ==================
      WARNING: DATA RACE
      Read at 0x00c000086218 by main goroutine:
      main.main()
         D:/code/Go/src/demo2/09demo/01/main.go:39 +0x116
      
      Previous write at 0x00c000086218 by goroutine 25:
      main.test()
         D:/code/Go/src/demo2/09demo/01/main.go:26 +0x7c
      main.main.func1()
         D:/code/Go/src/demo2/09demo/01/main.go:32 +0x39
      
      Goroutine 25 (finished) created at:
      main.main()
         D:/code/Go/src/demo2/09demo/01/main.go:32 +0x84
      ==================
      map[19]=121645100408832000
      map[4]=24
      map[10]=3628800
      map[14]=87178291200
      map[16]=20922789888000
      map[12]=479001600
      map[18]=6402373705728000
      map[20]=2432902008176640000
      map[3]=6
      map[2]=2
      map[5]=120
      map[9]=362880
      map[17]=355687428096000
      map[7]=5040
      map[8]=40320
      map[13]=6227020800
      map[15]=1307674368000
      map[1]=1
      map[6]=720
      map[11]=39916800
      Found 2 data race(s) # 显示2个资源竞争
      
    4. 示意图:

      在这里插入图片描述



1)不同goroutine如何通讯

  1. 全局变量的互斥锁
  2. 使用管道channel 来解决

2)使用全局变量加锁

  • 因为没有对全局变量m 加锁,因此会出现资源争夺问题,代码会出现错误,提示concurrent map writes

  • 解决方案:加入互斥锁,排队进行写入数据

    在这里插入图片描述

  • 我们的数的阶乘很大,结果会越界,可以将求阶乘改成sum += uint64(i)

  • 代码改进

    package main
    
    import (
    	"fmt"
    	"sync"
    	"time"
    )
    
    //需求:现在要计算1-200 的各个数的阶乘,并且把各个数的阶乘放入到map 中。
    //最后显示出来。要求使用goroutine 完成
    
    //思路
    //1.编写一个函数,来计算各个数的阶乘,并放到map中
    //2.我们启动多个协程,将统计的结果放到map中
    //3.map应该做出一个全局的。
    
    var (
    	myMap = make(map[int64]int64, 10) //声明一个全局的map
    
    	//声明一个全局的互斥锁
    	//lock 是一个全局的互斥锁
    	//sync 是包:synchornized
    	lock sync.Mutex
    )
    
    func test(n int64) {
    	var res int64 = 1
    	for i := 1; int64(i) <= n; i++ {
    		res *= int64(i) //乘等于
    	}
    	lock.Lock()    //加锁 ,等待一个test(i)函数运行完毕
    	myMap[n] = res //将res放入到myMap中
    	lock.Unlock()  //解锁 ,函数运行完毕后解锁
    }
    
    func main() {
    
    	for i := 1; i <= 20; i++ { //开启多个协程完成
    		go test(int64(i))
    	}
    
    	time.Sleep(time.Second * 5) //休眠5秒,防止主程序过快关闭,导致test()函数没有执行完毕
    	// lock.Lock()
    	for i, v := range myMap { //打印map
    		fmt.Printf("map[%d]=%d\n", i, v)
    	}
    	// lock.Unlock()
    }
    
    

    在这里插入图片描述


3)为什么需要channel

  1. 前面使用全局变量加锁同步来解决goroutine 的通讯,但不完美
  2. 主线程在等待所有goroutine 全部完成的时间很难确定,我们这里设置10 秒,仅仅是估算。
  3. 如果主线程休眠时间长了,会加长等待时间,如果等待时间短了,可能还有goroutine 处于工作状态,这时也会随主线程的退出而销毁
  4. 通过全局变量加锁同步来实现通讯,也并不利用多个协程对全局变量的读写操作。
  5. 上面种种分析都在呼唤一个新的通讯机制-channel


2.基本介绍

  1. channle 本质就是一个数据结构-队列【示意图】

  2. 数据是先进先出【FIFO : first in first out】

  3. 线程安全,多goroutine 访问时,不需要加锁,就是说channel 本身就是线程安全的

  4. channel 有类型的,一个string 的channel 只能存放string 类型数据。

  5. 示意图:

    在这里插入图片描述


3.定义/声明channel

  • var 变量名 chan 数据类型

  • 举例:

    var intChan chan int            //(intChan 用于存放int 数据)
    var mapChan chan map[int]string //(mapChan 用于存放map[int]string 类型)
    var perChan chan Person
    var perChan2 chan *Person
    ...
    
  • 说明

    channel 是引用类型

    channel 必须初始化才能写入数据, 即make 后才能使用

    管道是有类型的,intChan 只能写入整数int

  • 管道的初始化,写入数据到管道,从管道读取数据及基本的注意事项

    package main
    
    import "fmt"
    
    func main() {
    	//演示管道的使用
    
    	//1.创建一个可以存放3个int类型的管道
    	// var intChan chan int
    	// intChan = make(chan int, 3)
    	var intChan chan int = make(chan int, 3)
    
        
    	//2.intChan是引用类型,指向了一个地址
    	fmt.Printf("intChan的值是=%v  地址是=%p\n", intChan, &intChan) //0xc00007c080
    
        
    	//3.向管道写入数据,当我们向管道写入数据时,不能超出管道的容量
    	intChan <- 10
    	num := 211
    	intChan <- num
    
        
    	//4.查看管道的长度和cap(容量)
    	fmt.Printf("intChan的长度是=%v  容量是=%v\n", len(intChan), cap(intChan))
    
        
    	//5.从管理读取数据
    	var num2 int = <-intChan
    	fmt.Println(num2)                                                  //取出一个数据
    	fmt.Printf("intChan的长度是=%v  容量是=%v\n", len(intChan), cap(intChan)) //长度减小,容量不变
    
        
    	//6.在没有使用协程的情况下,如果管道的数据已经全部去取出,再取就会报告 deadlock(死锁)
    	num3 := <-intChan
    	fmt.Println(num3)
    	fmt.Printf("intChan的长度是=%v  容量是=%v\n", len(intChan), cap(intChan)) //长度减小,容量不变
    
    	num4 := <-intChan //管道已经空了,继续取会报错
    	fmt.Println(num4) //fatal error: all goroutines are asleep - deadlock!
        
    }
    

4.使用的注意事项

  1. channel 中只能存放指定的数据类型
  2. channel 的数据放满后,就不能再放入了
  3. 如果从channel 取出数据后,可以继续放入
  4. 在没有使用协程的情况下,如果channel 数据取完了,再取,就会报dead lock
  5. 不要数据可以使用<-intChan扔掉数据

5.案例演示

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

package main

import "fmt"

type Cat struct {
	Name string
	Age  int
}

func main() {
	allChan := make(chan interface{}, 3)

	allChan <- 10
	allChan <- "tom"
	cat := Cat{"黑猫警长", 7}
	allChan <- cat

	//我们希望获得到管道中的第三个元素,则先将前2个推出
	<-allChan
	<-allChan

	newCat := <-allChan //从管道取出的是

	fmt.Printf("newCat=%T  newCat=%v\n", newCat, newCat) //查看类型和值

	//下面的写法是错误的!编译不通过
	// fmt.Printf("newCat .Name=%v", newCat.Name)

	//因为interface{}空接口可以使用任意类型,导致编译器无法确认数据类型
	//需要使用类型断言
	a := newCat.(Cat)
	fmt.Println(a.Name)

}


6.练习

说明: 请完成如下案例

  1. 创建一个 Person 结构体[Name,Age,Addressl
  2. 使用rand方法配合随机创建10个Person 实例,并放入到channel中
  3. 遍历channel ,将各个Person实例的信息显示在终端…

7.channel遍历和关闭

1)channel 的关闭

使用内置函数close 可以关闭channel, 当channel 关闭后,就不能再向channel 写数据了,但是仍然可以从该channel 读取数据

案例:

package main

import "fmt"

func main() {
	intChan := make(chan int, 3)
	intChan <- 100
	intChan <- 200
	close(intChan) //关闭管道

	//关闭后无法写入数据,运行会报错
	//intChan <- 300 //panic: send on closed channel

	n1 := <-intChan //关闭后可以正常读取
	n2 := <-intChan
	fmt.Println(n1, n2)

}

2)channel 的遍历

channel 支持for–range 的方式进行遍历,请注意两个细节

  1. 在遍历时,如果channel管道没有关闭,则回出现deadlock 死锁的错误
  2. 在遍历时,如果channel管道已经关闭,则会正常遍历数据,遍历完后,就会退出遍历。

代码:

package main

import "fmt"

func main() {
	//给管道生成数据
	intChan := make(chan int, 10)
	for i := 0; i < 10; i++ {
		intChan <- i * 2 //放入10个数据到管道
	}
	intChan1 := make(chan int, 10)
	for i := 0; i < 10; i++ {
		intChan1 <- i * 2 //放入10个数据到管道
	}

	//关闭管道,不然会出现死锁错误
	close(intChan)
	close(intChan1)

	// 使用range遍历
	for v := range intChan { //管道没有下标,直接取值
		fmt.Println(v)
	}

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

8.应用案例1

请完成goroutinechannel协同工作的案例,具体要求:

  1. 开启一个writeData协程,向管道intChan中写入50个整数
  2. 开启一个readData协程,从管道intChan中读取writeData写入的数据
  3. 注意: writeDatareadDate操作的是同一个管道
  4. 主线程需要等待writeDatareadDate协程都完成工作才能退出【管道】

思路

在这里插入图片描述

代码

package main

import "fmt"

//写入数据到管道
func writeData(intChan chan int) {
	for i := 1; i <= 100; i++ {
		fmt.Println("写入数据:", i)
		intChan <- i
	}

	close(intChan) //写入完后关闭管道
}

//读取数据函数
func readData(intChan chan int, exitChan chan bool) {
	for {
		v, ok := <-intChan
		if !ok {
			break
		}
		fmt.Printf("readData数据为=%v\n", v)
	}

	//readData读取完成后
	//exitChan <- true
	close(exitChan) //写入完后关闭管道
}

func main() {
	//创建2个管道
	intChan := make(chan int, 50)  //创建一个数据管道
	extiChan := make(chan bool, 1) //创建一个退出管道

	go writeData(intChan)
	go readData(intChan, extiChan)

	for { //检测
		_, ok := <-extiChan //在管道关闭后,ok会被修改为false
		if !ok {            //判断exitChan管道无法读数据后退出
			break

		}
	}
}

1)练习1

在这里插入图片描述

2)练习二

在这里插入图片描述


9.应用案例2

  • 需求:

    要求统计1-200000 的数字中,哪些是素数?这个问题在本章开篇就提出了,现在我们有goroutine
    channel 的知识后,就可以完成了[测试数据: 80000]

  • 分析思路:

    传统的方法,就是使用一个循环,循环的判断各个数是不是素数【ok】。

    使用并发/并行的方式,将统计素数的任务分配给多个(4个)goroutine 去完成,完成任务时间短。

  • 示意图

    在这里插入图片描述

  • 代码

    package main
    
    import "fmt"
    
    //向 intchan放入 1-8000个数
    func putNum(intChan chan int) {
    	for i := 1; i <= 8000; i++ {
    		intChan <- i
    	}
    
    	//写入完成后关闭管道
    	close(intChan)
    	fmt.Println("数据写入完成!!!!!!!!!!!!!!")
    }
    
    //从intChan管道取数据,并计算出素数,放入primeChan管道
    func PrimeNum(intChan chan int, primeChan chan int, exitChan chan bool) {
    	for {
    		num, ok := <-intChan
    		if !ok { //判断intChan是否有值
    			break
    		}
    		judged := true //定义一个判断变量
    		for i := 2; i < num; i++ {
    			if num%i == 0 { //num % 2到<num = 0 能除没有余数的 表示不是素数
    				judged = false
    				break //退出for循环
    			}
    
    		}
    		if judged { //判断 judged为true,将num放入primeChan
    			primeChan <- num
    		}
    	}
    	fmt.Println("有一个协程找不到数据,退出")
    	exitChan <- true //完成后向退出管道写入数据
    }
    
    //素数只能被1和自己整除
    func main() {
    	intChan := make(chan int, 2000)   //存入1-8000所有值
    	primeChan := make(chan int, 2000) //存入计算后的素数
    	exitChan := make(chan bool, 4)    //开启的线程数,然后判断退出
    
    	//开启一个协程,向intChan放入1-8000
    	go putNum(intChan)
    
    	//开启4个协程,从intChan取数据,并判断是否为素数,如果事,就放入primeChan
    	for i := 0; i < 4; i++ {
    		go PrimeNum(intChan, primeChan, exitChan)
    	}
    
    	go func() { //使用匿名函数,开启协程,完成primeChan管道关闭
    		//主线程处理,现在primeChan管道都没有关闭
    		for i := 0; i < 4; i++ { //由于管道没有关闭,for循环会一直等待  取到4个数后关闭管道
    			<-exitChan
    		}
    		close(primeChan) //关闭管道
    	}()
    
    	for { // 因为前面管道关闭了,现在可以使用ok检测里面的数据
    		res, ok := <-primeChan //取出primeChan管道数据
    		if !ok {               //当没有数据后,退出打印
    			break
    		}
    		fmt.Println("素数结果为:", res)
    	}
    }
    

可以使用runtime.NumCPU()查看cpu逻辑处理器核心数


10.管道阻塞机制

在这里插入图片描述


11.channel使用细节和注意事项

  1. channel 可以声明为只读,或者只写性质【案例演示】

    package main
    
    import "fmt"
    
    func main() {
    	//管道可以声明为只读或者只写
    
    	//1.在默认情况下,管道事双向的
    	// var chan1 chan int //可读可写
    	chan1 := make(chan int, 3)
    	chan1 <- 10
    	num1 := <-chan1
    	fmt.Println(num1)
    
    	//2.声明为只写
    	// var chan2 chan<- int //声明为只写
    	chan2 := make(chan<- int, 3)
    	chan2 <- 20
    	// num2 := <-chan2 //错误
    	fmt.Println(chan2)
    
    	//3.声明为只读
    	// var chan2 <-chan int //声明为只读
    	chan3 := make(<-chan int, 3)
    	// chan3 <- 30 //错误
    	num3 := <-chan3
    	fmt.Println(num3)
    
    }
    
  2. channel 只读和只写的最佳实践案例

    在这里插入图片描述

  3. 使用select 可以解决从管道取数据的阻塞问题

    package main
    
    import (
    	"fmt"
    )
    
    func main() {
    	//使用select可以解决从管道取数据的阻塞问
    	//1.定义一个管道 10个数据int
    	intChan := make(chan int, 10)
    	for i := 0; i < 10; i++ {
    		intChan <- i
    	}
    
    	//2.定义一个管道 5个数据string
    	stringChan := make(chan string, 5)
    	for i := 0; i < 5; i++ {
    		stringChan <- "hello" + fmt.Sprintf("%d", i)
    	}
    
    	//传统的方法在遍历管道时,如果不关闭会阻塞而导致 deadlock
    
    	//问题,在实际开发中,可能我们不好确定什么关闭该管道
    	//可以使用select方式解决
    	for {
    		judged := true
    		select {
    		case v := <-intChan: //注意,如果intChan一直没有关闭,不会一直阻塞deadlock
    			fmt.Println("从intChan读取了数据", v)
    		case v := <-stringChan:
    			fmt.Println("从stringChan读取了数据", v)
    		default:
    			fmt.Println("都取不到数据了...")
    			judged = false
    		}
    
    		if !judged { //退出循环
    			break
    		}
    	}
    }
    
  4. goroutine 中使用recover,解决协程中出现panic,导致程序崩溃问题

    在这里插入图片描述

    package main
    
    import (
    	"fmt"
    	"time"
    )
    
    func sayHello() {
    	for i := 0; i < 10; i++ {
    		time.Sleep(time.Second)
    		fmt.Println("hello,world", i)
    	}
    }
    
    func test() {
    	//定义一个错误的map
    	//使用defer + recover捕获错误,让程序继续执行
    	defer func() { //使用匿名函数
    		//捕获test抛出的panic
    		if err := recover(); err != nil { //声明并判断
    			fmt.Println("test()发生错误", err)
    		}
    	}() //使用匿名函数
    
    	//错误的定义
    	var myMap map[int]string
    	myMap[0] = "golang"
    
    }
    
    func main() {
    	go sayHello()
    	go test()
    	for i := 0; i < 10; i++ {
    		time.Sleep(time.Second)
    		fmt.Println("mian()等待", i)
    	}
    }
    


章节目录

【Golang第1~3章:基础】如何安装golang、第一个GO程序、golang的基础

【Golang第4章:函数】Golang包的引用,return语句、指针、匿名函数、闭包、go函数参数传递方式,golang获取当前时间

【Golang第5章:数组与切片】golang如何使用数组、数组的遍历和、使用细节和内存中的布局;golang如何使用切片,切片在内存中的布局

【Golang第6章:排序和查找】golang怎么排序,golang的顺序查找和二分查找,go语言中顺序查找二分查找介绍和案例

【Golang第7章:map】go语言中map的基本介绍,golang中map的使用案例,go语言中map的增删改查操作,go语言对map的值进行排序

【Golang第8章:面向对象编程】Go语言的结构体是什么,怎么声明;Golang方法的调用和声明;go语言面向对象实例,go语言工厂模式;golang面向对象的三大特性:继承、封装、多态

【Golang第9章:项目练习】go项目练习家庭收支记账软件项目、go项目练习客户管理系统项目

【Golang第10章:文件操作】GO语言的文件管理,go语言读文件和写文件、GO语言拷贝文件、GO语言判断文件是否存在、GO语言Json文件格式和解析

【Golang第11章:单元测试】GO语言单元测试

【Golang第12章:goroutine协程与channel管道】GO语言goroutine协程和channel管道的基本介绍、goroutine协

posted @ 2023-01-04 09:32  雪花凌落的盛夏  阅读(95)  评论(0编辑  收藏  举报  来源