这是我纯手写的《Go语言入门》,手把手教你入门Go。源码+文章,看了你就会🥴!
文章中所有的代码我都放到了github.com/GanZhiXiong/go_learning这个仓库中!
看文章时,对照仓库中代码学习效果更佳哦!
目录
前言
并发编程的难度在于协调,而协调就要通过交流,从这个角度看来,并发单元间的通信是最大的问题。
常用的并发通信模型有两种:共享数据和消息机制
共享数据
是指多个并发单元分别通过内存数据块或磁盘文件或网络数据等(实际应用中共享内存用得最多)保持对同一个数据的引用,实现对该数据的共享。
消息通信机制
共享数据的方式在并发时,为了防止数据被意外改变而导致访问的数据不准确,在操作时需要给数据加锁,操作完成后,再将锁打开。
想象一下,在一个大的系统中具有无数的锁、无数的共享变量、无数的业务逻辑与错误处理分支,那将是一场噩梦。这噩梦就是众多 C/C++ 开发者正在经历的,其实 Java 和 C# 开发者也好不到哪里去。
而Go语言提供的是另一种通信模型,即以消息机制而非共享内存作为通信方式。
消息机制认为每个并发单元是自包含的、独立的个体,并且都有自己的变量,但在不同并发单元间这些变量不共享。每个并发单元的输入和输出只有一种,那就是消息。
这有点类似于进程的概念,每个进程不会被其他进程打扰,它只做好自己的工作就可以了。不同进程间靠消息来通信,它们不会共享内存。
Go语言提供的消息通信机制被称为 channel
chan
Go语言提倡使用消息通信机制的方法代替共享内存,当一个资源需要在 goroutine 之间共享时,通道在 goroutine 之间架起了一个管道,并提供了确保同步交换数据的机制。声明通道时,需要指定将要被共享的数据的类型。可以通过通道共享内置类型、命名类型、结构类型和引用类型的值或者指针。
这里的通信方法就是使用通道(channel)即chan:
chan的特性
Go语言中的通道(channel)是一种特殊的类型。在任何时候,同时只能有一个 goroutine 访问通道进行发送和获取数据。goroutine 间通过通道就可以通信。
通道像一个传送带或者队列,总是遵循先入先出(First In First Out)的规则,保证收发数据的顺序。
声明chan
通道本身需要一个类型进行修饰,就像切片类型需要标识元素类型。通道的元素类型就是在其内部传输的数据类型,声明如下:
var 通道变量 chan 通道类型
chan类型的空值为nil,是没有分配空间的,声明后需要配合make后才能使用。
创建chan
通道是引用类型,需要使用 make 进行创建,格式如下:
通道实例 := make(chan 数据类型)
如:
ch1 := make(chan int)
ch2 := make(chan interface{})
type a struct {}
ch3 := make(chan *a)
使用chan发送数据
通道创建后,就可以使用通道进行发送和接收操作。
- 通道发送数据的格式
通道变量 <- 值
注意发送值可以使变量、常量、表达式或函数返回值等,但值的类型必须与通道的元素的类型一致。
如:
func TestChan1(t *testing.T) {
ch := make(chan interface{})
ch <- 0
ch <- "hello"
}
什么情况,上面的测试用例运行后却报错了:
=== RUN TestChan1
fatal error: all goroutines are asleep - deadlock!
原因是把数据往通道中发送时,如果接收方一直都没有接收,那么发送操作将持续阻塞。Go程序运行时能智能地发现一些永远无法发送成功的语句并作出提示。
使用chan接收数据
通道接收同样使用<-
操作符。
chan接收的特性
- 通道的收发操作在不同的两个goroutine间进行
- 发送方将持续阻塞直到接收方接收数据
- 接收方将持续阻塞直到发送方发送数据
- 通道一次只能接收一个数据元素
chan接收的写法
一共四种写法。
1、阻塞接收数据
data := <-ch
执行该语句时将会阻塞,直到接收到数据并赋值给data变量。
2、非阻塞接收数据
data, ok := <-ch
- data为接收到的数据,未接收到数据时,data为通道类型的零值。
- ok为是否接收到数据。
执行该语句时不会阻塞。
该方式可能会造成高的CPU占用,因此很少使用。
如果是需要实现接收超时检测,可以配合select和计时器channel进行,后面我会讲到的。
3、阻塞接收任意数据,忽略接收的数据
<-ch
该方式实际上只是通过通道在goroutine间阻塞收发实现并发同步。
如:
// 使用通道做并发同步
func TestChan2(t *testing.T) {
// 构建一个通道
ch := make(chan int)
// 开启一个并发匿名函数
go func() {
t.Log("start goroutine")
// 通过通道通知TestChan2的goroutine
ch <- 0
t.Log("exit goroutine")
}()
t.Log("wait goroutine")
// 等待匿名goroutine
<-ch
t.Log("all done")
}
4、循环接收
通道的数据接收可以借用for range语句进行多个元素的接收操作:
for data := range ch {
}
通道ch是可以进行遍历的,遍历的结果就是接收到的数据。
如:
// 通道接收数据方式4、循环接收
func TestChan3(t *testing.T) {
// 构建一个通道
ch := make(chan int)
// 开启一个并发匿名函数
go func() {
for i := 3; i >= 0; i-- {
t.Log("go func for", i)
// 发送3到0之间的数值
ch <- i
// 每次发送完成时等待1s
time.Sleep(time.Second)
}
}()
// 遍历接收通道数据
for data := range ch {
t.Log(data)
if data == 0 {
break
}
}
}
=== RUN TestChan3
25_chan_test.go:57: go func for 3
25_chan_test.go:68: 3
25_chan_test.go:57: go func for 2
25_chan_test.go:68: 2
25_chan_test.go:57: go func for 1
25_chan_test.go:68: 1
25_chan_test.go:57: go func for 0
25_chan_test.go:68: 0
--- PASS: TestChan3 (3.00s)
PASS
测试题
请先自己猜测结果。
改代码也位于github.com/GanZhiXiong/go_learning这个仓库中!
测试1
// 通道接收数据方式4、循环接收
func TestChan4(t *testing.T) {
// 构建一个通道
ch := make(chan int)
// 开启一个并发匿名函数
go func() {
for i := 3; i >= 0; i-- {
t.Log("go func for", i)
// 发送3到0之间的数值
ch <- i
// 每次发送完成时等待1s
time.Sleep(time.Second)
}
}()
data := <-ch
t.Log(data)
}
测试2
// 测试题目2
func TestChanTest2(t *testing.T) {
// 构建一个通道
ch := make(chan int)
// 开启一个并发匿名函数
go func() {
for i := 3; i >= 0; i-- {
t.Log("go func for", i)
// 发送3到0之间的数值
ch <- i
// 每次发送完成时等待1s
time.Sleep(time.Second)
}
}()
data := <-ch
t.Log(data)
time.Sleep(time.Second*4)
}
支持🤟
- 🎸 [关注❤️我吧],我会持续更新的。
- 🎸 [点个👍赞吧],码字不易麻烦了。