Channel là các đường ống dùng để kết nối giữa các goroutine bên trong ứng dụng dựa trên Go, cho phép trao đổi thông tin và sau đó truyền các giá trị đến và từ các biến. Giống như cách nước chảy từ đầu bên này sang đầu bên kia đường ống, dữ liệu có thể được gửi từ một đầu và được nhận từ đầu bên kia bằng channel. Nó rất tiện dụng và giúp chúng ta giải quyết được các bài toán hiệu xuất cao
Khai báo
Mỗi channel có một kiểu riêng gắn với nó. Đây là kiểu dữ liệu channel cho phép truyền đi
chan T
là channel kiểu dữ liệu T
Giá trị 0 của channel là nil
. Channel nil
tức là không sử dụng và do đó channel phải được định nghĩa sử dụng make
giống như map và slice.
Hãy cùng xem ví dụ về đơn giản về khai báo channel như sau
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) } }
channel a
được khai báo với giá trị nil
. Do đó câu lệnh bên trong điều kiện if sẽ được chạy và channel được định nghĩa sử dụng hàm make
. a
được khai báo là int channel.
Kết quả
channel a is nil, going to define it Type of a is chan int
Gửi và nhận từ channel
Cú pháp gửi và nhận dữ liệu từ môt channel như sau
data := <- a // đọc từ channel a a <- data // ghi vào channel 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. Mũi tên hướng ra bên ngoài a
tức là đang đọc dữ liệu từ a
và gán và data. Còn mũi tên hướng vào a
tức là đang ghi dữ liệu vào a
Hãy cùng xem ví dụ sau
package main import ( "fmt" "math/rand" ) func CalculateValue(values chan int) { value := rand.Intn(10) fmt.Println("Calculated Random Value: {}", value) values <- value } func main() { fmt.Println("Go Channel Tutorial") values := make(chan int) defer close(values) go CalculateValue(values) value := <-values fmt.Println(value) }
Trong hàm main
, chúng ta gọi values := make(chan int)
để tạo một channel mới và sau đó sử dụng bên trong goroutine CalculateValue
Sau khi tạo channel, tiếp theo gọi hàm defer close(values)
dùng để đóng channel khi hàm main
kết thúc. Tiếp theo chạy một goroutine CalculateValue(values)
truyền vào một tham số values
là channel. Bên trong hàm CalculateValue
, chúng ta tính toán giá trị ngẫu nhiên đơn từ 1-10 và in ra các giá trị và sau đó gửi các giá trị này đến channel values
bằng cách gọi values <- value
Quay trở lại hàm main
, chúng ta gọi value := <-values
để nhận giá trị từ channel values
.
Kết quả
Go Channel Tutorial Calculated Random Value: {} 7 7
Gửi và nhận mặc định bị chặn
Gửi và nhận cho một channel mặc định bị chặn. Nghĩa là, khi dữ liệu được gửi đến một channel, việc kiểm soát câu lệnh gửi bị chặn cho tới 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 này sẽ bị chặn cho tới khi một số goroutine ghi dữ liệu cho channel đó.
Đặc tính này của channel giúp cho những gì goroutine truyền đi hiệu quả mà không cần phải khóa rõ ràng hoặc các biến điều kiện được sử dụng phổ biến trong các ngôn ngữ lập trình khác.
Channel không có bộ đệm
Sử dụng channel truyền thống bên trong goroutine thường có khả năng dẫn đến những vấn đề mà bạn không mong đợi. Với các channel truyền thống không có bộ đệm, bất cứ khi nào một goroutine gửi giá trị đến channel này, goroutine ngay lập tức bị chặn cho tới khi giá trị được nhận từ channel.
Hãy cùng xem ví dụ sau, code tương tự như ví dụ bên trên. Tuy nhiên, chúng ta thay đổi hàm CalculateValue()
cho việc thực hiện fmt.Println
sau khi nó đã gửi các tính toán giá trị ngẫu nhiên cho channel.
Trong hàm main
, chúng ta thêm vào gọi lại lần 2 cho go CalculateValue(valueChannel)
, chúng ta mong muốn giá trị 2 gửi cho channel này một cách nhanh chóng
package main import ( "fmt" "math/rand" "time" ) func CalculateValue(c chan int) { value := rand.Intn(10) fmt.Println("Calculated Random Value: {}", value) time.Sleep(1000 * time.Millisecond) c <- value fmt.Println("Only Executes after another goroutine performs a receive on the channel") } func main() { fmt.Println("Go Channel Tutorial") valueChannel := make(chan int) defer close(valueChannel) go CalculateValue(valueChannel) go CalculateValue(valueChannel) values := <-valueChannel fmt.Println(values) }
Sau khi chạy chương trình này bạn sẽ thấy rằng chi có câu lệnh in cuối cùng của goroutine đầu tiên thực sự được chạy. Nguyên nhân cho điều này là việc gọi c <- value
đã bị chặn trong goroutine thứ 2 và sau đó hàm main
kết thúc chạy trước khi goroutine thứ 2 có cơ hội hoàn thành phần chạy của mình.
Kết quả
Go Channel Tutorial Calculated Random Value: {} 1 Calculated Random Value: {} 7 1 Only Executes after another goroutine performs a receive on the channel
Channel với bộ đệm
Để khắc phục hành vi bị chặn này chúng ta sẽ sử dụng channel với bộ đệm. Những channel với những bộ đệm này bản chất là các hàng đợi có kích thước nhất định có thể được sử dụng để liên lạc các goroutine chéo nhau. Mục đích để tạo các channel với bộ đệm thay vì không có bộ đệm, chúng ta sử dụng tham số capacity
trong câu lệnh make
.
bufferedChannel := make(chan int, 3)
Với việc thay đổi thành kênh với bộ đệm, hoạt động gửi, c <- value
chỉ chặn bên trong các goroutine nên channel bị đầy.
Hãy cùng nhau cập nhập ví dụ bên trên để sử dụng channel với bộ đệm và hãy cùng xem kết quả. Chú ý rằng, chúng ta phải thêm time.Sleep()
ở đoạn cuối cùng hàm main
với mục đích để chờ cho tới khi các goroutine kết thúc việc thực thi
package main import ( "fmt" "math/rand" "time" ) func CalculateValue(c chan int) { value := rand.Intn(10) fmt.Println("Calculated Random Value: {}", value) time.Sleep(1000 * time.Millisecond) c <- value fmt.Println("This executes regardless as the send is now non-blocking") } func main() { fmt.Println("Go Channel Tutorial") valueChannel := make(chan int, 2) defer close(valueChannel) go CalculateValue(valueChannel) go CalculateValue(valueChannel) values := <-valueChannel fmt.Println(values) time.Sleep(1000 * time.Millisecond) }
Khi chạy chương trình, chúng ta nên nhìn thấy rằng goroutine thứ 2 quả thực tiếp tục thực thi bất kể goroutine thứ 2 nhận được đã không được gọi trong hàm main
. Nhưng nhờ có hàm time.Sleep()
, chúng ta có thể nhìn thấy sự khác nhau giữa channel không bộ đệm và bản chất chặn của chúng và channel với bộ đệm và bản chất không chặn (khi không bị đầy).
Kết quả
Go Channel Tutorial Calculated Random Value: {} 1 Calculated Random Value: {} 7 7 This executes regardless as the send is now non-blocking This executes regardless as the send is now non-blocking