Tooling & Deployment
One go command instead of a build tool, formatting and linting, static binaries and small containers, embedded assets, and tuning the GC instead of -Xmx.
Section 28 of 314 min read
A Java project assembles its toolchain from parts: a JDK, Maven or Gradle, plugins for formatting, linting, coverage and packaging, and a JVM at runtime. Go puts almost all of it in one go command and produces a single native executable with no runtime to install.
The go command
| Java | Go |
|---|---|
mvn compile / gradle build | go build ./... |
mvn exec:java / gradle run | go run ./cmd/api |
mvn test | go test ./... |
mvn install (a CLI tool) | go install example.com/tool@latest |
| Spotless / google-java-format | gofmt (and goimports) |
| Compiler warnings + ErrorProne | go vet ./... |
| OpenRewrite migrations | go fix ./... (modernizers, Go 1.26+) |
| Javadoc | go doc, and pkg.go.dev for published modules |
| Annotation processors | go generate + //go:generate comments |
| OWASP dependency-check | govulncheck ./... |
Formatting and linting
gofmtsettles style. Tabs, spacing and alignment are not configurable, so they are never debated in code review. Run it on save;goimportsalso adds and removes import lines.go vetcatches real bugs: mismatchedPrintfverbs, copied mutexes, unreachable code, lostcontextcancel functions, malformed struct tags.go testruns a subset automatically.- staticcheck is the most respected third-party analyser. golangci-lint runs it together with dozens of other linters (
errcheck,gosec,revive,exhaustive) from one config file. go fix(revamped in Go 1.26) rewrites code to use newer idioms and APIs, for example replacing hand-written loops withslices.Containsorinterface{}withany.govulncheckreports known vulnerabilities only when your code actually calls the affected function, which keeps the signal high.
Building: one static binary
# A static binary with no libc dependency (pure-Go code)
CGO_ENABLED=0 go build -o bin/api ./cmd/api
# Cross-compile: no toolchain to install for the target
GOOS=linux GOARCH=arm64 go build -o bin/api-linux-arm64 ./cmd/api
GOOS=windows GOARCH=amd64 go build -o bin/api.exe ./cmd/api
# Stamp a version at link time; strip paths for reproducible builds
go build -trimpath -ldflags "-s -w -X main.version=v1.4.2" -o bin/api ./cmd/api
# Which module versions went into a binary? (build info is embedded)
go version -m bin/apiWhat changes when there is no JVM
Startup takes milliseconds rather than seconds, with no warm-up: code is compiled ahead of time, so there is no JIT and no class loading. That makes Go a good fit for CLIs, serverless functions and fast autoscaling.
Memory for a small service is typically tens of megabytes, not hundreds. There is no heap to pre-size and no metaspace.
Deployment is one file. The same idea as a GraalVM native image, except it is the default and builds in seconds.
Containers
# ---- build stage ----
FROM golang:1.27 AS build
WORKDIR /src
# Cache dependencies separately from source changes
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -trimpath -ldflags "-s -w" -o /out/api ./cmd/api
# ---- runtime stage: no shell, no package manager ----
FROM gcr.io/distroless/static-debian12:nonroot
COPY --from=build /out/api /api
USER nonroot:nonroot
EXPOSE 8080
ENTRYPOINT ["/api"]Embedding files in the binary
// src/main/resources/templates/welcome.html
try (InputStream in = getClass()
.getResourceAsStream("/templates/welcome.html")) {
String html = new String(in.readAllBytes(), UTF_8);
}import "embed"
// The files are compiled into the binary at build time
//go:embed templates/*.html
var templates embed.FS
//go:embed schema.sql
var schema string
//go:embed static
var static embed.FS
html, err := templates.ReadFile("templates/welcome.html")
// embed.FS plugs into http.FileServerFS and html/template
mux.Handle("GET /static/", http.FileServerFS(static))Tuning the garbage collector
| Java | Go |
|---|---|
-Xmx2g: a hard heap limit | GOMEMLIMIT=1800MiB: a soft limit; GC works harder as it approaches |
-XX:+UseG1GC, ZGC, Shenandoah… | One concurrent, low-latency GC (Green Tea, the default since Go 1.26) |
| Dozens of GC tuning flags | GOGC (default 100): heap growth before the next cycle |
-XX:ActiveProcessorCount | GOMAXPROCS, container-aware by default since Go 1.25 |
| JIT profiling at runtime | Profile-guided optimisation: commit a default.pgo CPU profile |