Go Runtime #

Nakama server can run trusted game server code written in Go, allowing you to separate sensitive code such as purchases, daily rewards, etc., from running on clients.

Choosing to write your game server custom logic using Go brings with it the advantage that Go runtime code has full low-level access to the server and its environment.

When writing your server runtime code in Go, also refer to the follow up documentation detailing common issues with dependency version mismatches and how to solve them.
Keep in mind
The video tutorial above and written guide below offer different variations of how to setup your project. You can choose to follow one or the other, but not a combination of both.

Prerequisites #

You will need to have these tools installed to use the Nakama Go server runtimes:

  • The Go Binaries
  • Basic UNIX tools or knowledge on the Windows equivalents
  • Docker Desktop if you’re planning to run Nakama using Docker

Nakama Common version #

Be sure your project’s go.mod file references the correct Nakama Common version for your Nakama release:

Nakama VersionNakama Common Version
3.24.21.34.0
3.24.11.34.0
3.24.01.34.0
3.23.01.33.0
3.22.01.32.0
3.21.11.31.0
3.21.01.31.0
3.20.11.30.1
3.20.01.30.1
3.19.01.30.0
3.18.01.29.0
3.17.11.28.1
3.17.01.28.0
3.16.01.27.0
3.15.01.26.0
3.14.01.25.0
3.13.11.24.0
3.12.01.23.0
3.11.01.22.0
3.10.01.21.0
3.9.01.20.0
3.8.01.19.0
3.7.01.18.0
3.6.01.17.0
3.5.01.16.0
3.4.01.15.0
3.3.01.14.0
3.2.11.13.1
3.2.01.13.0
3.1.21.12.1
3.1.11.12.1
3.1.01.12.0
3.0.01.11.0

Restrictions #

Before getting started with Go runtime code, be aware of the following restrictions and limitations:

Compatibility #

Go runtime code can make use of the full range of standard library functions and packages.

Go runtime available functionality depends on the version of Go each Nakama release is compiled with. This is usually the latest stable version at the time of release. Check server startup logs for the exact Go version used by your Nakama installation.

Single threaded #

The use of multi-threaded processing (goroutines) in your runtime code is discouraged due to difficulties of implementation in a multi-node environment.

Global state #

The Go runtime can use global variables as a way to store state in memory and store and share data as needed, but concurrency and access controls are the responsibility of the developer.

Sharing state is discouraged and should be avoided in your runtime code as it is not supported in multi-node environments.

Sandboxing #

There is no sandboxing when using Go for your runtime code. Go runtime code has full low-level access to the server and its environment.

This allows full flexibility and control to include powerful features and offer high performance, but cannot guarantee error safety. The server does not guard against fatal errors in Go runtime code, such as segmentation faults or pointer dereference failures.

Initialize the project #

These steps will set up a workspace to write all your project code to be run by the game server.

Define the folder name that will be the workspace for the project.

1
2
mkdir go-project
cd go-project

Use Go to initialize the project, providing a valid Go module path, and install the Nakama runtime package.

1
2
go mod init example.com/go-project
go get github.com/heroiclabs/nakama-common/runtime

Develop code #

All code must start execution from a function that the game server looks for in the global scope at startup. This function must be called InitModule and is how you register RPCs, before/after hooks, and other event functions managed by the server.

The code below is a simple Hello World example which uses the Logger to write a message. Name the source file main.go. You can write it in your favorite editor or IDE.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
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
}

With this code added, head back to your Terminal/Command Prompt and run the following command to vendor your Go package dependencies.

1
go mod vendor

When you Vendor your Go package dependencies it will place a copy of them inside a vendor/ folder at the root of your project, as well as a go.sum file. Both of these should be checked in to your source control repository.

Next add a local.yml Nakama server configuration file. You can read more about what configuration options are available.

1
2
logger:
    level: DEBUG

Error handling #

Go functions typically return error values when an error occurs. To handle the error thrown by a custom function or one provided by the runtime, you must inspect the error return value.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
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)
}

We recommend you use this pattern and wrap all runtime API calls for error handling and inspection.

1
2
3
4
5
// 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)
}

Returning errors to the client #

When writing your own custom runtime code, you should ensure that any errors that occur when processing a request are passed back to the client appropriately. This means that the error returned to the client should contain a clear and informative error message and an appropriate HTTP status code.

Internally the Nakama runtime uses gRPC error codes and converts them to the appropriate HTTP status codes when returning the error to the client.

You can define the gRPC error codes as constants in your Go module as shown below:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
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
)

Once you have defined the error code constants, you can use them to define error objects using the runtime.NewError("error message", GRPC_CODE) function. The following are some examples of errors you might define in your module.

1
2
3
4
5
6
7
8
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)
)

Below is an example of how you would return appropriate errors both in an RPC call and in a Before Hook.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
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
}

Build the Go shared object #

In order to use your custom logic inside the Nakama server, you need to compile it into a shared object.

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

If you are using Windows you will not be able to run this command as there is currently no support for building Go Plugins on Windows. You can use the Dockerfile example below instead to run the server using Docker.

If you’re using the Docker method of running the Nakama server below, you do not need to build the Go Shared Object separately as the Dockerfile will take of this.

Running the project #

With Docker #

The easiest way to run your server locally is with Docker. For your Go module to work with Nakama it needs to be compiled using the same version of Go as was used to compile the Nakama binary itself. You can guarantee this by using the same version tags of the nakama-pluginbuilder and nakama images as you can see below.

Create a file called Dockerfile.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
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

Next create a docker-compose.yml file. For more information see the Install Nakama with Docker Compose documentation.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
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:

Now run the server with the command:

1
docker compose up --build

Without Docker #

Install a Nakama binary stack for Linux, Windows, or macOS. When this is complete you can run the game server and have it load your code:

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

Confirming the server is running #

The server logs will show this output or similar which shows that the code we wrote above was loaded and executed at startup.

1
2
3
4
5
6
7
{
  "level": "info",
  "ts": "....",
  "caller": "go-project/main.go:10",
  "msg": "Hello World!",
  "runtime": "go"
}

Register Raw HTTP Handlers #

Power Users Only
This is a Power User feature, most developers will not need to register their own HTTP handlers. Be careful when registering your endpoints as you could accidentally overwrite existing Nakama endpoints if you use existing paths.
Security
These routes are not secured by server key, HTTP key or session tokens. They are however, covered by the same CORS, max message size, max connection read/write/idle times, and transparent compression/decompression (gzip etc) as the rest of the server configurations.

You can attach new HTTP handlers to specified paths on the main client API server endpoint.

1
2
3
4
5
if err := initializer.RegisterHttp("/test", func(w http.ResponseWriter, r *http.Request) {
  _, _ = w.Write([]byte("You hit the new endpoint!"))
}); err != nil {
  return err
}

You can also register new HTTP handles for specific methods only. The methods that can be registered are: GET, HEAD, POST, PUT, PATCH, DELETE, CONNECT, OPTIONS, and TRACE. If you don’t specify any method when defining the handler, such as the example above, it will register the endpoint for all of them.

Example of GET only:

1
2
3
4
5
if err := initializer.RegisterHttp("/test", func(w http.ResponseWriter, r *http.Request) {
  _, _ = w.Write([]byte("You hit the new endpoint, it allows GET only!"))
}, http.MethodGet); err != nil {
  return err
}

Or of POST or PUT only:

1
2
3
4
5
if err := initializer.RegisterHttp("/test", func(w http.ResponseWriter, r *http.Request) {
  _, _ = w.Write([]byte("You hit the new endpoint, it allows POST or PUT only!"))
}, http.MethodPost, http.MethodPut); err != nil {
  return err
}

Next steps #

Have a look at the Nakama project template which covers the following Nakama features:

Related Pages