☰ Mục lục series
Tài liệu Go tiếng Việt
  1. Bắt đầu (Getting started)
    1. Tổng quan
    2. Tải về và cài đặt
    3. Bắt đầu với Go
    4. Quản lý các bản cài đặt Go
    5. Plugin trình soạn thảo và IDE
  2. Dạo quanh Go (A Tour of Go)
    1. Chào mừng
    2. Package, biến và hàm
    3. Điều khiển luồng
    4. Struct, slice và map
    5. Method và interface
    6. Generics
    7. Xử lý đồng thời
  3. Hướng dẫn (Tutorials)
    1. Danh sách hướng dẫn
    2. Tạo một Go module
    3. Gọi mã của bạn từ một module khác
    4. Trả về và xử lý lỗi
    5. Trả về lời chào ngẫu nhiên
    6. Trả về lời chào cho nhiều người
    7. Thêm một test
    8. Biên dịch và cài đặt ứng dụng
    9. Kết luận
    10. Bắt đầu với workspace nhiều module
    11. Phát triển RESTful API với Go và Gin
    12. Truy cập cơ sở dữ liệu quan hệ
    13. Bắt đầu với generics
    14. Bắt đầu với fuzzing
    15. Làm việc với JSON
    16. Tìm và sửa các dependency có lỗ hổng với govulncheck
    17. Tìm và khắc phục phụ thuộc có lỗ hổng với VS Code Go
    18. Viết ứng dụng web
  4. Viết code Go
    1. Cách viết mã Go
    2. Effective Go
    3. Doc Comment trong Go
  5. Module
    1. Phát triển và publish module
    2. Quản lý phụ thuộc
    3. Tổ chức một module Go
    4. Quản lý mã nguồn của module
    5. Cách đánh số phiên bản module
    6. Quy trình phát hành và đánh phiên bản module
    7. Publish một module
    8. Phát triển một bản cập nhật major version
    9. Tham chiếu tệp go.mod
    10. Ngừng hỗ trợ 'go get' cho việc cài đặt tệp thực thi
  6. Cơ sở dữ liệu (Database)
    1. Truy cập cơ sở dữ liệu quan hệ
    2. Mở một handle cơ sở dữ liệu
    3. Thực thi câu lệnh SQL không trả về dữ liệu
    4. Truy vấn dữ liệu
    5. Dùng prepared statement
    6. Thực thi transaction
    7. Huỷ các thao tác đang chạy
    8. Quản lý kết nối
    9. Tránh rủi ro SQL injection
  7. Chẩn đoán và hiệu năng
    1. Chẩn đoán
    2. Hướng dẫn về Go Garbage Collector
    3. Tối ưu hoá dựa trên profile
    4. Data Race Detector
    5. Gỡ lỗi mã Go bằng GDB
    6. Hỗ trợ coverage profiling cho integration test
  8. Bảo mật (Security)
    1. Tổng quan bảo mật
    2. Thực hành tốt về bảo mật cho lập trình viên Go
    3. Quản lý lỗ hổng trong Go
    4. Cơ sở dữ liệu lỗ hổng của Go
    5. Quét lỗ hổng trong IDE
    6. Chính sách CNA của Go
    7. Go Fuzzing
    8. Chi tiết kỹ thuật của Go Fuzzing
    9. Chính sách bảo mật của Go
    10. Threat Model của Go
    11. Các quyết định về bảo mật của Go
    12. Tuân thủ FIPS 140-3
  9. Công cụ và toolchain
    1. Tài liệu về các lệnh
    2. Về lệnh go
    3. Go Toolchains
    4. Telemetry của Go
    5. Cài đặt Go từ mã nguồn
    6. Cài đặt và sử dụng gccgo
  10. Tham khảo
    1. Câu hỏi thường gặp (FAQ)
    2. Go 1 và tương lai của các chương trình Go
    3. Hướng dẫn chuyển đổi sang encoding/json/v2
    4. Hướng dẫn đóng góp
    5. Đóng góp cho frontend gccgo
  11. Phụ lục
    1. Bảng thuật ngữ
Dạo quanh Go (A Tour of Go)

Tính đồng thời

Bản dịch từ go.dev/tour/concurrency/1

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.

Hai cây nhị phân khác hình dạng nhưng cùng lưu dãy giá trị 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:

  • Lock
  • Unlock

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.

Nội dung gốc: Go Documentation © The Go Authors, giấy phép CC BY 3.0. Bản dịch và ghi chú: Lê Văn Dũng.