☰ 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

Chẩn đoán

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

Giới thiệu

Hệ sinh thái Go cung cấp một bộ API và công cụ đồ sộ để chẩn đoán các vấn đề về logic và hiệu năng trong chương trình Go. Trang này tóm tắt các công cụ sẵn có và giúp người dùng Go chọn đúng công cụ cho vấn đề cụ thể của mình.

Các giải pháp chẩn đoán có thể chia thành những nhóm sau:

  • Profiling (đo đạc hiệu năng lúc chạy): các công cụ profiling phân tích độ phức tạp và chi phí của một chương trình Go, chẳng hạn mức dùng bộ nhớ và những hàm được gọi thường xuyên, nhằm xác định các phần tốn kém của chương trình.
  • Tracing (theo dõi dòng thực thi): tracing là cách gắn mã đo đạc để phân tích độ trễ suốt vòng đời của một lời gọi hay một yêu cầu của người dùng. Trace cho bạn cái nhìn tổng quan về mức độ đóng góp của từng thành phần vào tổng độ trễ của hệ thống. Trace có thể trải dài qua nhiều tiến trình Go.
  • Debugging (gỡ lỗi): gỡ lỗi cho phép chúng ta tạm dừng một chương trình Go và xem xét quá trình thực thi của nó. Trạng thái và luồng chương trình có thể được kiểm chứng khi gỡ lỗi.
  • Thống kê và sự kiện của runtime: việc thu thập và phân tích các thống kê cùng sự kiện của runtime cho ta cái nhìn tổng quan về tình trạng sức khoẻ của chương trình Go. Những đỉnh/đáy bất thường của các chỉ số giúp chúng ta nhận ra thay đổi về thông lượng, mức tận dụng tài nguyên và hiệu năng.

Lưu ý: một số công cụ chẩn đoán có thể gây nhiễu lẫn nhau. Ví dụ, memory profiling chính xác sẽ làm lệch CPU profile, còn profiling thời gian goroutine bị chặn lại ảnh hưởng tới trace của scheduler. Hãy dùng riêng từng công cụ để có thông tin chính xác hơn.

Profiling

Profiling hữu ích để xác định những phần mã tốn kém hoặc được gọi thường xuyên. Go runtime cung cấp dữ liệu profiling theo định dạng mà công cụ trực quan hoá pprof mong đợi. Dữ liệu profiling có thể được thu thập trong lúc kiểm thử qua go test hoặc qua các endpoint do package net/http/pprof cung cấp. Người dùng cần thu thập dữ liệu profiling rồi dùng các công cụ pprof để lọc và trực quan hoá những đường thực thi tốn kém nhất.

Các profile được định nghĩa sẵn bởi package runtime/pprof:

  • cpu: CPU profile xác định chương trình dành thời gian ở đâu khi thực sự tiêu thụ chu kỳ CPU (khác với lúc đang ngủ hoặc chờ I/O).
  • heap: heap profile báo cáo các mẫu cấp phát bộ nhớ; dùng để giám sát mức dùng bộ nhớ hiện tại và trong quá khứ, cũng như để phát hiện rò rỉ bộ nhớ.
  • threadcreate: profile tạo luồng báo cáo những phần của chương trình dẫn tới việc tạo luồng hệ điều hành mới.
  • goroutine: goroutine profile báo cáo stack trace của mọi goroutine đang tồn tại.
  • block: block profile cho thấy goroutine bị chặn ở đâu khi chờ các nguyên thuỷ đồng bộ hoá (kể cả channel của timer). Block profile không bật mặc định; hãy dùng runtime.SetBlockProfileRate để bật.
  • mutex: mutex profile báo cáo tình trạng tranh chấp khoá. Khi bạn cho rằng CPU chưa được tận dụng hết do tranh chấp mutex, hãy dùng profile này. Mutex profile không bật mặc định, xem runtime.SetMutexProfileFraction để bật.

Tôi còn có thể dùng những công cụ profiling nào khác cho chương trình Go?

Trên Linux, bộ công cụ perf có thể dùng để profiling chương trình Go. Perf có thể profiling và lần ngược stack cho mã cgo/SWIG lẫn kernel, nên nó hữu ích khi cần hiểu rõ các điểm nghẽn hiệu năng ở tầng native/kernel. Trên macOS, có thể dùng bộ Instruments để profiling chương trình Go.

Tôi có thể profiling các dịch vụ chạy thật (production) không?

Có. Việc profiling chương trình trong môi trường production là an toàn, nhưng bật một số profile (ví dụ CPU profile) sẽ phát sinh chi phí. Bạn nên lường trước việc hiệu năng giảm sút. Có thể ước lượng mức thiệt hại hiệu năng bằng cách đo phần chi phí phụ trội của profiler trước khi bật nó trong production.

Bạn có thể muốn profiling dịch vụ production của mình theo định kỳ. Đặc biệt trong hệ thống có nhiều bản sao của cùng một tiến trình, việc chọn ngẫu nhiên một bản sao theo định kỳ là lựa chọn an toàn. Hãy chọn một tiến trình production, profiling nó trong X giây sau mỗi Y giây rồi lưu kết quả để trực quan hoá và phân tích; sau đó lặp lại theo định kỳ. Kết quả có thể được xem xét thủ công và/hoặc tự động để tìm ra vấn đề. Việc thu thập các profile có thể gây nhiễu lẫn nhau, nên khuyến nghị mỗi lần chỉ thu thập một profile duy nhất.

Đâu là những cách tốt nhất để trực quan hoá dữ liệu profiling?

Các công cụ Go cung cấp cách trực quan hoá dữ liệu profile dạng văn bản, đồ thị và callgrind thông qua go tool pprof. Hãy đọc Profiling Go programs để thấy chúng hoạt động trong thực tế.

Kết quả pprof dạng văn bản: danh sách các hàm tốn CPU nhiều nhất
Danh sách các lời gọi tốn kém nhất dưới dạng văn bản.

Đồ thị lời gọi hàm do pprof vẽ, ô càng lớn thì hàm càng tốn thời gian
Trực quan hoá các lời gọi tốn kém nhất dưới dạng đồ thị.

Chế độ xem weblist hiển thị những phần tốn kém của mã nguồn theo từng dòng trong một trang HTML. Trong ví dụ dưới đây, 530ms được tiêu tốn trong runtime.concatstrings và chi phí của mỗi dòng đều được trình bày trong danh sách.

Chế độ weblist của pprof: mã nguồn kèm thời gian tiêu tốn trên từng dòng
Trực quan hoá các lời gọi tốn kém nhất dưới dạng weblist.

Một cách khác để trực quan hoá dữ liệu profile là flame graph. Flame graph cho phép bạn di chuyển theo một nhánh tổ tiên cụ thể, nhờ đó bạn có thể phóng to/thu nhỏ vào từng phần mã cụ thể. pprof bản upstream có hỗ trợ flame graph.

Flame graph của pprof cho thấy các đường gọi hàm tốn nhiều tài nguyên
Flame graph cung cấp cách trực quan hoá để phát hiện những đường thực thi tốn kém nhất.

Tôi có bị giới hạn ở các profile dựng sẵn không?

Ngoài những gì runtime cung cấp, người dùng Go có thể tạo profile riêng của mình qua pprof.Profile rồi dùng các công cụ sẵn có để xem xét chúng.

Tôi có thể phục vụ các handler của profiler (/debug/pprof/…) trên một đường dẫn và cổng khác không?

Có. Package net/http/pprof mặc định đăng ký các handler của nó vào mux mặc định, nhưng bạn cũng có thể tự đăng ký chúng bằng các handler được package này export ra.

Ví dụ, đoạn mã sau sẽ phục vụ handler pprof.Profile trên cổng :7777 tại đường dẫn /custom_debug_path/profile:

package main

import (
	"log"
	"net/http"
	"net/http/pprof"
)

func main() {
	mux := http.NewServeMux()
	mux.HandleFunc("/custom_debug_path/profile", pprof.Profile)
	log.Fatal(http.ListenAndServe(":7777", mux))
}

Tracing

Tracing là cách gắn mã đo đạc để phân tích độ trễ suốt vòng đời của một chuỗi lời gọi. Go cung cấp package golang.org/x/net/trace như một backend tracing tối giản cho mỗi node Go, kèm theo một thư viện đo đạc tối giản với bảng điều khiển đơn giản. Go cũng cung cấp một execution tracer để theo dõi các sự kiện của runtime trong một khoảng thời gian.

Tracing cho phép chúng ta:

  • Gắn mã đo đạc và phân tích độ trễ của ứng dụng trong một tiến trình Go.
  • Đo chi phí của những lời gọi cụ thể trong một chuỗi lời gọi dài.
  • Tìm ra mức tận dụng tài nguyên và những chỗ có thể cải thiện hiệu năng. Điểm nghẽn không phải lúc nào cũng lộ rõ nếu không có dữ liệu tracing.

Trong hệ thống nguyên khối, việc thu thập dữ liệu chẩn đoán từ các khối cấu thành chương trình tương đối dễ. Mọi module đều nằm trong một tiến trình và chia sẻ chung tài nguyên để ghi log, lỗi và các thông tin chẩn đoán khác. Một khi hệ thống của bạn vượt ra ngoài phạm vi một tiến trình và bắt đầu trở nên phân tán, việc lần theo một lời gọi đi từ máy chủ web front-end tới toàn bộ các back-end cho tới khi phản hồi được trả về cho người dùng trở nên khó hơn nhiều. Đây chính là lúc distributed tracing đóng vai trò lớn trong việc đo đạc và phân tích hệ thống production của bạn.

Distributed tracing (tracing phân tán) là cách gắn mã đo đạc để phân tích độ trễ suốt vòng đời của một yêu cầu người dùng. Khi hệ thống đã phân tán và khi các công cụ profiling cùng gỡ lỗi thông thường không còn đáp ứng nổi quy mô, bạn có thể muốn dùng công cụ distributed tracing để phân tích hiệu năng của các yêu cầu người dùng và các lời gọi RPC.

Distributed tracing cho phép chúng ta:

  • Gắn mã đo đạc và profiling độ trễ của ứng dụng trong một hệ thống lớn.
  • Theo dõi toàn bộ các RPC trong vòng đời của một yêu cầu người dùng và thấy được những vấn đề tích hợp chỉ bộc lộ trong môi trường production.
  • Tìm ra những cải thiện hiệu năng có thể áp dụng cho hệ thống của chúng ta. Nhiều điểm nghẽn không hề lộ rõ trước khi thu thập dữ liệu tracing.

Hệ sinh thái Go cung cấp nhiều thư viện distributed tracing khác nhau, có loại gắn với một hệ thống tracing cụ thể, có loại không phụ thuộc backend.

Có cách nào tự động chặn mọi lời gọi hàm và tạo trace không?

Go không cung cấp cách tự động chặn mọi lời gọi hàm để tạo span cho trace. Bạn phải tự gắn mã đo đạc để tạo, kết thúc và chú giải các span.

Tôi nên truyền các header của trace trong thư viện Go như thế nào?

Bạn có thể truyền các định danh và nhãn của trace trong context.Context. Hiện ngành vẫn chưa có khoá trace chuẩn mực hay cách biểu diễn chung cho các header của trace. Mỗi nhà cung cấp tracing có trách nhiệm cung cấp các tiện ích truyền tải trong thư viện Go của họ.

Còn những sự kiện cấp thấp nào khác từ thư viện chuẩn hoặc runtime có thể đưa vào một trace?

Thư viện chuẩn và runtime đang cố gắng cung cấp thêm một số API để thông báo về các sự kiện nội bộ ở mức thấp. Ví dụ, httptrace.ClientTrace cung cấp các API để theo dõi những sự kiện cấp thấp trong vòng đời của một yêu cầu đi ra ngoài. Hiện có một nỗ lực đang tiến hành nhằm lấy các sự kiện runtime cấp thấp từ execution tracer của runtime và cho phép người dùng tự định nghĩa cũng như ghi lại sự kiện của riêng họ.

Gỡ lỗi

Gỡ lỗi là quá trình xác định vì sao một chương trình hoạt động sai. Debugger cho phép chúng ta hiểu luồng thực thi và trạng thái hiện tại của chương trình. Có nhiều kiểu gỡ lỗi; phần này chỉ tập trung vào việc gắn một debugger vào chương trình và gỡ lỗi từ core dump.

Người dùng Go chủ yếu dùng những debugger sau:

  • Delve: Delve là debugger dành cho ngôn ngữ lập trình Go. Nó hỗ trợ các khái niệm của runtime Go và các kiểu dữ liệu dựng sẵn. Delve hướng tới việc trở thành một debugger đầy đủ tính năng và đáng tin cậy cho chương trình Go.
  • GDB: Go hỗ trợ GDB thông qua trình biên dịch Go tiêu chuẩn và Gccgo. Cách quản lý stack, luồng và runtime có những khía cạnh khác biệt đủ nhiều so với mô hình thực thi mà GDB mong đợi, tới mức chúng có thể làm debugger rối loạn, ngay cả khi chương trình được biên dịch bằng gccgo. Dù GDB có thể dùng để gỡ lỗi chương trình Go, nó không phải lựa chọn lý tưởng và có thể gây nhầm lẫn.

Debugger làm việc tốt tới đâu với chương trình Go?

Trình biên dịch gc thực hiện các tối ưu hoá như inline hàm và đưa biến vào thanh ghi. Những tối ưu hoá này đôi khi khiến việc gỡ lỗi bằng debugger khó hơn. Hiện có một nỗ lực đang tiến hành nhằm nâng cao chất lượng thông tin DWARF được sinh ra cho các binary đã tối ưu. Cho tới khi những cải thiện đó sẵn sàng, chúng tôi khuyến nghị tắt tối ưu hoá khi build đoạn mã đang cần gỡ lỗi. Câu lệnh sau build một package mà không bật tối ưu hoá của trình biên dịch:

$ go build -gcflags=all="-N -l"

Là một phần của nỗ lực cải thiện đó, Go 1.10 giới thiệu cờ biên dịch mới -dwarflocationlists. Cờ này khiến trình biên dịch bổ sung các danh sách vị trí (location list) giúp debugger làm việc được với binary đã tối ưu. Câu lệnh sau build một package có tối ưu hoá nhưng kèm các location list của DWARF:

$ go build -gcflags="-dwarflocationlists=true"

Giao diện người dùng nào được khuyến nghị cho debugger?

Dù cả delve lẫn gdb đều có giao diện dòng lệnh, phần lớn các tích hợp trong trình soạn thảo và các IDE đều cung cấp giao diện người dùng dành riêng cho việc gỡ lỗi.

Có thể gỡ lỗi hậu kiểm (postmortem) với chương trình Go không?

Tệp core dump là tệp chứa bản kết xuất bộ nhớ của một tiến trình đang chạy cùng trạng thái tiến trình đó. Nó chủ yếu được dùng để gỡ lỗi hậu kiểm một chương trình và để hiểu trạng thái của nó khi nó vẫn đang chạy. Hai trường hợp này khiến việc gỡ lỗi core dump trở thành công cụ chẩn đoán tốt để phân tích hậu kiểm các dịch vụ production. Bạn hoàn toàn có thể lấy tệp core từ chương trình Go rồi dùng delve hoặc gdb để gỡ lỗi, xem trang core dump debugging để có hướng dẫn từng bước.

Thống kê và sự kiện của runtime

Runtime cung cấp các thống kê và báo cáo về sự kiện nội bộ để người dùng chẩn đoán vấn đề hiệu năng và mức tận dụng tài nguyên ở tầng runtime.

Người dùng có thể giám sát những thống kê này để hiểu rõ hơn tình trạng sức khoẻ tổng thể và hiệu năng của chương trình Go. Một số thống kê và trạng thái thường được giám sát:

  • runtime.ReadMemStats báo cáo các chỉ số liên quan tới việc cấp phát trên heap và garbage collection. Thống kê bộ nhớ hữu ích để giám sát một tiến trình đang tiêu thụ bao nhiêu tài nguyên bộ nhớ, tiến trình có tận dụng bộ nhớ tốt hay không, và để phát hiện rò rỉ bộ nhớ.
  • debug.ReadGCStats đọc thống kê về garbage collection. Nó hữu ích để thấy bao nhiêu tài nguyên bị tiêu tốn cho các lần tạm dừng của GC. Nó cũng báo cáo dòng thời gian các lần tạm dừng của garbage collector và các phân vị thời gian tạm dừng.
  • debug.Stack trả về stack trace hiện tại. Stack trace hữu ích để thấy hiện có bao nhiêu goroutine đang chạy, chúng đang làm gì, và chúng có bị chặn hay không.
  • debug.WriteHeapDump tạm dừng việc thực thi của mọi goroutine và cho phép bạn kết xuất heap ra một tệp. Bản kết xuất heap là ảnh chụp bộ nhớ của một tiến trình Go tại một thời điểm. Nó chứa mọi đối tượng đã cấp phát cùng các goroutine, finalizer và nhiều thứ khác.
  • runtime.NumGoroutine trả về số goroutine hiện tại. Giá trị này có thể được giám sát để xem đã tận dụng đủ goroutine hay chưa, hoặc để phát hiện rò rỉ goroutine.

Execution tracer

Go đi kèm một execution tracer của runtime để ghi lại nhiều loại sự kiện runtime. Việc lập lịch, syscall, các lần garbage collection, kích thước heap và nhiều sự kiện khác được runtime thu thập và có thể trực quan hoá bằng go tool trace. Execution tracer là công cụ để phát hiện vấn đề về độ trễ và mức tận dụng tài nguyên. Bạn có thể xem xét CPU được tận dụng tốt tới đâu, và khi nào hoạt động mạng hay syscall là nguyên nhân khiến goroutine bị chiếm quyền thực thi.

Tracer hữu ích để:

  • Hiểu các goroutine của bạn thực thi ra sao.
  • Hiểu một số sự kiện cốt lõi của runtime, chẳng hạn các lần GC chạy.
  • Nhận diện việc thực thi song song hoá kém.

Tuy nhiên, nó không mạnh trong việc xác định các điểm nóng, ví dụ như phân tích nguyên nhân dùng bộ nhớ hay CPU quá mức. Hãy dùng các công cụ profiling trước để xử lý những vấn đề đó.

Giao diện execution tracer: nhiều goroutine chạy song song lúc đầu rồi dồn về chạy tuần tự vì tranh chấp lock

Ở trên, hình trực quan hoá của go tool trace cho thấy quá trình thực thi khởi đầu ổn, rồi sau đó bị tuần tự hoá. Điều này gợi ý rằng có thể đang có tranh chấp khoá trên một tài nguyên dùng chung, tạo thành điểm nghẽn.

Xem go tool trace để thu thập và phân tích trace của runtime.

GODEBUG

Runtime cũng phát ra các sự kiện và thông tin nếu biến môi trường GODEBUG được đặt tương ứng.

  • GODEBUG=gctrace=1 in ra các sự kiện của garbage collector tại mỗi lần thu gom, tóm tắt lượng bộ nhớ đã thu hồi và độ dài thời gian tạm dừng.
  • GODEBUG=inittrace=1 in ra bản tóm tắt thời gian thực thi và thông tin cấp phát bộ nhớ cho phần việc khởi tạo package đã hoàn tất.
  • GODEBUG=schedtrace=X in ra các sự kiện lập lịch sau mỗi X mili giây.

Biến môi trường GODEBUG có thể được dùng để tắt việc sử dụng các phần mở rộng tập lệnh trong thư viện chuẩn và runtime.

  • GODEBUG=cpu.all=off tắt việc sử dụng mọi phần mở rộng tập lệnh tuỳ chọn.
  • GODEBUG=cpu.extension=off tắt việc sử dụng các lệnh thuộc phần mở rộng tập lệnh được chỉ định.
    extension là tên viết thường của phần mở rộng tập lệnh, chẳng hạn sse41 hay avx.

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.