Project Layout & Ecosystem
How Go services are laid out on disk, which Java library maps to which Go tool, and when code generation (sqlc, easyjson) is worth it.
Section 23 of 31 4 min read
Go has no Maven standard directory layout, and it does not need one: the compiler only cares about packages (directories) and the internal/ rule. Start flat, and add structure when the code asks for it. The official guidance is short and worth reading: Organizing a Go module (opens in a new tab).
A typical service layout
messaging/
├── go.mod
├── go.sum
├── cmd/
│ └── messaging/
│ └── main.go wiring: config, dependencies, servers
├── internal/ importable only inside this module
│ ├── message/ the domain: types + business rules
│ │ ├── message.go
│ │ ├── service.go
│ │ └── service_test.go tests live beside the code
│ ├── postgres/ storage implementation
│ │ ├── queries.sql
│ │ ├── queries.sql.go generated by sqlc: do not edit
│ │ └── store.go
│ └── httpapi/ HTTP handlers and middleware
│ ├── routes.go
│ └── messages.go
├── migrations/ SQL schema migrations
└── Dockerfilecmd/<name>/main.goholds one directory per binary. A single-binary project can keepmain.goat the root.internal/is the default home for everything. Only create importable packages outside it when other modules really should depend on them.- Package by domain, not by layer.
internal/messagebeatsinternal/services+internal/models+internal/repositories; Go packages that mirror Spring layers tend to import each other in circles. - Skip
pkg/. The popular "golang-standards/project-layout" repository is not an official standard, and the Go team does not recommend apkg/directory.
By domain: imports point inward
By layer: imports tend to circle back
main wires everything, the HTTP and storage packages depend on the domain, and the domain depends on none of them. Packages that mirror Spring layers tend to need each other, and one import back up the chain is a cycle Go refuses to compile.Java tools and their Go counterparts
| Java | Go |
|---|---|
| Spring Boot web / Spring MVC | net/http (Go 1.22 routing); chi, Echo or Gin if you want more |
| Spring DI container | Constructor functions wired in main(); wire or fx for huge graphs |
| Jackson | encoding/json (backed by the faster v2 engine since Go 1.27) |
| JDBC + HikariCP | database/sql (has a built-in pool) or pgx + pgxpool for Postgres |
| Hibernate / JPA | GORM, ent or Bun; most teams prefer SQL-first tools |
| jOOQ / MyBatis | sqlc (SQL files → typed Go code) or Squirrel |
| Flyway / Liquibase | golang-migrate, goose or Atlas |
| JUnit 5 + AssertJ | testing + testify (or plain if checks) |
| Mockito | Hand-written fakes behind small interfaces; gomock or mockery if needed |
| Testcontainers | testcontainers-go |
| SLF4J + Logback | log/slog |
| Micrometer + Actuator | Prometheus client_golang or OpenTelemetry; net/http/pprof |
| Resilience4j | golang.org/x/time/rate, sony/gobreaker, cenkalti/backoff |
| Caffeine | ristretto, otter, or a map + mutex for small caches |
java.util.UUID | The uuid package (Go 1.27); github.com/google/uuid before that |
| Lombok | Nothing: no getters or setters to generate |
| Checkstyle / SpotBugs / PMD | gofmt, go vet, staticcheck, golangci-lint |
| GraalVM native-image | The default: go build produces a static native binary |
SQL-first data access with sqlc
@Entity
@Table(name = "messages")
class Message {
@Id @GeneratedValue
private Long id;
@Column(nullable = false)
private String topic;
}
interface MessageRepository
extends JpaRepository<Message, Long> {
// JPQL derived from the method name at startup
List<Message> findByTopicOrderByCreatedAtDesc(String topic);
}// queries.sql
// -- name: ListByTopic :many
// SELECT id, topic, payload, created_at
// FROM messages
// WHERE topic = $1
// ORDER BY created_at DESC;
// "go tool sqlc generate" writes queries.sql.go with:
func (q *Queries) ListByTopic(ctx context.Context, topic string) ([]Message, error)
// Usage: typed, no reflection, no runtime query building
msgs, err := q.ListByTopic(ctx, "orders")When code generation for JSON is worth it
Start with encoding/json
encoding/json uses reflection, and in Go 1.27 it is backed by the new v2 implementation, which is much faster at decoding. Generators such as easyjson produce hand-written-style marshalling code for extra speed, at the cost of generated files to keep in sync. Adopt one only after profiling shows JSON is a real bottleneck on a hot path, and when the types are stable.
//go:generate go tool easyjson -all message.go
type Message struct {
ID int64 `json:"id"`
Topic string `json:"topic"`
Payload []byte `json:"payload"`
}
// "go generate ./..." writes message_easyjson.go with
// MarshalJSON/UnmarshalJSON methods (and faster MarshalEasyJSON).
// For the full speed-up call the easyjson package directly:
data, err := easyjson.Marshal(msg)