Chào mừng bạn đến với bài hướng dẫn 22 trong loạt bài hướng dẫn Golang. Trong trường hợp bạn bỏ lỡ phần bài hướng dẫn trước, bạn có thể đọc nó ở đây.

Trong hướng dẫn trước, chúng ta đã thảo luận về cách đạt được sự tương tranh trong Go bằng cách sử dụng Goroutines. Trong hướng dẫn này, chúng ta sẽ thảo luận về các kênh (sau đây gọi là channels) và cách Goroutines giao tiếp bằng cách sử dụng channels.

Channels là gì
Channels có thể được coi là các đường ống sử dụng mà Goroutines giao tiếp. Tương tự như cách nước chảy từ đầu này sang đầu kia trong đường ống, dữ liệu có thể được gửi từ một đầu và nhận từ đầu kia bằng channels.

Khai báo channel
Mỗi channel có một loại liên kết với nó. Loại này là loại dữ liệu mà channel được phép vận chuyển. Không có loại khác được phép vận chuyển bằng cách sử dụng channel.

chan T là một channel loại T

Giá trị 0 của một channel là không. Channels nil không được sử dụng và do đó channel phải được xác định bằng cách sử dụng make tương tự như map và slice.

Ví dụ:

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)
    }
}

Run in playground

Channel a khai báo chưa có giá trị. Do đó, các câu lệnh bên trong điều kiện if được thực thi và channel được xác định. a trong chương trình trên là channel int. Chương trình này sẽ xuất ra,

channel a is nil, going to define it  
Type of a is chan int 

Như thường lệ, khai báo  cũng là một cách hợp lệ và ngắn gọn để xác định channel.

a := make(chan int)  

Dòng mã trên cũng xác định channel int a.

Gửi và nhận từ channel
Cú pháp gửi và nhận dữ liệu từ channel được đưa ra dưới đây,

data := <- a // đọc từ kênh a  
a <- data // gửi từ kênh a 

Hướng của mũi tên đối với channel chỉ định liệu dữ liệu được gửi hay nhận.

Trong dòng đầu tiên, mũi tên chỉ ra từ a và do đó chúng ta đang đọc từ channel a và lưu trữ giá trị vào biến data.

Trong dòng thứ hai, mũi tên chỉ về phía a và do đó chúng ta đang ghi vào channel a.

Gửi và nhận được chặn theo mặc định
Gửi và nhận đến một channel đang bị chặn theo mặc định. Điều đó có nghĩa là gì? Khi dữ liệu được gửi đến một channel, điều khiển sẽ bị chặn trong câu lệnh gửi cho đến khi một số Goroutine khác đọc từ channel đó. Tương tự khi dữ liệu được đọc từ một channel, việc đọc bị chặn cho đến khi một số Goroutine ghi dữ liệu vào channel đó.

Thuộc tính này của channels là thứ giúp Goroutines giao tiếp hiệu quả mà không cần sử dụng khóa rõ ràng hoặc các biến có điều kiện khá phổ biến trong các ngôn ngữ lập trình khác.

Ví dụ chương trình sử dụng channel
Chúng ta viết một chương trình để hiểu cách Goroutines giao tiếp bằng cách sử dụng các kênh.

package main

import (  
    "fmt"
    "time"
)

func hello() {  
    fmt.Println("Hello world goroutine")
}
func main() {  
    go hello()
    time.Sleep(1 * time.Second)
    fmt.Println("main function")
}

Run in playground

Đây là chương trình từ phần hướng dẫn trước Gorountine. 

Chúng tôi sẽ viết lại chương trình trên bằng cách sử dụng channels.

package main

import (  
    "fmt"
)

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")
}

Run in playground

Trong chương trình trên, chúng tôi tạo ra một channel bool  và truyền nó dưới dạng tham số cho Goroutine hello. Tiếp theo đọc dữ liệu từ channel để thực hiện. Câu lệnh này đang bị chặn, điều đó có nghĩa là cho đến khi một số Goroutine ghi dữ liệu vào channel đã thực hiện, trình điều khiển sẽ không chuyển sang câu lệnh tiếp theo. Do đó, điều này giúp loại bỏ sự kéo dài thời gian. Câu lệnh  <-done nhận dữ liệu từ channel đã thực hiện nhưng không sử dụng hoặc lưu trữ dữ liệu đó trong bất kỳ biến nào. Điều này là hoàn toàn có thể.

Khi Goroutine chính bị chặn chờ dữ liệu trên channel đã hoàn thành. Goroutine hello nhận channel này dưới dạng tham số, in Hello world goroutine và sau đó ghi vào channel đã thực hiện. Khi quá trình ghi này hoàn tất, Goroutine chính sẽ nhận dữ liệu từ kênh đã hoàn thành, nó được bỏ chặn và sau đó chức năng chính của văn bản được in.

Chương trình xuất ra

Hello world goroutine  
main function 

Chúng ta sửa đổi chương trình này bằng cách sử dụng Sleep trong Goroutine hello để hiểu rõ hơn về khái niệm chặn này.

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")
}

Run in playground

Chương trình này trước tiên hàm hMain sẽ gọi hello go goroutine. Sau đó, Goroutine hello sẽ được bắt đầu và nó sẽ in hello go routine is going to sleep. Sau khi được in, Goroutine hello sẽ ngủ trong 4 giây và trong thời gian này, Goroutine chính sẽ bị chặn do nó đang chờ dữ liệu từ channel đã thực hiện trong dòng <-done. Sau 4 giây hello go routine awake and going to write to doneh sẽ được in theo sau là dữ liệu nhận chính.

Một ví dụ khác cho channels
Hãy viết thêm một chương trình để hiểu các channel tốt hơn. Chương trình này sẽ in tổng bình phương và hình khối của các chữ số riêng lẻ của một số.

Ví dụ: nếu 123 là đầu vào, thì chương trình này sẽ tính đầu ra là

squares = (1 * 1) + (2 * 2) + (3 * 3) 
cubes = (1 * 1 * 1) + (2 * 2 * 2) + (3 * 3 * 3) 
output = squares + cubes = 50

Chúng tôi sẽ cấu trúc chương trình sao cho các hình vuông được tính theo một Goroutine riêng, các hình khối trong một Goroutine khác và tổng kết cuối cùng xảy ra trong Goroutine chính.

package main

import (  
    "fmt"
)

func calcSquares(number int, squareop chan int) {  
    sum := 0
    for number != 0 {
        digit := number % 10
        sum += digit * digit
        number /= 10
    }
    squareop <- sum
}

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)
}

Run in playground

Hàm calcSquares tính tổng bình phương của các chữ số riêng lẻ của số và gửi nó đến channel squaeop. Tương tự hàm calcCubes tính tổng các khối của các chữ số riêng lẻ của số và gửi nó đến channel cubeop.

Hai chức năng này được chạy dưới dạng Goroutines riêng biệt và mỗi channel được truyền vào một channel để ghi vào tham số. Goroutine chính chờ dữ liệu từ cả hai channel này trong. Một khi dữ liệu được nhận từ cả hai channel, chúng được lưu trữ trong các biến squaeop và cubeop và đầu ra cuối cùng được tính toán và in. Chương trình này sẽ in

Final output 1536 

Khoá chết
Một yếu tố quan trọng để xem xét trong khi sử dụng các channel là khoá chết. Nếu một Goroutine đang gửi dữ liệu trên một channel, thì dự kiến một số Goroutine khác sẽ nhận được dữ liệu. Nếu điều này không xảy ra, thì chương trình sẽ lỗi khi chạy với Deadlock.

Tương tự nếu một Goroutine đang chờ để nhận dữ liệu từ một channel, thì một số Goroutine khác dự kiến sẽ ghi dữ liệu trên channel đó, nếu không chương trình sẽ báo lỗi.

package main


func main() {  
    ch := make(chan int)
    ch <- 5
}

Run in playground

Trong chương trình trên, một channel ch được tạo và chúng tôi gửi 5 đến channel ở dòng ch <- 5. Trong chương trình này, không có Goroutine nào khác nhận dữ liệu từ kênh ch. Do đó chương trình này sẽ báo lỗi với lỗi thời gian chạy sau đây.

fatal error: all goroutines are asleep - deadlock!

goroutine 1 [chan send]:  
main.main()  
    /tmp/sandbox249677995/main.go:6 +0x80

Channel một chiều
Tất cả channels chúng ta đã thảo luận là các kênh hai chiều, đó là dữ liệu có thể được gửi và nhận trên chúng. Cũng có thể tạo các channel một chiều, đó là các channel chỉ gửi hoặc nhận dữ liệu.

package main

import "fmt"

func sendData(sendch chan<- int) {  
    sendch <- 10
}

func main() {  
    sendch := make(chan<- int)
    go sendData(sendch)
    fmt.Println(<-sendch)
}

Run in playground

Trong chương trình trên, chúng ta tạo chỉ gửi kênh gửi, chan <- int biểu thị một channel chỉ gửi khi mũi tên đang chỉ vào chan. Chúng ta cố gắng nhận dữ liệu từ một channel chỉ gửi. Điều này không được phép và khi chương trình được chạy, trình biên dịch sẽ lỗi,

prog.go:12:17: invalid operation: <-sendch (receive from send-only type chan<- int)
package main

import "fmt"

func sendData(sendch chan<- int) {  
    sendch <- 10
}

func main() {  
    chnl := make(chan int)
    go sendData(chnl)
    fmt.Println(<-chnl)
}

Run in playground

Trong chương trình trên, một channel chnl hai chiều được tạo. Nó được truyền dưới dạng tham số cho sendData Goroutine. Hàm sendData chuyển đổi channel này thành kênh chỉ gửi trong tham số sendch chan <- int. Vì vậy, bây giờ channel chỉ được gửi bên trong sendData Goroutine nhưng nó là hai chiều trong Goroutine chính. Chương trình này sẽ in 10 là đầu ra.

Đóng channel và chạy các vòng lặp trên các channel
Người gửi có khả năng đóng channel để thông báo cho người nhận rằng sẽ không có thêm dữ liệu nào được gửi trên channel.

Người nhận có thể sử dụng một biến bổ sung trong khi nhận dữ liệu từ channel để kiểm tra xem channel đã được đóng chưa.

v, ok := <- ch  

Trong câu lệnh trên ok là đúng nếu giá trị được nhận bởi thao tác gửi thành công tới channel. Nếu ok là sai, điều đó có nghĩa là chúng ta đang đọc từ một channel kín. Giá trị được đọc từ một channel kín sẽ là giá trị 0 của loại channel. Ví dụ: nếu channel là channel int, thì giá trị nhận được từ channel đóng sẽ là 0.

Trong chương trình trên, Goroutine ghi 0 đến 9 vào kênh chnl và sau đó đóng channel. Hàm chính có một vòng lặp vô hạn để kiểm tra xem channel có được đóng hay không bằng cách sử dụng biến ok trong dòng số. Nếu ok là sai, điều đó có nghĩa là channel bị đóng và do đó vòng lặp bị hỏng. Khác giá trị nhận được và giá trị ok được in. Chương trình này in,

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 

Dạng cho phạm vi của vòng lặp for có thể được sử dụng để nhận các giá trị từ một channel cho đến khi nó được đóng lại.

Viết lại chương trình ở trên bằng cách sử dụng vòng lặp.

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 := range ch {
        fmt.Println("Received ",v)
    }
}

Run in playground

Vòng lặp nhận dữ liệu từ channel ch cho đến khi nó được đóng lại. Khi ch được đóng, vòng lặp sẽ tự động thoát. Chương trình này đầu ra,

Received  0  
Received  1  
Received  2  
Received  3  
Received  4  
Received  5  
Received  6  
Received  7  
Received  8  
Received  9  

Nếu bạn xem kỹ chương trình, bạn có thể nhận thấy mã tìm các chữ số riêng lẻ của một số được lặp lại trong cả hàm calcSquares và hàm calcCubes. Chúng ta sẽ di chuyển mã đó đến chức năng riêng của nó và gọi nó đồng thời.

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)
}

Run in playground

Hàm chữ số trong chương trình trên bây giờ chứa logic để lấy các chữ số riêng lẻ từ một số và nó được gọi bởi cả hai hàm calcSquares và calcCubes đồng thời. Khi không còn chữ số nào nữa, channel sẽ được đóng. Các Goroutines calcSquares và calcCubes lắng nghe trên các channel tương ứng của chúng bằng cách sử dụng vòng lặp for cho đến khi nó được đóng lại. Phần còn lại của chương trình là như nhau. Chương trình này cũng sẽ in

Final output 1536 

Đến đây chúng ta đã tìm hiểu xong về Channels, hy vọng bạn sẽ thích và vui lòng để lại phản hồi ở phía dưới nhé!

Mời các bạn xem bài hướng dẫn tiếp theo: Phần 23 - Bufered Channels and Worker Pools 

Nguồn: https://golangbot.com/learn-golang-series/