package main // 堆代码 duidaima.com import ( "fmt" "time" ) func main() { simple() } func simple() { fmt.Println(time.Now(), "0") time.Sleep(time.Second) fmt.Println(time.Now(), "1") time.Sleep(time.Second) fmt.Println(time.Now(), "2") time.Sleep(time.Second) fmt.Println("done") } 2022-08-14 16:22:46.782569233 +0900 KST m=+0.000033220 0 2022-08-14 16:22:47.782728963 +0900 KST m=+1.000193014 1 2022-08-14 16:22:48.782996361 +0900 KST m=+2.000460404 2 done上面的代码打印出当前时间和一个字符串。每条打印语句的运行时间为一秒。总的来说,这段代码大约需要三秒钟的时间来完成。现在让我们把它与一个并发的代码进行比较。
func main() { simpleConc() } func simpleConc() { for i := 0; i < 3; i++ { go func(index int) { fmt.Println(time.Now(), index) }(i) } time.Sleep(time.Second) fmt.Println("done") } 2022-08-14 16:25:14.379416226 +0900 KST m=+0.000049175 2 2022-08-14 16:25:14.379446063 +0900 KST m=+0.000079012 0 2022-08-14 16:25:14.379450313 +0900 KST m=+0.000083272 1 done上面的代码启动了三个goroutines,分别打印当前时间和i。这段代码花了大约一秒钟完成。这比顺序版本快了三倍左右。
func main() { simpleConcFail() } func simpleConcFail() { for i := 0; i < 3; i++ { go func(index int) { fmt.Println(time.Now(), index) }(i) } fmt.Println("done") } done嗯......。程序确实在没有任何慌乱的情况下退出了,但我们缺少来自goroutines的输出。为什么它们被跳过?这是因为在默认情况下,Go并不等待goroutine的完成。你知道main也是在goroutine里面运行的吗?主程序通过调用simpleConcFail来启动工作程序,但它在工作程序完成工作之前就退出了。
ch := make(chan int)每个通道都是强类型的,并且只允许该类型的数据通过。让我们看看我们如何使用这个。
func main() { unbufferedCh() } func unbufferedCh() { ch := make(chan int) go func() { ch <- 1 }() res := <-ch fmt.Println(res) } 1很简单,对吗?我们做了一个名为ch的通道。我们有一个goroutine,向ch发送1,我们接收该数据并将其保存到res。你问,为什么我们在这里需要一个goroutine?因为不这样做会导致死锁。
func main() { unbufferedChFail() } func unbufferedChFail() { ch := make(chan int) ch <- 1 res := <-ch fmt.Println(res) } fatal error: all goroutines are asleep - deadlock!我们碰到了一个新词。什么是死锁?死锁就是你的程序被卡住了。为什么上面的代码会卡在死锁中?为了理解这一点,我们需要知道通道的一个重要特性。我们创建了一个无缓冲的通道,这意味着在某一特定时间内没有任何东西可以被存储在其中。这意味着发送方和接收方都必须同时准备好,才能在通道上传输数据。
func main() { unbufferedCh() } func unbufferedCh() { ch2 := make(chan int) close(ch2) res2 := <-ch2 fmt.Println(res2) } 0关闭通道意味着不能再向它发送数据。我们仍然可以从该通道中接收它。对于未缓冲的通道,从一个关闭的通道接收将返回一个通道类型的零值。
func main() { bufferedCh() } func bufferedCh() { ch := make(chan int, 1) ch <- 1 res := <-ch fmt.Println(res) } 1在这里,1被储存在ch里面,直到我们收到它。很明显,我们不能向一个满了缓冲区的通道发送更多的信息。你需要在缓冲区内有空间才能发送更多。
func main() { bufferedChFail() } func bufferedChFail() { ch := make(chan int, 1) ch <- 1 ch <- 2 res := <-ch fmt.Println(res) } fatal error: all goroutines are asleep - deadlock!你也不能从一个空的缓冲通道接收。
func main() { bufferedChFail2() } func bufferedChFail2() { ch := make(chan int, 1) ch <- 1 res := <-ch res2 := <-ch fmt.Println(res, res2) } fatal error: all goroutines are asleep - deadlock!如果一个通道已满,发送操作将等待,直到有可用的空间。这在这段代码中得到了证明。
func main() { bufferedCh2() } func bufferedCh2() { ch := make(chan int, 1) ch <- 1 go func() { ch <- 2 }() res := <-ch fmt.Println(res) } 1我们接收一次是为了取出1,这样goroutine就可以发送2到通道。我们没有从ch接收两次,所以只接收1。我们也可以从封闭的缓冲通道接收。在这种情况下,我们可以在封闭的通道上设置范围来迭代里面的剩余项目。
func main() { bufferedChRange() } func bufferedChRange() { ch := make(chan int, 3) ch <- 1 ch <- 2 ch <- 3 close(ch) for res := range ch { fmt.Println(res) } // you could also do this // fmt.Println(<-ch) // fmt.Println(<-ch) // fmt.Println(<-ch) } 1 2 3在一个开放的通道上测距将永远不会停止。这意味着在某些时候,通道将是空的,测距循环将试图从一个空的通道接收,从而导致死锁。
4.你可以在一个封闭的通道上进行迭代,以接收缓冲区内的剩余值。
func main() { basicSyncing() } func basicSyncing() { done := make(chan struct{}) go func() { for i := 0; i < 5; i++ { fmt.Printf("%s worker %d start\n", fmt.Sprint(time.Now()), i) time.Sleep(time.Duration(rand.Intn(5)) * time.Second) } close(done) }() <-done fmt.Println("exiting...") }我们做了一个done通道,负责阻断代码,直到goroutine完成。done可以是任何类型,但struct{}经常被用于这些类型的通道。它的目的不是为了传输结构,所以它的类型并不重要。一旦工作完成,worker goroutine 将关闭 done。此时,我们可以从 done 中接收,它将是一个空结构。接收动作解除了代码的阻塞,使其可以退出。