注意,最容易引发混乱的是main函数,因为main函数是可执行Go文件的必须元素,同时必须是指定package也为main,因此我们尽量不要在main函数所在的Go文件中添加与main无关的内容,否则我们很难通过包名或者文件名定位函数的意思。
注意,Go中最没用的就是Go文件名了,包引用都是通过package。
正确的引用包是:将被调用函数所在文件,声明package为其所在文件夹名字,
注意,所有的该文件夹下的Go文件的package声明必须为同一个,不能出现第二个值,对外部调用者来讲,这些文件好似在一起,都是从一个package读取,并无区分。
然后在调用函数的地方import进来被调用函数声明的package即可。
所以总结一下,文件夹名即包名,文件夹内给Go文件起名要能够解释清楚文件内容,main函数文件指定到有意义的文件夹下,导入所需函数包。
main函数 func main() { in := pipeline.Gen(2, 3) c1 := pipeline.Sq(in) c2 := pipeline.Sq(in) // 将c1和c2通道内的值合并到一起 for n := range merge(c1, c2) { fmt.Println(n) } } // Output: // 4 // 9等价于
out:= merge(c1,c2) fmt.Println(<-out) fmt.Println(<-out) fmt.Println(<-out)// 第三次输出,通道已无值,输出零值,如果通道输出完毕时即关闭的话,这一行会报错 // Output: // 4 // 9 // 0 发现问题?发送次数少于接收次数上面的管道函数有一个模式:
所有的发送操作完成时,阶段会关闭他们的导出通道。
阶段会一直从导入通道中接收值,直到那些通道被关闭。
这个模式允许每个接收的阶段可以被作为一个range循环写入,并且保证一旦所有的值都已经成功发送下游,所有的goroutine退出。
但是在真实的管道里,阶段不会总是能接收到所有的导入值。有时候这是由于一个设计:
接收者可能只需要一个值的子集来取得进展。
更常见的是,一个阶段早早退出是因为一个导入值代表了一个更早阶段的error。在这两种情况下,接收方不应该等待其余值的到达,并且我们想要更早的阶段来停止那些后期阶段不需要的生产时期的值。
在我们的例子中,
out:= merge(c1,c2) fmt.Println(<-out) // Output: // 4实际上out通道中还有一个9没有被输出,通道的值此时没有被完全消费,这时goroutine就会不断尝试发送该值,并且会被无限期阻塞。
这是一个资源泄露,goroutine消耗内存和运行时资源,并且在goroutine栈中的堆引用会一直防止数据被垃圾收集器回收。goroutine不可被垃圾收集,只能必须靠自己exit。
所以,我们需要找到方式,能够在下游阶段接收所有导入值失败的时候,上游阶段的管道仍旧能够退出:
一种方式是改变导出通道让它又有一个buffer缓冲区,一个缓冲区能够持有一个固定数量的值,如果缓冲区内仍有空间,发送操作就立即完成。
缓冲区的内容我们在前面的文章中有仔细介绍。
总之就是可以释放goroutine的发送操作到缓冲区,不会被无限期阻塞。
在我们的管道中,返回到被阻塞的goroutine,我们可能考虑到添加一个缓冲区到merge函数返回的导出通道:
out := make(chan int, 1)// 增加一个缓冲区,可以存放通道中未发送的值但是问题仍在发生,我们这里是因为知道我们上面只写了一遍发送,而通道已知有两次接收值,所以我们可以这么干,但是这个代码是不好的,易碎的,一旦条件发生改变,就要对代码进行调整。
因此,我们需要为下游阶段提供一种方式来象征发送者,来停止接收输入。
明确的取消机制当main函数决定退出,而不再接收任何out通道的值的时候,它必须告诉上游的goroutine,放弃他们试图发送的值。
在一个通道中如此操作发送值,被称作done。
它发送两个值因为有两个潜在的阻塞发送者。
我们修改merge,给它加入一个参数是struct{}结构体通道。然后修改merge中的output函数,将原来的
out <- n:替换为:
select { case out <- n: case <-done: }意思是:如果n还有未发送的值,就正常发送,如果done有未发送的值就发送done。然后我们再修改一下main函数:
done := make(chan struct{}, 2) out := merge(done, c1, c2) fmt.Println(<-out) done <- struct{}{} done <- struct{}{}当out只被输出一次的时候,此时循环还剩两次(总共三次,因为merge函数的参数有三个通道,会循环三次),为了避免循环阻塞在out输出的位置,我们给done通道传入了结构体零值,merge函数中那个循环就会放弃发送out值,而去执行done的发送。