How to simplify and speed up the Golang module development?

Godot + Nakama with Golang modules

I’ve just started using Golang with Nakama and it has been a rocky start, nevertheless thanks to the helpful folks over at Gitter I’ve managed to get it running in no time(2days). Now after messing with it for some time I have a couple of questions about development. My process currently looks like this:

I have the test.go file that I edit in notepad (no autocomplete…) and whenever I make a change I have to run docker run --rm -w "/builder" -v "${PWD}:/builder" heroiclabs/nakama-pluginbuilder:2.12.0 build -buildmode=plugin -trimpath -o ./modules/plugin_code.so and when it’s finished I copy the .so file from the modules dir to the nakama servers modules bin and restart the server. This seems awful and slow especially that I cannot use any of the nakamas autocomplete functions and I have no idea about the syntax they use.

  1. How can I speed up or automate this process?
  2. Can I use an editor (Atom has support for Go) to get some help while writing the modules?

Any other tips are highly appreciated! Hopefully, this will be helpful to other beginners as well.

1 Like

Here, check this out. Setting up Goland to compile plugins on Windows

There is no goland involved. Just VS Code and Linux, WSL in my case.

This way, it’s just a matter of one second after you run the shell script. Made a change, run it again, one second, boom :boom: you’re up and running again.

2 Likes

@Nikola-Milovic What I tend to use on Go projects with Nakama is a simple multi-stage Docker file like:

FROM heroiclabs/nakama-pluginbuilder:2.12.0 AS builder

ENV GO111MODULE on
ENV CGO_ENABLED 1

WORKDIR /backend
COPY . .

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

FROM heroiclabs/nakama:2.12.0

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

With a Docker compose file which uses it to build a container of both my custom Go code and package it into the Nakama container:

version: '3'
services:
  postgres:
    container_name: game_backend_postgres
    environment:
      - POSTGRES_DB=nakama
      - POSTGRES_PASSWORD=localdb
    expose:
      - "8080"
      - "5432"
    image: postgres:9.6-alpine
    ports:
      - "5432:5432"
      - "8080:8080"
    volumes:
      - data:/var/lib/postgresql/data

  nakama:
    build: .
    container_name: game_backend
    depends_on:
      - postgres
    entrypoint:
      - "/bin/sh"
      - "-ecx"
      - >
        /nakama/nakama migrate up --database.address postgres:localdb@postgres:5432/nakama &&
        exec /nakama/nakama --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:

To build the Docker container and run the entire setup in one step it becomes:

docker-compose -f ./docker-compose.yml up -d --build nakama

You can see a full example of it on this gist:

https://gist.github.com/novabyte/cc6d57022e2d3baa40e98d8fc91dc4c8

This does require a bit more commitment to learn how to take advantage of Docker though. As well as knowledge of commands like: docker ps, docker prune ..., etc, etc.

2 Likes

Using @novabyte 's answer I’ll pitch in and show it how I got it working with cockroach db.

version: '3'
services:
 cockroachdb:
   container_name: cockroachdb
   image: cockroachdb/cockroach:v19.2.5
   command: start --insecure --store=attrs=ssd,path=/var/lib/cockroach/
   restart: always
   volumes:
     - data:/var/lib/cockroach
   expose:
     - "8080"
     - "26257"
   ports:
     - "26257:26257"
     - "8080:8080"
 nakama:
   build:
     context: ./../backend
   container_name: game_backend
   depends_on:
     - cockroachdb
   entrypoint:
     - "/bin/sh"
     - "-ecx"
     - >
       /nakama/nakama migrate up --database.address root@cockroachdb:26257 &&
        exec /nakama/nakama --name nakama1 --database.address root@cockroachdb:26257
   expose:
     - "7349"
     - "7350"
     - "7351"
   healthcheck:
     test: ["CMD", "curl", "-f", "http://localhost:7350/"]
     interval: 10s
     timeout: 5s
     retries: 5
   links:
     - "cockroachdb:db"
   ports:
     - "7349:7349"
     - "7350:7350"
     - "7351:7351"
   restart: unless-stopped

volumes:
 data:

KEY TAKEAWAY: (i didn’t know this so anyone starting out might not know either)
Basically the context: is where your .go and go.mod and dockerfile is!!! It’s relative to where you run your docker-compose from. (so navigate from your docker-compose to your module folder, leave the other locations as is as they are related to your docker and not machine)

Make a shell command using this
docker-compose -f ./docker-compose.yml up --build nakama

In your directory with .go and go.mod create a Dockerfile without a type and paste this

FROM heroiclabs/nakama-pluginbuilder:2.12.0 AS builder

ENV GO111MODULE on
ENV CGO_ENABLED 1

WORKDIR /backend
COPY . .

RUN go mod download

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

FROM heroiclabs/nakama:2.12.0

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

If you’re more experienced this probably won’t help you much, but if you’re getting started I think that just following and copy pasting what I wrote can get you up and running in no time! (definitely less than it took me)

2 Likes

Thank you for providing this guide, I’m new to Golang and happy to find any useful information.
I also read this article, it’s about how performance monitoring in Golang can be conducted and how to make changes that will affect overall performance.

Long story short, they started the profiler:
defer profile.Start(profile.MemProfileRate(1), profile.ProfilePath(".")).Stop()
profile.MemProfileRate(1)` configure profiler to collect information about each allocation.

Then they started the service to make a single request, analyzed appeared mem.pprof file with the command go tool pprof mem.pprof. Next they went command top main:

Active filters:
   focus=main
Showing nodes accounting for 6MB, 97.61% of 6.15MB total:
Dropped 51 nodes (cum <= 0.03MB)
      flat  flat%   sum%        cum   cum%
       6MB 97.61% 97.61%        6MB 97.61%  bytes.makeSlice
         0     0% 97.61%        6MB 97.61%  bytes.(*Buffer).ReadFrom
         0     0% 97.61%        6MB 97.61%  bytes.(*Buffer).grow
         0     0% 97.61%        6MB 97.61%  io/ioutil.ReadAll
         0     0% 97.61%        6MB 97.61%  io/ioutil.readAll
         0     0% 97.61%        6MB 97.61%  main.main.func1
         0     0% 97.61%        6MB 97.61%  net/http.(*ServeMux).ServeHTTP
         0     0% 97.61%        6MB 97.61%  net/http.(*conn).serve
         0     0% 97.61%        6MB 97.61%  net/http.HandlerFunc.ServeHTTP
         0     0% 97.61%        6MB 97.61%  net/http.serverHandler.ServeHTTP

After that they run command list main.main.func1 to see the allocation per line:

Total: 6.15MB
ROUTINE ======================== main.main.func1 in /Users/user/go/src/article/main.go
         0        6MB (flat, cum) 97.61% of Total
         .          .     30:
         .          .     31:func main() {
         .          .     32:    defer profile.Start(profile.MemProfileRate(1), profile.ProfilePath(".")).Stop()
         .          .     33:
         .          .     34:    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
         .   6MB     35:        bb, _ := ioutil.ReadAll(r.Body)
         .          .     36:        rbody := bytes.NewReader(bb)
         .          .     37:        n, _ := countLetters(rbody)
         .          .     38:        _, _ = fmt.Fprint(w, n)
         .          .     39:    })
         .          .     40:

As it turned out, the line 35 is reading the whole body of requests, instead of just reading characters (counting characters is the function of the service). So they replaced lines 35 and 36 with rbody := bufio.NewReader(bb). It provided Reader, that is going to optimize performance:

http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
    rbody := bufio.NewReader(r.Body)
    n, _ := countLetters(rbody)
    _, _ = fmt.Fprint(w, n)
})

Thus, resource consumption was significantly reduced, with only several actions made.

A great example of how important performance monitoring can be.

1 Like

@Siliyid Nice notes, but just to be clear this is test code you’ve put together and not related to Nakama server. :slight_smile: