go mod init myproject && go build -o myservice ./cmd/myserviceextends keyword.context.Context as the first parameter in functions that perform I/O — Go has no implicit request-scoped state like Spring's RequestContextHolder.make(map[K]V) or literal syntax) — var m map[K]V creates a nil map that panics on assignment.go vet ./... and go test -race ./... before every deployment — Go's static analysis and race detector catch concurrency bugs that Java's type system does not prevent.| Java Pattern | Go Equivalent | Example |
|---|---|---|
class Foo extends Bar | Struct embedding | type Foo struct { Bar } |
class Foo implements Iface | Implicit interface | Just implement the methods — no implements keyword |
try { } catch (Exception e) { } | Multiple return values | val, err := doSomething(); if err != nil { return err } |
@Autowired / DI container | Constructor injection via params | func NewService(repo Repo) *Service { return &Service{repo: repo} } |
Thread / ExecutorService | Goroutines + channels | go process(item); results <- output |
synchronized / ReentrantLock | sync.Mutex or channels | mu.Lock(); defer mu.Unlock() |
Optional<T> | Pointer or comma-ok idiom | val, ok := m[key] or *T (nil = absent) |
Stream.map().filter().collect() | For loops with slices | for _, v := range items { if v > 0 { out = append(out, v) } } |
HashMap<K, V> | Built-in map[K]V | m := map[string]int{"a": 1} |
ArrayList<T> | Slice []T | s := []int{1, 2, 3}; s = append(s, 4) |
interface Foo { void bar(); } | type Foo interface { Bar() } | Uppercase = exported; no access modifiers |
public / private / protected | Capitalization | Exported (public) vs unexported (package-private) |
@Override annotation | No equivalent needed | Implicit interface satisfaction — compiler checks at use site |
try-with-resources / finally | defer | defer file.Close() — runs when function returns |
package com.foo.bar (deep nesting) | Flat packages | package bar — one level, short names |
START
+-- Is this a monolithic Java application?
| +-- YES --> Break into service boundaries first, then migrate one service at a time
| +-- NO (already microservices) v
+-- Does the service have heavy Java framework dependencies (Spring, Hibernate)?
| +-- YES --> Map framework features to Go stdlib + lightweight libraries (see Quick Reference)
| +-- NO v
+-- Is the service CPU-bound or I/O-bound?
| +-- CPU-BOUND --> Go excels here -- goroutines + GOMAXPROCS for parallelism
| +-- I/O-BOUND --> Use goroutines + channels for concurrent I/O (replaces CompletableFuture)
+-- Does the service use complex ORM patterns (JPA/Hibernate)?
| +-- YES --> Replace with sqlx or pgx + raw SQL (Go favors explicit queries over magic ORM)
| +-- NO v
+-- Does the team have Go experience?
| +-- NO --> Start with a non-critical service as a pilot; invest 2-4 weeks in Go training first
| +-- YES v
+-- DEFAULT --> Rewrite service in idiomatic Go: structs for data, interfaces for behavior, error returns for control flow
Create the Go project with a standard layout. Go projects use a flat, simple directory structure compared to Java's deep package hierarchy. [src1]
mkdir myservice && cd myservice
go mod init github.com/yourorg/myservice
mkdir -p cmd/myservice internal/handler internal/service internal/repository
Verify: go mod tidy runs without errors.
Replace class hierarchies with composition. Define small, focused interfaces (1–3 methods) and let structs satisfy them implicitly. [src1, src3]
// Java: public interface UserRepository { User findById(long id); }
type UserRepository interface {
FindByID(ctx context.Context, id int64) (*User, error)
}
type User struct {
ID int64 `json:"id" db:"id"`
Name string `json:"name" db:"name"`
}
// Implicit interface satisfaction (no "implements" keyword)
type PostgresUserRepo struct { db *sql.DB }
func (r *PostgresUserRepo) FindByID(ctx context.Context, id int64) (*User, error) {
var u User
err := r.db.QueryRowContext(ctx, "SELECT id, name FROM users WHERE id = $1", id).
Scan(&u.ID, &u.Name)
if err != nil {
return nil, fmt.Errorf("find user %d: %w", id, err)
}
return &u, nil
}
Verify: go vet ./... passes with no issues.
Convert try/catch blocks to Go's explicit error checking. Wrap errors with fmt.Errorf("context: %w", err) for debugging. [src1, src7]
func (s *UserService) GetUser(ctx context.Context, id int64) (*UserDTO, error) {
user, err := s.repo.FindByID(ctx, id)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, fmt.Errorf("user %d not found: %w", id, ErrNotFound)
}
return nil, fmt.Errorf("get user %d: %w", id, err)
}
return toDTO(user), nil
}
// Sentinel errors (like custom exception classes)
var ErrNotFound = errors.New("not found")
var ErrForbidden = errors.New("forbidden")
Verify: go build ./... compiles; error paths tested with errors.Is(err, ErrNotFound).
Wire dependencies explicitly through constructor functions. No annotation-based DI containers. [src2, src3]
type UserService struct {
repo UserRepository
email EmailSender
}
func NewUserService(repo UserRepository, email EmailSender) *UserService {
return &UserService{repo: repo, email: email}
}
// Wire in main() -- this IS your DI container
func main() {
db := connectDB()
repo := repository.NewPostgresUserRepo(db)
emailer := email.NewSMTPSender(smtpConfig)
userSvc := service.NewUserService(repo, emailer)
handler := handler.NewUserHandler(userSvc)
mux := http.NewServeMux()
mux.HandleFunc("GET /users/{id}", handler.GetUser)
log.Fatal(http.ListenAndServe(":8080", mux))
}
Verify: go run ./cmd/myservice starts the server.
Replace ExecutorService and CompletableFuture with goroutines and channels. [src1, src4]
// Goroutines with bounded concurrency (replaces fixed thread pool)
func processAll(ctx context.Context, tasks []Task) ([]Result, error) {
results := make([]Result, len(tasks))
sem := make(chan struct{}, 10) // max 10 concurrent
var wg sync.WaitGroup
for i, task := range tasks {
wg.Add(1)
go func(i int, t Task) {
defer wg.Done()
sem <- struct{}{}
defer func() { <-sem }()
results[i], _ = process(ctx, t)
}(i, task)
}
wg.Wait()
return results, nil
}
Verify: go test -race ./... passes — the race detector catches concurrent access bugs.
Replace Spring Boot's embedded Tomcat with Go's net/http stdlib. Go 1.22+ includes built-in pattern routing with path parameters, eliminating the need for third-party routers in many cases. Middleware replaces Spring interceptors. [src1, src2]
// Go 1.22+ stdlib pattern routing (replaces @RestController + @GetMapping)
func (h *UserHandler) GetUser(w http.ResponseWriter, r *http.Request) {
id, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
if err != nil {
http.Error(w, "invalid id", http.StatusBadRequest)
return
}
user, err := h.svc.GetUser(r.Context(), id)
if err != nil {
if errors.Is(err, service.ErrNotFound) {
http.Error(w, "not found", http.StatusNotFound)
return
}
http.Error(w, "internal error", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(user)
}
// Middleware (replaces Spring Filter/Interceptor)
func loggingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
next.ServeHTTP(w, r)
log.Printf("%s %s %v", r.Method, r.URL.Path, time.Since(start))
})
}
Verify: curl http://localhost:8080/users/1 returns JSON. Server startup <100ms.
Go compiles to a single static binary with no runtime dependencies. Replaces JVM + JAR deployment. Go 1.24+ automatically embeds version info from VCS tags into the binary. [src2, src5]
# Build for production (like mvn package -DskipTests)
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o myservice ./cmd/myservice
# Docker image: ~10-20MB (Go from scratch) vs ~200-400MB (Java + JVM)
Verify: ./myservice runs without JVM. Docker image <20MB. Startup <100ms.
// Input: A Java Spring Boot @RestController + @Service + @Repository
// Output: Equivalent Go service with same API contract
package main
import (
"context"
"database/sql"
"encoding/json"
"errors"
"fmt"
"log"
"net/http"
"strconv"
_ "github.com/lib/pq"
)
type Product struct {
ID int64 `json:"id" db:"id"`
Name string `json:"name" db:"name"`
Price float64 `json:"price" db:"price"`
}
type ProductRepository interface {
FindByID(ctx context.Context, id int64) (*Product, error)
FindAll(ctx context.Context) ([]Product, error)
Save(ctx context.Context, p *Product) error
}
type pgProductRepo struct { db *sql.DB }
func (r *pgProductRepo) FindByID(ctx context.Context, id int64) (*Product, error) {
var p Product
err := r.db.QueryRowContext(ctx,
"SELECT id, name, price FROM products WHERE id = $1", id,
).Scan(&p.ID, &p.Name, &p.Price)
if errors.Is(err, sql.ErrNoRows) { return nil, nil }
return &p, err
}
func (r *pgProductRepo) FindAll(ctx context.Context) ([]Product, error) {
rows, err := r.db.QueryContext(ctx, "SELECT id, name, price FROM products")
if err != nil { return nil, err }
defer rows.Close()
var products []Product
for rows.Next() {
var p Product
if err := rows.Scan(&p.ID, &p.Name, &p.Price); err != nil { return nil, err }
products = append(products, p)
}
return products, rows.Err()
}
type ProductHandler struct { repo ProductRepository }
func (h *ProductHandler) GetProduct(w http.ResponseWriter, r *http.Request) {
id, _ := strconv.ParseInt(r.PathValue("id"), 10, 64)
product, err := h.repo.FindByID(r.Context(), id)
if err != nil { http.Error(w, "internal error", 500); return }
if product == nil { http.Error(w, "not found", 404); return }
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(product)
}
func main() {
db, _ := sql.Open("postgres", "postgres://user:pass@localhost/mydb?sslmode=disable")
defer db.Close()
repo := &pgProductRepo{db: db}
handler := &ProductHandler{repo: repo}
mux := http.NewServeMux()
mux.HandleFunc("GET /products/{id}", handler.GetProduct)
log.Fatal(http.ListenAndServe(":8080", mux))
}
// Input: Java ExecutorService with Callable tasks and Future results
// Output: Go worker pool with goroutines and channels
package main
import (
"context"
"fmt"
"sync"
"time"
)
type Job struct { ID int; Payload string }
type Result struct { JobID int; Output string; Err error }
func WorkerPool(ctx context.Context, workers int, jobs <-chan Job) <-chan Result {
results := make(chan Result, len(jobs))
var wg sync.WaitGroup
for i := 0; i < workers; i++ {
wg.Add(1)
go func(workerID int) {
defer wg.Done()
for job := range jobs {
select {
case <-ctx.Done():
results <- Result{JobID: job.ID, Err: ctx.Err()}
return
default:
output, err := processJob(job)
results <- Result{JobID: job.ID, Output: output, Err: err}
}
}
}(i)
}
go func() { wg.Wait(); close(results) }()
return results
}
func processJob(j Job) (string, error) {
time.Sleep(100 * time.Millisecond)
return fmt.Sprintf("processed: %s", j.Payload), nil
}
// Input: Java unit test with Mockito mocks
// Output: Go test using interface-based test doubles (no framework needed)
package service
import (
"context"
"errors"
"testing"
)
type mockUserRepo struct {
users map[int64]*User
err error
}
func (m *mockUserRepo) FindByID(ctx context.Context, id int64) (*User, error) {
if m.err != nil { return nil, m.err }
u, ok := m.users[id]
if !ok { return nil, ErrNotFound }
return u, nil
}
func TestGetUser_Success(t *testing.T) {
repo := &mockUserRepo{users: map[int64]*User{1: {ID: 1, Name: "Alice"}}}
svc := NewUserService(repo, nil)
user, err := svc.GetUser(context.Background(), 1)
if err != nil { t.Fatalf("unexpected error: %v", err) }
if user.Name != "Alice" { t.Errorf("got %q, want %q", user.Name, "Alice") }
}
func TestGetUser_NotFound(t *testing.T) {
repo := &mockUserRepo{users: map[int64]*User{}}
svc := NewUserService(repo, nil)
_, err := svc.GetUser(context.Background(), 999)
if !errors.Is(err, ErrNotFound) { t.Errorf("got %v, want ErrNotFound", err) }
}
// BAD -- treating embedding as inheritance (it's not)
type Animal struct { Name string }
func (a *Animal) Speak() string { return "..." }
type Dog struct { Animal } // NOT inheritance
func (d *Dog) Speak() string { return "Woof" }
// Bug: Animal.Speak() still accessible; no polymorphism
// GOOD -- interfaces for polymorphic behavior
type Speaker interface { Speak() string }
type Dog struct{ Name string }
func (d Dog) Speak() string { return "Woof" }
type Cat struct{ Name string }
func (c Cat) Speak() string { return "Meow" }
func greetAll(speakers []Speaker) {
for _, s := range speakers { fmt.Println(s.Speak()) }
}
// BAD -- using panic for control flow (Java exception habit)
func findUser(id int64) *User {
user, err := db.FindByID(id)
if err != nil {
panic(fmt.Sprintf("user not found: %d", id))
}
return user
}
func handler(w http.ResponseWriter, r *http.Request) {
defer func() {
if r := recover(); r != nil {
http.Error(w, "internal error", 500)
}
}()
user := findUser(42)
json.NewEncoder(w).Encode(user)
}
// GOOD -- explicit error returns (idiomatic Go)
func findUser(ctx context.Context, id int64) (*User, error) {
user, err := db.FindByID(ctx, id)
if err != nil {
return nil, fmt.Errorf("find user %d: %w", id, err)
}
return user, nil
}
func handler(w http.ResponseWriter, r *http.Request) {
user, err := findUser(r.Context(), 42)
if err != nil {
if errors.Is(err, ErrNotFound) {
http.Error(w, "not found", 404)
return
}
http.Error(w, "internal error", 500)
return
}
json.NewEncoder(w).Encode(user)
}
// BAD -- Java-style boilerplate
type User struct { name string; email string }
func (u *User) GetName() string { return u.name }
func (u *User) SetName(n string) { u.name = n }
func (u *User) GetEmail() string { return u.email }
func (u *User) SetEmail(e string) { u.email = e }
// GOOD -- exported fields, no getters/setters needed
type User struct {
Name string `json:"name"`
Email string `json:"email"`
}
// Direct access: user.Name = "Alice"
// BAD -- interface for a single concrete implementation
type UserServiceInterface interface {
GetUser(ctx context.Context, id int64) (*User, error)
CreateUser(ctx context.Context, u *User) error
DeleteUser(ctx context.Context, id int64) error
}
type UserServiceImpl struct { /* fields */ }
// Unnecessary abstraction when only one implementation exists
// GOOD -- define interfaces where they are used (consumer side)
type UserGetter interface {
GetUser(ctx context.Context, id int64) (*User, error)
}
type UserHandler struct {
users UserGetter // Narrow interface, easy to test
}
// The concrete service satisfies this implicitly
// BAD -- no context propagation
func fetchData(url string) ([]byte, error) {
resp, err := http.Get(url) // No timeout, no cancellation
if err != nil { return nil, err }
defer resp.Body.Close()
return io.ReadAll(resp.Body)
}
// GOOD -- context for cancellation, timeouts, request-scoped values
func fetchData(ctx context.Context, url string) ([]byte, error) {
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil { return nil, err }
resp, err := http.DefaultClient.Do(req)
if err != nil { return nil, fmt.Errorf("fetch %s: %w", url, err) }
defer resp.Body.Close()
return io.ReadAll(resp.Body)
}
new HashMap<>() is safe. In Go, var m map[string]int creates a nil map that panics on write. Fix: Always initialize: m := make(map[string]int). [src4]context.Context for cancellation. [src1]go vet and staticcheck: Java developers rely on IDE warnings. Go has powerful static analysis. Fix: Run go vet ./... and staticcheck ./... in CI. [src1]init() for complex setup: Java developers replicate @PostConstruct with init(). Creates hidden dependencies. Fix: Use explicit initialization in main(). [src3]? extends T), no type erasure. Go 1.24 adds generic type aliases; Go 1.25 removes core types. Fix: Use generics sparingly for data structures; prefer interfaces for behavioral abstraction. [src4]Close() explicitly. Fix: defer resource.Close() immediately after open. [src7]context.Context in APIs: Java has no equivalent; Go passes context explicitly. Fix: Accept ctx context.Context as first parameter in all I/O functions. [src1]go.uber.org/automaxprocs or upgrade to Go 1.25+ which auto-adjusts. [src8]# Initialize a new Go project
go mod init github.com/yourorg/myservice
# Download dependencies (like mvn dependency:resolve)
go mod tidy
# Build and check for compilation errors
go build ./...
# Run all tests with race detector enabled
go test -race -v ./...
# Run static analysis (catches bugs go build misses)
go vet ./...
# Check test coverage (like JaCoCo)
go test -coverprofile=coverage.out ./... && go tool cover -html=coverage.out
# Format all code (like google-java-format but enforced)
gofmt -w .
# Profile CPU/memory (like JFR/VisualVM)
go test -bench=. -cpuprofile=cpu.out -memprofile=mem.out ./...
go tool pprof cpu.out
# Cross-compile for Linux (no JVM needed on target)
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o myservice ./cmd/myservice
# Track tool dependencies in go.mod (Go 1.24+)
go get -tool golang.org/x/tools/cmd/stringer
| Version | Status | Breaking Changes | Migration Notes |
|---|---|---|---|
| Go 1.24 (2025) | Current | Generic type aliases, Swiss Table maps, tool directives in go.mod, FIPS 140-3 crypto | 15–25% GC pause improvement; use go get -tool for tool deps |
| Go 1.23 (2024) | Supported | Iterator support (range-over-func), enhanced net/http | Use range-over-func for custom iterators |
| Go 1.22 (2024) | Supported | Enhanced net/http routing with path params | mux.HandleFunc("GET /users/{id}", handler) replaces Chi/Gorilla for simple routing |
| Go 1.21 (2023) | Supported | log/slog structured logging, slices/maps | Replace third-party logging for simple cases |
| Go 1.18 (2022) | Maintenance | Generics (type constraints), fuzzing | Use generics for data structures, not behavioral interfaces |
| Java 25 (2025) | LTS (source) | Virtual threads finalized, structured concurrency redesigned, scoped values | Virtual threads narrow Go's concurrency advantage for I/O-bound work; synchronized no longer pins carrier threads |
| Java 21 (2023) | LTS (source) | Virtual threads, pattern matching, record patterns | Virtual threads reduce Go's concurrency advantage for I/O-bound work |
| Java 17 (2021) | LTS (source) | Sealed classes, records | Records map well to Go structs |
| Use When | Don't Use When | Use Instead |
|---|---|---|
| Microservices with high concurrency needs | Heavy enterprise integration (ESB, JMS, BPMN) | Stay with Java + Spring Boot |
| You need fast startup and low memory (serverless, K8s) | Team has deep Java/Spring expertise, no Go experience | Invest in Java optimization (GraalVM native) |
| Building CLI tools, infrastructure, or DevOps tooling | Complex ORM-heavy CRUD applications | Java + JPA/Hibernate or consider Kotlin |
| You want single-binary deployment with no runtime deps | Project depends on JVM-only libraries (Kafka Streams, Spark) | Keep those components in Java |
| Team is willing to learn Go idioms (not just syntax) | Tight deadline — rewriting under pressure leads to Java-in-Go code | Incremental migration or stay |
| Containerized microservices where image size matters (~10MB vs ~300MB) | Application heavily uses Java reflection, bytecode manipulation, or annotation processing | Stay with JVM; Go has no equivalent runtime reflection power |
if err != nil.implements. This is powerful but can confuse Java developers who expect explicit contracts.WithTimeout(5*time.Second)) or config structs instead.go mod init for new projects.pprof and reduce allocations for predictable latency.null maps roughly to Go's nil, but Go's zero values are type-specific (0 for int, "" for string, nil for pointers/slices/maps). A nil slice is valid (length 0), but a nil map panics on write.