Goroutine
goroutine là một luồng thực thi nhẹ do Go runtime quản lý — hiểu đơn giản là một việc chạy song song, rất nhẹ.
go f(x, y, z)
khởi động một goroutine mới chạy
f(x, y, z)
Việc tính giá trị của f, x, y và z diễn ra trong goroutine hiện tại, còn việc chạy f diễn ra trong goroutine mới.
Các goroutine chạy trong cùng một không gian địa chỉ, nên mọi truy cập vào bộ nhớ dùng chung đều phải được đồng bộ. Package sync cung cấp vài công cụ cơ bản hữu ích, dù bạn sẽ không cần đến chúng nhiều trong Go vì đã có những công cụ khác. (Xem trang tiếp theo.)
package main
import (
"fmt"
"time"
)
func say(s string) {
for i := 0; i < 5; i++ {
time.Sleep(100 * time.Millisecond)
fmt.Println(s)
}
}
func main() {
go say("world")
say("hello")
}
Chạy thử ví dụ này trong Tour of Go ↗
Channel
Channel (đường ống để các goroutine gửi dữ liệu cho nhau) là một kênh dẫn có kiểu dữ liệu, qua đó bạn gửi và nhận các giá trị bằng toán tử channel, <-.
ch <- v // Send v to channel ch.
v := <-ch // Receive from ch, and
// assign value to v.
(Dữ liệu chảy theo chiều của mũi tên.)
Giống map và slice, channel phải được tạo ra trước khi dùng:
ch := make(chan int)
Mặc định, lệnh gửi và lệnh nhận sẽ chặn lại cho tới khi phía bên kia sẵn sàng. Nhờ vậy các goroutine có thể đồng bộ với nhau mà không cần khoá (lock) hay biến điều kiện tường minh.
Đoạn mã ví dụ tính tổng các số trong một slice, chia việc ra cho hai goroutine. Khi cả hai goroutine tính xong, nó ghép lại thành kết quả cuối cùng.
package main
import "fmt"
func sum(s []int, c chan int) {
sum := 0
for _, v := range s {
sum += v
}
c <- sum // send sum to c
}
func main() {
s := []int{7, 2, 8, -9, 4, 0}
c := make(chan int)
go sum(s[:len(s)/2], c)
go sum(s[len(s)/2:], c)
x, y := <-c, <-c // receive from c
fmt.Println(x, y, x+y)
}
Chạy thử ví dụ này trong Tour of Go ↗
Buffered channel
Channel có thể có bộ_đệm (buffered channel — channel có sẵn chỗ chứa tạm). Hãy truyền độ dài bộ đệm làm đối số thứ hai cho make để khởi tạo một buffered channel:
ch := make(chan int, 100)
Lệnh gửi vào một buffered channel chỉ bị chặn khi bộ đệm đã đầy. Lệnh nhận bị chặn khi bộ đệm rỗng.
Hãy sửa ví dụ để đổ quá đầy bộ đệm và xem điều gì xảy ra.
package main
import "fmt"
func main() {
ch := make(chan int, 2)
ch <- 1
ch <- 2
fmt.Println(<-ch)
fmt.Println(<-ch)
}
Chạy thử ví dụ này trong Tour of Go ↗
Range và close
Phía gửi có thể close một channel để báo rằng sẽ không còn giá trị nào được gửi nữa. Phía nhận có thể kiểm tra xem channel đã đóng hay chưa bằng cách gán thêm một tham số thứ hai cho biểu thức nhận: sau
v, ok := <-ch
ok sẽ là false nếu không còn giá trị nào để nhận và channel đã đóng.
Vòng lặp for i:=range c nhận giá trị từ channel liên tục cho tới khi channel được đóng.
Lưuý:* Chỉ phía gửi mới nên đóng channel, phía nhận thì không bao giờ. Gửi vào một channel đã đóng sẽ gây panic (lỗi khiến chương trình dừng đột ngột).
Mộtlưu*ýnữa: Channel không giống tệp; bạn thường không cần đóng chúng. Chỉ cần đóng khi phía nhận phải được báo rằng không còn giá trị nào đến nữa, chẳng hạn để kết thúc một vòng lặp range.
package main
import (
"fmt"
)
func fibonacci(n int, c chan int) {
x, y := 0, 1
for i := 0; i < n; i++ {
c <- x
x, y = y, x+y
}
close(c)
}
func main() {
c := make(chan int, 10)
go fibonacci(cap(c), c)
for i := range c {
fmt.Println(i)
}
}
Chạy thử ví dụ này trong Tour of Go ↗
Select
Câu lệnh select (chờ nhiều thao tác trên channel cùng lúc) cho phép một goroutine chờ trên nhiều thao tác giao tiếp.
select chặn lại cho tới khi một trong các case của nó chạy được, rồi nó chạy case đó. Nếu nhiều case cùng sẵn sàng, nó chọn ngẫu nhiên một case.
package main
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("quit")
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)
}
Chạy thử ví dụ này trong Tour of Go ↗
Nhánh default trong select
Case default trong select sẽ chạy nếu không case nào khác sẵn sàng.
Hãy dùng case default để thử gửi hoặc nhận mà không bị chặn:
select {
case i := <-c:
// use i
default:
// receiving from c would block
}
package main
import (
"fmt"
"time"
)
func main() {
start := time.Now()
tick := time.Tick(100 * time.Millisecond)
boom := time.After(500 * time.Millisecond)
elapsed := func() time.Duration {
return time.Since(start).Round(time.Millisecond)
}
for {
select {
case <-tick:
fmt.Printf("[%6s] tick.\n", elapsed())
case <-boom:
fmt.Printf("[%6s] BOOM!\n", elapsed())
return
default:
fmt.Printf("[%6s] .\n", elapsed())
time.Sleep(50 * time.Millisecond)
}
}
}
Chạy thử ví dụ này trong Tour of Go ↗
Bài tập: Cây nhị phân tương đương
Có thể có rất nhiều cây nhị phân khác nhau cùng chứa một dãy giá trị giống hệt nhau. Ví dụ, đây là hai cây nhị phân cùng lưu dãy 1, 1, 2, 3, 5, 8, 13.

Một hàm kiểm tra xem hai cây nhị phân có lưu cùng một dãy hay không thường khá phức tạp trong hầu hết các ngôn ngữ. Chúng ta sẽ dùng tính đồng thời và channel của Go để viết một lời giải đơn giản.
Ví dụ này dùng package tree, nơi định nghĩa kiểu dữ liệu:
type Tree struct {
Left *Tree
Value int
Right *Tree
}
Phần mô tả tiếp tục ở trang sau.
Bài tập: Cây nhị phân tương đương
1. Hiện thực hàm Walk.
2. Kiểm thử hàm Walk.
Hàm tree.New(k) dựng một cây nhị phân có cấu trúc ngẫu nhiên (nhưng luôn được sắp thứ tự) chứa các giá trị k, 2k, 3k, …, 10k.
Hãy tạo một channel mới tên ch và cho bộ duyệt chạy:
go Walk(tree.New(1), ch)
Rồi đọc và in ra 10 giá trị từ channel. Kết quả phải là các số 1, 2, 3, …, 10.
3. Hiện thực hàm Same, dùng Walk để xác định xem t1 và t2 có lưu cùng các giá trị hay không.
4. Kiểm thử hàm Same.
Same(tree.New(1), tree.New(1)) phải trả về true, còn Same(tree.New(1), tree.New(2)) phải trả về false.
Tài liệu về Tree nằm ở đây.
package main
import "golang.org/x/tour/tree"
// Walk walks the tree t sending all values
// from the tree to the channel ch.
func Walk(t *tree.Tree, ch chan int)
// Same determines whether the trees
// t1 and t2 contain the same values.
func Same(t1, t2 *tree.Tree) bool
func main() {
}
Chạy thử ví dụ này trong Tour of Go ↗
sync.Mutex
Chúng ta đã thấy channel rất hợp để các goroutine giao tiếp với nhau.
Nhưng nếu ta không cần giao tiếp thì sao? Nếu ta chỉ muốn chắc chắn rằng mỗi lúc chỉ có một goroutine được truy cập vào một biến, để tránh xung đột?
Khái niệm này gọi là loại_trừ_lẫn_nhau (mutual exclusion), và tên thường dùng cho cấu trúc dữ liệu làm việc đó là mutex (khoá đảm bảo mỗi lúc chỉ một goroutine vào được).
Thư viện chuẩn của Go cung cấp cơ chế loại trừ lẫn nhau qua sync.Mutex với hai method:
LockUnlock
Ta có thể quy định một khối lệnh được chạy trong trạng thái loại trừ lẫn nhau bằng cách bọc nó giữa lời gọi Lock và Unlock, như trong method Inc.
Ta cũng có thể dùng defer (hoãn thực thi tới khi hàm trả về) để bảo đảm mutex chắc chắn được mở khoá, như trong method Value.
package main
import (
"fmt"
"sync"
"time"
)
// SafeCounter is safe to use concurrently.
type SafeCounter struct {
mu sync.Mutex
v map[string]int
}
// Inc increments the counter for the given key.
func (c *SafeCounter) Inc(key string) {
c.mu.Lock()
// Lock so only one goroutine at a time can access the map c.v.
c.v[key]++
c.mu.Unlock()
}
// Value returns the current value of the counter for the given key.
func (c *SafeCounter) Value(key string) int {
c.mu.Lock()
// Lock so only one goroutine at a time can access the map c.v.
defer c.mu.Unlock()
return c.v[key]
}
func main() {
c := SafeCounter{v: make(map[string]int)}
for i := 0; i < 1000; i++ {
go c.Inc("somekey")
}
time.Sleep(time.Second)
fmt.Println(c.Value("somekey"))
}
Chạy thử ví dụ này trong Tour of Go ↗
Bài tập: Web crawler
Trong bài tập này, bạn sẽ dùng các tính năng đồng thời của Go để song song hoá một web crawler (chương trình tự động đi thu thập các trang web).
Hãy sửa hàm Crawl để lấy các URL song song với nhau mà không lấy cùng một URL hai lần.
Gợi_ý: bạn có thể lưu các URL đã lấy vào một map để làm bộ nhớ đệm, nhưng bản thân map thì không an toàn khi dùng đồng thời!
package main
import (
"fmt"
)
type Fetcher interface {
// Fetch returns the body of URL and
// a slice of URLs found on that page.
Fetch(url string) (body string, urls []string, err error)
}
// Crawl uses fetcher to recursively crawl
// pages starting with url, to a maximum of depth.
func Crawl(url string, depth int, fetcher Fetcher) {
// TODO: Fetch URLs in parallel.
// TODO: Don't fetch the same URL twice.
// This implementation doesn't do either:
if depth <= 0 {
return
}
body, urls, err := fetcher.Fetch(url)
if err != nil {
fmt.Println(err)
return
}
fmt.Printf("found: %s %q\n", url, body)
for _, u := range urls {
Crawl(u, depth-1, fetcher)
}
return
}
func main() {
Crawl("https://golang.org/", 4, fetcher)
}
// fakeFetcher is Fetcher that returns canned results.
type fakeFetcher map[string]*fakeResult
type fakeResult struct {
body string
urls []string
}
func (f fakeFetcher) Fetch(url string) (string, []string, error) {
if res, ok := f[url]; ok {
return res.body, res.urls, nil
}
return "", nil, fmt.Errorf("not found: %s", url)
}
// fetcher is a populated fakeFetcher.
var fetcher = fakeFetcher{
"https://golang.org/": &fakeResult{
"The Go Programming Language",
[]string{
"https://golang.org/pkg/",
"https://golang.org/cmd/",
},
},
"https://golang.org/pkg/": &fakeResult{
"Packages",
[]string{
"https://golang.org/",
"https://golang.org/cmd/",
"https://golang.org/pkg/fmt/",
"https://golang.org/pkg/os/",
},
},
"https://golang.org/pkg/fmt/": &fakeResult{
"Package fmt",
[]string{
"https://golang.org/",
"https://golang.org/pkg/",
},
},
"https://golang.org/pkg/os/": &fakeResult{
"Package os",
[]string{
"https://golang.org/",
"https://golang.org/pkg/",
},
},
}
Chạy thử ví dụ này trong Tour of Go ↗
Đi tiếp từ đây…
Tài liệu Go là một nơi tuyệt vời để bắt đầu. Ở đó có tài liệu tham khảo, hướng dẫn, video và nhiều thứ khác.
Để biết cách tổ chức và làm việc với mã nguồn Go, hãy đọc How to Write Go Code.
Nếu bạn cần trợ giúp về thư viện chuẩn, hãy xem tài liệu tra cứu package. Còn về bản thân ngôn ngữ, bạn có thể sẽ bất ngờ khi thấy Language Spec khá dễ đọc.
Để tìm hiểu sâu hơn về mô hình đồng thời của Go, hãy xem Go Concurrency Patterns (slide) và Advanced Go Concurrency Patterns (slide) rồi đọc codewalk Share Memory by Communicating. Tinh thần của nó là: đừng giao tiếp bằng cách chia sẻ bộ nhớ; thay vào đó, hãy chia sẻ bộ nhớ bằng cách giao tiếp (“Do not communicate by sharing memory; instead, share memory by communicating”).
Để bắt đầu viết ứng dụng web, hãy xem A simple programming environment (slide) rồi đọc hướng dẫn Writing Web Applications.
Codewalk First Class Functions in Go mang lại một góc nhìn thú vị về các kiểu hàm của Go.
Go Blog có một kho lưu trữ lớn các bài viết bổ ích về Go.
Hãy ghé trang chủ Go để biết thêm.