# 通过Delve调试

**URL:** https://heroiclabs.com/docs/zh/nakama/guides/server-framework/debugging-with-delve/
**Summary:** 本指南演示如何在Docker容器中使用Delve调试自定义Go服务器运行时代码。

---


# 通过Delve调试

本指南假定您已熟悉在Docker容器内运行Nakama。更多信息，请参阅[通过Docker Compose安装Nakama](../../../getting-started/install/docker/)文档。

## main.go文件

我们在本指南中使用一个示例性`main.go`文件，此文件注册单个测试RPC函数，以便可以通过[Delve](https://github.com/go-delve/delve)进行调试。

```go
package main

import (
	"context"
	"database/sql"
	"fmt"

	"github.com/heroiclabs/nakama-common/runtime"
)
func InitModule(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, initializer runtime.Initializer) error {
	logger.Debug("GO SERVER RUNTIME CODE LOADED")
	initializer.RegisterRpc("RpcTest", RpcTest)
	return nil
}

func RpcTest(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, payload string) (string, error) {
	logger.Debug("RpcTest RPC called")

    payloadExists := false
    
    if payload != "" {
        payloadExists = true
    }

    return fmt.Sprintf("{ \"payloadExists\": %v }", payloadExists), nil
}
```

## 创建Dockerfile

为了以可通过`dlv`进行调试的方式构建服务器运行时代码，我们需要制作一个Dockerfile，以确保在没有优化的情况下构建运行时代码和Nakama二进制都。

创建新的Dockerfile并调用它`Dockerfile`：

```dockerfile
FROM registry.heroiclabs.com/heroiclabs/nakama-pluginbuilder:3.22.0 AS builder
ENV GO111MODULE on
ENV CGO_ENABLED 1
WORKDIR /backend
COPY go.mod .
COPY vendor/ vendor/
COPY *.go .

RUN apt-get update && \
    apt-get -y upgrade && \
    apt-get install -y --no-install-recommends gcc libc6-dev

RUN go build --trimpath --gcflags "all=-N -l" --mod=vendor --buildmode=plugin -o ./backend.so
RUN go install github.com/go-delve/delve/cmd/dlv@latest

FROM registry.heroiclabs.com/heroiclabs/nakama-dsym:3.22.0

COPY --from=builder /go/bin/dlv /nakama
COPY --from=builder /backend/backend.so /nakama/data/modules/
COPY local.yml /nakama/data/

ENTRYPOINT [ "/bin/bash" ]
```

此处有几个注意事项。在`builder`步骤中，我们安装了一些包，因此可以使用`--gcflags "all=-N -l"`标志构建Go运行时插件，有效禁用所得插件文件中的优化。

我们还使用`RUN go install github.com/go-delve/delve/cmd/dlv@latest`安装`dlv`。我们将在下一步将其复制到docker映像中。

在最终Docker步骤中，我们使用`nakama-dsym`映像而不是标准`nakama`映像。此映像提供的Nakama二进制文件中优化已禁用，因此适合与`dlv`一起运行，就像插件一样。 

然后正常复制`dlv`二进制文件，以及服务器运行时插件和`local.yml`配置。

这里的另一个修改是我们覆盖了默认`ENTRYPOINT`，这样Nakama就不会尝试自动启动。我们可以`docker exec`到容器内，并自行运行`dlv`。

## 添加Docker Compose文件

我们将用于调试的`docker-compose.yml`文件与[通过Docker Compose安装Nakama](../../../getting-started/install/docker/)文档中的标准文件相似，但我们做了一些调整。此处我们确保构建自己的`Dockerfile`映像，移除`nakama`服务器上的`entrypoint`属性，确保在运行容器时，Nakama不启动。我们还设置了在容器内成功运行`dlv`所需的一些值。这些属性是`security_opt`、`stdin_open`和`tty`。

```yaml
version: '3'
services:
  postgres:
    command: postgres -c shared_preload_libraries=pg_stat_statements -c pg_stat_statements.track=all
    environment:
      - POSTGRES_DB=nakama
      - POSTGRES_PASSWORD=localdb
    expose:
      - "8080"
      - "5432"
    image: postgres:12.2-alpine
    ports:
      - "5432:5432"
      - "8080:8080"
    volumes:
      - data:/var/lib/postgresql/data
  nakama:
    build:
        context: .
        dockerfile: Dockerfile
    depends_on:
      - postgres
    expose:
      - "7349"
      - "7350"
      - "7351"
      - "2345"
    healthcheck:
      test: ["CMD", "/nakama/nakama", "healthcheck"]
      interval: 10s
      timeout: 5s
      retries: 5
    links:
      - "postgres:db"
    ports:
      - "7349:7349"
      - "7350:7350"
      - "7351:7351"
      - "2345:2345"
    restart: unless-stopped
    security_opt:
      - "seccomp:unconfined"
    stdin_open: true
    tty: true
volumes:
  data:
```

## Exec'ing到Docker中

完成Docker配置文件后，我们现在将启动Nakama和Postgres容器：

```bash
docker compose up
```

伴随着我们容器的运行，打开一个新的终端窗口，并`exec`到Nakama docker容器中（其中，`server-nakama-1`是Nakama容器的名称）。

```bash
docker exec -it server-nakama-1 /bin/bash
```

这会将您放到Nakama容器中的`bash`壳内。

## 手动迁移Nakama数据库

从这里开始，我们将首先确保迁移Postgres数据库以便与Nakama结合使用。

```bash
/nakama/nakama migrate up --database.address postgres:localdb@postgres:5432/nakama
```

## 通过Delve运行Nakama

迁移数据库后可以通过Delve运行Nakama。注意Nakama二进制文件路径和Nakama配置标志之间额外的`--`。 包含它很重要，否则dlv将不能正确地将那些标志传给Nakama二进制文件。

```bash
./dlv --log --log-output=debugger exec /nakama/nakama -- --config /nakama/data/local.yml --database.address postgres:localdb@postgres:5432/nakama
```

此处使用[`dlv exec`命令](https://github.com/go-delve/delve/blob/master/Documentation/usage/dlv_exec.md)执行预编译的Nakama二进制文件，并开始调试会话。将显示`dlv`命令行界面：

```bash
(dlv) 
```

## 通过Delve调试自定义服务器运行时代码

Delve正等待用户输入告诉它如何继续。此时，如果键入`continue`，Delve将继续执行Nakama，可以看到标准的Nakama服务器输出日志。然而，由于Nakama在启动完成后进入等待终止信号的状态，如果这样做，将无法调试运行时代码，因为无法插入Delve。

应该在Nakama进入这个等待阶段之前将断点设置到某个点，但之后我们的自定义服务器运行时插件已经加载。

为此，我们在`main.go`的181行设置断点。

```bash
(dlv) break main.go:181
```

您应该看到类似于以下确认断点的输出：

```bash
2022-06-30T12:29:48Z info layer=debugger created breakpoint: &api.Breakpoint{ID:1, Name:"", Addr:0x1f266cd, Addrs:[]uint64{0x1f266cd}, File:"github.com/heroiclabs/nakama/v3/main.go", Line:181, FunctionName:"main.main", Cond:"", HitCond:"", Tracepoint:false, TraceReturn:false, Goroutine:false, Stacktrace:0, Variables:[]string(nil), LoadArgs:(*api.LoadConfig)(nil), LoadLocals:(*api.LoadConfig)(nil), WatchExpr:"", WatchType:0x0, VerboseDescr:[]string(nil), HitCount:map[string]uint64{}, TotalHitCount:0x0, Disabled:false, UserData:interface {}(nil)}
Breakpoint 1 set at 0x1f266cd for main.main() github.com/heroiclabs./v3/main.go:181
```

现在，继续执行Nakama直到到达断点。

```bash
(dlv) continue
> main.main() github.com/heroiclabs./v3/main.go:181 (hits goroutine(1):1 total:1) (PC: 0x1f266cd)
```

此时，应该加载我们的自定义服务器运行时代码。我们使用`libraries`命令和查找插件`.so`文件来确认这一点。

```bash
(dlv) libraries
0. 0x7f8099127000 /lib/x86_64-linux-gnu/libdl.so.2
1. 0x7f8099106000 /lib/x86_64-linux-gnu/libpthread.so.0
2. 0x7f8098f45000 /lib/x86_64-linux-gnu/libc.so.6
3. 0x7f8099131000 /lib64/ld-linux-x86-64.so.2
4. 0x7f8062758000 /lib/x86_64-linux-gnu/libnss_files.so.2
5. 0x7f806177c000 /nakama/data/modules/backend.so
```

如上所述，自定义服务器运行时代码作为`/nakama/data/modules/backend.so`列出。

对于本例，自定义服务器运行时有一个`RpcTest`函数，它记录传送到Nakama服务器控制台的消息，并返回一个JSON响应，该响应的值指示是否提供了有效负载。

验证函数的存在：

```bash
(dlv) funcs RpcTest
heroiclabs.com/nakama-server-sandbox.RpcTest
```

可在此函数上设置断点：

```bash
(dlv) break RpcTest
2022-06-30T12:37:16Z info layer=debugger created breakpoint: &api.Breakpoint{ID:2, Name:"", Addr:0x7f395d54970a, Addrs:[]uint64{0x7f395d54970a}, File:"heroiclabs.com/nakama-server-sandbox/main.go", Line:14, FunctionName:"heroiclabs.com/nakama-server-sandbox.RpcTest", Cond:"", HitCond:"", Tracepoint:false, TraceReturn:false, Goroutine:false, Stacktrace:0, Variables:[]string(nil), LoadArgs:(*api.LoadConfig)(nil), LoadLocals:(*api.LoadConfig)(nil), WatchExpr:"", WatchType:0x0, VerboseDescr:[]string(nil), HitCount:map[string]uint64{}, TotalHitCount:0x0, Disabled:false, UserData:interface {}(nil)}
Breakpoint 2 set at 0x7f395d54970a for heroiclabs.com/nakama-server-sandbox.RpcTest() heroiclabs.com.-server-sandbox/main.go:14
```

然后继续执行：

```bash
(dlv) continue
```

现在转到Nakama控制台并从用户触发此Rpc将使我们能够调试此函数。

一旦我们到达断点，Delve将让我们重新获得控制，并允许检查各种情况：

```bash
> heroiclabs.com/nakama-server-sandbox.RpcTest() heroiclabs.com.-server-sandbox/main.go:14 (hits goroutine(276):1 total:1) (PC: 0x7f395d54970a)
(dlv)
```

在此，首先可以检查我们已使用`args`命令传递给RPC的参数。

```bash
(dlv) args
ctx = context.Context(*context.valueCtx) 0xbeef000000000008
logger = github.com/heroiclabs/nakama-common/runtime.Logger(*github.com/heroiclabs/nakama/v3/server.RuntimeGoLogger) 0xbeef000000000108
db = ("*database/sql.DB")(0xc0004781a0)
nk = github.com/heroiclabs/nakama-common/runtime.NakamaModule(*github.com/heroiclabs/nakama/v3/server.RuntimeGoNakamaModule) 0xbeef000000000308
payload = "{\"hello\":\"world\"}"
~r0 = ""
~r1 = error nil
```

从上面我们可以看到，除了典型的RPC参数（上下文、记录器、数据库等），我们还看到传递给RPC的JSON有效负载的值。

接下来可以执行函数，查看对记录器的调用执行，然后检查局部变量。这可以使用`next`和`locals`命令来完成。

```bash
(dlv) next
2022-06-30T13:18:38Z debug layer=debugger nexting
{"level":"debug","ts":"2022-06-30T13:18:38.878Z","caller":"nakama-server-sandbox/main.go:17","msg":"RpcTest RPC called","runtime":"go","rpc_id":"RpcTest"}
> heroiclabs.com/nakama-server-sandbox.RpcTest() heroiclabs.com.-server-sandbox/main.go:19 (PC: 0x7f819f58d830)
```

通过调用`next`继续执行，直到到达第21行，然后使用`locals`检查局部变量。

```bash
(dlv) next
2022-06-30T13:18:46Z debug layer=debugger nexting
> heroiclabs.com/nakama-server-sandbox.RpcTest() heroiclabs.com.-server-sandbox/main.go:21 (PC: 0x7f819f58d835)
(dlv) locals
payloadExists = false
```

我们使用`next`继续执行，观察`payloadExists`值变化。

```bash
(dlv) next
2022-06-30T13:22:57Z debug layer=debugger nexting
> heroiclabs.com/nakama-server-sandbox.RpcTest() heroiclabs.com.-server-sandbox/main.go:25 (PC: 0x7f819f58d84d)
(dlv) locals
payloadExists = true
```

可以看到，`payloadExists`的值发生了变化，因为`if`语句求值为`true`，因此该变量的值发生变化。

我们调用`continue`，完全继续执行，让RPC返回。

```bash
(dlv) continue
```

现在，您应该在Nakama控制台API Explorer中看到来自RPC的适当响应。

```json
{
  "payloadExists": true
}
```

## 进一步调试

上面演示了一个非常基本的示例，即逐步执行自定义服务器运行时RPC，检查参数与局部变量，并继续执行。

Delve是一个功能强大的调试器，如果您想进一步了解可用的命令，请查看[Delve CLI官方文档](https://github.com/go-delve/delve/tree/master/Documentation/cli)。