From 9d9ce603e8495f886006363d29dc8699f4e4c0bd Mon Sep 17 00:00:00 2001 From: Tinywan <756684177@qq.com> Date: Tue, 21 Aug 2018 10:45:05 +0800 Subject: [PATCH] =?UTF-8?q?22=20-=20=E4=BF=A1=E9=81=93?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Tinywan <756684177@qq.com> --- README.md | 2 +- demo/channel/deadlock.go | 6 + demo/channel/demo01.go | 19 +- demo/channel/goroutines.go | 27 ++ demo/channel/range.go | 29 ++ demo/channel/single_chan.go | 18 ++ demo/channel/sleep_goroutines.go | 21 ++ demo/channel/squares.go | 36 +++ demo/string/demo01.go | 40 +++ demo/string/range.go | 16 ++ docs/golang_tutorial_18-1.md | 459 +------------------------------ docs/golang_tutorial_22.md | 412 +++++++++++++++++++++++---- 12 files changed, 565 insertions(+), 520 deletions(-) create mode 100644 demo/channel/deadlock.go create mode 100644 demo/channel/goroutines.go create mode 100644 demo/channel/range.go create mode 100644 demo/channel/single_chan.go create mode 100644 demo/channel/sleep_goroutines.go create mode 100644 demo/channel/squares.go create mode 100644 demo/string/demo01.go create mode 100644 demo/string/range.go diff --git a/README.md b/README.md index 6f6dc53..5e3176b 100644 --- a/README.md +++ b/README.md @@ -56,7 +56,7 @@ $ go get github.com/Tinywan/golang-tutorial ### 20 - 并发介绍 ### [ 21 - 协程](/docs/golang_tutorial_21.md) -### 22 - 管道 +### [22 - 信道](/docs/golang_tutorial_22.md) ### 23 - 缓冲信道和工作池 ### 24 - Select ### 25 - Mutex diff --git a/demo/channel/deadlock.go b/demo/channel/deadlock.go new file mode 100644 index 0000000..d6510ca --- /dev/null +++ b/demo/channel/deadlock.go @@ -0,0 +1,6 @@ +package main + +func main() { + m := make(chan int) + m <- 5 +} diff --git a/demo/channel/demo01.go b/demo/channel/demo01.go index cd5e00b..51210e4 100644 --- a/demo/channel/demo01.go +++ b/demo/channel/demo01.go @@ -5,17 +5,14 @@ import ( ) func main() { - var m map[string]string - if m == nil { - fmt.Println("this is nil map") + var c chan int // 信道 c 的值为 nil + if c == nil { + fmt.Println("channel a is nil, going to define it") + c = make(chan int) + fmt.Printf("Type of c is %T", c) // 打印通道类型 } - m = make(map[string]string) - m["name"] = "Tinywan" - fmt.Println(m) - m1 := map[string]int{} - fmt.Println(m1) - m1["age"] = 24 - m1["dateTime"] = 20180909 - fmt.Println(m1) + fmt.Printf("\n") + a := make(chan int) + fmt.Printf("%T", a) // chan int } diff --git a/demo/channel/goroutines.go b/demo/channel/goroutines.go new file mode 100644 index 0000000..31f8338 --- /dev/null +++ b/demo/channel/goroutines.go @@ -0,0 +1,27 @@ +package main + +import ( + "fmt" +) + +func hello(done chan bool) { + fmt.Println("This is hello goroutines") + done <- true +} + +func getName(name chan string) { + fmt.Println("This is getName goroutines") + name <- "Tinywan" +} + +func main() { + done := make(chan bool) + go hello(done) + <-done + + name := make(chan string) + go getName(name) + fmt.Println(<-name) + + fmt.Println("This is main function") +} diff --git a/demo/channel/range.go b/demo/channel/range.go new file mode 100644 index 0000000..4fcc2b9 --- /dev/null +++ b/demo/channel/range.go @@ -0,0 +1,29 @@ +package main + +import ( + "fmt" +) + +func producer(num chan int) { + for i := 0; i < 10; i++ { + num <- i + } + close(num) +} + +func main() { + ch := make(chan int) + go producer(ch) + + // for { + // v, ok := <-ch + // if ok == false { // 如果 ok 是 false 表示信道已经被关闭 + // break // 通过 break 退出循环 + // } + // fmt.Println("Received", v, ok) + // } + + for v := range ch { + fmt.Println("Received ", v) + } +} diff --git a/demo/channel/single_chan.go b/demo/channel/single_chan.go new file mode 100644 index 0000000..9f27c33 --- /dev/null +++ b/demo/channel/single_chan.go @@ -0,0 +1,18 @@ +package main + +import () +import "fmt" + +func sendData(d chan<- int) { + d <- 5 +} + +func main() { + d := make(chan<- int) + go sendData(d) + //fmt.Println(<-d) //试图从一个只写信道中接收数据 + + m := make(chan int) // 将双向信道转换为只写或只读信道 + go sendData(m) + fmt.Println(<-m) +} diff --git a/demo/channel/sleep_goroutines.go b/demo/channel/sleep_goroutines.go new file mode 100644 index 0000000..bab1294 --- /dev/null +++ b/demo/channel/sleep_goroutines.go @@ -0,0 +1,21 @@ +package main + +import ( + "fmt" + "time" +) + +func hello(done chan bool) { + fmt.Println("hello go routine is going to sleep") + time.Sleep(4 * time.Second) + fmt.Println("hello go routine awake and going to write to done") + done <- true +} + +func main() { + done := make(chan bool) + fmt.Println("Main going to call hello go goroutine") + go hello(done) + <-done + fmt.Println("Main received data") +} diff --git a/demo/channel/squares.go b/demo/channel/squares.go new file mode 100644 index 0000000..72fb8f6 --- /dev/null +++ b/demo/channel/squares.go @@ -0,0 +1,36 @@ +package main + +import ( + "fmt" +) + +// 计算 number 每一位的平方和 +func calcSquares(number int, squareop chan int) { + sum := 0 + for number != 0 { + digit := number % 10 + sum += digit * digit + number /= 10 + } + squareop <- sum // 将结果发送给信道 squareop +} + +func calcCubes(number int, cubeop chan int) { + sum := 0 + for number != 0 { + digit := number % 10 + sum += digit * digit * digit + number /= 10 + } + cubeop <- sum +} + +func main() { + number := 598 + squareop := make(chan int) + cubeop := make(chan int) + go calcSquares(number, squareop) + go calcCubes(number, cubeop) + squares, cubes := <-squareop, <-cubeop + fmt.Println("Final output", squares+cubes) +} diff --git a/demo/string/demo01.go b/demo/string/demo01.go new file mode 100644 index 0000000..0fccb4b --- /dev/null +++ b/demo/string/demo01.go @@ -0,0 +1,40 @@ +package main + +import ( + "fmt" +) + +// 打印字符串中的字节 +func printBytes(s string) { + for i := 0; i < len(s); i++ { + fmt.Printf("%x ", s[i]) + } +} + +// 打印字符串中的字符 +func printChars(s string) { + for i := 0; i < len(s); i++ { + fmt.Printf("%c", s[i]) + } +} + +func main() { + name := "Hello World" + printBytes(name) + fmt.Printf("\n") + printChars(name) + + fmt.Printf("\n") + // d + name = "Señor" + printBytes(name) + fmt.Printf("\n") + printChars(name) + + fmt.Printf("\n") + name = "萬少波" + printBytes(name) + fmt.Printf("\n") + printChars(name) + +} diff --git a/demo/string/range.go b/demo/string/range.go new file mode 100644 index 0000000..c46f5de --- /dev/null +++ b/demo/string/range.go @@ -0,0 +1,16 @@ +package main + +import ( + "fmt" +) + +func printCharsAndBytes(s string) { + for index, rune := range s { + fmt.Printf("%c starts at byte %d\n", rune, index) + } +} + +func main() { + name := "Señor" + printCharsAndBytes(name) +} diff --git a/docs/golang_tutorial_18-1.md b/docs/golang_tutorial_18-1.md index 2ce93dc..b6f14d4 100644 --- a/docs/golang_tutorial_18-1.md +++ b/docs/golang_tutorial_18-1.md @@ -1,5 +1,5 @@ -17 - 方法 +18-1 - 接口1 ======================== 上一节:[第十六篇 结构体](/docs/golang_tutorial_16.md) @@ -17,460 +17,3 @@ func (t Type) methodName(parameter list) { } ``` - -上面的代码片段创建了一个名为 `methodName` 的方法,该方法有一个类型为 Type 的接收者。 - -## 案例 - -让我们编写一个简单的程序,它创建一个结构体类型的方法并调用它。 - -```golang -package main - -import ( - "fmt" -) - -type Employee struct { - name string - salary int - currency string -} - -/* - displaySalary() method has Employee as the receiver type -*/ -func (e Employee) displaySalary() { - fmt.Printf("Salary of %s is %s%d", e.name, e.currency, e.salary) -} - -func main() { - emp1 := Employee { - name: "Sam Adolf", - salary: 5000, - currency: "$", - } - emp1.displaySalary() //Calling displaySalary() method of Employee type -} -``` -[在 Playground 中运行](https://play.golang.org/p/3khFtFJdbee) - -上面程序的第 6 行,我们为 `Employee` 创建了一个名为 `displaySalary` 的方法。在 `displaySalary()` 方法内部可以访问它的接收者 `e` (类型为 `Employee`)。在第 17 行,我们使用接收者 `e`,并打印它的 `name`,`currency` 以及 `salary`。 - -程序的输出为: - -```golang -Salary of Sam Adolf is $5000 -``` - -## 为什么使用方法而不是函数? - -上面的程序可以使用函数而不是方法重写如下: - -```golang -package main - -import ( - "fmt" -) - -type Employee struct { - name string - salary int - currency string -} - -/* - displaySalary() method converted to function with Employee as parameter -*/ -func displaySalary(e Employee) { - fmt.Printf("Salary of %s is %s%d", e.name, e.currency, e.salary) -} - -func main() { - emp1 := Employee{ - name: "Sam Adolf", - salary: 5000, - currency: "$", - } - displaySalary(emp1) -} -``` - -[在 Playground 中运行](https://play.golang.org/p/xKqdal-DqHT) - -在上面的程序中,我们使用 `displaySalary` 函数替换了方法,并将 `Employee` 结构体作为参数传给它。该程序的输出与上面的程序输出一样:`Salary of Sam Adolf is $5000`。 - -那么为什么我们可以用函数完成同样的工作,却还要使用方法呢?这里有几个原因,我们一个一个地看。 - -* [Go 不是一个纯面向对象的编程语言](https://golang.org/doc/faq#Is_Go_an_object-oriented_language),它不支持 `class` 类型。因此通过在一个类型上建立方法来实现与 `class` 相似的行为。 -* 同名方法可以定义在不同的类型上,但是 Go 不允许同名函数。假设我们有一个 `Square` 和 `Circle` 两个结构体。在 `Square` 和 `Circle` 上定义同名的方法是合法的,比如下面的程序: - -```golang -package main - -import ( - "fmt" - "math" -) - -type Rectangle struct { - length int - width int -} - -type Circle struct { - radius float64 -} - -func (r Rectangle) Area() int { - return r.length * r.width -} - -func (c Circle) Area() float64 { - return math.Pi * c.radius * c.radius -} - -func main() { - r := Rectangle{ - length: 10, - width: 5, - } - fmt.Printf("Area of rectangle %d\n", r.Area()) - c := Circle{ - radius: 12, - } - fmt.Printf("Area of circle %f", c.Area()) -} -``` - -程序的输出为: -```golang -Area of rectangle 50 -Area of circle 452.389342 -``` - -接口正是应用了这一点(译者注:*相同的方法名可以用在不同的接收者类型上*)。我们将在下面的教程中讨论接口的细节。 - -## 指针接收者 vs. 值接收者 - -目前为止我们看到的都是以值作为接收者。以指针为接收者也是可以的。两者的区别在于,以指针作为接收者时,方法内部对其的修改对于调用者是可见的,但是以值作为接收者却不是。让我们通过下面的程序帮助理解。 - -```golang -package main - -import ( - "fmt" -) - -type Employee struct { - name string - age int -} - -/* -Method with value receiver -*/ -func (e Employee) changeName(newName string) { - e.name = newName -} - -/* -Method with pointer receiver -*/ -func (e *Employee) changeAge(newAge int) { - e.age = newAge -} - -func main() { - e := Employee{ - name: "Mark Andrew", - age: 50, - } - fmt.Printf("Employee name before change: %s", e.name) - e.changeName("Michael Andrew") - fmt.Printf("\nEmployee name after change: %s", e.name) - - fmt.Printf("\n\nEmployee age before change: %d", e.age) - (&e).changeAge(51) - fmt.Printf("\nEmployee age after change: %d", e.age) -} -``` - -上面的程序中, `changeName` 方法有一个值接收者 (`e Employee`),而 `changeAge` 方法有一个指针接收者 (`e *Employee`)。在 `changeName` 中改变 `Employee` 的字段 `name` 的值对调用者而言是不可见的,因此程序在调用 `e.changeName("Michael Andrew")` 方法之前和之后,打印的 `name` 是一致的。而因为 `changeAge` 的接受者是一个指针 (`e *Employee`),因此通过调用方法 `(&e).changeAge(51)` 来修改 `age` 对于调用者是可见的。 程序的输出如下: - -```golang -Employee name before change: Mark Andrew -Employee name after change: Mark Andrew - -Employee age before change: 50 -Employee age after change: 51 -``` - -在上面的程序第36行,我们用 `(&e).changeAge(51)` 来调用 `changeAge` 方法。因为 `changeAge` 有一个指针类型的接收者我们必须使用 `(&e)` 来调用。这不是必须的,Go允许我们省略 `&` 符号,因此可以只写为 `e.changeAge(51)`。Go 将 `e.changeAge(51)` 解析为 `(&e).changeAge(51)`。 - -下面的程序使用 `e.changeAge(51)` 来替代 `(&e).changeAge(51)`。它与上面的程序的打印结果是一样的。 - -程序的输出为: - -```golang -package main - -import ( - "fmt" -) - -type Employee struct { - name string - age int -} - -/* -Method with value receiver -*/ -func (e Employee) changeName(newName string) { - e.name = newName -} - -/* -Method with pointer receiver -*/ -func (e *Employee) changeAge(newAge int) { - e.age = newAge -} - -func main() { - e := Employee{ - name: "Mark Andrew", - age: 50, - } - fmt.Printf("Employee name before change: %s", e.name) - e.changeName("Michael Andrew") - fmt.Printf("\nEmployee name after change: %s", e.name) - - fmt.Printf("\n\nEmployee age before change: %d", e.age) - e.changeAge(51) - fmt.Printf("\nEmployee age after change: %d", e.age) -} -``` - -## 何时使用指针接收者,何时使用值接收者? - -一般来讲,指针接收者可用于对接收者的修改应该对调用者可以见的场合。 - -指针接收者也可用于拷贝结构体代价较大的场合。考虑一个包含较多字段的结构体,若使用值作为接收者则必须拷贝整个结构体,这样的代价很大。这种情况下使用指针接收者将避免结构体的拷贝,而仅仅是指向结构体指针的拷贝。 - -其他情况下可以使用值接收者。 - -## 匿名字段函数 - -匿名字段的方法可以被包含该匿名字段的结构体的变量调用,就好像该匿名字段的方法属于包含该字段的结构体一样。 - -```golang -package main - -import ( - "fmt" -) - -type address struct { - city string - state string -} - -func (a address) fullAddress() { - fmt.Printf("Full address: %s, %s", a.city, a.state) -} - -type person struct { - firstName string - lastName string - address -} - -func main() { - p := person{ - firstName: "Elon", - lastName: "Musk", - address: address { - city: "Los Angeles", - state: "California", - }, - } - p.fullAddress() //accessing fullAddress method of address struct -} -``` - -在上面的程序中,第32行,我们通过 `p.fullAddress()` 调用了 `address` 的方法 `fullAddress()`。像 `p.address.fullAddress()` 这样的直接调用是不必要的。程序的输出为: - -```golang -Full address: Los Angeles, California -``` - -## 方法的值接收者 vs. 函数的值参数 - -这是很多新手遇到的问题。我们将尽可能把它说明白。 - -当一个函数有一个值参数时,它只接受一个值参数。 - -当一个方法有一个值接收者时,它可以接受值和指针接收者。 - -让我们通过程序说明这一点。 - -```golang -package main - -import ( - "fmt" -) - -type rectangle struct { - length int - width int -} - -func area(r rectangle) { - fmt.Printf("Area Function result: %d\n", (r.length * r.width)) -} - -func (r rectangle) area() { - fmt.Printf("Area Method result: %d\n", (r.length * r.width)) -} - -func main() { - r := rectangle{ - length: 10, - width: 5, - } - area(r) - r.area() - - p := &r - /* - compilation error, cannot use p (type *rectangle) as type rectangle - in argument to area - */ - //area(p) - - p.area()//calling value receiver with a pointer -} -``` - -第12行,函数 `func area(r rectangle)` 接受一个值参数,而方法 `func (r rectangle) area()` 接受一个值接收者。 - -在第25行,我们传递了一个值来调用 `area` 函数 `area(r)`,它将工作。同样地,我们通过值接收者调用 `area` 方法 `r.area()` 它也可以工作。 - -在第28行,我们创建了一个指向 `r` 的指针 `p`。如果我们试图将这个指针传递给只接受值的 area 函数那么编译器将报错:`compilation error, cannot use p (type *rectangle) as type rectangle in argument to area`.。这是我们预期的。 - -现在来到了微妙的地方,第35行 `p.area()` 使用指针接收者 `p` 调用了接受一个值接收者的方法 `area` 。这是完全合法的。原因是对于 `p.area()`,Go 将其解析为 `(&p).area()`,因为 `area` 方法必须接受一个值接收者。这是为了方便 Go 给我们提供的语法糖。 - -程序的输出为: - -```golang -Area Function result: 50 -Area Method result: 50 -Area Method result: 50 -``` - -## 方法的指针接收者 vs. 函数的指针参数 - -与值参数相似,一个接受指针参数的函数只能接受指针,而一个接收者为指针的方法可以接受值接收者和指针接收者。 - -```golang -package main - -import ( - "fmt" -) - -type rectangle struct { - length int - width int -} - -func perimeter(r *rectangle) { - fmt.Println("perimeter function output:", 2*(r.length+r.width)) - -} - -func (r *rectangle) perimeter() { - fmt.Println("perimeter method output:", 2*(r.length+r.width)) -} - -func main() { - r := rectangle{ - length: 10, - width: 5, - } - p := &r //pointer to r - perimeter(p) - p.perimeter() - - /* - cannot use r (type rectangle) as type *rectangle in argument to perimeter - */ - //perimeter(r) - - r.perimeter()//calling pointer receiver with a value -} -``` - -在上面的程序中,第12行定义了一个函数 `perimeter`,该函数接受一个指针作为参数,而17行定义了一个方法,有一个指针接收者。 - -27行我们通过指针参数调用 `perimeter` 函数,在第28行我们通过一个指针接收者调用 `perimeter` 方法。一切都好。 - -在被注释掉的第33行,我们试图通以一个值 `r` 调用 `perimeter` 函数。这是非法的,因为一个接受指针为参数的函数不能接受一个值作为参数。如果去掉注释运行程序,则编译将报错:`main.go:33: cannot use r (type rectangle) as type *rectangle in argument to perimeter.`。 - -在第35行我们通过一个值接收者 `r` 调用接受一个指针接收者的 `perimeter` 方法。这是合法的,`r.perimeter()` 这一行将被 Go 解析为 `(&r).perimeter()`。这是为了方便 Go 给我们提供的语法糖。程序的输出为: - -```golang -perimeter function output: 30 -perimeter method output: 30 -perimeter method output: 30 -``` - -## 定义非结构体类型的方法 - -现在我们定义的都是结构体类型的方法。同样可以定义非结构体类型的方法,不过这里需要注意一点。为了定义某个类型的方法,接收者类型的定义与方法的定义必须在同一个包中。目前为止,我们定义的结构体和相应的方法都是在`main`包中的,因此没有任何问题。 - -```golang -package main - -func (a int) add(b int) { -} - -func main() { - -} -``` - -在上面的程序中,第3行我们试图添加一个方法 `add` 给内置类型 `int`。这是不允许的,因为定义方法 add 所在的包和定义类型 `int` 的包不是同一个包。这个程序将会报编译错误:`cannot define new methods on non-local type int`。 - -使其工作的方法为定义内置类型的别名,然后以这个新类型为接收者创建方法。 - -```golang -package main - -import "fmt" - -type myInt int - -func (a myInt) add(b myInt) myInt { - return a + b -} - -func main() { - num1 := myInt(5) - num2 := myInt(10) - sum := num1.add(num2) - fmt.Println("Sum is", sum) -} -``` - -上面的程序中,第5行,我们创建了新的类型,一个 `int` 的别名 `myInt`。在第7行,我们定义了一个方法 add,以 `myInt` 作为接收者。 - -程序的输出为: `Sum is 15`。 - -我已经创建了一个程序,其中包含了目前为止所讨论的所有概念,可以在 github 上找到它。 - -希望你喜欢阅读。请留下宝贵的意见和反馈:) \ No newline at end of file diff --git a/docs/golang_tutorial_22.md b/docs/golang_tutorial_22.md index cda1f43..81bbe92 100644 --- a/docs/golang_tutorial_22.md +++ b/docs/golang_tutorial_22.md @@ -1,34 +1,90 @@ -21 - 协程 +22 - 信道 ======================== 上一节:[第十六篇 结构体](/docs/golang_tutorial_16.md) 下一节:[第十八篇 接口一](/docs/golang_tutorial_18.md) -这是本Golang系列教程的第21篇。 +这是本Golang系列教程的第22篇。 -在上一篇教程中,我们讨论了并发,以及并发和并行的区别。在这篇教程中我们将讨论在Go中如何通过Go协程实现并发。 +在[上一篇教程](http://blog.csdn.net/u011304970/article/details/75096323)中,我们讨论了如何使用协程实现并发。在这篇教程中,我们将讨论信道以及如何使用信道实现协程间通信。 -## 什么是协程? +## 什么是信道? -Go协程(`Goroutine`)是与其他[函数](/docs/golang_tutorial_6.md)或[方法](/docs/golang_tutorial_17.md)同时运行的函数或方法。可以认为Go协程是轻量级的线程。与创建线程相比,创建Go协程的成本很小。因此在Go中同时运行上千个协程是很常见的。 +信道`(Channel)`可以被认为是协程之间通信的管道。与水流从管道的一端流向另一端一样,数据可以从信道的一端发送并在另一端接收。 -## Go协程对比线程的优点 +## 声明信道 -* 与线程相比,Go协程的开销非常小。Go协程的堆栈大小只有几kb,它可以根据应用程序的需要而增长和缩小,而线程必须指定堆栈的大小,并且堆栈的大小是固定的。 -* Go协程被多路复用到较少的OS线程。在一个程序中数千个Go协程可能只运行在一个线程中。如果该线程中的任何一个Go协程阻塞(比如等待用户输入),那么Go会创建一个新的OS线程并将其余的Go协程移动到这个新的OS线程。所有这些操作都是 runtime 来完成的,而我们程序员不必关心这些复杂的细节,只需要利用 Go 提供的简洁的 API 来处理并发就可以了。 -* Go 协程之间通过信道(`channel`)进行通信。信道可以防止多个协程访问共享内存时发生竟险(race condition)。信道可以想象成多个协程之间通信的管道。我们将在下一篇教程中介绍信道。 +每个信道都有一个与之关联的类型。此类型是允许信道传输的数据类型,除此类型外不能通过信道传输其他类型。 -## 如何创建一个协程? +`chan T` 是一个 `T` 类型的信道。 -在函数或方法调用之前加上关键字 `go`,这样便开启了一个并发的Go协程。 +信道的 `0` 值为 `nil`。值为 `nil` 的信道变量没有任何用处,我们需要通过内置函数 `make` 来创建一个信道,就像创建 `map` 和 `slice` 一样。 + +下面的代码声明了一个信道: + +```golang +package main + +import "fmt" + +func main() { + var a chan int + if a == nil { + fmt.Println("channel a is nil, going to define it") + a = make(chan int) + fmt.Printf("Type of a is %T", a) + } +} +``` +因为信道的 `0` 值为 `nil`,因此第 6 行声明的信道 `a` 的值为 `nil`。因此执行 `if` 里面的语句创建信道。上面的程序中 `a` 是一个 `int` 类型的信道。程序的输出为: + +```golang +channel a is nil, going to define it +Type of a is chan int +``` +像往常一样,速记声明也是定义信道的一种有效而简洁的方式: +```golang +a := make(chan int) +``` +上面的这行代码同样定义了一个 `int` 型的**信道**。 + +## 通过信道发送和接收数据 + +通过信道发送和接收数据的语法如下: + +```golang +data := <- a // read from channel a +a <- data // write to channel a +``` +箭头的指向说明了数据是发送还是接收。 + +在第一行,**箭头**的方向是从 `a` 向外指,因此我们正在从信道 `a` 中读取数据并将读取的值赋值给变量 `data` 。 + +在第二行,**箭头**的方式是指向 `a` ,因此我们正在向信道 `a` 中写入数据。 + +## 发送和接收默认是阻塞的 + +通过信道发送和接收数据默认是阻塞的。这是什么意思呢?当数据发送给信道后,程序流程在发送语句处阻塞,直到其他协程从该信道中读取数据。 + +同样地,当从信道读取数据时,程序在读取语句处阻塞,直到其他协程发送数据给该信道。 + +信道的这种特性使得协程间通信变得高效,而不是向其他编程语言一样,显式的使用锁和条件变量来达到此目的。 + +## 信道的一个例子 + +理论到此为止:) 让我们通过一个程序来理解协程之间如何使用信道进行通信。 + +我们将用信道来重写在上一篇教程协程中的一个例子。 + +如下是那篇教程中的一个例子: -让我们创建一个协程: ```golang package main import ( "fmt" + "time" ) func hello() { @@ -36,24 +92,48 @@ func hello() { } func main() { go hello() + time.Sleep(1 * time.Second) fmt.Println("main function") } ``` -第11行,`go hello()` 开启了一个新的协程。现在 `hello()` 函数将和 `main()` 函数一起运行。`main` 函数在单独的协程中运行,这个协程称为主协程。 +这是上一篇教程中的例子,我们通过使用 `Sleep` 来使主协程休眠,以等待 `hello` 协程执行结束。如果你不明白这是为什么,请阅读上一篇教程:`协程` 。 -运行这个程序,你将得到一个惊喜。程序仅输出了一行文本: `main function`。 +我们用信道重写上面的程序,如下: -**我们创建的协程发生了什么?我们需要了解Go协程的两个属性,以了解为什么发生这种情况。** +```golang +package main -* 当创建一个Go协程时,创建这个Go协程的语句立即返回。与函数不同,程序流程不会等待Go协程结束再继续执行。程序流程在开启Go协程后立即返回并开始执行下一行代码,忽略Go协程的任何返回值。 -* 在主协程存在时才能运行其他协程,主协程终止则程序终止,其他协程也将终止。 +import ( + "fmt" +) -我想你已经知道了为什么我们的协程为什么没有运行。在11行调用 `go hello()`后,程序的流程直接调转到下一条语句执行,并没有等待 `hello` 协程退出,然后打印 `main function`。 +func hello(done chan bool) { + fmt.Println("Hello world goroutine") + done <- true +} +func main() { + done := make(chan bool) + go hello(done) + <-done + fmt.Println("main function") +} +``` + +在上面的程序中,我们在第 12 行定义了一个 `bool` 类型的信道 `done`,然后将它作为参数传递给 `hello` 协程。在第 14 行,我们从信道 `done` 中读取数据。程序将在这一行被阻塞直到其他协程向信道 `done` 里写入数据,在未读取到数据之前程序将在这一行一直等待而不会执行下一行语句。因此这里消除了在原程序中使用 `time.Sleep` 来阻止主协程退出的必要。 + +`<-done` 这一行从信道 `done` 中读取数据,但是没有使用该数据,也没有将它赋值给其他变量,这是完全合法的。 -接着主协程结束运行,不会再执行任何代码,因此 `hello` 协程没有得到运行的机会。 +现在我们的 `main` 协程被阻塞,等待从信道 `done` 中读取数据。`hello` 协程接受信道 `done` 作为参数,打印 `Hello world goroutine` 然后将数据写入信道 `done` 中。当写入完毕后,`main` 协程从信道 `done` 中接收到数据,`main` 协程解除阻塞,继续执行下一条语句,打印:`main function`。 -让我们修复这个问题: +程序的输出为: + +```golang +Hello world goroutine +main function +``` + +让我们修改上面程序,在 `hello` 协程中加入一个休眠,来更好的理解阻塞的概念。 ```golang package main @@ -63,67 +143,299 @@ import ( "time" ) -func hello() { - fmt.Println("Hello world goroutine") +func hello(done chan bool) { + fmt.Println("hello go routine is going to sleep") + time.Sleep(4 * time.Second) + fmt.Println("hello go routine awake and going to write to done") + done <- true } func main() { - go hello() - time.Sleep(1 * time.Second) - fmt.Println("main function") + done := make(chan bool) + fmt.Println("Main going to call hello go goroutine") + go hello(done) + <-done + fmt.Println("Main received data") } ``` -上面的程序中,第13行,我们调用 `time` 包的 `Sleep` 函数来使调用该函数所在的协程休眠。在这里是让主协程休眠1秒钟。现在调用 `go hello()` 有了足够的时间得以在主协程退出之前执行。该程序首先打印 `Hello world goroutine`,等待1秒钟之后打印 `main function`。 +在上面程序中的第 10 行,我们在 `hello` 函数中增加了`4` 秒钟的休眠。 + +该程序首先打印 `Main going to call hello go goroutine` 。然后 `hello` 协程开始执行,它将打印 `hello go routine is going to sleep`,然后 `hello` 协程休眠 `4` 秒,在这期间, `main` 协程由于在等待从信道 `done` 中读取数据而始终阻塞(在 `<-done` 这一行)。`4` 秒中之后, `hello` 协程打印:`hello go routine awake and going to write to done`,接着 `main` 协程打印:`Main received data` 。 -在主协程中使用 `Sleep` 函数等待其他协程结束的方法是不正规的,我们用在这里只是为了说明**Go协程**是如何工作的。信道可以用于阻塞主协程,直到其他协程执行完毕。我们将在下一篇教程中讨论信道。 +## 信道的另一个例子 +让我们再写一个例子来更好的理解信道。该程序打印一个数字的每一位的**平方和**与**立方和**,并将平方和与立方和相加得出最后的结果。 -## 开启多个协程 +例如,输入`123` ,程序将做如下计算以得出最后结果: + +```golang +squares = (1 * 1) + (2 * 2) + (3 * 3) +cubes = (1 * 1 * 1) + (2 * 2 * 2) + (3 * 3 * 3) +output = squares + cubes = 49 +``` -让我们写一个程序开启多个协程来更好的理解协程。 +我们将平方和的计算与立方和的计算分别放在一个协程中执行,最后在主协程中将它们的计算结果求和。 ```golang package main import ( "fmt" - "time" ) -func numbers() { - for i := 1; i <= 5; i++ { - time.Sleep(250 * time.Millisecond) - fmt.Printf("%d ", i) +func calcSquares(number int, squareop chan int) { + sum := 0 + for number != 0 { + digit := number % 10 + sum += digit * digit + number /= 10 } + squareop <- sum } -func alphabets() { - for i := 'a'; i <= 'e'; i++ { - time.Sleep(400 * time.Millisecond) - fmt.Printf("%c ", i) + +func calcCubes(number int, cubeop chan int) { + sum := 0 + for number != 0 { + digit := number % 10 + sum += digit * digit * digit + number /= 10 } + cubeop <- sum +} + +func main() { + number := 589 + sqrch := make(chan int) + cubech := make(chan int) + go calcSquares(number, sqrch) + go calcCubes(number, cubech) + squares, cubes := <-sqrch, <-cubech + fmt.Println("Final output", squares + cubes) } +``` + +在第 7 行,函数 `calcSquares` 计算 `number` 每一位的平方和,并将结果发送给信道 `squareop`。 + +同样地,在第 17 行,函数 `calcCubes` 计算 `number` 每一位的立方和,并将结果发送给信道 `cubeop`。 + +这两个函数接受不同的信道作为参数,并分别运行在各自的协程中(第31行和32行),最后将结果写入各自的信道。主协程在第 33 行同时等待这两个信道中的数据。一旦从这两个信道中接收到数据,它们分别被存放在变量 `squares` 和 `cubes` 中,最后将它们的和打印出来。程序的输出为: + +```golang +Final output 1536 +``` +## 死锁 +使用信道是要考虑的一个重要因素是死锁`(Deadlock`)。如果一个协程发送数据给一个信道,而没有其他的协程从该信道中接收数据,那么程序在运行时会遇到死锁,并触发 `panic` 。 + +同样地,如果一个协程从一个信道中接收数据,而没有其他的协程向这个信道发送数据,那么程序同样造成死锁,触发 `panic` 。 + +```golang +package main + func main() { - go numbers() - go alphabets() - time.Sleep(3000 * time.Millisecond) - fmt.Println("main terminated") + ch := make(chan int) + ch <- 5 } ``` -上面的程序在第21和22行开启了两个协程。现在这两个协程同时执行。`numbers` 协程最初睡眠 `250` 毫秒,然后打印 `1`,接着再次睡眠然后打印`2`,以此类推,直到打印到 `5`。类似地,`alphabets` 协程打印从 `a` 到 `e` 的字母,每个字母之间相隔 `400` 毫秒。主协程开启 `numbers` 和 `alphabets` 协程,等待 `3000` 毫秒,最后终止。 +上面的程序中,创建了一个信道 `ch`,并通过 `ch <- 5` 向其中写入 `5` 。这个程序中没有其他协程从 `ch` 中接收数据,因此程序在运行时触发 `panic`,错误如下: -程序的输出为: +```golang +fatal error: all goroutines are asleep - deadlock! + +goroutine 1 [chan send]: +main.main() + /tmp/sandbox249677995/main.go:6 +0x80 +``` + +## 单向信道 +目前我们讨论的信道都是双向信道,数据既可以发送到双向信道,也可以从双向信道中读取。同样也可以创建单向信道,即只能发送数据或只能接收数据的信道。 ```golang -1 a 2 3 b 4 c 5 d e main terminated +package main + +import "fmt" + +func sendData(sendch chan<- int) { + sendch <- 10 +} + +func main() { + sendch := make(chan<- int) + go sendData(sendch) + fmt.Println(<-sendch) +} +``` + +在上面程序中的第 10 行,我们创建了一个只写`(send only)`信道 `sendch` 。`chan<- int` 表示只能发送数据,因为箭头的方向指向 `chan`。在第 12 行,我们试图从一个只写信道中接收数据,这是非法的,程序将无法通过编译,编译器报错如下: + +```golang +main.go:11: invalid operation: <-sendch (receive from send-only type chan<- int) +``` + +一切都很好,但是如果无法读取,创建一个只写通道有什么用呢? + +这就是信道转型的用途。可以将双向信道转换为只写或只读信道,但是反过来却不行。 + +```golang +package main + +import "fmt" + +func sendData(sendch chan<- int) { + sendch <- 10 +} + +func main() { + chnl := make(chan int) + go sendData(chnl) + fmt.Println(<-chnl) +} +``` + +在上面程序中的第 10 行,创建了一个双向信道 `chnl`。在第 11 行它被作为参数传递给协程 `sendData`。第 5 行,`sendData` 函数通过形参 `sendch chan<- int` 将该信道转换成只写信道。因此在 `sendData` 中该信道为只写信道,而在主协程中该信道为双向信道。程序将打印:`10`。 + +## 关闭信道以及使用 range for 遍历信道 +发送者可以关闭信道以通知接收者将不会再发送数据给信道。 + +在从信道接收数据时,接收者可以通过一个额外的变量来检测信道是否已经被关闭。 + +```golang +v, ok := <- ch +``` +上面的语句中 `ok` 返回 `true` 表示成功的接收到了发送的数据,如果 `ok` 返回 `false` 则表示信道已经被关闭。从已关闭的信道中读取到的数据为信道类型的 `0` 值。比如从一个被关闭的 `int` 信道中读取数据,那么将得到 `0` 。 + +```golang +package main + +import ( + "fmt" +) + +func producer(chnl chan int) { + for i := 0; i < 10; i++ { + chnl <- i + } + close(chnl) +} +func main() { + ch := make(chan int) + go producer(ch) + for { + v, ok := <-ch + if ok == false { + break + } + fmt.Println("Received ", v, ok) + } +} +``` + +上面的程序中,协程 `producer` 向信道 `chnl` 中写入 `0` 到 `9` 并关闭该信道。主协程在第 16 行进行无限 `for` 循环,并在循环中检测 `ok` 的值判断信道是否已经被关闭(第 18 行)。如果 `ok` 是 `false` 表示信道已经被关闭,则通过 `break` 退出循环。否则接收数据并打印 `ok` 的值。程序的输出为: + +```golang +Received 0 true +Received 1 true +Received 2 true +Received 3 true +Received 4 true +Received 5 true +Received 6 true +Received 7 true +Received 8 true +Received 9 true ``` +`range for` 可以用来接收一个信道中的数据,直到该信道关闭。 -下面的图片描述了这个程序是如何工作的,请在新的标签中打开图像以获得更好的效果:) +让我们用 `range for` 重写上面的程序: -![Goroutines-explained](../images/Goroutines-explained.png) +```golang +package main -上图中,**蓝色**的线框表示 `numbers` 协程,**栗色**的线框表示 `alphabets` 协程。**绿色**的线框表示主协程。**黑色**的线框合并了上述三个协程,向我们展示了该程序的工作原理。每个框顶部的 `0ms`,`250 ms` 的字符串表示以**毫秒**为单位的时间,在每个框底部的 `1`,`2`,`3` 表示输出。 +import ( + "fmt" +) + +func producer(chnl chan int) { + for i := 0; i < 10; i++ { + chnl <- i + } + close(chnl) +} +func main() { + ch := make(chan int) + go producer(ch) + for v := range ch { + fmt.Println("Received ",v) + } +} +``` +在第6 行,`for range` 不断从信道 `ch` 中接收数据,直到该信道关闭。一旦 `ch` 关闭,循环自动退出。程序的输出如下: +```golang +Received 0 +Received 1 +Received 2 +Received 3 +Received 4 +Received 5 +Received 6 +Received 7 +Received 8 +Received 9 +``` + +如果仔细观察该程序,你可以注意到,在 `calcSquares` 和 `calcCubes` 函数中查找一个数的每一位的代码重复了。我们将这段代码提取到一个单独的函数,并异步调用它。 + +```golang +package main + +import ( + "fmt" +) + +func digits(number int, dchnl chan int) { + for number != 0 { + digit := number % 10 + dchnl <- digit + number /= 10 + } + close(dchnl) +} +func calcSquares(number int, squareop chan int) { + sum := 0 + dch := make(chan int) + go digits(number, dch) + for digit := range dch { + sum += digit * digit + } + squareop <- sum +} + +func calcCubes(number int, cubeop chan int) { + sum := 0 + dch := make(chan int) + go digits(number, dch) + for digit := range dch { + sum += digit * digit * digit + } + cubeop <- sum +} + +func main() { + number := 589 + sqrch := make(chan int) + cubech := make(chan int) + go calcSquares(number, sqrch) + go calcCubes(number, cubech) + squares, cubes := <-sqrch, <-cubech + fmt.Println("Final output", squares+cubes) +} +``` + +在上面的程序中,函数 `digits` 包含查找一个数的每一位的逻辑,该函数在 `calcSquares` 和 `calcCubes` 中异步调用。在第 13 行,当数字中没有更多的位数时,信道被关闭。`calcSquares` 和 `calcCubes` 分别在各自的信道上使用 `range for` 直到信道被关闭。程序中的其他部分都是一样的。程序的输出依然是: + +```golang +Final output 1536 +``` -蓝色的线框告诉我们在 250ms 的时候打印了`1`,在 `500ms` 的时候打印了`2`,以此类推。因此最后一个线框底部的输出:`1 a 2 3 b 4 c 5 d e main terminated` 也是整个程序的输出。上面的图像是很好理解的,您将能够了解该程序的工作原理。 +这就来到了本教程的最后。信道中还有更多的概念,比如缓冲信道,工作池和 `select` 。我们将在单独的教程中讨论它们。感谢阅读,祝你有美好的一天。 -Go协程的介绍就到这里。祝你有美好的一天! 希望你喜欢阅读。请留下宝贵的意见和反馈:) \ No newline at end of file