◎ Go for
Java Developers
Section 18

Init & Main Lifecycle

Go programs have a precise startup sequence. Understanding it is essential for reading main.go files — and for understanding how Go's manual wiring compares to Spring's automatic wiring.

Startup order

Go program startup — exact sequence
1. Package-level variables initialised, in import dependency order (deepest import first)

2. init() functions run — all of them, in import order, then current package

3. main() runs

If A imports B imports C: C vars → C init() → B vars → B init() → A vars → A init() → main()

init() — automatic, invisible, hard to test

init() rules
package config

// Package-level var — initialised before init()
var defaultTimeout = 30 * time.Second

// init() runs automatically before main()
// Cannot be called manually
// No parameters, no return values
// Multiple init() in same file/package are allowed — all run
func init() {
    if os.Getenv("ENV") == "" {
        os.Setenv("ENV", "development")
    }
}

// A second init() in the same package — also runs
func init() {
    setupLogger()
}
!Use init() only for driver registration and truly one-time global setup. It cannot be tested directly and runs invisibly. Prefer explicit InitX() functions called from main().

Spring DI vs Go manual wiring

☕ Java Spring — @Autowired, IoC container
@Repository
class PostgresMessageStore implements MessageStore { ... }

@Service
class MessageService {
    @Autowired
    MessageStore store;  // Spring injects this
}

@RestController
class MessageController {
    @Autowired
    MessageService service;  // Spring injects this
}

// Spring scans, reflects, injects — invisible wiring
// Hard to trace: where does store come from?
◎ Go — manual wiring in main()
// In Go — wire everything explicitly in main()
func main() {
    // Each dependency constructed and passed explicitly
    // You can read main() and trace every connection

    db := mustOpenDB(cfg.DatabaseURL)

    // store depends on db
    store := store.NewPostgresStore(db)

    // service depends on store
    service := service.NewMessageService(store)

    // handler depends on service
    handler := handler.NewMessageHandler(service)

    // router depends on handler
    router := setupRouter(handler)

    http.ListenAndServe(":8080", router)
}
Why Go does thisSpring uses reflection and classpath scanning to wire dependencies at startup — convenient but opaque. Go wires everything explicitly in main() — more code, but you can trace every dependency by reading the function. No surprises, no magic, no annotation scanning. The trade-off: more boilerplate in main() for the benefit of complete transparency.

Full production main.go — everything wired

main.go — the complete lifecycle
func main() {
    // ── 1. LOAD CONFIG ────────────────────────────────────────────
    cfg, err := config.Load(os.Getenv("CONFIG_PATH"))
    if err != nil { log.Fatal("config:", err) }

    // ── 2. CONNECT INFRASTRUCTURE ─────────────────────────────────
    db, err := sql.Open("postgres", cfg.DatabaseURL)
    if err != nil { log.Fatal("db:", err) }
    defer db.Close()   // last in, first closed

    // ── 3. BUILD LAYERS BOTTOM-UP ────────────────────────────────
    // Each layer only knows about the layer below it via interface
    store   := store.NewPostgresStore(db)
    engine  := engine.NewEngine(store, cfg)
    handler := handler.NewHandler(engine)

    // ── 4. ROOT CONTEXT — cancelling this stops everything ─────────
    ctx, cancel := context.WithCancel(context.Background())
    defer cancel()  // ensure cancel is always called on exit

    // ── 5. START ENGINE GOROUTINES ────────────────────────────────
    if err := engine.Start(ctx); err != nil { log.Fatal("engine:", err) }

    // ── 6. HTTP SERVER IN BACKGROUND GOROUTINE ──────────────────
    srv := &http.Server{Addr: cfg.HTTPAddr, Handler: handler.Router()}
    go func() {
        if err := srv.ListenAndServe(); err != http.ErrServerClosed {
            log.Printf("http server error: %v", err)
        }
    }()
    log.Printf("listening on %s", cfg.HTTPAddr)

    // ── 7. BLOCK UNTIL OS SIGNAL ─────────────────────────────────
    // quit is a buffered channel — signal.Notify needs buffer ≥ 1
    quit := make(chan os.Signal, 1)
    signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
    <-quit   // main goroutine blocks here — all other goroutines stay alive
    // Ctrl+C or SIGTERM (k8s pod shutdown) unblocks this

    // ── 8. GRACEFUL SHUTDOWN ─────────────────────────────────────
    log.Println("shutdown signal received — draining...")
    cancel()  // broadcasts to ALL goroutines using ctx — they see ctx.Done()

    // Give in-flight requests 30s to complete
    shutCtx, shutCancel := context.WithTimeout(context.Background(), 30*time.Second)
    defer shutCancel()
    srv.Shutdown(shutCtx)  // stops accepting new, waits for in-flight
    engine.Stop()          // drains message queues
    log.Println("shutdown complete")
}
The <-quit line is the program's heartbeat — main blocks there, keeping every goroutine alive. When k8s sends SIGTERM, this unblocks, cancel() fires, all goroutines exit their select loops, and the server drains cleanly before the process terminates.