Golang,Docker,Gradle的项目结构

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.

Sample project

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

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.

Dockerfile

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

Solution A: A shell script

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

Solution B: a Makefile

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.