Saturday, October 15, 2016

Bài 24: Gỡ lỗi ứng dụng

Việc sử dụng go test giúp chúng ta xác định tính đúng đắn của một hàm. Khi một hàm không vượt qua được việc kiểm thử, chúng ta cần phải xác định nguyên nhân để sửa lại cho đúng. Có rất nhiều cách khác nhau để xác định lỗi nhưng hai cách phổ biến nhất là xuất giá trị nghi ngờ ra màn hình và sử dụng công cụ gỡ lỗi (debuger).

Xuất giá trị nghi ngờ


Khi một hàm lỗi, chúng ta thường xem xét lại logic xử lý. Đôi lúc, lỗi chỉ được phát hiện khi thực thi vì giá trị biến đổi theo ngữ cảnh khiến chúng ta không lường hết được. Trong trường hợp đó chúng ta cần nắm giá trị các biến liên quan để xác định chính xác chỗ bị sai logic. Để biết giá trị biến qua các bước xử lý, chúng ta sẽ sử dụng cách in giá trị biến đó ra màn hình bằng các hàm Print của package fmt:
func Print(a ...interface{}) (n int, err error)
func Printf(format string, a ...interface{}) (n int, err error)
func Println(a ...interface{}) (n int, err error)
- Hàm Println in ra các giá trị được khai báo liên tiếp nhau trong các tham số rồi xuống hàng. Đây là hàm thông dụng nhất khi cần hiển thị giá trị lên màn hình. 
- Khi muốn hiển thị có định dạng đẹp hơn chúng ta có thể sử dụng Printf. 
- Hàm Print tương tự Println nhưng không xuống hàng nên ít khi được sử dụng trong trường hợp này do chúng ta đang cần hiển thị rõ ràng để dễ tìm ra lỗi. 

Một số bạn sẽ thắc mắc package log cũng cung cấp mấy hàm này mà, tại sao không sử dụng? Thực ra sử dụng các hàm Print trong log cũng được nhưng log xuất ra thêm thời gian và chúng ta không cần đến chúng lắm. Các hàm log phù hợp cho việc ghi lỗi xuống file để chúng ta xem lại khi ứng dụng đã chạy thực tế. Log cung cấp hàm SetOutput xác định nguồn ghi ra, fmt chỉ xuất ra màn hình.

Trong ví dụ ở bài trước về hàm xác định chuỗi đối xứng, khi test với chuỗi "été" lại bị FAIL. Để xác định nguyên nhân, chúng ta thêm dòng in giá trị s[i] và s[len(s)-1-i] lên màn hình như dòng 4 dưới đây:
func IsPalindrome(s string) bool {
    n := len(s) / 2
    for i := 0; i <= n; i++ {
        fmt.Println(s[i], s[len(s)-1-i])
        if s[i] != s[len(s)-1-i] {
            return false
        }
    }
    return true
}
Lúc này khi chạy chúng ta có giá trị sau xuất ra: 195 169. Cả 2 giá trị đều không phải chữ é như chúng ta nghĩ. Nếu sửa lại mã nguồn cho chúng tiếp tục xuất ra ta sẽ thấy ra cặp 169 195 rồi 116 116. 116 đúng là mã chữ t rồi. Vậy 195 và 169 sao lại là mã của é? Lúc này chúng ta mới sực nhớ ra là chuỗi được Go mặc định mã hóa UTF-8. Mà ở UTF-8, é được tách thành 2 byte có giá trị lần lượt là 195 và 169. Như vậy, hàm IsPalindrome ở trên fail do nguyên nhân này. Để so sánh đúng, mỗi phần tử so sánh chứa đúng giá trị một ký tự. Để làm được chuyện đó, chúng ta cần chuyển về slice []rune như sau:
func IsPalindrome(s string) bool {
    s1 := []rune(s)
    n := len(s1) / 2
    for i := 0; i <= n; i++ {
        fmt.Println(s1[i], s1[len(s1)-1-i])
        if s1[i] != s1[len(s1)-1-i] {
            return false
        }
    }
    return true
}
Kết quả xuất ra màn hình khi chạy cho chúng ta kết quả như ý:
233 233
116 116

Gỡ lỗi với GDB


GDB do FSF (Free Software Foundation) phát triển, là công cụ dùng để gỡ lỗi (debug) các ngôn ngữ biên dịch như C/C++, Pascal, Ada, Go, v.v... trên môi trường họ Unix. GDB là Gnu DeBuger chứ không phải Go DeBuger và nó cũng không phải là trình gỡ lỗi lý tưởng cho Go. GDB không hiểu ứng dụng Go cho lắm nhất là ở quản lý stack, các kiểu dữ liệu riêng của Go hay thread, ... 

GDB không có sẵn khi cài Go nên chúng ta cần cài thêm như sau (không hỗ trợ Windows):
- Tải file mã tại đây. Lưu ý GDB phiên bản > 7.1 mới hỗ trợ gỡ lỗi Go.
- Xả nén thành thư mục gdb-x.yz với x.yz là phiên bản gdb, hiện tại là 7.12.
- Tạo thư mục build trong thư mục gdb-x.yz và chuyển đến thư mục build.
- Thực hiện ../configure
- Thực hiện make
- Thực hiện make install với quyền root. (Trong file readme của gói tải về hướng dẫn chép thư mục chương trình gdb/gdb vào /usr/local/bin. Việc này cũng được nhưng khi chạy gdb sẽ bị lỗi ở 1 số chỗ).

Kiểm tra việc cài đặt bằng cách chạy: gdb, nếu đúng, GDB sẽ thực thi, thông báo phiên bản, các thông tin liên quan và hiển thị dấu nhắc sau gdb: gdb>_. Thoát dòng lệnh gdb bằng cách gõ quit hoặc q.

GDB cung cấp các lệnh sau phục vụ cho việc debug:
- run: thực thi ứng dụng. Quá trinh gỡ lỗi bắt đầu sau khi gọi lệnh này.
- list hoặc l: hiển thị 10 dòng mã nguồn trong đó dòng nêu trong lệnh nằm trung tâm. Ví dụ l 10 sẽ hiển thị từ dòng 5 đến 14. Muốn hiển thị ở file khác, ta dùng list <tên file .go>:<số dòng>
- break hoặc b: đặt điểm dừng (breakpoint), có tham số là dòng đặt breakpoint. Ví dụ b 23 sẽ đặt ở dòng 23.
- delete hoặc d: xóa breakpoint đã đặt trước đó nếu đó, tham số là thứ tự breakpoint đã đặt, bắt đầu từ 1. Có thể xem danh sách breakpoint với lệnh info breakpoints. Ví dụ d 1 sẽ bỏ breakpoint đã đặt ở dòng 23 như ví dụ bên trên.
- backtrace hoặc bt: in chi tiết thực hiện chương trình.
- info: dùng để xuất thông tin cần xem, tùy theo tham số:
 + info locals: hiển thị danh sách các biến và giá trị tại thời điểm đó
 + info breakpoints: hiển thị danh sách các breakpoint
 + info goroutines: hiển thị danh sách các goroutine
- print hoặc p: hiển thị các biến hoặc thông tin liên quan. Ví dụ p $len(s) sẽ hiển thị chiều dài chuỗi s.
- whatis: hiển thị kiểu dữ liệu. Ví dụ whatis s sẽ hiển thị type = struct string
- next hoặc n: nhảy đến bước thực thi kế tiếp.
- continue hoặc c: nhảy qua breakpoint và tiếp tục thực thi. Tham số kèm theo là số lần nhảy qua breakpoint. Mặc định là 1.
- set variable: gán giá trị mới cho biến. Cú pháp: set variable <biến>=<giá trị>

Để có thể sử dụng GDB gỡ lỗi ứng dụng, ứng dụng cần biên dịch với tham số -gcflags "-N -l" để tránh việc bị loại bỏ các thông tin debug.

Chúng ta cùng thử dùng GDB để gỡ lỗi cho hàm IsPalindrome ở trên như sau:
01    package main
02    
03    import (
04        "fmt"
05    )
06    
07    func IsPalindrome(s string) bool {
08        n := len(s) / 2
09        for i := 0; i <= n; i++ {
10            if s[i] != s[len(s)-1-i] {
11                return false
12            }
13        }
14        return true
15    }
16    
17    func main() {
18        s := "été"
19        if IsPalindrome(s) {
20            fmt.Println(s, "là chuỗi đối xứng")
21        } else {
22            fmt.Println(s, "không phải chuỗi đối xứng")
23        }
24    }
- Đầu tiên, biên dịch file debugo.go ở trên với:
go build -gcflags "-N -l" debugo.go 
- Tiếp theo sử dụng gdb để bắt đầu gỡ rối:
gdb debugo
- Bắt đầu gỡ lỗi với GDB như sau:
 + Mấu chốt của hàm IsPalindrome là ở dòng so sánh nên ta đặt điểm dừng ở đó để xem xét nên đầu tiên dùng list để xem số dòng của nó và chúng ta biết nó ở dòng 10.
(gdb) list
3    import (
4    "fmt"
5)
6
7    func IsPalindrome(s string) bool {
8        n := len(s) / 2
9        for i := 0; i <= n; i++ {
10          if s[i] != s[len(s)-1-i] {
11              return false
12          }
 + Tiếp theo đặt breakpoint tại đó với lệnh b 10. Lệnh run giúp thực thi ứng dụng và nó sẽ dừng tại dòng 10 nơi ta đã đặt breakpoint.
(gdb) b 10
Breakpoint 1 at 0x40105c: file /home/local/golang/src/debugo/debugo.go, line 10.
(gdb) run
Starting program: /home/local/golang/src/debugo/debugo
...
Thread 1 "debugo" hit Breakpoint 1, main.IsPalindrome (s=..., ~r1=false) at /home/local/golang/src/debugo/debugo.go:10
10     if s[i] != s[len(s)-1-i] {
+ Ta gọi info locals để lấy các biến cục bộ n và i. Với p s ta có giá trị của s. Tuy nhiên khi gọi p s[i] thì GDB không hiển thị được kết quả cho chúng ta. s là kiểu string như chúng ta thấy qua lệnh whatis. Đây là giới hạn của GDB như đã nói bên trên. Do string không phải kiểu con trỏ C nên GDB không xử lý bên trong được.
(gdb) info locals
n = 2
i = 0
(gdb) p s
$1 = 0x4a4f94 "été"
(gdb) p s[i]
Structure has no component named operator[].
(gdb) whatis s
type = struct string
+ Lệnh n nhảy đến câu lệnh kế còn c thực thi ứng dụng tiếp tục cho đến khi gặp điểm dừng tiếp hoặc kết thúc chương trình:
(gdb) n
11 return false
(gdb) c
Continuing.
été không phải chuỗi đối xứng
...
[Inferior 1 (process 13870) exited normally]
Các thông tin các về GDB cho Go, các bạn có thể tìm hiểu thêm tại đây.


Gỡ lỗi với Delve


Delve là trình gỡ lỗi riêng cho Go gọn nhẹ và đơn giản sử dụng, có nhiều lệnh tương tự như GDB. Delve được cài đặt qua go get github.com/derekparker/delve/cmd/dlv . Lưu ý để sử dụng Delve cần Go phiên bản trên 1.5. File thực thi dlv (dlv.exe trên Windows) nằm trong thư mục {GO_PATH}/bin.

Để dùng Delve debug, tại thư mục chứa file nguồn, thay vì gọi go build, chúng ta gọi {GO_PATH}/bin/dlv debug. Ngoài ra Delve hỗ trợ một số cách tương tác khác với ứng dụng để thực hiện gỡ lỗi:
- dlv test: Biên dịch gói test và thực hiện gỡ lỗi.
- dlv attach <pid>: Gắn vào tiến trình đang chạy và thực hiện gỡ lỗi. Tham số kèm theo là id của tiến trình: pid
- dlv exec <file thực thi>: Thực hiện gỡ lỗi với chương trình đã biên dịch. Tham số là đường dẫn đến file thực thi.

Một số lệnh phổ biến để gỡ lỗi với Delve:
- list hoặc ls: hiển thị 10 dòng mã nguồn trong đó dòng nêu trong lệnh nằm trung tâm. Khác với GDB, vị trí ở Delve dựa trên hàm. Ví dụ để hiển thị code bắt đầu hàm main, ta dùng ls main.main.
- break hoặc b: đặt điểm dừng (breakpoint), có tham số là dòng đặt breakpoint. Dòng đặt breakpoint cũng dựa theo hàm hoặc dòng tính từ hàm như sau: b main.main:2 sẽ đặt breakpoint tại dòng thứ 2 tính từ đầu hàm main.
- clear: xóa breakpoint đã đặt trước đó nếu đó, tham số là nơi đặt breakpoint hoặc id của breakpoint, bắt đầu từ 1. Có thể xem danh sách breakpoint với lệnh breakpoints.
- clearall: xóa mọi breakpoint,
- condition hoặc cond: đặt breakpoint kèm điều kiện: condition <tên hoặc id breakpoint> <giá trị bool>, nếu giá trị là true, breakpoint sẽ được đặt.
- locals: hiển thị danh sách các biến và giá trị tại thời điểm đó.
- breakpoints: hiển thị danh sách các breakpoint.
- goroutines: hiển thị danh sách các goroutine.
- print hoặc p: hiển thị các biến hoặc thông tin liên quan. Ví dụ p $len(s) sẽ hiển thị chiều dài chuỗi s.
- next hoặc n: nhảy đến bước thực thi kế tiếp.
- continue hoặc c: nhảy qua breakpoint và tiếp tục thực thi. Tham số kèm theo là số lần nhảy qua breakpoint. Mặc định là 1.
- set variable: gán giá trị mới cho biến, chỉ có tác dụng với kiểu số và con trỏ. Cú pháp: set variable <biến>=<giá trị>
 Ngoài ra có nhiều lệnh khác. Chi tiết xem tại đây

Gỡ lỗi với Delve dễ dàng hơn so với GDB:
  + Các lệnh ở Delve phải gắn với hàm nên ở đây sau khi gọi list IsPalindrome dù biết dòng so sánh ở dòng 10 nhưng gọi b 10 sẽ bị lỗi mà chúng ta phải gọi lệnh break  kèm hàm như sau:
(dlv) ls IsPalindrome
2:
3:    import (
4:        "fmt"
5:    )
6:
7:    func IsPalindrome(s string) bool {
8:        n := len(s) / 2
9:        for i := 0; i <= n; i++ {
10:          if s[i] != s[len(s)-1-i] {
11:              return false
12:          }
(dlv) b 10
Command failed: could not determine current location (scope is nil)
(dlv) b IsPalindrome:3
Breakpoint 2 set at 0x4010a3 for main.IsPalindrome() c:/Hung/Go/src/debugo/debugo.go:10
 + Lệnh c sẽ thực hiện ứng dụng đến dấu breakpoint ở dòng 10. Các lệnh hiển thị biến được thực hiện dễ dàng ở Delve nên ta biết được lý do lỗi:
(dlv) c
> main.IsPalindrome() c:/Hung/Go/src/debugo/debugo.go:10 (hits goroutine(1):1 total:1) (PC: 0x4010a3)
5: )
6:
7:    func IsPalindrome(s string) bool {
8:        n := len(s) / 2
9:        for i := 0; i <= n; i++ {
=> 10:     if s[i] != s[len(s)-1-i] {
11:               return false
12:           }
13:       }
14:       return true
15:    }
(dlv) locals
n = 2
i = 0
(dlv) p s
"été"
(dlv) p s[i]
195
(dlv) p s[len(s)-1-i]
169
 + Lệnh c tiếp theo sẽ thực thi chương trình cho đến hết:
(dlv) c
été không phải chuỗi đối xứng
Process 1908 has exited with status 0

Gỡ lỗi với IDE


Một số IDE như LiteIDE, Zeus, Visual Studio, IntelliJ IDEA, ... hỗ trợ debug trên môi trường phát triển. Do tôi dùng LiteIDE nên sẽ đề cập trên LiteIDE.

LiteIDE sử dụng hỗ trợ debug cả bằng GDB và Delve:
- Chọn menu Debug trong IDE để thay đổi trình gỡ lỗi GDB hay Delve.
- Bắt đầu gỡ lỗi với F5 hoặc gỡ lỗi test với F6.
- F9: đặt breakpoint tại dòng hiện hành.
- F11 nhảy vào hàm tại dòng thực thi, F10 là nhảy qua hàm trong khi Shift+F11 là nhảy ra khỏi hàm đang thực thi.
- Tiếp tục thực thi với F5 còn Shift+F5 sẽ ngưng gỡ lỗi.
- Các biến có thể xem ở cửa sổ bên dưới.

Việc gỡ lỗi và kiểm thử là hai việc đan xen vào nhau và chỉ kết thúc khi chúng ta không còn tìm thấy lỗi nữa. Lúc này ứng dụng đã có thể đưa vào sử dụng. Tuy nhiên điều này không có nghĩa là ứng dụng đã hết lỗi. Ở những nơi dễ xảy ra lỗi, nhất là có trả về biến error, chúng ta nên dùng các hàm Print, Fatal hay Panic của log để ghi chú lại lỗi giúp xem lại sau và thực thi các tác vụ phù hợp.

Qua 24 bài, tôi hy vọng các bạn đã nắm được kha khá các kiến thức cơ bản về Go. Việc tìm hiểu về Go xin tạm dừng ở đây. Trong loạt bài tới tôi sẽ đề cập đến lập trình dịch vụ web với ngôn ngữ Go.
Tóm tắt:
- Cách phát hiện lỗi nhanh nhất là dùng các hàm in ra màn hình: fmt.Println hay fmt.Printf.
- Có 2 trình gỡ lỗi thông dụng là GDB và Delve.
- GDB là trình gỡ lỗi chung cho các ngôn ngữ biên dịch nên không hỗ trợ tốt cho Go. Không hỗ trợ Windows.
- Delve viết riêng cho Go nên hỗ trợ tốt hơn, cài đặt và thực thi cũng đơn giản hơn. Hỗ trợ trên cả Windows và Linux, Mac

No comments:

Post a Comment