1. Giới thiệu cơ bản
Channel là một thành phần quan trọng trong Go, hoạt động như một ống dẫn để các đơn vị xử lý đồng thời có thể gửi và nhận dữ liệu để giao tiếp.
1.1 Toán tử
Toán tử của Channel là mũi tên <-, hướng của mũi tên biểu thị hướng di chuyển của dữ liệu.
dichuyen <- gia_tri // Gửi giá trị vào channel dichuyen
gia_tri := <-dichuyen // Nhận dữ liệu từ channel dichuyen
Tương tự như map và slice, trước khi sử dụng, cần phải tạo ra channel.
dichuyen := make(chan int)
1.2 Kiểu Channel
Định dạng định nghĩa kiểu Channel như sau:
Kiểu_Channel = ("chan" | "chan" "<-" | "<-" "chan") Kiểu_Dữ_Liệu .
Nó bao gồm ba loại định nghĩa, với tùy chọn <- đại diện cho hướng của channel. Nếu không chỉ định hướng, thì Channel sẽ là hai chiều, vừa có thể nhận vừa có thể gửi dữ liệu.
chan T // Có thể nhận và gửi dữ liệu kiểu T
chan<- float64 // Chỉ dùng để gửi dữ liệu kiểu float64
<-chan int // Chỉ dùng để nhận dữ liệu kiểu int
Khi khởi tạo bằng make, có thể thiết lập dung lượng:
make(chan int, 1000)
Dung lượng (capacity) đại diện cho số lượng tối đa của các phần tử mà Channel có thể chứa, tương ứng với kích thước bộ đệm của nó. **Sử dụng bộ đệm có thể giảm thiểu tình trạng chặn, cải thiện hiệu suất ứng dụng.**
Nếu không đặt dung lượng hoặc đặt dung lượng là 0, Channel không có bộ đệm, chỉ xảy ra giao tiếp khi cả sender và receiver đều sẵn sàng. Nếu có bộ đệm, send sẽ bị chặn khi buffer đầy, và receive sẽ bị chặn khi buffer trống. Một channel nil sẽ không giao tiếp.
Có thể đóng Channel bằng phương thức nội tại close().
Channel là một hàng đợi FIFO (First In First Out), thứ tự nhận và gửi dữ liệu giống nhau.
1.3 Send
Câu lệnh send được dùng để gửi dữ liệu vào Channel, ví dụ: dichuyen <- 3. Định nghĩa như sau:
SendStmt = Kênh "<-" Biểu_Thức .
Kênh = Biểu_Thức .
Trước khi bắt đầu truyền thông, Kênh và biểu thức phải được tính toán trước, ví dụ dưới đây (1+2) được tính toán thành 3 trước khi gửi vào Channel:
c := make(chan int)
defer close(c)
go func() { c <- 1 + 2 }()
i := <-c
fmt.Println(i)
Câu lệnh send sẽ bị chặn cho đến khi giao tiếp hoàn tất. Với channel không có bộ đệm, send chỉ thực hiện khi receiver đã sẵn sàng. Nếu có bộ đệm và chưa đầy, send sẽ được thực hiện.
Lưu ý: Gửi dữ liệu vào một channel đã đóng sẽ gây ra lỗi runtime panic, gửi dữ liệu vào một channel nil sẽ bị chặn mãi mãi.
1.4 Receive
<-dichuyen dùng để nhận dữ liệu từ channel dichuyen, biểu thức này sẽ bị chặn cho đến khi có dữ liệu có thể nhận.
Lưu ý: Nhận dữ liệu từ một channel nil sẽ bị chặn mãi mãi. Nhận dữ liệu từ một channel đã đóng sẽ không bị chặn mà trả về ngay lập tức, sau khi nhận xong dữ liệu đã gửi, sẽ trả về giá trị zero của kiểu dữ liệu đó.
Có thể sử dụng một tham số trả về bổ sung để kiểm tra channel đã đóng hay chưa:
x, ok := <-dichuyen
x, ok = <-dichuyen
var x, ok = <-dichuyen
Nếu ok là false, điều này có nghĩa là x là giá trị zero, Channel đã đóng hoặc rỗng.
1.5 Blocking
Theo mặc định, gửi và nhận sẽ bị chặn cho đến khi bên kia sẵn sàng. Cách này có thể dùng để đồng bộ hóa giữa goroutine mà không cần sử dụng khóa hiển thị hoặc biến điều kiện.
Ví dụ trong mã nguồn chính thức x, y := <-c, <-c sẽ chờ kết quả được gửi vào channel.
import "fmt"
func tong(s []int, c chan int) {
tong := 0
for _, v := range s {
tong += v
}
c <- tong // gửi tổng sang c
}
func main() {
s := []int{7, 2, 8, -9, 4, 0}
c := make(chan int)
go tong(s[:len(s)/2], c)
go tong(s[len(s)/2:], c)
x, y := <-c, <-c // nhận từ c
fmt.Println(x, y, x+y)
}
1.6 Range
Câu lệnh for ... range có thể xử lý Channel.
func main() {
go func() {
time.Sleep(1 * time.Hour)
}()
dichuyen := make(chan int)
go func() {
for i := 0; i < 10; i++ {
dichuyen <- i
}
close(dichuyen)
}()
for i := range dichuyen {
fmt.Println(i)
}
fmt.Println("Hoàn thành")
}
Range dichuyen tạo các giá trị lặp lại từ các giá trị được gửi qua Channel, nó sẽ tiếp tục lặp cho đến khi channel bị đóng. Trong ví dụ trên, nếu bỏ qua dòng close(dichuyen), chương trình sẽ bị chặn ở câu lệnh for ... range.
1.7 Select
Câu lệnh select chọn một nhóm các thao tác send và receive có thể để xử lý. Nó tương tự switch nhưng chỉ dùng để xử lý các thao tác giao tiếp.
Case có thể là câu lệnh send, cũng có thể là câu lệnh receive, hoặc default. Câu lệnh receive có thể gán giá trị cho một hoặc hai biến. Nó phải là một thao tác receive. Chỉ có tối đa một case default, có thể đặt ở bất kỳ vị trí nào trong danh sách case, mặc dù thường đặt cuối cùng.
import "fmt"
func fibonacci(c, quit chan int) {
x, y := 0, 1
for {
select {
case c <- x:
x, y = y, x+y
case <-quit:
fmt.Println("Thoát")
return
}
}
}
func main() {
c := make(chan int)
quit := make(chan int)
go func() {
for i := 0; i < 10; i++ {
fmt.Println(<-c)
}
quit <- 0
}()
fibonacci(c, quit)
}
Nếu có nhiều case cần xử lý, ví dụ nhiều channel có thể nhận dữ liệu, Go sẽ chọn một cách ngẫu nhiên (pseudo-random). Nếu không có case nào cần xử lý, sẽ chọn default nếu tồn tại. Nếu không có case default, select sẽ bị chặn cho đến khi có case cần xử lý.
Lưu ý: Các thao tác trên channel nil sẽ bị chặn mãi mãi, nếu không có case default, select sẽ bị chặn mãi mãi.
Câu lệnh select giống switch, không phải vòng lặp, chỉ chọn một case để xử lý. Nếu muốn liên tục xử lý channel, có thể thêm một vòng lặp vô hạn bên ngoài:
for {
select {
case c <- x:
x, y = y, x+y
case <-quit:
fmt.Println("Thoát")
return
}
}
1.8 Timeout
Một ứng dụng quan trọng của select là xử lý timeout. Vì nếu không có case nào cần xử lý, select sẽ bị chặn mãi mãi. Lúc này có thể cần một thao tác timeout để xử lý trường hợp vượt quá thời gian quy định.
Ví dụ dưới đây sẽ gửi dữ liệu vào channel c1 sau 2 giây, nhưng select đặt timeout là 1 giây, do đó sẽ in ra "timeout 1" thay vì "result 1".
import "time"
import "fmt"
func main() {
c1 := make(chan string, 1)
go func() {
time.Sleep(time.Second * 2)
c1 <- "result 1"
}()
select {
case res := <-c1:
fmt.Println(res)
case <-time.After(time.Second * 1):
fmt.Println("timeout 1")
}
}
Thực chất nó sử dụng phương thức time.After, trả về một channel <-chan Time, gửi một giá trị thời gian hiện tại vào channel sau một khoảng thời gian nhất định.
1.9 Timer và Ticker
Timer là một bộ hẹn giờ, biểu diễn một sự kiện duy nhất trong tương lai, bạn có thể yêu cầu timer chờ bao lâu, nó cung cấp một channel, tại thời điểm tương lai, channel này sẽ cung cấp một giá trị thời gian. Dưới đây là ví dụ, dòng thứ hai sẽ bị chặn khoảng 2 giây cho đến khi thời gian hết.
timer1 := time.NewTimer(time.Second * 2)
<-timer1.C
fmt.Println("Timer 1 hết hạn")
Nếu chỉ muốn chờ đợi đơn giản, có thể sử dụng time.Sleep.
Có thể sử dụng timer.Stop để dừng bộ hẹn giờ.
timer2 := time.NewTimer(time.Second)
go func() {
<-timer2.C
fmt.Println("Timer 2 hết hạn")
}()
stop2 := timer2.Stop()
if stop2 {
fmt.Println("Timer 2 dừng")
}
Ticker là một bộ hẹn giờ kích hoạt định kỳ, nó sẽ gửi một sự kiện (thời gian hiện tại) vào Channel theo một khoảng thời gian cố định, và người nhận có thể đọc sự kiện từ Channel theo khoảng thời gian cố định. Ví dụ dưới đây ticker kích hoạt mỗi 500 mili giây, có thể quan sát thời gian đầu ra.
ticker := time.NewTicker(time.Millisecond * 500)
go func() {
for t := range ticker.C {
fmt.Println("Tick lúc", t)
}
}()
Tương tự như timer, ticker cũng có thể dừng bằng phương thức Stop. Sau khi dừng, người nhận không còn nhận dữ liệu từ channel nữa.
1.10 Đồng bộ hóa
Channel có thể dùng để đồng bộ hóa giữa các goroutine.
Trong ví dụ dưới đây, goroutine chính thông qua channel done chờ worker hoàn thành nhiệm vụ. Worker sau khi hoàn thành nhiệm vụ chỉ cần gửi một dữ liệu vào channel để thông báo cho goroutine chính rằng nhiệm vụ đã hoàn thành.
import (
"fmt"
"time"
)
func cong_viec(hoan_thanh chan bool) {
time.Sleep(time.Second)
// Thông báo nhiệm vụ đã hoàn thành
hoan_thanh <- true
}
func main() {
hoan_thanh := make(chan bool, 1)
go cong_viec(hoan_thanh)
// Chờ nhiệm vụ hoàn thành
<-hoan_thanh
}