您现在的位置是:首页 >技术教程 >go 协程.通道.select网站首页技术教程

go 协程.通道.select

nicepainkiller 2024-10-12 12:01:03
简介go 协程.通道.select

go中的方法

方法的申明:

func (t Type) methodName(parameter list) return List{
    逻辑执行
}
  • go 中相同的方法名 可以定义在不同的类型(结构)上相同名字的函数是 不被容许

  • go 中修改结构体字段 需要使用指针接收器或指针(值类型或者值接收器 不可以); 指针接收器内部属性的改变对于 调用者是可见的, 值接收器内部属性的改变 对于调用者不可见;所有当想改变实例属性的时候 必须使用指针

  • t Type 接收器(可以是结构体或非机构体类型,接收器可在方法内部访问) 可以缺省

  • parameter list 参数列表 可缺省

  • return List 返回值列表 可缺省

  

type Person struct {
	name string
	icon string
	age  uint8
}

type Author struct {
	name string
}

func (p Person) PrintInfo() {
	fmt.Println("Person name:", p.name)
	fmt.Println("Person icon:", p.icon)
	fmt.Println("Person age:", p.age)
}
func (p Person) ChangePersonName(name string) {
	p.name = name
	fmt.Println("通过结构体修改 Person 新名字:", p.name)
}
func (p *Person) ChangePersonNameByPoint(name string) {
	p.name = name
	fmt.Println("通过指针修改 Person 新名字:", p.name)
}

func (au Author) PrintInfo() {
	fmt.Println("Author name:", au.name)

}

func PrintInfo(p Person) {
	fmt.Println("name:", p.name)
	fmt.Println("icon:", p.icon)
	fmt.Println("age:", p.age)
}



输出:

Person name: 王二狗
Person icon: https://aaa.png
Person age: 23
name: 王二狗
icon: https://aaa.png
age: 23
Author name: 王大锤
---------------------------
通过结构体修改 Person 新名字: 我是男一号
Person name: 王二狗
Person icon: https://aaa.png
Person age: 23
---------------------------
通过指针修改 Person 新名字: 我是男二号
Person name: 我是男二号
Person icon: https://aaa.png
Person age: 23
x.add(y) 154

接口

协程

协程 是与其他函数或方法 一起并发运行的工作方式, 协程可以看做是 轻量级线程;所以创建一个协程的成本更小

协程不是原子性的
开启协程关键字 go

 

func FuncPrintInfo() {
	fmt.Println("我是一个函数我的名称叫 PrintInfo")
}

// 多个协程
func PrintNum(num int) {
	for i := 0; i < 3; i++ {
		fmt.Println("PrintNum:", num)
		//time.Sleep(100 * time.Millisecond)
	}
}


func CoroutineMethod() {

	go FuncPrintInfo()
	//如果不加 time.Sleep(1 * time.Second) 则 主程序运行完成后还未等协程运行完毕 程序就已经停止了
	time.Sleep(1 * time.Second)
	fmt.Println("CoroutineMethod")
	time.Sleep(1 * time.Second)

	 开启1号协程
	go PrintNum(1)

	 开启2号协程
	go PrintNum(2)
	time.Sleep(time.Second)
}



输出:
我是一个函数我的名称叫 PrintInfo
CoroutineMethod
PrintNum: 1
PrintNum: 1
PrintNum: 1
PrintNum: 2
PrintNum: 2
PrintNum: 2

通道

通道 channel 是一个管道,可以想现象成 Go协程之间通讯的管道。 他是一种队列式的 数据结构,遵循 先进先出的规则;通道使用完毕需要关闭:

通道主要分为两种:

  1. 缓冲通道
    c := make(chan int,5)
  2. 无缓冲通道:在无缓冲通道中无法存储数据, 所有接收端先于 发送端开始操作,当发送端 发送数据,接收端立马取走数据。否则 发送端就会造成阻塞
    c := make(chan int)

创建通道的时候 可以设置第二个参数 容量,当容量为0的时候 通道里面不能存放数据,发送数据的时候必须有人 立马去接收负责就会报错。此时的通道叫做 无缓冲通道
如果创建通道的时候 设置 容量为1,此时通道里面已经有一个数据,此时再往里面发送数据 就会报错 造成阻塞。**可以利用此特性 来做锁**

通道特性:

cap 获取通道容量
len 获取通道长度

//指定通道参数长度
chInt := make(chan int, 3)
fmt.Printf("chInt 初始化之后的容量长度:%d, 内容长度%d
", cap(chInt), len(chInt))
chInt <- 0
chInt <- 1
chInt <- 2

fmt.Printf("chInt 传入参数之后的容量长度:%d, 内容长度%d
", cap(chInt), len(chInt))
<-chInt
fmt.Printf("chInt 取出值之后的容量长度:%d, 内容长度%d
", cap(chInt), len(chInt))

输入:
chInt 初始化之后的容量长度:3, 内容长度0
chInt 传入参数之后的容量长度:3, 内容长度3
chInt 取出值之后的容量长度:3, 内容长度2
  • 只发送通道
  • 只接收通道

  • 双向通道

//只发 送通道
type Sender = chan<- string
//只接收通道
type Receiver = <-chan string
//双向通道
var chDouble = make(chan string)

  

通道的遍历:

var ch2 = make(chan int, 5)
go func() {
	for i := 0; i < 10; i++ {
		ch2 <- i
	}

}()
go func() {
	for v := range ch2 {
		fmt.Println(v)
	}
}()

在无缓冲通道 中无法存储数据, 所有接收端先于 发送端开始操作,当发送端 发送数据,接收端立马取走数据。否则 发送端就会造成异常阻塞,出现异常2 fatal error: all goroutines are asleep - deadlock!

 

// 出现异常1 fatal error: all goroutines are asleep - deadlock!
//ch0 := make(chan int)
//ch0 <- 1
// 出现异常2 fatal error: all goroutines are asleep - deadlock!
//ch1 := make(chan int)
//ch1 <- 1
//println("取数据", <-ch1)

上面的问题 可以使用协程来解决:

ch := make(chan int)
go func() {
		println("取数据", <-ch)
}()
ch <- 1
//延迟 用于打印内容
time.Sleep(time.Second)


输出:
取数据 1

协程不是 原子操作.应该避免对个协程对同一个数据进行操作,使用通道容量为 1的通道,达到锁的效果


// 通道 ch 容量为1
// ch 传入一个,再去出一个 形成一个锁
func increment(ch chan bool, x *int) {
	ch <- true
	*x = *x + 1 //不是原子操作,应该避免对个协程对同一个数据进行操作,使用通道容量为 1的通道,达到锁的效果
	<-ch
}


ch3 := make(chan bool, 1)
var x int
for i := 0; i < 10000; i++ {
	go increment(ch3, &x)
}
time.Sleep(time.Second)
fmt.Println("x = ", x)
输入:x =  10000

不使用锁的情况下

func increment(x *int) {
	*x = *x + 1  
}

var x int
for i := 0; i < 10000; i++ {
	go increment(&x)
}
time.Sleep(time.Second)
fmt.Println("x = ", x)

输出:x =  9840

 协程中的异步转同步

开发过程中我们经常要等待 一个协程的完成;才进行下一步骤:

  1.  使用通道等待协程完成
    isDone := make(chan bool)
    go func() {
    	for i := 0; i < 5; i++ {
    		fmt.Println(i)
            //模拟 耗时操作
    		time.Sleep(time.Second)
    	}
    	isDone <- true
    }()
    
    <-isDone
    fmt.Println("等待所有完成")
    
    
    输出:
    0
    1
    2
    3
    4
    等待所有完成
    
  2. 使用 WaitGroup 等待一组协程完成
    /*
    	WaitGroup  等待一组(协程)任务结束,在执行其他业务
    	Add()     初始值为0  累加协程数量
    	Done()    当某个子协程完成后  计数器减去1, 通常用 defer 调用
    	Wait()    阻塞当前协程, 直到实例中的计数器归零
    */
    
    
    func DoTask(taskNum int, wg *sync.WaitGroup) {
    	defer wg.Done()
    	for i := 0; i < 3; i++ {
    		fmt.Printf("task:%d : %d
    ", taskNum, i)
    		time.Sleep(180 * time.Millisecond)
    	}
    }
    
    
    
    var wg sync.WaitGroup
    wg.Add(3)
    //开启协程1
    go DoTask(1, &wg)
    //开启协程2
    go DoTask(2, &wg)
    //开启协程3
    go DoTask(3, &wg)
    
    fmt.Println("等待 所有协程完成")
    wg.Wait()
    fmt.Println("完成")
    
    
    输出:
    等待 所有协程完成
    task:2 : 0
    task:3 : 0
    task:1 : 0
    task:3 : 1
    task:2 : 1
    task:1 : 1
    task:2 : 2
    task:3 : 2
    task:1 : 2
    完成
    

select:

  • select (针对通道中进行选择) 语句用在多个 发送/接收通道操作中进行选择
  • 使用 select 语句会一直阻塞 直到发送者或者接受者准备就绪
  • 如果有多个通道同时准备完毕 select 会随机选取其中一个执行,否则会先返回 第一个,忽略第一个响应
  • 如果有耗时操作,并且select 有default 则 default 会直接执行 忽略其他 通道
/*
	select{
		case exp1:
			code
		case exp2:
			code
		case exp3:
			code
		default:
			code
	}
*/

select  中有 同时返回的:select 会随 机选 取其中一个执行
ch1 := make(chan string, 1)
ch2 := make(chan string, 1)
ch3 := make(chan string, 1)
ch1 <- "我是王大锤"
ch2 <- "我是王二锤"
ch3 <- "我是王三锤"

select {
case message1 := <-ch1:
	fmt.Println("message1:", message1)
case message2 := <-ch2:
	fmt.Println("message2:", message2)
case message3 := <-ch3:
	fmt.Println("message3:", message3)
default:
	fmt.Println("default: 什么也没收到")
}


随机 输出:
message2: 我是王二锤

select   如果有耗时操作,且没有default 则 default; 则会 执行第一个通道 返回。否则会执行 default 

func task1(ch chan string) {
	time.Sleep(3 * time.Second)
	ch <- "向通道发送数据1"
}
func task2(ch chan string) {
	time.Sleep(2 * time.Second)
	ch <- "向通道发送数据2"
}
func task3(ch chan string) {
	time.Sleep(4 * time.Second)
	ch <- "向通道发送数据3"
}

ch1 := make(chan string)
ch2 := make(chan string)
ch3 := make(chan string)
go task1(ch1)
go task2(ch2)
go task3(ch3)

select {
case message1 := <-ch1:
	fmt.Println("message1:", message1)
case message2 := <-ch2:
	fmt.Println("message2:", message2)
case message3 := <-ch3:
	fmt.Println("message3:", message3)
	//default:
	//	fmt.Println("default 执行了 没有等待其他通道")
}

输出:
message2: 向通道发送数据2

▶ select 死锁的情况

  1.  通道没有使用存入值得时候,直接取值 会造成 报错死锁
    // 通道没有使用存入值得时候,直接取值 会造成 报错死锁
    // fatal error: all goroutines are asleep - deadlock!
    
    func selectLock() {
    	ch1 := make(chan string, 1)
    	ch2 := make(chan string, 1)
    	ch3 := make(chan string, 1)
    
    	select {
    	case message1 := <-ch1:
    		fmt.Println("message1:", message1)
    	case message2 := <-ch2:
    		fmt.Println("message2:", message2)
    	case message3 := <-ch3:
    		fmt.Println("message3:", message3)
    	}
    }
  2. 空 select
    func emptySelect() {
    	//select-case  和  switch-case 很相似,区别 switch数顺序的  select 不是顺序的
    
    	//发生死锁 fatal error: all goroutines are asleep - deadlock!
    	select {}
    }

风语者!平时喜欢研究各种技术,目前在从事后端开发工作,热爱生活、热爱工作。