# Go运行时

**URL:** https://heroiclabs.com/docs/zh/nakama/server-framework/go-runtime/
**Summary:** 了解如何使用Go为您的游戏服务器自定义逻辑配置和开发项目。

---


# Go运行时

Nakama服务器可以运行Go中编写的可信游戏服务器代码，允许您将购买、每日奖励等敏感代码与客户端上运行的代码区分开来。

选择使用Go编写游戏服务器自定义逻辑带来的好处是，Go运行时代码对服务器及服务器环境具有全部初级访问权限。

{{< note "important" >}}
在Go中编写您的服务器运行时代码时，请同时参阅后续文档对[依赖版本不匹配的常见问题以及如何解决这些问题的详细说明](./go-dependencies/)。
{{< / note >}}

{{< youtube "Ru3RZ6LkJEk" >}}

## 前提条件

您需要安装这些工具方可使用Nakama Go服务器运行时：

- [Go二进制文件](https://golang.org/dl/)
- 基本的UNIX工具或有关Windows等效工具的知识
- 如果您准备使用Docker运行Nakama，请使用[Docker Desktop](https://www.docker.com/products/docker-desktop)

### Nakama通用版本

确保您的项目`go.mod`文件为您的Nakama版本引用了正确的[Nakama通用](https://github.com/heroiclabs/nakama-common/releases)版本：

{{< table name="nakama.getting-started.release-notes.nakama-common-version" >}}

## 初始化项目

这些步骤将设置一个工作区，以编写将由游戏服务器运行的所有项目代码。

定义项目工作区的文件夹名称。

```sh
mkdir go-project
cd go-project
```

使用Go初始化项目，以提供[有效的Go模块路径](https://golang.org/ref/mod#module-path)，并安装Nakama运行时包。

```sh
go mod init example.com/go-project
go get github.com/heroiclabs/nakama-common/runtime
```

## 开发代码

所有代码必须从游戏服务器在启动时在全局范围内查找的函数开始执行。此函数必须调用 `InitModule`，您以此注册 RPC、before/after 挂钩和服务器管理的其他事件函数。

以下代码是使用 `Logger` 编写消息的简单 Hello World 示例。命名源文件`main.go` 。您可以使用自己喜欢的编辑器或 IDE 进行编写。

```go
package main

import (
    "context"
    "database/sql"
    "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.Info("Hello World!")
    return nil
}
```

添加此代码后，返回到您的终端/命令提示符并运行以下命令来供应您的Go依赖包。

```sh
go mod vendor
```

当您供应您的Go依赖包时，它会将依赖的副本放在项目根目录下的`vendor/`文件夹中，同时将放置一个`go.sum`文件。上述副本和文件应该签入到您的源代码控制存储库中。

然后添加`local.yml`Nakama服务器配置文件。您可以阅读有关可用[配置选项](../../getting-started/configuration/)的更多信息。

```yaml
logger:
    level: DEBUG
```

## 错误处理

发生错误时，Go函数通常返回错误值。要处理自定义函数或运行时提供的函数引发的错误，必须检查错误返回值。

```go
func willError() (string, error) {
	return "", errors.New("i'm an error")
}

response, err := willError()

// Handle error.
if err != nil {
  logger.Error("an error occurred: %v", err)
}
```

我们建议您使用此模式，并包装所有运行时 API 调用以进行错误处理和检查。

```go
// Will throw an error because this function expects a valid user ID.
account, err := nk.AccountGetId(ctx, "invalid_id")
if err != nil {
  logger.Error("account not found: %v", err)
}
```

## 向客户端返回错误

在编写自己的自定义运行时代码时，应确保在处理请求时发生的任何错误都将会以适当方式传回客户端。这意味着返回给客户端的错误应该包含一条明确的通知错误消息和一个适当的 HTTP 状态代码。

Nakama 运行时在内部使用 gRPC 错误代码，并在将错误返回给客户端时将其转换为适当的 HTTP 状态代码。

您可以在您的Go模块中将gRPC错误代码定义为常量，如下所示：

``` go
const (
	OK                  = 0
	CANCELED            = 1
	UNKNOWN             = 2
	INVALID_ARGUMENT    = 3
	DEADLINE_EXCEEDED   = 4
	NOT_FOUND           = 5
	ALREADY_EXISTS      = 6
	PERMISSION_DENIED   = 7
	RESOURCE_EXHAUSTED  = 8
	FAILED_PRECONDITION = 9
	ABORTED             = 10
	OUT_OF_RANGE        = 11
	UNIMPLEMENTED       = 12
	INTERNAL            = 13
	UNAVAILABLE         = 14
	DATA_LOSS           = 15
	UNAUTHENTICATED     = 16
)
```

定义了错误代码常量后，可以使用这些常量通过`runtime.NewError("error message", GRPC_CODE)`函数定义`error`对象。以下是您可以在自己的模块中定义的一些错误示例。

``` go
var (
	errBadInput           = runtime.NewError("input contained invalid data", INVALID_ARGUMENT)
	errInternalError      = runtime.NewError("internal server error", INTERNAL)
	errGuildAlreadyExists = runtime.NewError("guild name is in use", ALREADY_EXISTS)
	errFullGuild          = runtime.NewError("guild is full", RESOURCE_EXHAUSTED)
	errNotAllowed         = runtime.NewError("operation not allowed", PERMISSION_DENIED)
	errNoGuildFound       = runtime.NewError("guild not found", NOT_FOUND)
)
```

以下示例显示如何在 [RPC](../introduction/#rpc-functions) 调用和 [Before 挂钩](../introduction/hooks/#before-hooks)中返回合适的错误。

``` go
func CreateGuildRpc(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, payload string) (string, error) {
	// ... check if a guild already exists and set value of `alreadyExists` accordingly
	var alreadyExists bool = true

	if alreadyExists {
		return "", errGuildAlreadyExists
	}

	return "", nil
}

func BeforeAuthenticateCustom(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, in *api.AuthenticateCustomRequest) (*api.AuthenticateCustomRequest, error) {
  // Only match custom Id in the format "cid-000000"
  pattern := regexp.MustCompile("^cid-([0-9]{6})$")

  if !pattern.MatchString(in.Account.Id) {
    return nil, errBadInput
  }

  return in, nil
}
```


## 构建Go共享对象

为了在Nakama服务器中使用您的自定义逻辑，您需要将其编译为共享对象。

```sh
go build --trimpath --mod=vendor --buildmode=plugin -o ./backend.so
```

如果您正在使用Windows，您将无法运行此命令，因为目前不支持在Windows上构建Go插件。您可以替代性使用以下Dockerfile示例通过Docker运行服务器。

如果您正在使用Docker方法运行下面的Nakama服务器，则不需要单独构建Go共享对象，因为`Dockerfile`将会执行该操作。

## 限制

### 兼容性

Go运行时代码可以利用所有[标准库](https://pkg.go.dev/std)函数和包。

Go运行时可用的功能取决于编译每个Nakama版本时使用的Go版本。它通常是编译版本时最新的稳定版本。检查服务器启动日志，获取安装Nakama时使用的确切Go版本。

### 单一线程

由于难以在多节点环境中实现，所以不鼓励在运行时代码中使用多线程处理（[轻量级线程](https://go.dev/tour/concurrency/1)）。

### 全局状态

Go运行时可以使用全局变量在内存中存储状态并根据需要存储和共享数据，但并发和访问控制需要由开发人员负责。

不建议共享状态，而且应该在运行时代码中加以避免，因为该功能在多节点环境中**不受支持**。

### 沙箱

在运行时代码中使用Go时没有沙箱。Go运行时代码对服务器及服务器环境具有全部初级访问权限。 

这允许充分的灵活性和控制，可以容纳强大的功能并实现高性能，但**无法保证错误的安全性**。服务器无法防止Go运行时代码出现严重错误，例如段错误或指针取消引用错误。

## 运行项目

### 通过 Docker

通过 Docker 在本地运行服务器是最简便的方法。为了让您的Go模块与Nakama能够结合使用，需要使用与编译Nakama二进制文件相同的Go版本编译Go模块。如下所示，您可以通过使用`nakama-pluginbuilder`和`nakama`图像的相同版本标签来实现该功能。

创建一个名为 `Dockerfile` 的文件。

```dockerfile
FROM heroiclabs/nakama-pluginbuilder:3.22.0 AS builder

ENV GO111MODULE on
ENV CGO_ENABLED 1

WORKDIR /backend
COPY . .

RUN go build --trimpath --buildmode=plugin -o ./backend.so

FROM heroiclabs/nakama:3.22.0

COPY --from=builder /backend/backend.so /nakama/data/modules
COPY --from=builder /backend/local.yml /nakama/data/
COPY --from=builder /backend/*.json /nakama/data/modules
```

然后创建 `docker-compose.yml` 文件。如需更多信息，请参阅[通过 Docker Compose 安装 Nakama](../../getting-started/install/docker/)文档。

```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: .
    depends_on:
      - postgres
    entrypoint:
      - "/bin/sh"
      - "-ecx"
      - >
        /nakama/nakama migrate up --database.address postgres:localdb@postgres:5432/nakama &&
        exec /nakama/nakama --config /nakama/data/local.yml --database.address postgres:localdb@postgres:5432/nakama
    expose:
      - "7349"
      - "7350"
      - "7351"
    healthcheck:
      test: ["CMD", "/nakama/nakama", "healthcheck"]
      interval: 10s
      timeout: 5s
      retries: 5
    links:
      - "postgres:db"
    ports:
      - "7349:7349"
      - "7350:7350"
      - "7351:7351"
    restart: unless-stopped

volumes:
  data:
```

现在，使用命令运行服务器：

```bash
docker compose up
```

### 不使用 Docker

为 [Linux](../../getting-started/install/linux/)、[Windows](../../getting-started/install/windows/) 或 [macOS](../../getting-started/install/macos/) 安装二进制文件堆栈。完成此步骤后，可以运行游戏服务器，使其加载代码：

```bash
nakama --config local.yml --database.address <DATABASE ADDRESS>
```

### 确认服务器正在运行

服务器日志将显示此输出或类似输出，显示在启动时加载并执行了上述编写的代码。

``` json
{
  "level": "info",
  "ts": "....",
  "caller": "go-project/main.go:10",
  "msg": "Hello World!",
  "runtime": "go"
}
```

## 后续步骤

请查看涉及以下 Nakama 功能的 [Nakama 项目模板](https://github.com/heroiclabs/nakama-project-template)：

- [授权多玩家匹配处理程序](../../concepts/multiplayer/authoritative/)
- [应用程序内通知](../../concepts/notifications/)
- [存储](../../concepts/storage/collections/)
- [RPC](../introduction/#functionality)
- [用户钱包](../../concepts/user-accounts/#virtual-wallet)
