This post is a quick how-to for starting a new project in Go. It features:
- Hot code reloading
- Running multiple Docker containers with Docker Compose
- Using Go Modules for managing dependencies
It’s best to show the above working together with an example project. We’re going to set up two separate services communicating with messages over NATS. The first one will receive messages on an HTTP endpoint and then publish them to a NATS topic. The other will subscribe to this topic and print incoming messages on the standard output.
It’s pretty simple architecture, right? To keep the examples short and make handling messages easier, we’re going to use the Watermill library.
I won’t go through all Go code here, but you can clone the example repository.
Go Modules were introduced in Go 1.11 and are (hopefully) the final solution for managing dependencies.
I recommend keeping the repository outside of
GOPATH if you’re going to use them. If you’d
prefer to use
GOPATH anyway, make sure you’ve set
Let’s initialize modules for each of our packages (
go mod init
go get github.com/ThreeDotsLabs/watermill go get github.com/nats-io/go-nats-streaming
Add an extra ULID library only for the
go get github.com/oklog/ulid
The next step is creating a Docker image for containers that will run both applications.
Dockerfile is fairly simple:
FROM golang:1.11.2-stretch RUN go get github.com/cespare/reflex COPY reflex.conf / ENTRYPOINT ["reflex", "-c", "/reflex.conf"]
The image is based on the official Go image. Usually, you’d want to stick with the smaller
alpine version, but since it is missing some dependencies for compiling
reflex, I’m using the
It installs and runs reflex which will be used for hot recompiling the code. It can be very useful for quickly testing your changes.
Reflex is configured by
reflex.conf, which in our case is just one line:
-r '(\.go$|go\.mod)' -s go run .
What the command means is: “watch for changes to
go.mod and all files ending in
go run . when it happens. The
-s flag stands for
service and will make reflex kill
previously run command before starting it again, which is exactly what we want.
We’ll use Docker Compose for running multiple services within a shared network. It allows you to spin up containers with services or databases without cluttering your local machine. Also, other developers can set up the project by cloning the repository and running one command, instead of manually installing dependencies and compiling the application.
docker-compose.yml file in the repository:
version: '3' services: publisher: build: . volumes: - ./publisher:/app - $GOPATH/pkg/mod/cache:/go/pkg/mod/cache working_dir: /app env_file: - .env ports: - 5000:5000 subscriber: build: . volumes: - ./subscriber:/app - $GOPATH/pkg/mod/cache:/go/pkg/mod/cache working_dir: /app env_file: - .env nats: image: nats-streaming:0.11.2 restart: on-failure
It defines three services. Both
subscriber are based on the
created before (this is done by the
build: . option). Each application’s code is mounted in the
working directory, so
go run . will run the proper package. The
ports part in the
service will bind port 5000 on your local system and map it to the port inside container.
I’ve also mounted
$GOPATH/pkg/mod/cache directory, to use existing cache for dependencies, speeding
The services are configured by environment variables, kept in a separate file:
PORT=5000 NATS_TOPIC=example NATS_CLUSTER_ID=test-cluster NATS_URL=nats://nats:4222
nats hostname in the
NATS_URL variable - this is the name of the service defined in
To start the environment, run:
You’ll see the output of all three services. In another terminal, try sending an example request to
the publisher service with
$ curl -X POST http://localhost:5000 --data "this is my message" Sent message: this is my message with ID 01D09P02SBW5D0QPWP14QQZJWH
You should see the message delivered in the
subscriber log output:
subscriber_1 |  2019/01/03 11:01:27 received message: 01D09P02SBW5D0QPWP14QQZJWH, payload: this is my message
Now, try editing either of the
main.go files in your editor. After you save the file, appropriate
service should be recompiled and restarted.
When finished, you can kill the environment with
Ctrl-C in the terminal it is running. You can
also delete all containers by running
Now that you’ve built Docker images of the services it’s the first step towards deploying it. We’ll look into it in the next post.
If you’d like to explore more docker-compose definitions, check Watermill Getting started for Kafka and Google Cloud Pub/Sub examples.