Server Framework Go Setup

Nakama server can run trusted game server code written in Go, TypeScript and Lua. This allows you to separate sensitive code from running on clients e.g. purchases, daily rewards etc.

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.

If 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.

Prerequisites

You will need to have these tools installed to work 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

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 favourite 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

Returning errors

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 as well as 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. The following table shows the mapping between gRPC code and HTTP code.

ErrorgRPC CodeHTTP Code
OK0200
CANCELED1499
UNKNOWN2500
INVALID_ARGUMENT3400
DEADLINE_EXCEEDED4504
NOT_FOUND5404
ALREADY_EXISTS6409
PERMISSION_DENIED7403
RESOURCE_EXHAUSTED8429
FAILED_PRECONDITION9400
ABORTED10409
OUT_OF_RANGE11400
UNIMPLEMENTED12501
INTERNAL13500
UNAVAILABLE14503
DATA_LOSS15500
UNAUTHENTICATED16401

You can use these error code to define error objects using the runtime.NewError("error message", GRPC_CODE) function. The following are some examples of errors you might wish to define in your module.

1
2
3
4
5
6
7
8
var (
	errBadInput           = runtime.NewError("input contained invalid data", 3)
	errInternalError      = runtime.NewError("internal server error", 13)
	errGuildAlreadyExists = runtime.NewError("guild name is in use", 6)
	errFullGuild          = runtime.NewError("guild is full", 8)
	errNotAllowed         = runtime.NewError("operation not allowed", 7)
	errNoGuildFound       = runtime.NewError("guild not found", 5)
)

Below shows an example of how you would return appropriate errors both in an RPC call as well as 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.

Restrictions

Compatibility

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.

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

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.

Sandboxing

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.

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
16
17
FROM heroiclabs/nakama-pluginbuilder:3.3.0 AS go-builder

ENV GO111MODULE on
ENV CGO_ENABLED 1

WORKDIR /backend

COPY go.mod .
COPY main.go .
COPY vendor/ vendor/

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

FROM heroiclabs/nakama:3.11.0

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

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", "curl", "-f", "http://localhost:7350/"]
      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

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"
}

Next steps

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