一、channel的定义,用于两个 go 之间的通信

方法一:make(chan Type)等价于make(chan Type,0)其中 Type 代表数据类型。

方法二:make(chan Type,capacity)

二、channel的使用

写入 channel 方法:channel <- value

从 channe 中读:<-channel 接收并将其丢弃

                            x := <-channel 接收并赋值给 x

                            x ,ok := <-channel 接收并赋值给 x,同时检查通道是否已关闭或者是否为空

Channel通道在使用的时候,有以下几个注意点:

  • 1.用于goroutine,传递消息的。

  • 2.通道,每个都有相关联的数据类型, nil chan,不能使用,类似于nil map,不能直接存储键值对

  • 3.使用通道传递数据:<- chan <- data,发送数据到通道。向通道中写数据 data <- chan,从通道中获取数据。从通道中读数据

  • 4.阻塞: 发送数据:chan <- data,阻塞的,直到另一条goroutine,读取数据来解除阻塞 读取数据:data <- chan,也是阻塞的。直到另一条goroutine,写出数据解除阻塞。

  • 5.本身channel就是同步的,意味着同一时间,只能有一条goroutine来操作。

最后:通道是goroutine之间的连接,所以通道的发送和接收必须处在不同的goroutine中。

看一段代码吧:

package main

import (
	"fmt"
)

func main() {
	c := make(chan int)
	go func() {
		defer fmt.Println("goroution 结束")
		fmt.Println("goroutio 正在运行。。。。")
		c <- 666 //将666发送个c
	}()
	m := <-c
	fmt.Println("num=", m)
	fmt.Println("main goroution 结束。。。")
}
三、channel有缓冲与无缓冲同步问题

首先来看一下有缓冲的channel运行的过程吧,如下图

 那么无缓冲的会是什么过程呢?接着来看下一图

 上面的一段代码是无缓冲的,之前学习的所有通道基本上都没有缓冲。发送和接收到一个未缓冲的通道是阻塞的。一次发送操作对应一次接收操作,对于一个goroutine来讲,它的一次发送,在另一个goroutine接收之前都是阻塞的。同样的,对于接收来讲,在另一个goroutine发送之前,它也是阻塞的。

那么有缓冲的该怎么写的呢?

缓冲通道就是指一个通道,带有一个缓冲区。发送到一个缓冲通道只有在缓冲区满时才被阻塞。类似地,从缓冲通道接收的信息只有在缓冲区为空时才会被阻塞。

可以通过将额外的容量参数传递给make函数来创建缓冲通道,该函数指定缓冲区的大小。

语法:

ch := make(chan type, capacity)  

上述语法的容量应该大于0,以便通道具有缓冲区。默认情况下,无缓冲通道的容量为0,因此在之前创建通道时省略了容量参数。

我们来看下一段代码:

package main

import (
	"fmt"
	"time"
)

func main() {
	c := make(chan int, 3) //带有缓冲的
	fmt.Println("len(c)=", len(c), ",cpa(c)=", cap(c))
	go func() {
		defer fmt.Println("子go 结束")
		for i := 0; i < 4; i++ {
			c <- i
			fmt.Println("子go程正在运行,发送的元素=", i, "len(c)=", len(c), ",cpa(c)=", cap(c))
		}

	}()
	time.Sleep(2 * time.Second)
	for i := 0; i < 3; i++ {
		num := <-c
		fmt.Println("num=", num)
	}
	fmt.Println("main go 结束。。。")
}

为了方便我们看是否有缓冲,我加了这样一句 time.Sleep(2 * time.Second) 目的是让子go中全部放入缓冲中之后,main go再去拿。然而如果我将子 go 中的循环条件 i 变成 i<4,那么我们会发现channel 会有个等待的过程

由此可见channel的特点:(前提channel没有被关闭)

1.当 channel 已经满,再向里面写数据,就会阻塞。

2.当 channel 为空,从里面取数据也会阻塞。

四、channel关闭

 我们看一下这段代码:

package main

import (
	"fmt"
)

func main() {
	c := make(chan int)

	go func() {
		for i := 0; i < 5; i++ {
			c <- i
		}
		//close(c) //关闭
	}()
	for {
		if data, ok := <-c; ok { //如果channel没有关闭,那么ok为true,否则为false
			fmt.Println(data)
		} else {
			break
		}
	}
	fmt.Println("Finishede...")
}

如果不关闭的话,就会出现死锁。因为子 go 中都已放入暖冲后没有可用的供给 main go就会导致main go一直等待。

总结:

  • channel 不像文件一样需要经常关闭,只有当你确实没有任何发送数据了,或者你想显式结束range循环之类的,才去关闭 channel。
  • 关闭 channel 后,无法向 channel 再发送数据(引发 panic 错误后导致接收立即返回零值);
  • 关闭 channel 后,可以继续从 channel 接收数据;
  • 对于 nil channel,无论收发都会阻塞。(即没有使用 make()初始化)
五、channel 与 range 

 上一段代码中的 for 循环可以用 for range 代替,range 可以用来迭代不断操作 channel ,如下面代码:

package main

import (
	"fmt"
)

func main() {
	c := make(chan int)

	go func() {
		for i := 0; i < 5; i++ {
			c <- i
		}
		close(c) //关闭
	}()
	// for {
	// 	if data, ok := <-c; ok { //如果channel没有关闭,那么ok为true,否则为false
	// 		fmt.Println(data)
	// 	} else {
	// 		break
	// 	}
	// }
	//可以使用range来迭代不断操作channel
	for data := range c {
		fmt.Println(data)
	}
	fmt.Println("Finishede...")
}
六、channel 与 select

单流程下一个 go 只能监控一个 channel 的状态,select 可以完成监控多个 channel 的状态。

select语句类型于switch语句
   但是select语句会随机执行一个可运行的case
   如果没有case可以运行,要看是否有default,如果有就执行default,否则就进入阻塞,直到有case可以运行

说明:

  • 每个case都必须是一个通信

  • 所有channel表达式都会被求值

  • 所有被发送的表达式都会被求值

  • 如果有多个case都可以运行,select会随机公平地选出一个执行。其他不会执行。

  • 否则:

    如果有default子句,则执行该语句。

    如果没有default字句,select将阻塞,直到某个通信可以运行;Go不会重新对channel或值进行求值。

 看下面一段代码:

package main

import (
	"fmt"
)

func fibonacil(c, quite chan int) {
	x, y := 1, 1
	for {
		select {
		case c <- x:
			//如果c可写,则会运行该case
			x = y
			y = x + y
		case <-quite:
			fmt.Println("quit")
			return
		}

	}
}
func main() {
	c := make(chan int)
	quite := make(chan int)
	go func() {
		for i := 0; i < 6; i++ {
			fmt.Println(<-c)
		}
		quite <- 0
	}()
	fibonacil(c, quite)
}
七、双向通道

通道,channel,是用于实现goroutine之间的通信的。一个goroutine可以向通道中发送数据,另一条goroutine可以从该通道中获取数据。截止到现在我们所学习的通道,都是既可以发送数据,也可以读取数据,我们又把这种通道叫做双向通道。

 双向定义方法: 

ch1 := make(chan int)//双向,读,写
八、单向通道

单向通道,也就是定向通道。

之前我们学习的通道都是双向通道,我们可以通过这些通道接收或者发送数据。我们也可以创建单向通道,这些通道只能发送或者接收数据。

单向:定向
        chan <- T,
            只支持写,
        <- chan T,
            只读

单向定义方法 :

data := <- a // read from channel a  
a <- data // write to channel a
ch2 := make(chan <- int) // 单向,只写,不能读
ch3 := make(<- chan int) //单向,只读,不能写

实例代码:

package main

import "fmt"

func main()  {
	/*
	双向:
		chan T -->
			chan <- data,写出数据,写
			data <- chan,获取数据,读
	单向:定向
		chan <- T,
			只支持写,
		<- chan T,
			只读
	 */
	ch1 := make(chan string) // 双向,可读,可写
	done := make(chan bool)
	go sendData(ch1, done)
	data :=<- ch1 //阻塞
	fmt.Println("子goroutine传来:", data)
	ch1 <- "我是main。。" // 阻塞

	<-done
	fmt.Println("main...over....")
}
//子goroutine-->写数据到ch1通道中
//main goroutine-->从ch1通道中取
func sendData(ch1 chan string, done chan bool)  {
	ch1 <- "我是小明"// 阻塞
	data := <-ch1 // 阻塞
	fmt.Println("main goroutine传来:",data)

	done <- true
}

九、time包中的通道相关函数

主要就是定时器,标准库中的Timer让用户可以定义自己的超时逻辑,尤其是在应对select处理多个channel的超时、单channel读写的超时等情形时尤为方便。

1.time.NewTimer 

Timer是一次性的时间触发事件,这点与Ticker不同,Ticker是按一定时间间隔持续触发时间事件。

Timer常见的创建方式:(创建一个计时器,d时间以后触发)

func NewTimer(d Duration) *Timer
timer := time.NewTimer(3 *time.Second)//这里数字就是定义阻塞多少秒
fmt.Printf("%T\n",timer) //*time.Timer
fmt.Println(time.Now()) //2022-07-31 08:08:38.3434183 +0800 CST m=+0.002415501
//此处等待channel中的数值,会阻塞3秒
ch2 := timer.C
fmt.Println(<-ch2 ) //2022-07-31 08:09:56.1214098 +0800 CST m=+3.011776701
  • 用于在指定的Duration类型时间后调用函数或计算表达式。
  • 如果只是想指定时间之后执行,使用time.Sleep()
  • 使用NewTimer(),可以返回的Timer类型在计时器到期之前,取消该计时器
  • 直到使用<-timer.C发送一个值,该计时器才会过期

2.timer.Stop

示例代码:

 //新建一个计时器
	 timer2 := time.NewTimer(5*time.Second)
	 //开始goroutine,来处理触发后的事件
	 go func() {
	 	<- timer2.C
	 	fmt.Println("Timer 2 结束了。。。开始。。。。")
	 }()
	//由于上面的等待信号是在新线程中,所以代码会继续往下执行,停掉计时器
	 time.Sleep(3*time.Second)
	 flag := timer2.Stop()
	 if flag{
	 	fmt.Println("Timer 2 停止了。。。")
	 }

3.time.After()

在等待持续时间之后,然后在返回的通道上发送当前时间。它相当于NewTimer(d).C。在计时器触发之前,垃圾收集器不会恢复底层计时器。如果效率有问题,使用NewTimer代替,并调用Timer。如果不再需要计时器,请停止。

func After(d Duration) <-chan Time

 该函数返回一个通道:chan,存储的是d时间间隔之后的当前时间。相当于:return NewTimer(d).C

加入go友会,我们一起进步吧!

 邀请码:Gopher-12538-1769