Go语言的通道

本文深入探讨Go语言中的并发编程技巧与通道的使用方法,包括通道的基本概念、声明与创建,以及如何通过通道实现进程间的通信。文章还介绍了通道在实际应用中的多种场景,并通过具体示例展示了如何有效利用通道进行数据传输。

《Effective Go》有一句话很好地说明了Go语言的并发实现理念:“不要通过共享内在来通信,而通过通信来共享内存。”

通道的创建语法如下:

c:=make(chan string)

使用内置函数make创建一个通道,这是使用关键字chan指定的。

向通道发送消息的语法如下:

c<="Hello World"

从通道那里接收消息的语法如下:

msg:=<-c

要从通道那里接收消息,需要在<-后面加上通道名。可使用简短变量赋值,将来自通道的消息直接赋给变量。箭头向左表示数据离开通道(接收),箭头向右表示数据进入通道(发送)。

使用通道进行通信,程序清单如下:(代码错误,没搞清楚。)

package main

import(
  "fmt"
  "time"
)
func slowFunc(){
  time.Sleep(time.Second*2)
  c <- "slowFunc() finished"
}
func main(){
  c:=make(chan string)
  go slowFunc()
  msg:=<-c
  fmt.Println(msg)
}

运行结果如下:

# command-line-arguments
./main.go:9:3: undefined: c

让缓冲通道接收两条消息,程序清单如下:

package main

import(
  "fmt"
  "time"
)
func receiver(c chan string){
  for msg:=range c{
    fmt.Println(msg)
  }
}
func main(){
  messages:=make(chan string,2)
  messages<-"hello"
  messages<-"world"
  close(messages)
  fmt.Println("Pushed two messages onto Channel with no receivers")
  time.Sleep(time.Second*1)
  receiver(messages)
}

运行结果如下:

Pushed two messages onto Channel with no receivers
hello
world

通道和流程控制,程序清单如下:

package main

import(
  "fmt"
  "time"
)
func pinger(c chan string){
  t:=time.NewTicker(1*time.Second)
  for{
    c<-"ping"
    <-t.C
  }
}
func main(){
  messages:=make(chan string)
  go pinger(messages)
  msg:=<-messages
  fmt.Println(msg)
}

运行结果如下:

ping

不断地执行指定的操作,程序清单如下:

package main

import(
  "fmt"
  "time"
)
func pinger(c chan string){
  t:=time.NewTicker(1*time.Second)
  for{
    c<-"ping"
    <-t.C
  }
}
func main(){
  messages:=make(chan string)
  go pinger(messages)
  for{
    msg:=<-messages
    fmt.Println(msg)
  }
}

运行结果如下:

不断地出现ping,每一个ping的间隔有一些时间。死循环。

代码如下:

package main

import(
  "fmt"
  "time"
)
func pinger(c chan string){
  t:=time.NewTicker(1*time.Second)
  for{
    c<-"ping"
    <-t.C
  }
}
func main(){
  messages:=make(chan string)
  go pinger(messages)
  for i:=0;i<5;i++{
    msg:=<-messages
    fmt.Println(msg)
  }
}

运行结果如下:(有时间间隔)

ping
ping
ping
ping
ping

select语句,程序清单如下:

package main

import(
  "fmt"
  "time"
)
func ping1(c chan string){
  time.Sleep(time.Second*1)
  c<-"ping on channel1"
}
func ping2(c chan string){
  time.Sleep(time.Second*2)
  c<-"ping on channel2"
}
func main(){
  channel1:=make(chan string)
  channel2:=make(chan string)
  go ping1(channel1)
  go ping2(channel2)
  select{
  case msg1:=<-channel1:
    fmt.Println("received",msg1)
  case msg2:=<-channel2:
    fmt.Println("received",msg2)
  }
}

运行结果如下:

received ping on channel1

代码如下:

package main

import(
  "fmt"
  "time"
)
func ping1(c chan string){
  time.Sleep(time.Second*3)
  c<-"ping on channel1"
}
func ping2(c chan string){
  time.Sleep(time.Second*2)
  c<-"ping on channel2"
}
func main(){
  channel1:=make(chan string)
  channel2:=make(chan string)
  go ping1(channel1)
  go ping2(channel2)
  select{
  case msg1:=<-channel1:
    fmt.Println("received",msg1)
  case msg2:=<-channel2:
    fmt.Println("received",msg2)
  }
}

运行结果如下:

received ping on channel2

给select语句指定超时时间,代码如下:

package main

import(
  "fmt"
  "time"
)
func ping1(c chan string){
  time.Sleep(time.Second*3)
  c<-"ping on channel1"
}
func ping2(c chan string){
  time.Sleep(time.Second*2)
  c<-"ping on channel2"
}
func main(){
  channel1:=make(chan string)
  channel2:=make(chan string)
  go ping1(channel1)
  go ping2(channel2)
  select{
  case msg1:=<-channel1:
    fmt.Println("received",msg1)
  case msg2:=<-channel2:
    fmt.Println("received",msg2)
  case <-time.After(500*time.Millisecond):
    fmt.Println("no message received. giving up.")
  }
}

运行结果如下:

no message received. giving up.

使用退出通道,程序清单如下:

package main

import(
  "fmt"
  "time"
)
func sender(c chan string){
  t:=time.NewTicker(1*time.Second)
  for{
    c<-"I'm sending a message"
    <-t.C
  }
}
func main(){
  messages:=make(chan string)
  stop:=make(chan bool)
  go sender(messages)
  go func(){
    time.Sleep(time.Second*2)
    fmt.Println("Time's up!")
    stop<-true
  }()
  for{
    select{
    case <-stop:
      return
    case msg:=<-messages:
      fmt.Println(msg)
    }
  }
}

运行结果如下:(除了倒数两行,其它都有时间间隔。)

I'm sending a message
I'm sending a message
I'm sending a message
Time's up!

或者:

I'm sending a message
I'm sending a message
Time's up!

通道(channel)——在多个goroutine间通信的管道 

一、通道的特征

二、声明通道类型

声明如下:

var 通道变量 chan 通道类型

通道类型:通道内的数据类型。

通道变量:保存通道的变量。

chan类型的空值是nil,声明后需要配合make后才能使用。

三、创建通道

通道是引用类型,需要使用make进行创建,格式如下:

通道实例:=make(chan 数据类型)

数据类型:通道内传输的元素类型。

通道实例:通过make创建的通道句柄。

四、使用通道发送数据

1、通道发送数据的格式

通道的发送使用特殊的操作符“<-”,将数据通过通道发送的格式为:

通道变量<-值 

通道变量:通过make创建好的通道实例。

值:可以是变量、常量、表达式或者函数返回值等。值的类型必须与ch通道的元素类型一致。

2、通过通道发送数据的例子

使用make创建一个通道后,就可以使用“<-”向通道发送数据。

3、发送将持续阻塞直到数据被接收

把数据往通道中发送时,如果接收方一直都没有接收,那么发送操作将持续阻塞。

五、使用通道接收数据

1、阻塞接收数据

格式如下:

data:=<-ch

2、非阻塞接收数据

格式如下:

data,ok:=<-ch

data:表示接收到的数据。未接收到数据时,data为通道类型的零值。

ok:表示是否接收到数据。

3、接收任意数据,忽略接收的数据

格式如下:

<-ch

代码如下:

package main

import (
	"fmt"
)

func main() {
	//构建一个通道
	ch := make(chan int)
	//开启一个并发匿名函数
	go func() {
		fmt.Println("start goroutine")
		//通过通道通知main的goroutine
		ch <- 0
		fmt.Println("exit goroutine")
	}()
	fmt.Println("wait goroutine")
	//等待匿名goroutine
	<-ch
	fmt.Println("all done")
}

运行结果如下:

wait goroutine
start goroutine
exit goroutine
all done

4、循环接收

代码如下:

package main

import (
	"fmt"
	"time"
)

func main() {
	//构建一个通道
	ch := make(chan int)
	//开启一个并发匿名函数
	go func() {
		//从3循环到0
		for i := 3; i >= 0; i-- {
			//发送3到0之间的数值
			ch <- i
			//每次发送完时等待
			time.Sleep(time.Second)
		}
	}()
	//遍历接收通道数据
	for data := range ch {
		//打印通道数据
		fmt.Println(data)
		//当遇到数据0时,退出接收循环
		if data == 0 {
			break
		}
	}
}

运行结果如下:

3
2
1
0

六、示例:并发打印

代码如下:

package main

import (
	"fmt"
)

func printer(c chan int) {
	//开始无限循环等待数据
	for {
		//从channel中获取一个数据
		data := <-c
		//将0视为数据结束
		if data == 0 {
			break
		}
		//打印数据
		fmt.Println(data)
	}
	//通知main已经结束循环(我搞定了!)
	c <- 0
}

func main() {
	//创建一个channel
	c := make(chan int)
	//并发执行printer,传入channel
	go printer(c)

	for i := 1; i <= 10; i++ {
		//将数据通过channel投送给printer
		c <- i
	}
	//通知并发的printer结束循环(没数据啦!)
	c <- 0
	//等待printer结束(搞定咕我!)
	<-c
}

运行结果如下:

1
2
3
4
5
6
7
8
9
10

七、单向通道——通道中的单行道

1、单向通道的声明格式

只能发送的通道类型为chan<-,只能接收的通道类型为<-chan,格式如下

var 通道实例 chan<- 元素类型       

var 通道实例 <-chan 元素类型

元素类型:通道包含的元素类型

通道实例:声明的通道变量。

2、单向通道的使用例子

3、time包中的单向通道

time包中的计时器会返回一个timer实例,代码如下:

timer:=time.NewTimer(time.Second)

八、带缓冲的通道

1、创建带缓冲的通道

通道实例:=make(chan 通道实例,缓冲大小)

通道类型:和无缓冲通道用法一致,影响通道发送和接收的数据类型。

缓冲大小:决定通道最多可以保存的元素数量。

通道实例:被创建出的通道实例。

代码如下:

package main

import (
	"fmt"
)

func main() {
	//创建一个3个元素缓冲大小的整型通道
	ch := make(chan int, 3)
	//查看当前通道的大小
	fmt.Println(len(ch))
	//发送3个整形元素的通道
	ch <- 1
	ch <- 2
	ch <- 3
	//查看当前通道的大小
	fmt.Println(len(ch))
}

运行结果如下:

PS C:\Users\a-xiaobodou\OneDrive - Microsoft\Projects\Go> go run main.go
0
3

2、阻塞条件

(1)带缓冲通道被填满时,尝试再次发送数据时发生阻塞。

(2)带缓冲通道为空时,尝试接收数据 时发生阻塞。

九、通道的多路复用——同时处理接收和发送多个通道的数据

select{

        case 操作1:

                响应操作1

        case 操作2:

                响应操作2

        ……

        default:

                没有操作情况

}

十、示例:模拟远程过程调用(RPC)

代码如下:

package main

import (
	"errors"
	"fmt"
	"time"
)

// 模拟RPC客户端的请求和接收消息封装
func RPCClient(ch chan string, req string) (string, error) {

	// 向服务器发送请求
	ch <- req

	// 等待服务器返回
	select {
	case ack := <-ch: // 接收到服务器返回数据
		return ack, nil
	case <-time.After(time.Second): // 超时
		return "", errors.New("Time out")
	}
}

// 模拟RPC服务器端接收客户端请求和回应
func RPCServer(ch chan string) {
	for {
		// 接收客户端请求
		data := <-ch

		// 打印接收到的数据
		fmt.Println("server received:", data)

		// 反馈给客户端收到
		ch <- "roger"
	}
}

func main() {

	// 创建一个无缓冲字符串通道
	ch := make(chan string)

	// 并发执行服务器逻辑
	go RPCServer(ch)

	// 客户端请求数据和接收数据
	recv, err := RPCClient(ch, "hi")
	if err != nil {
		// 发生错误打印
		fmt.Println(err)
	} else {
		// 正常接收到数据
		fmt.Println("client received", recv)
	}

}

运行结果如下:

[Running] go run "c:\Users\a-xiaobodou\OneDrive - Microsoft\Projects\Go\main.go"
server received: hi
client received roger

[Done] exited with code=0 in 2.429 seconds

十一、示例:使用通道响应计时器的事件

1、一段时间之后 (time.After)

代码如下:

package main

import (
	"fmt"
	"time"
)

func main() {
	// 声明一个退出用的通道
	exit := make(chan int)

	// 打印开始
	fmt.Println("start")

	// 过1秒后,调用匿名函数
	time.AfterFunc(time.Second, func() {

		// 1秒后,打印结果
		fmt.Println("one second after")

		// 通知main的goroutine已经结束
		exit <- 0
	})

	// 等待结束
	<-exit
}

运行结果如下:

[Running] go run "c:\Users\a-xiaobodou\OneDrive - Microsoft\Projects\Go\main.go"
start
one second after

[Done] exited with code=0 in 2.205 seconds

2、定点计时

代码如下:

package main

import (
	"fmt"
	"time"
)

func main() {

	// 创建一个断续器,每500毫秒触发一次
	ticker := time.NewTicker(time.Millisecond * 500)

	// 创建一个计时器,2秒后触发
	stopper := time.NewTimer(time.Second * 2)

	// 声明计数变量
	var i int

	// 不断的检查通道情况
	for {

		// 多路复用通道
		select {
		case <-stopper.C: // 计时器到时了

			fmt.Println("stop")

			// 跳出循环
			goto StopHere

		case <-ticker.C: // 断续器触发了
			// 记录触发了多少次
			i++
			fmt.Println("tick", i)
		}
	}

	// 退出的标签,使用goto跳转
StopHere:
	fmt.Println("done")

}

运行结果如下:

[Running] go run "c:\Users\a-xiaobodou\OneDrive - Microsoft\Projects\Go\main.go"
tick 1
tick 2
tick 3
tick 4
stop
done

[Done] exited with code=0 in 5.162 seconds

十二、关闭通道后继续使用通道

1、格式

使用close()来关闭一个通道:

close(ch)

2、给被关闭通道发送数据将会触发panic

代码如下:

package main

import (
	"fmt"
)

func main() {

	// 创建一个整型的胶片
	ch := make(chan int)

	//关闭通道
	close(ch)

	//打印通道的指针,容量和长度
	fmt.Printf("ptr:%p cap:%d len:%d\n", ch, cap(ch), len(ch))

	//给关闭的通道发送数据
	ch <- 1

}

运行结果如下:

[Running] go run "c:\Users\a-xiaobodou\OneDrive - Microsoft\Projects\Go\main.go"
ptr:0xc000086060 cap:0 len:0
panic: send on closed channel

goroutine 1 [running]:
main.main()
	c:/Users/a-xiaobodou/OneDrive - Microsoft/Projects/Go/main.go:19 +0x105
exit status 2

[Done] exited with code=1 in 1.243 seconds

3、从已关闭的通道接收数据时将不会发生阻塞

代码如下:

package main

import "fmt"

func main() {
	// 创建一个整形带2个缓冲的通道
	ch := make(chan int, 2)

	// 给通道放入两个数据
	ch <- 0
	ch <- 1

	// 关闭缓冲
	close(ch)

	// 遍历缓冲所有数据,且多遍历1个
	for i := 0; i < cap(ch)+1; i++ {

		// 从通道中取出数据
		v, ok := <-ch

		// 打印取出数据的状态
		fmt.Println(v, ok)
	}
}

运行结果如下:

[Running] go run "c:\Users\a-xiaobodou\OneDrive - Microsoft\Projects\Go\main.go"
tick 1
tick 2
tick 3
tick 4
stop
done

[Done] exited with code=0 in 5.162 seconds

示例:Telnet回音服务器——TCP服务器的基本结构

同步——保证并发环境下数据访问的正确性

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值