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 runfunc init() { if os.Getenv("ENV") == "" { os.Setenv("ENV", "development") }}// A second init() in the same package — also runsfunc 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
@Repositoryclass PostgresMessageStore implements MessageStore { ... }@Serviceclass MessageService { @Autowired MessageStore store; // Spring injects this}@RestControllerclass 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.