Go Runtime #
LayerG 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.
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 LayerG Go server runtimes:
- The Go Binaries
- Basic UNIX tools or knowledge on the Windows equivalents
- Docker Desktop if you’re planning to run LayerG using Docker
LayerG Common version #
Be sure your project’s go.mod
file references the correct LayerG Common version for your Layerg release:
Layerg Version | Layerg Common Version |
---|---|
0.0.1 | 0.0.1 |
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 Layerg 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 Layerg 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.
mkdir go-project
cd go-project
Use Go to initialize the project, providing a valid Go module path, and install the Layerg runtime package.
go mod init example.com/go-project
go get github.com/u2u-labs/go-layerg-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.
package main
import (
"context"
"database/sql"
"github.com/u2u-labs/layerg-core/runtime"
)
func InitModule(ctx context.Context, logger runtime.Logger, db *sql.DB, lg runtime.LayerGModule, 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.
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
Layerg server configuration file. You can read more about what configuration options are available.
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.
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.
// Will throw an error because this function expects a valid user ID.
account, err := lg.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 Layerg 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:
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.
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.
func CreateGuildRpc(ctx context.Context, logger runtime.Logger, db *sql.DB, lg runtime.LayerGModule, 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, lg runtime.LayerGModule, 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 Layerg server, you need to compile it into a shared object.
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 Layerg 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 Layerg it needs to be compiled using the same version of Go as was used to compile the Layerg binary itself. You can guarantee this by using the same version tags of the layerg-pluginbuilder
and layerg
images as you can see below.
Create a file called Dockerfile
.
FROM layergdev/layerg-pluginbuilder:latest AS builder
ENV GO111MODULE on
ENV CGO_ENABLED 1
WORKDIR /backend
COPY . .
RUN go build --trimpath --buildmode=plugin -o ./backend.so
FROM layergdev/layerg:latest
COPY --from=builder /backend/backend.so /layerg/data/modules
COPY --from=builder /backend/local.yml /layerg/data/
`COPY --from=builder /backend/*.json /layerg/data/modules`
Next create a docker-compose.yml
file. For more information see the Install Layerg with Docker Compose documentation.
version: '3'
services:
postgres:
command: postgres -c shared_preload_libraries=pg_stat_statements -c pg_stat_statements.track=all
environment:
- POSTGRES_DB=layerg
- POSTGRES_PASSWORD=localdb
expose:
- "8080"
- "5432"
image: postgres:12.2-alpine
ports:
- "5432:5432"
- "8080:8080"
volumes:
- data:/var/lib/postgresql/data
layerg:
build: .
depends_on:
- postgres
entrypoint:
- "/bin/sh"
- "-ecx"
- >
/layerg/layerg migrate up --database.address postgres:localdb@postgres:5432/layerg &&
exec /layerg/layerg --config /layerg/data/local.yml --database.address postgres:localdb@postgres:5432/layerg
expose:
- "7349"
- "7350"
- "7351"
healthcheck:
test: ["CMD", "/layerg/layerg", "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:
docker compose up --build
Without Docker #
Install a Layerg binary stack for Linux, Windows, or macOS. When this is complete you can run the game server and have it load your code:
layerg --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.
{
"level": "info",
"ts": "....",
"caller": "go-project/main.go:10",
"msg": "Hello World!",
"runtime": "go"
}
Register Raw HTTP Handlers #
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 Layerg endpoints if you use existing paths.
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.
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**:
```js showLineNumbers
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:
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
}