☰ 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

Gỡ lỗi mã Go bằng GDB

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

Các hướng dẫn sau áp dụng cho toolchain tiêu chuẩn (trình biên dịch Go gc và các công cụ đi kèm). Gccgo có hỗ trợ gdb sẵn có.

Lưu ý rằng Delve là lựa chọn thay thế tốt hơn GDB khi gỡ lỗi chương trình Go được build bằng toolchain tiêu chuẩn. Nó hiểu runtime, cấu trúc dữ liệu và biểu thức của Go tốt hơn GDB. Hiện Delve hỗ trợ Linux, OSX và Windows trên amd64. Để có danh sách nền tảng được hỗ trợ mới nhất, vui lòng xem tài liệu của Delve.

GDB không hiểu rõ chương trình Go. 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 và cho ra kết quả sai ngay cả khi chương trình được biên dịch bằng gccgo. Hệ quả là, dù GDB có thể hữu ích trong một số tình huống (ví dụ gỡ lỗi mã Cgo, hoặc gỡ lỗi chính bản thân runtime), nó không phải là một debugger đáng tin cậy cho chương trình Go, nhất là những chương trình dùng nhiều tính đồng thời. Hơn nữa, việc xử lý những vấn đề này không phải là ưu tiên của dự án Go, vì chúng rất khó.

Nói ngắn gọn, các hướng dẫn dưới đây chỉ nên được xem như chỉ dẫn về cách dùng GDB khi nó hoạt động được, chứ không phải lời bảo đảm thành công. Ngoài phần tổng quan này, bạn có thể muốn tham khảo thêm sách hướng dẫn GDB.

Giới thiệu

Khi bạn biên dịch và liên kết chương trình Go bằng toolchain gc trên Linux, macOS, FreeBSD hay NetBSD, các binary tạo ra sẽ chứa thông tin gỡ lỗi DWARFv4 mà những phiên bản GDB gần đây (≥7.5) có thể dùng để xem xét một tiến trình đang chạy hoặc một core dump.

Truyền cờ '-w' cho linker để bỏ qua thông tin gỡ lỗi (ví dụ, go build -ldflags=-w prog.go).

Mã do trình biên dịch gc sinh ra có bao gồm việc inline các lời gọi 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 gdb khó hơn. Nếu bạn thấy cần tắt các tối ưu hoá đó, hãy build chương trình bằng go build -gcflags=all="-N -l".

Nếu bạn muốn dùng gdb để xem xét một core dump, bạn có thể kích hoạt việc kết xuất khi chương trình sập, trên những hệ thống cho phép điều đó, bằng cách đặt GOTRACEBACK=crash trong môi trường (xem tài liệu package runtime để biết thêm).

Các thao tác thường dùng

  • Hiển thị tệp và số dòng của mã, đặt breakpoint và dịch ngược ra hợp ngữ:

    (gdb) list
    (gdb) list line
    (gdb) list file.go:line
    (gdb) break line
    (gdb) break file.go:line
    (gdb) disas
  • Hiển thị backtrace và lần ngược các khung stack:

    (gdb) bt
    (gdb) frame n
  • Hiển thị tên, kiểu và vị trí trên khung stack của các biến cục bộ, đối số và giá trị trả về:

    (gdb) info locals
    (gdb) info args
    (gdb) p variable
    (gdb) whatis variable
  • Hiển thị tên, kiểu và vị trí của các biến toàn cục:

    (gdb) info variables regexp

Phần mở rộng cho Go

Một cơ chế mở rộng gần đây của GDB cho phép nó nạp các script mở rộng cho một binary cụ thể. Toolchain tận dụng điều này để bổ sung cho GDB một số câu lệnh nhằm xem xét phần bên trong của mã runtime (chẳng hạn các goroutine) và để in đẹp các kiểu dựng sẵn map, slice và channel.

  • In đẹp một string, slice, map, channel hay interface:

    (gdb) p var
  • Hàm $len() và $cap() cho string, slice và map:

    (gdb) p $len(var)
  • Một hàm để ép interface về kiểu động của nó:

    (gdb) p $dtype(var)
    (gdb) iface var

    Vấn đề đã biết: GDB không thể tự động tìm ra kiểu động của một giá trị interface nếu tên dài của nó khác với tên ngắn (khá phiền khi in stack trace, bộ in đẹp sẽ quay về in tên kiểu ngắn kèm một pointer).

  • Xem xét các goroutine:

    (gdb) info goroutines
    (gdb) goroutine n cmd
    (gdb) help goroutine

    Ví dụ:

    (gdb) goroutine 12 bt

    Bạn có thể xem xét toàn bộ goroutine bằng cách truyền all thay cho ID của một goroutine cụ thể. Ví dụ:

    (gdb) goroutine all bt

Nếu bạn muốn xem cơ chế này hoạt động ra sao, hoặc muốn mở rộng nó, hãy xem src/runtime/runtime-gdb.py trong bản phân phối mã nguồn Go. Nó phụ thuộc vào một số kiểu ma thuật đặc biệt (hash<T,U>) và biến (runtime.m và runtime.g) mà linker (src/cmd/link/internal/ld/dwarf.go) bảo đảm là được mô tả trong mã DWARF.

Nếu bạn tò mò thông tin gỡ lỗi trông như thế nào, hãy chạy objdump -W a.out rồi duyệt qua các phần .debug_*.

Các vấn đề đã biết

  1. Việc in đẹp chuỗi chỉ kích hoạt cho kiểu string, chứ không cho các kiểu dẫn xuất từ nó.
  2. Thiếu thông tin về kiểu cho những phần viết bằng C của thư viện runtime.
  3. GDB không hiểu cách định danh theo tên của Go và coi "fmt.Print" là một literal phi cấu trúc chứa dấu "." cần được đặt trong dấu nháy. Nó còn phản ứng mạnh hơn nữa với tên phương thức dạng pkg.(*MyType).Meth.
  4. Kể từ Go 1.11, thông tin gỡ lỗi được nén theo mặc định. Các phiên bản gdb cũ hơn, chẳng hạn bản có sẵn mặc định trên MacOS, không hiểu được cách nén này. Bạn có thể sinh ra thông tin gỡ lỗi không nén bằng cách dùng go build -ldflags=-compressdwarf=false. (Cho tiện, bạn có thể đặt tuỳ chọn -ldflags vào biến môi trường GOFLAGS để khỏi phải chỉ định nó mỗi lần.)

Hướng dẫn thực hành

Trong hướng dẫn này chúng ta sẽ xem xét binary của bộ unit test thuộc package regexp. Để build binary đó, hãy chuyển sang thư mục $GOROOT/src/regexp rồi chạy go test -c. Lệnh này sẽ tạo ra một tệp thực thi tên là regexp.test.

Bắt đầu

Khởi chạy GDB để gỡ lỗi regexp.test:

$ gdb regexp.test
GNU gdb (GDB) 7.2-gg8
Copyright (C) 2010 Free Software Foundation, Inc.
License GPLv  3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
Type "show copying" and "show warranty" for licensing/warranty details.
This GDB was configured as "x86_64-linux".

Reading symbols from  /home/user/go/src/regexp/regexp.test...
done.
Loading Go Runtime support.
(gdb)

Thông báo “Loading Go Runtime support” nghĩa là GDB đã nạp phần mở rộng từ $GOROOT/src/runtime/runtime-gdb.py.

Để giúp GDB tìm được mã nguồn runtime của Go và script hỗ trợ đi kèm, hãy truyền $GOROOT của bạn qua cờ '-d':

$ gdb regexp.test -d $GOROOT

Nếu vì lý do nào đó GDB vẫn không tìm được thư mục hay script đó, bạn có thể nạp nó thủ công bằng cách báo cho gdb (giả sử bạn có mã nguồn go trong ~/go/):

(gdb) source ~/go/src/runtime/runtime-gdb.py
Loading Go Runtime support.

Xem xét mã nguồn

Dùng câu lệnh "l" hoặc "list" để xem xét mã nguồn.

(gdb) l

Liệt kê một phần cụ thể của mã nguồn bằng cách truyền cho "list" một tên hàm (tên này phải kèm theo tên package của nó).

(gdb) l main.main

Liệt kê theo tệp và số dòng cụ thể:

(gdb) l regexp.go:1
(gdb) # Hit enter to repeat last command. Here, this lists next 10 lines.

Cách đặt tên

Tên biến và tên hàm phải kèm theo tên của package mà chúng thuộc về. Hàm Compile của package regexp được GDB biết đến dưới tên 'regexp.Compile'.

Các phương thức phải kèm theo tên kiểu receiver của chúng. Ví dụ, phương thức String của kiểu *Regexp được biết đến dưới tên 'regexp.(*Regexp).String'.

Những biến che khuất biến khác sẽ được tự động thêm hậu tố là một con số trong thông tin gỡ lỗi. Các biến được closure tham chiếu sẽ xuất hiện dưới dạng pointer có tiền tố tự động là ‘&’.

Đặt breakpoint

Đặt một breakpoint tại hàm TestFind:

(gdb) b 'regexp.TestFind'
Breakpoint 1 at 0x424908: file /home/user/go/src/regexp/find_test.go, line 148.

Chạy chương trình:

(gdb) run
Starting program: /home/user/go/src/regexp/regexp.test

Breakpoint 1, regexp.TestFind (t=0xf8404a89c0) at /home/user/go/src/regexp/find_test.go:148
148	func TestFind(t *testing.T) {

Quá trình thực thi đã tạm dừng tại breakpoint. Hãy xem những goroutine nào đang chạy và chúng đang làm gì:

(gdb) info goroutines
  1  waiting runtime.gosched
* 13  running runtime.goexit

goroutine được đánh dấu * là goroutine hiện tại.

Xem xét stack

Hãy nhìn vào stack trace tại chỗ chúng ta đã tạm dừng chương trình:

(gdb) bt  # backtrace
#0  regexp.TestFind (t=0xf8404a89c0) at /home/user/go/src/regexp/find_test.go:148
#1  0x000000000042f60b in testing.tRunner (t=0xf8404a89c0, test=0x573720) at /home/user/go/src/testing/testing.go:156
#2  0x000000000040df64 in runtime.initdone () at /home/user/go/src/runtime/proc.c:242
#3  0x000000f8404a89c0 in ?? ()
#4  0x0000000000573720 in ?? ()
#5  0x0000000000000000 in ?? ()

goroutine còn lại, số 1, đang kẹt trong runtime.gosched, bị chặn ở một thao tác nhận từ channel:

(gdb) goroutine 1 bt
#0  0x000000000040facb in runtime.gosched () at /home/user/go/src/runtime/proc.c:873
#1  0x00000000004031c9 in runtime.chanrecv (c=void, ep=void, selected=void, received=void)
 at  /home/user/go/src/runtime/chan.c:342
#2  0x0000000000403299 in runtime.chanrecv1 (t=void, c=void) at/home/user/go/src/runtime/chan.c:423
#3  0x000000000043075b in testing.RunTests (matchString={void (struct string, struct string, bool *, error *)}
 0x7ffff7f9ef60, tests=  []testing.InternalTest = {...}) at /home/user/go/src/testing/testing.go:201
#4  0x00000000004302b1 in testing.Main (matchString={void (struct string, struct string, bool *, error *)}
 0x7ffff7f9ef80, tests= []testing.InternalTest = {...}, benchmarks= []testing.InternalBenchmark = {...})
at /home/user/go/src/testing/testing.go:168
#5  0x0000000000400dc1 in main.main () at /home/user/go/src/regexp/_testmain.go:98
#6  0x00000000004022e7 in runtime.mainstart () at /home/user/go/src/runtime/amd64/asm.s:78
#7  0x000000000040ea6f in runtime.initdone () at /home/user/go/src/runtime/proc.c:243
#8  0x0000000000000000 in ?? ()

Khung stack cho thấy hiện chúng ta đang thực thi hàm regexp.TestFind, đúng như mong đợi.

(gdb) info frame
Stack level 0, frame at 0x7ffff7f9ff88:
 rip = 0x425530 in regexp.TestFind (/home/user/go/src/regexp/find_test.go:148);
    saved rip 0x430233
 called by frame at 0x7ffff7f9ffa8
 source language minimal.
 Arglist at 0x7ffff7f9ff78, args: t=0xf840688b60
 Locals at 0x7ffff7f9ff78, Previous frame's sp is 0x7ffff7f9ff88
 Saved registers:
  rip at 0x7ffff7f9ff80

Câu lệnh info locals liệt kê mọi biến cục bộ của hàm cùng giá trị của chúng, nhưng hơi nguy hiểm khi dùng, vì nó cũng sẽ cố in cả những biến chưa được khởi tạo. Các slice chưa khởi tạo có thể khiến gdb cố in ra những mảng lớn tuỳ ý.

Các đối số của hàm:

(gdb) info args
t = 0xf840688b60

Khi in đối số, hãy để ý rằng nó là pointer trỏ tới một giá trị Regexp. Lưu ý GDB đã đặt sai dấu * sang phía bên phải tên kiểu và tự bịa ra từ khoá ‘struct’, theo phong cách C truyền thống.

(gdb) p re
(gdb) p t
$1 = (struct testing.T *) 0xf840688b60
(gdb) p t
$1 = (struct testing.T *) 0xf840688b60
(gdb) p *t
$2 = {errors = "", failed = false, ch = 0xf8406f5690}
(gdb) p *t->ch
$3 = struct hchan<*testing.T>

Cái struct hchan<*testing.T> kia chính là cách biểu diễn nội bộ của runtime cho một channel. Hiện nó đang rỗng, nếu không gdb đã in đẹp nội dung bên trong.

Bước tiếp:

(gdb) n  # execute next line
149             for _, test := range findTests {
(gdb)    # enter is repeat
150                     re := MustCompile(test.pat)
(gdb) p test.pat
$4 = ""
(gdb) p re
$5 = (struct regexp.Regexp *) 0xf84068d070
(gdb) p *re
$6 = {expr = "", prog = 0xf840688b80, prefix = "", prefixBytes =  []uint8, prefixComplete = true,
  prefixRune = 0, cond = 0 '\000', numSubexp = 0, longest = false, mu = {state = 0, sema = 0},
  machine =  []*regexp.machine}
(gdb) p *re->prog
$7 = {Inst =  []regexp/syntax.Inst = {{Op = 5 '\005', Out = 0, Arg = 0, Rune =  []int}, {Op =
    6 '\006', Out = 2, Arg = 0, Rune =  []int}, {Op = 4 '\004', Out = 0, Arg = 0, Rune =  []int}},
  Start = 1, NumCap = 2}

Chúng ta có thể bước vào bên trong lời gọi hàm String bằng "s":

(gdb) s
regexp.(*Regexp).String (re=0xf84068d070, noname=void) at /home/user/go/src/regexp/regexp.go:97
97      func (re *Regexp) String() string {

Lấy stack trace để xem chúng ta đang ở đâu:

(gdb) bt
#0  regexp.(*Regexp).String (re=0xf84068d070, noname=void)
    at /home/user/go/src/regexp/regexp.go:97
#1  0x0000000000425615 in regexp.TestFind (t=0xf840688b60)
    at /home/user/go/src/regexp/find_test.go:151
#2  0x0000000000430233 in testing.tRunner (t=0xf840688b60, test=0x5747b8)
    at /home/user/go/src/testing/testing.go:156
#3  0x000000000040ea6f in runtime.initdone () at /home/user/go/src/runtime/proc.c:243
....

Xem mã nguồn:

(gdb) l
92              mu      sync.Mutex
93              machine []*machine
94      }
95
96      // String returns the source text used to compile the regular expression.
97      func (re *Regexp) String() string {
98              return re.expr
99      }
100
101     // Compile parses a regular expression and returns, if successful,

In đẹp

Cơ chế in đẹp của GDB được kích hoạt bởi các so khớp biểu thức chính quy trên tên kiểu. Một ví dụ với slice:

(gdb) p utf
$22 =  []uint8 = {0 '\000', 0 '\000', 0 '\000', 0 '\000'}

Vì slice, mảng và chuỗi không phải là pointer kiểu C, GDB không thể diễn giải phép lấy phần tử theo chỉ số giúp bạn, nhưng bạn có thể nhìn vào cách biểu diễn bên trong của runtime để làm việc đó (tính năng tự hoàn thành bằng phím tab giúp ích ở đây):

(gdb) p slc
$11 =  []int = {0, 0}
(gdb) p slc-><TAB>
array  slc    len
(gdb) p slc->array
$12 = (int *) 0xf84057af00
(gdb) p slc->array[1]
$13 = 0

Các hàm mở rộng $len và $cap hoạt động với chuỗi, mảng và slice:

(gdb) p $len(utf)
$23 = 4
(gdb) p $cap(utf)
$24 = 4

Channel và map là các kiểu ‘tham chiếu’, mà gdb hiển thị như pointer trỏ tới các kiểu giống C++ dạng hash<int,string>*. Việc giải tham chiếu sẽ kích hoạt cơ chế in đẹp

Interface được runtime biểu diễn bằng một pointer trỏ tới bộ mô tả kiểu và một pointer trỏ tới giá trị. Phần mở rộng runtime GDB của Go giải mã điều này và tự động kích hoạt việc in đẹp cho kiểu tại runtime. Hàm mở rộng $dtype giải mã kiểu động giúp bạn (các ví dụ được lấy từ một breakpoint tại dòng 293 của regexp.go.)

(gdb) p i
$4 = {str = "cbb"}
(gdb) whatis i
type = regexp.input
(gdb) p $dtype(i)
$26 = (struct regexp.inputBytes *) 0xf8400b4930
(gdb) iface i
regexp.input: struct regexp.inputBytes *

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.