☰ 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ữ
Chẩn đoán và hiệu năng

Data Race Detector

Bản dịch từ go.dev/doc/articles/race_detector

Giới thiệu

Data race (hai goroutine truy cập cùng một vùng nhớ mà không đồng bộ) nằm trong số những loại lỗi phổ biến nhất và khó gỡ nhất của các hệ thống đồng thời. Một data race xảy ra khi hai goroutine truy cập đồng thời cùng một biến và ít nhất một trong các truy cập đó là ghi. Xem Mô hình bộ nhớ của Go để biết chi tiết.

Dưới đây là một ví dụ về data race có thể dẫn tới sập chương trình và hỏng bộ nhớ:

func main() {
	c := make(chan bool)
	m := make(map[string]string)
	go func() {
		m["1"] = "a" // First conflicting access.
		c <- true
	}()
	m["2"] = "b" // Second conflicting access.
	<-c
	for k, v := range m {
		fmt.Println(k, v)
	}
}

Cách dùng

Để hỗ trợ chẩn đoán những lỗi như vậy, Go có sẵn một race detector (công cụ phát hiện data race) tích hợp. Để dùng nó, thêm cờ -race vào lệnh go:

$ go test -race mypkg    // to test the package
$ go run -race mysrc.go  // to run the source file
$ go build -race mycmd   // to build the command
$ go install -race mypkg // to install the package

Định dạng báo cáo

Khi race detector phát hiện một data race trong chương trình, nó in ra một báo cáo. Báo cáo chứa stack trace của các truy cập xung đột, cũng như stack tại nơi các goroutine liên quan được tạo ra. Đây là một ví dụ:

WARNING: DATA RACE
Read by goroutine 185:
  net.(*pollServer).AddFD()
      src/net/fd_unix.go:89 +0x398
  net.(*pollServer).WaitWrite()
      src/net/fd_unix.go:247 +0x45
  net.(*netFD).Write()
      src/net/fd_unix.go:540 +0x4d4
  net.(*conn).Write()
      src/net/net.go:129 +0x101
  net.func·060()
      src/net/timeout_test.go:603 +0xaf

Previous write by goroutine 184:
  net.setWriteDeadline()
      src/net/sockopt_posix.go:135 +0xdf
  net.setDeadline()
      src/net/sockopt_posix.go:144 +0x9c
  net.(*conn).SetDeadline()
      src/net/net.go:161 +0xe3
  net.func·061()
      src/net/timeout_test.go:616 +0x3ed

Goroutine 185 (running) created at:
  net.func·061()
      src/net/timeout_test.go:609 +0x288

Goroutine 184 (running) created at:
  net.TestProlongTimeout()
      src/net/timeout_test.go:618 +0x298
  testing.tRunner()
      src/testing/testing.go:301 +0xe8

Tuỳ chọn

Biến môi trường GORACE đặt các tuỳ chọn cho race detector. Định dạng là:

GORACE="option1=val1 option2=val2"

Các tuỳ chọn gồm:

  • log_path (mặc định stderr): race detector ghi báo cáo của nó vào một tệp tên là log_path.pid. Hai tên đặc biệt stdout và stderr khiến báo cáo được ghi ra đầu ra chuẩn và đầu ra lỗi chuẩn, theo thứ tự đó.
  • exitcode (mặc định 66): trạng thái thoát dùng khi thoát sau lúc phát hiện một race.
  • strip_path_prefix (mặc định ""): cắt bỏ tiền tố này khỏi mọi đường dẫn tệp được báo cáo, để báo cáo ngắn gọn hơn.
  • history_size (mặc định 1): lịch sử truy cập bộ nhớ của mỗi goroutine là 32K * 2**history_size elements. Tăng giá trị này có thể tránh được lỗi “failed to restore the stack” trong báo cáo, nhưng đánh đổi bằng việc dùng nhiều bộ nhớ hơn.
  • halt_on_error (mặc định 0): điều khiển việc chương trình có thoát sau khi báo cáo data race đầu tiên hay không.
  • atexit_sleep_ms (mặc định 1000): số mili giây ngủ trong goroutine chính trước khi thoát.

Ví dụ:

$ GORACE="log_path=/tmp/race/report strip_path_prefix=/my/go/sources/" go test -race

Loại trừ các test

Khi bạn build với cờ -race, lệnh go định nghĩa thêm build tag race. Bạn có thể dùng tag này để loại trừ một số mã nguồn và test khi chạy race detector. Vài ví dụ:

// +build !race

package foo

// The test contains a data race. See issue 123.
func TestFoo(t *testing.T) {
	// ...
}

// The test fails under the race detector due to timeouts.
func TestBar(t *testing.T) {
	// ...
}

// The test takes too long under the race detector.
func TestBaz(t *testing.T) {
	// ...
}

Sử dụng thế nào

Để bắt đầu, hãy chạy các test của bạn với race detector (go test -race). Race detector chỉ tìm được những race xảy ra lúc runtime, nên nó không thể tìm ra race trong những nhánh mã không được thực thi. Nếu các test của bạn chưa phủ hết mã, bạn có thể tìm thêm được race bằng cách chạy một binary được build với -race dưới khối lượng công việc thực tế.

Những data race điển hình

Dưới đây là một số data race điển hình. Tất cả đều có thể được phát hiện bằng race detector.

Race trên biến đếm vòng lặp

func main() {
	var wg sync.WaitGroup
	wg.Add(5)
	var i int
	for i = 0; i < 5; i++ {
		go func() {
			fmt.Println(i) // Not the 'i' you are looking for.
			wg.Done()
		}()
	}
	wg.Wait()
}

Biến i trong hàm literal chính là biến mà vòng lặp đang dùng, nên việc đọc trong goroutine chạy đua với phép tăng của vòng lặp. (Chương trình này thường in ra 55555, chứ không phải 01234.) Có thể sửa chương trình bằng cách tạo một bản sao của biến:

func main() {
	var wg sync.WaitGroup
	wg.Add(5)
	var i int
	for i = 0; i < 5; i++ {
		go func(j int) {
			fmt.Println(j) // Good. Read local copy of the loop counter.
			wg.Done()
		}(i)
	}
	wg.Wait()
}

Biến bị chia sẻ ngoài ý muốn

// ParallelWrite writes data to file1 and file2, returns the errors.
func ParallelWrite(data []byte) chan error {
	res := make(chan error, 2)
	f1, err := os.Create("file1")
	if err != nil {
		res <- err
	} else {
		go func() {
			// This err is shared with the main goroutine,
			// so the write races with the write below.
			_, err = f1.Write(data)
			res <- err
			f1.Close()
		}()
	}
	f2, err := os.Create("file2") // The second conflicting write to err.
	if err != nil {
		res <- err
	} else {
		go func() {
			_, err = f2.Write(data)
			res <- err
			f2.Close()
		}()
	}
	return res
}

Cách sửa là khai báo biến mới bên trong các goroutine (chú ý việc dùng :=):

...
_, err := f1.Write(data)
...
_, err := f2.Write(data)
...

Biến toàn cục không được bảo vệ

Nếu đoạn mã sau được gọi từ nhiều goroutine, nó dẫn tới race trên map service. Việc đọc và ghi đồng thời trên cùng một map là không an toàn:

var service map[string]net.Addr

func RegisterService(name string, addr net.Addr) {
	service[name] = addr
}

func LookupService(name string) net.Addr {
	return service[name]
}

Để mã trở nên an toàn, hãy bảo vệ các truy cập bằng một mutex:

var (
	service   map[string]net.Addr
	serviceMu sync.Mutex
)

func RegisterService(name string, addr net.Addr) {
	serviceMu.Lock()
	defer serviceMu.Unlock()
	service[name] = addr
}

func LookupService(name string) net.Addr {
	serviceMu.Lock()
	defer serviceMu.Unlock()
	return service[name]
}

Biến kiểu nguyên thuỷ không được bảo vệ

Data race cũng có thể xảy ra trên các biến thuộc kiểu dữ liệu nguyên thuỷ (bool, int, int64, v.v.), như trong ví dụ này:

type Watchdog struct{ last int64 }

func (w *Watchdog) KeepAlive() {
	w.last = time.Now().UnixNano() // First conflicting access.
}

func (w *Watchdog) Start() {
	go func() {
		for {
			time.Sleep(time.Second)
			// Second conflicting access.
			if w.last < time.Now().Add(-10*time.Second).UnixNano() {
				fmt.Println("No keepalives for 10 seconds. Dying.")
				os.Exit(1)
			}
		}
	}()
}

Ngay cả những data race “vô hại” như vậy cũng có thể dẫn tới các vấn đề rất khó gỡ, gây ra bởi tính không nguyên tử của các truy cập bộ nhớ, sự can thiệp của các tối ưu hoá từ compiler, hoặc các vấn đề sắp xếp lại thứ tự khi truy cập bộ nhớ của bộ xử lý .

Cách sửa điển hình cho race này là dùng một channel hoặc một mutex. Để giữ được hành vi lock-free, bạn cũng có thể dùng package sync/atomic.

type Watchdog struct{ last int64 }

func (w *Watchdog) KeepAlive() {
	atomic.StoreInt64(&w.last, time.Now().UnixNano())
}

func (w *Watchdog) Start() {
	go func() {
		for {
			time.Sleep(time.Second)
			if atomic.LoadInt64(&w.last) < time.Now().Add(-10*time.Second).UnixNano() {
				fmt.Println("No keepalives for 10 seconds. Dying.")
				os.Exit(1)
			}
		}
	}()
}

Thao tác gửi và đóng không được đồng bộ

Như ví dụ này minh hoạ, các thao tác gửi và đóng không được synchronization trên cùng một channel cũng có thể là một race condition:

c := make(chan struct{}) // or buffered channel

// The race detector cannot derive the happens before relation
// for the following send and close operations. These two operations
// are unsynchronized and happen concurrently.
go func() { c <- struct{}{} }()
close(c)

Theo mô hình bộ nhớ của Go, một thao tác gửi trên channel happens-before thời điểm thao tác nhận tương ứng từ channel đó hoàn tất. Để đồng bộ thao tác gửi và đóng, hãy dùng một thao tác nhận để bảo đảm việc gửi đã xong trước khi đóng:

c := make(chan struct{}) // or buffered channel

go func() { c <- struct{}{} }()
<-c
close(c)

Yêu cầu

Race detector yêu cầu cgo phải được bật, và trên các hệ thống không phải Darwin thì yêu cầu có sẵn một trình biên dịch C. Race detector hỗ trợ linux/amd64, linux/ppc64le, linux/arm64, linux/s390x, linux/loong64, freebsd/amd64, netbsd/amd64, darwin/amd64, darwin/arm64, và windows/amd64.

Trên Windows, runtime của race detector nhạy cảm với phiên bản trình biên dịch C đã cài; kể từ Go 1.21, việc build một chương trình với -race yêu cầu một trình biên dịch C có kèm phiên bản 8 trở lên của các thư viện runtime mingw-w64. Bạn có thể kiểm tra trình biên dịch C của mình bằng cách gọi nó với các đối số --print-file-name libsynchronization.a. Một trình biên dịch C mới và tuân thủ sẽ in ra đường dẫn đầy đủ tới thư viện này, còn các trình biên dịch C cũ hơn thì chỉ lặp lại chính đối số đó.

Chi phí lúc chạy

Chi phí của việc phát hiện race thay đổi tuỳ chương trình, nhưng với một chương trình điển hình, lượng bộ nhớ sử dụng có thể tăng 5-10 lần và thời gian thực thi tăng 2-20 lần.

Race detector hiện cấp phát thêm 8 byte cho mỗi câu lệnh defer và recover. Những phần cấp phát thêm đó không được thu hồi cho tới khi goroutine kết thúc. Điều này nghĩa là nếu bạn có một goroutine chạy lâu và định kỳ gọi defer và recover, lượng bộ nhớ chương trình dùng có thể tăng không giới hạn. Những phần cấp phát bộ nhớ này sẽ không xuất hiện trong đầu ra của runtime.ReadMemStats hay runtime/pprof.

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.