> For the complete documentation index, see [llms.txt](https://willh.gitbook.io/build-web-application-with-golang-zhtw/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://willh.gitbook.io/build-web-application-with-golang-zhtw/11.0/11.2.md).

# 使用 GDB 除錯

開發程式過程中除錯程式碼是開發者經常要做的一件事情，Go 語言不像 PHP、Python 等動態語言，只要修改不需要編譯就可以直接輸出，而且可以動態的在執行環境下列印資料。當然 Go 語言也可以透過 Println 之類別的列印資料來除錯，但是每次都需要重新編譯，這是一件相當麻煩的事情。我們知道在 Python 中有 pdb/ipdb 之類別的工具除錯，Javascript 也有類似工具，這些工具都能夠動態的顯示變數資訊，單步除錯等。不過慶幸的是 Go 也有類似的工具支援：GDB。Go 內部已經內建支援了 GDB，所以，我們可以透過 GDB 來進行除錯，那麼本小節就來介紹一下如何透過 GDB 來除錯 Go 程式。

另外建議純 go 程式碼使用[delve](https://github.com/derekparker/delve)可以很好的進行 Go 程式碼除錯

## GDB 除錯簡介

GDB 是 FSF(自由軟體基金會)釋出的一個強大的類別 UNIX 系統下的程式除錯工具。使用 GDB 可以做如下事情：

1. 啟動程式，可以按照開發者的自訂要求執行程式。
2. 可讓被除錯的程式在開發者設定的調置的斷點處停住。（斷點可以是條件表示式）
3. 當程式被停住時，可以檢查此時程式中所發生的事。
4. 動態的改變當前程式的執行環境。

目前支援除錯 Go 程式的 GDB 版本必須大於 7.1。

編譯 Go 程式的時候需要注意以下幾點

1. 傳遞參數-ldflags "-s"，忽略 debug 的列印資訊
2. 傳遞-gcflags "-N -l" 參數，這樣可以忽略 Go 內部做的一些優化，聚合變數和函式等優化，這樣對於 GDB 除錯來說非常困難，所以在編譯的時候加入這兩個參數避免這些優化。

## 常用命令

GDB 的一些常用命令如下所示

* list

  簡寫命令`l`，用來顯示原始碼，預設顯示十行程式碼，後面可以帶上參數顯示的具體行，例如：`list 15`，顯示十行程式碼，其中第 15 行在顯示的十行裡面的中間，如下所示。

  ```
    10            time.Sleep(2 * time.Second)
    11            c <- i
    12        }
    13        close(c)
    14    }
    15
    16    func main() {
    17        msg := "Starting main"
    18        fmt.Println(msg)
    19        bus := make(chan int)
  ```
* break

  簡寫命令 `b`，用來設定斷點，後面跟上參數設定斷點的行數，例如`b 10`在第十行設定斷點。
* delete 簡寫命令 `d`，用來刪除斷點，後面跟上斷點設定的序號，這個序號可以透過`info breakpoints`取得相應的設定的斷點序號，如下是顯示的設定斷點序號。

  ```
    Num     Type           Disp Enb Address            What
    2       breakpoint     keep y   0x0000000000400dc3 in main.main at /home/xiemengjun/gdb.go:23
    breakpoint already hit 1 time
  ```
* backtrace

  簡寫命令 `bt`，用來列印執行的程式碼過程，如下所示：

  ```
    #0  main.main () at /home/xiemengjun/gdb.go:23
    #1  0x000000000040d61e in runtime.main () at /home/xiemengjun/go/src/pkg/runtime/proc.c:244
    #2  0x000000000040d6c1 in schedunlock () at /home/xiemengjun/go/src/pkg/runtime/proc.c:267
    #3  0x0000000000000000 in ?? ()
  ```
* info

  info 命令用來顯示資訊，後面有幾種參數，我們常用的有如下幾種：

  * `info locals`

    顯示當前執行的程式中的變數值
  * `info breakpoints`

    顯示當前設定的斷點列表
  * `info goroutines`

    顯示當前執行的 goroutine 列表，如下程式碼所示，帶\*的表示當前執行的

    * 1  running runtime.gosched
    * 2  syscall runtime.entersyscall

      3  waiting runtime.gosched

      4 runnable runtime.gosched
* print

  簡寫命令`p`，用來列印變數或者其他資訊，後面跟上需要列印的變數名，當然還有一些很有用的函式$len()和$cap()，用來回傳當前 string、slices 或者 maps 的長度和容量。
* whatis

  用來顯示當前變數的型別，後面跟上變數名，例如`whatis msg`，顯示如下：

  ```
    type = struct string
  ```
* next

  簡寫命令 `n`，用來單步除錯，跳到下一步，當有斷點之後，可以輸入 `n` 跳轉到下一步繼續執行
* continue

  簡稱命令 `c`，用來跳出當前斷點處，後面可以跟參數 N，跳過多少次斷點
* set variable

  該命令用來改變執行過程中的變數值，格式如：`set variable <var>=<value>`

## 除錯過程

我們透過下面這個程式碼來示範如何透過 GDB 來除錯 Go 程式，下面是將要示範的程式碼：

```go
package main

import (
    "fmt"
    "time"
)

func counting(c chan<- int) {
    for i := 0; i < 10; i++ {
        time.Sleep(2 * time.Second)
        c <- i
    }
    close(c)
}

func main() {
    msg := "Starting main"
    fmt.Println(msg)
    bus := make(chan int)
    msg = "starting a gofunc"
    go counting(bus)
    for count := range bus {
        fmt.Println("count:", count)
    }
}
```

編譯檔案，產生可執行檔案 gdbfile:

```
go build -gcflags "-N -l" gdbfile.go
```

透過 gdb 命令啟動除錯：

```
gdb gdbfile
```

啟動之後首先看看這個程式是不是可以執行起來，只要輸入 `run` 命令 Enter 後程序就開始執行，程式正常的話可以看到程式輸出如下，和我們在命令列直接執行程式輸出是一樣的：

```
(gdb) run
Starting program: /home/xiemengjun/gdbfile
Starting main
count: 0
count: 1
count: 2
count: 3
count: 4
count: 5
count: 6
count: 7
count: 8
count: 9
[LWP 2771 exited]
[Inferior 1 (process 2771) exited normally]
```

好了，現在我們已經知道怎麼讓程式跑起來了，接下來開始給程式碼設定斷點：

```
(gdb) b 23
Breakpoint 1 at 0x400d8d: file /home/xiemengjun/gdbfile.go, line 23.
(gdb) run
Starting program: /home/xiemengjun/gdbfile
Starting main
[New LWP 3284]
[Switching to LWP 3284]

Breakpoint 1, main.main () at /home/xiemengjun/gdbfile.go:23
23            fmt.Println("count:", count)
```

上面例子`b 23`表示在第 23 行設定了斷點，之後輸入 `run` 開始執行程式。現在程式在前面設定斷點的地方停住了，我們需要檢視斷點相應上下文的原始碼，輸入 `list` 就可以看到原始碼顯示從當前停止行的前五行開始：

```
(gdb) list
18        fmt.Println(msg)
19        bus := make(chan int)
20        msg = "starting a gofunc"
21        go counting(bus)
22        for count := range bus {
23            fmt.Println("count:", count)
24        }
25    }
```

現在 GDB 在運行當前的程式的環境中已經保留了一些有用的除錯資訊，我們只需顯示出相應的變數，檢視相應變數的型別及值：

```
(gdb) info locals
count = 0
bus = 0xf840001a50
(gdb) p count
$1 = 0
(gdb) p bus
$2 = (chan int) 0xf840001a50
(gdb) whatis bus
type = chan int
```

接下來該讓程式繼續往下執行，請繼續看下面的命令

```
(gdb) c
Continuing.
count: 0
[New LWP 3303]
[Switching to LWP 3303]

Breakpoint 1, main.main () at /home/xiemengjun/gdbfile.go:23
23 fmt.Println("count:", count)
(gdb) c
Continuing.
count: 1
[Switching to LWP 3302]

Breakpoint 1, main.main () at /home/xiemengjun/gdbfile.go:23
23 fmt.Println("count:", count)
```

每次輸入 `c` 之後都會執行一次程式碼，又跳到下一次 for 迴圈，繼續顯示出來相應的資訊。

設想目前需要改變上下文相關變數的資訊，跳過一些過程，並繼續執行下一步，得出修改後想要的結果：

```
(gdb) info locals
count = 2
bus = 0xf840001a50
(gdb) set variable count=9
(gdb) info locals
count = 9
bus = 0xf840001a50
(gdb) c
Continuing.
count: 9
[Switching to LWP 3302]

Breakpoint 1, main.main () at /home/xiemengjun/gdbfile.go:23
23 fmt.Println("count:", count)
```

最後稍微思考一下，前面整個程式執行的過程中到底建立了多少個 goroutine，每個 goroutine 都在做什麼：

```
(gdb) info goroutines
* 1 running runtime.gosched
* 2 syscall runtime.entersyscall
3 waiting runtime.gosched
4 runnable runtime.gosched
(gdb) goroutine 1 bt
#0 0x000000000040e33b in runtime.gosched () at /home/xiemengjun/go/src/pkg/runtime/proc.c:927
#1 0x0000000000403091 in runtime.chanrecv (c=void, ep=void, selected=void, received=void)
at /home/xiemengjun/go/src/pkg/runtime/chan.c:327
#2 0x000000000040316f in runtime.chanrecv2 (t=void, c=void)
at /home/xiemengjun/go/src/pkg/runtime/chan.c:420
#3 0x0000000000400d6f in main.main () at /home/xiemengjun/gdbfile.go:22
#4 0x000000000040d0c7 in runtime.main () at /home/xiemengjun/go/src/pkg/runtime/proc.c:244
#5 0x000000000040d16a in schedunlock () at /home/xiemengjun/go/src/pkg/runtime/proc.c:267
#6 0x0000000000000000 in ?? ()
```

透過檢視 goroutines 的命令我們可以清楚地了解 goruntine 內部是怎麼執行的，每個函式的呼叫順序已經明明白白地顯示出來了。

## 小結

本小節我們介紹了 GDB 除錯 Go 程式的一些基本命令，包括`run`、`print`、`info`、`set variable`、`coutinue`、`list`、`break` 等經常用到的除錯命令，透過上面的例子示範，我相信讀者已經對於透過 GDB 除錯 Go 程式有了基本的理解，如果你想取得更多的除錯技巧請參考官方網站的 GDB 除錯手冊，還有 GDB 官方網站的手冊。

## links

* [目錄](https://github.com/doggy8088/build-web-application-with-golang-zhtw/tree/4cbbaa31bdfc3678915eb91f23db2f1bad554a20/preface.md)
* 上一節: [錯誤處理](/build-web-application-with-golang-zhtw/11.0/11.1.md)
* 下一節: [Go 怎麼寫測試案例](/build-web-application-with-golang-zhtw/11.0/11.3.md)
