I am just starting with Go (golang) and want to get a new project folder structure set up for a project that will be built with Gradle and deployed to a Docker image. I'm struggling to determine what this project structure might look like, primarily because of the GOPATH structure and the fact that the Go language tooling seems to be antithetical to using Gradle or to configuring a project that can be cloned (Git).
The project will eventually contain various server-side code written in Go, client side code written in HTML and JavaScript, so I need a project structure that works well for Gradle to build and package all of these kinds of pieces.
Does anyone have a good working structure and tooling recommendations for this?
When I started with Go, I fiddled with a rather wide variety of build tools, from maven to gulp.
It turned out that at least for me, they were doing more harm than good, so I started to use Go's seemingly unimposing, but really well thought out features. One of them isgo generate
. Add simple shell scripts or occasionally Makefiles for automation.
I have put together a sample project to make this more clear
/Users/you/go/src/bitbucket.org/you/hello/
├── Dockerfile
├── Makefile
├── _templates
│ └── main.html
└── main.go
This is a simple web server which serves "Hello, World!" using a template which is embedded into the binary using the excellent go.rice tool:
//go:generate rice embed-go
package main
import (
"html/template"
"log"
"net/http"
rice "github.com/GeertJohan/go.rice"
)
func main() {
templateBox, err := rice.FindBox("_templates")
if err != nil {
log.Fatal(err)
}
// get file contents as string
templateString, err := templateBox.String("main.html")
if err != nil {
log.Fatal(err)
}
// parse and execute the template
tmplMessage, err := template.New("message").Parse(templateString)
if err != nil {
log.Fatal(err)
}
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
if err := tmplMessage.Execute(w, map[string]string{"Greeting": "Hello, world!"}); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
})
log.Fatal(http.ListenAndServe("127.0.0.1:8080", nil))
}
Note the line
//go:generate rice embed-go
When you call go generate
your source file will be scanned for such lines and the according commands will be executed. In this case, a file called rice-embed.go will be generated and your directory will look like this:
/Users/you/go/src/bitbucket.org/you/hello/
├── Dockerfile
├── Makefile
├── _templates
│ └── main.html
├── main.go
└── rice-box.go
You could call webpack in a //go generate
for example, to get your stuff together and another generate to create a rice-box.go
from the result. This way, all your stuff would be embedded in your binary and would become a breeze to deploy.
I have used a rather simple Dockerfile for this example:
FROM alpine:latest
MAINTAINER You <you@example.com>
COPY hello /usr/bin
EXPOSE 8080
CMD ["/usr/bin/hello"]
However this brings us to a problem: We can not use go:generate
to produce the docker image, as of the time when we need to call go:generate
, the new binary is not build yet. This would make us do ugly things like
go generate && go build && go generate
leading to the docker image build twice and whatnot. So, we need a different solution
We could of course come up with something like:
#!/bin/bash
# Checks for existence omitted for brevity
GO=$(which go)
DOCKER=$(which docker)
$GO generate
$GO test
$GO build
$DOCKER -t you/hello .
However, this comes with a problem: you will always do the whole sequence using the shell script. Even when you just want to run the tests, you would end up building the docker image. Over time, this adds up. In such situations I tend to use
A Makefile is a configuration file for GNU make
CC = $(shell which go 2>/dev/null)
DOCKER = $(shell which docker 2>/dev/null)
ifeq ($(CC),)
$(error "go is not in your system PATH")
else
$(info "go found")
endif
ifeq ($(DOCKER),)
$(error "docker not in your system path")
else
$(info "docker found")
endif
.PHONY: clean generate tests docker all
all: clean generate tests hello docker
clean:
$(RM) hello rice-box.go cover.out
generate:
$(CC) generate
tests: generate
$(CC) test -coverprofile=cover.out
hello: tests
$(CC) build
docker: hello
$(DOCKER) build -t sosample/hello .
A full explanation is beyond the scope of this answer, but what you can basically do here is that you can call make
and the all
target will be built: files from the old build are removed (clean
), a new rice-box.go
is generated (generate
) and so on. But in case you only want to run the tests for example, calling make test
would only execute the targets clean
, generate
and tests
.
You can take a look to my approach to structure your project https://github.com/alehano/gobootstrap It's a web framework.