Navigation

Go

Go Init Functions and Package Initialization Order: Complete Guide (2025)

Master Go init functions and package initialization order. Learn best practices for package setup, dependency management, and initialization patterns in Go development.

Table Of Contents

Introduction

The init() function in Go is a special function that plays a crucial role in package initialization. Understanding how and when init functions execute is essential for building robust Go applications, especially when dealing with package dependencies, configuration setup, and resource initialization.

Whether you're setting up database connections, registering handlers, or configuring logging systems, init functions provide a powerful mechanism for automatic package setup. In this comprehensive guide, you'll learn how init functions work, their execution order, best practices, and common patterns for effective package initialization.

Understanding Init Functions

Basic Init Function Syntax

The init() function is a special function that gets called automatically when a package is imported:

package main

import "fmt"

func init() {
    fmt.Println("This runs automatically when the package is imported")
}

func main() {
    fmt.Println("This runs when main is called")
}

// Output:
// This runs automatically when the package is imported
// This runs when main is called

Multiple Init Functions

A package can have multiple init() functions, and they'll execute in the order they appear in the source:

package config

import (
    "fmt"
    "os"
)

func init() {
    fmt.Println("First init function")
    // Initialize basic configuration
}

func init() {
    fmt.Println("Second init function")
    // Set up environment-specific settings
}

func init() {
    fmt.Println("Third init function")
    // Validate configuration
}

Init Functions Across Multiple Files

When a package spans multiple files, init functions from all files execute in lexicographical order of filenames:

// file: a_config.go
package mypackage

import "fmt"

func init() {
    fmt.Println("Init from a_config.go")
}

// file: b_database.go
package mypackage

import "fmt"

func init() {
    fmt.Println("Init from b_database.go")
}

// file: c_server.go
package mypackage

import "fmt"

func init() {
    fmt.Println("Init from c_server.go")
}

// Output:
// Init from a_config.go
// Init from b_database.go
// Init from c_server.go

Package Initialization Order

The Complete Initialization Sequence

Go follows a specific order for package initialization:

  1. Constant declarations are evaluated
  2. Variable declarations are evaluated (in dependency order)
  3. Init functions are executed (in order of appearance)
package initialization

import "fmt"

// 1. Constants are initialized first
const AppName = "MyApp"

// 2. Variables are initialized in dependency order
var (
    version = getVersion()           // Depends on getVersion()
    config  = &Config{Version: version} // Depends on version
    logger  = createLogger(config)   // Depends on config
)

// 3. Init functions run after variables
func init() {
    fmt.Printf("Initializing %s version %s\n", AppName, version)
    logger.Info("Package initialized")
}

func getVersion() string {
    fmt.Println("Getting version")
    return "1.0.0"
}

type Config struct {
    Version string
}

type Logger struct {
    config *Config
}

func (l *Logger) Info(msg string) {
    fmt.Printf("[INFO] %s\n", msg)
}

func createLogger(cfg *Config) *Logger {
    fmt.Println("Creating logger")
    return &Logger{config: cfg}
}

Cross-Package Initialization Order

When packages import other packages, Go ensures dependencies are initialized first:

// package: utils
package utils

import "fmt"

var UtilsReady bool

func init() {
    fmt.Println("Initializing utils package")
    UtilsReady = true
}

// package: database
package database

import (
    "fmt"
    "myapp/utils"
)

var DB *Database

func init() {
    fmt.Println("Initializing database package")
    if !utils.UtilsReady {
        panic("Utils not ready")
    }
    DB = &Database{}
}

type Database struct{}

// package: main
package main

import (
    "fmt"
    "myapp/database"
    "myapp/utils"
)

func init() {
    fmt.Println("Initializing main package")
}

func main() {
    fmt.Println("Main function started")
}

// Output:
// Initializing utils package
// Initializing database package
// Initializing main package
// Main function started

Common Init Function Patterns

1. Configuration Setup

package config

import (
    "encoding/json"
    "fmt"
    "os"
)

type AppConfig struct {
    Database DatabaseConfig `json:"database"`
    Server   ServerConfig   `json:"server"`
    Logging  LoggingConfig  `json:"logging"`
}

type DatabaseConfig struct {
    Host     string `json:"host"`
    Port     int    `json:"port"`
    Database string `json:"database"`
    Username string `json:"username"`
    Password string `json:"password"`
}

type ServerConfig struct {
    Host string `json:"host"`
    Port int    `json:"port"`
}

type LoggingConfig struct {
    Level string `json:"level"`
    File  string `json:"file"`
}

var Config *AppConfig

func init() {
    var err error
    Config, err = loadConfig()
    if err != nil {
        panic(fmt.Sprintf("Failed to load configuration: %v", err))
    }
    
    // Validate configuration
    if err := validateConfig(Config); err != nil {
        panic(fmt.Sprintf("Invalid configuration: %v", err))
    }
    
    fmt.Println("Configuration loaded successfully")
}

func loadConfig() (*AppConfig, error) {
    configFile := getConfigFile()
    
    file, err := os.Open(configFile)
    if err != nil {
        return nil, err
    }
    defer file.Close()
    
    var config AppConfig
    decoder := json.NewDecoder(file)
    if err := decoder.Decode(&config); err != nil {
        return nil, err
    }
    
    // Override with environment variables
    overrideWithEnv(&config)
    
    return &config, nil
}

func getConfigFile() string {
    if configFile := os.Getenv("CONFIG_FILE"); configFile != "" {
        return configFile
    }
    return "config.json"
}

func overrideWithEnv(config *AppConfig) {
    if dbHost := os.Getenv("DB_HOST"); dbHost != "" {
        config.Database.Host = dbHost
    }
    if serverPort := os.Getenv("SERVER_PORT"); serverPort != "" {
        // Parse and set server port
    }
}

func validateConfig(config *AppConfig) error {
    if config.Database.Host == "" {
        return fmt.Errorf("database host is required")
    }
    if config.Database.Port <= 0 {
        return fmt.Errorf("database port must be positive")
    }
    return nil
}

2. Database Connection Pool

package database

import (
    "database/sql"
    "fmt"
    "log"
    "time"
    
    _ "github.com/lib/pq"
    "myapp/config"
)

var (
    DB   *sql.DB
    Pool *ConnectionPool
)

type ConnectionPool struct {
    db           *sql.DB
    maxOpenConns int
    maxIdleConns int
    maxLifetime  time.Duration
}

func init() {
    var err error
    
    // Initialize database connection
    DB, err = initializeDatabase()
    if err != nil {
        log.Fatalf("Failed to initialize database: %v", err)
    }
    
    // Configure connection pool
    configureConnectionPool(DB)
    
    // Test connection
    if err := DB.Ping(); err != nil {
        log.Fatalf("Failed to ping database: %v", err)
    }
    
    log.Println("Database connection pool initialized successfully")
}

func initializeDatabase() (*sql.DB, error) {
    cfg := config.Config.Database
    
    connectionString := fmt.Sprintf(
        "host=%s port=%d user=%s password=%s dbname=%s sslmode=disable",
        cfg.Host, cfg.Port, cfg.Username, cfg.Password, cfg.Database,
    )
    
    db, err := sql.Open("postgres", connectionString)
    if err != nil {
        return nil, fmt.Errorf("failed to open database: %w", err)
    }
    
    return db, nil
}

func configureConnectionPool(db *sql.DB) {
    db.SetMaxOpenConns(25)
    db.SetMaxIdleConns(5)
    db.SetConnMaxLifetime(time.Hour)
    
    Pool = &ConnectionPool{
        db:           db,
        maxOpenConns: 25,
        maxIdleConns: 5,
        maxLifetime:  time.Hour,
    }
}

func GetDB() *sql.DB {
    return DB
}

func Close() error {
    if DB != nil {
        return DB.Close()
    }
    return nil
}

3. Registry Pattern

package handlers

import (
    "fmt"
    "net/http"
)

type Handler func(http.ResponseWriter, *http.Request)

var registry = make(map[string]Handler)

func init() {
    // Register built-in handlers
    Register("health", HealthHandler)
    Register("ready", ReadinessHandler)
    Register("metrics", MetricsHandler)
    
    fmt.Println("Built-in handlers registered")
}

func Register(name string, handler Handler) {
    if _, exists := registry[name]; exists {
        panic(fmt.Sprintf("Handler %s already registered", name))
    }
    registry[name] = handler
    fmt.Printf("Registered handler: %s\n", name)
}

func Get(name string) (Handler, bool) {
    handler, exists := registry[name]
    return handler, exists
}

func ListHandlers() []string {
    handlers := make([]string, 0, len(registry))
    for name := range registry {
        handlers = append(handlers, name)
    }
    return handlers
}

func HealthHandler(w http.ResponseWriter, r *http.Request) {
    w.WriteHeader(http.StatusOK)
    w.Write([]byte("OK"))
}

func ReadinessHandler(w http.ResponseWriter, r *http.Request) {
    // Check if all dependencies are ready
    w.WriteHeader(http.StatusOK)
    w.Write([]byte("Ready"))
}

func MetricsHandler(w http.ResponseWriter, r *http.Request) {
    // Return application metrics
    w.WriteHeader(http.StatusOK)
    w.Write([]byte("Metrics"))
}

4. Logging Setup

package logger

import (
    "io"
    "log"
    "os"
    "path/filepath"
    "strings"
    
    "myapp/config"
)

var (
    InfoLogger    *log.Logger
    WarningLogger *log.Logger
    ErrorLogger   *log.Logger
    DebugLogger   *log.Logger
)

func init() {
    setupLoggers()
    Info("Logger initialized successfully")
}

func setupLoggers() {
    cfg := config.Config.Logging
    
    // Create log file if specified
    var logFile *os.File
    if cfg.File != "" {
        var err error
        
        // Ensure log directory exists
        logDir := filepath.Dir(cfg.File)
        if err := os.MkdirAll(logDir, 0755); err != nil {
            log.Fatalf("Failed to create log directory: %v", err)
        }
        
        logFile, err = os.OpenFile(cfg.File, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
        if err != nil {
            log.Fatalf("Failed to open log file: %v", err)
        }
    }
    
    // Determine log level
    level := strings.ToUpper(cfg.Level)
    
    // Configure output writers
    var (
        infoWriter    io.Writer = os.Stdout
        warningWriter io.Writer = os.Stdout
        errorWriter   io.Writer = os.Stderr
        debugWriter   io.Writer = os.Stdout
    )
    
    if logFile != nil {
        infoWriter = io.MultiWriter(os.Stdout, logFile)
        warningWriter = io.MultiWriter(os.Stdout, logFile)
        errorWriter = io.MultiWriter(os.Stderr, logFile)
        debugWriter = io.MultiWriter(os.Stdout, logFile)
    }
    
    // Create loggers
    InfoLogger = log.New(infoWriter, "INFO: ", log.Ldate|log.Ltime|log.Lshortfile)
    WarningLogger = log.New(warningWriter, "WARNING: ", log.Ldate|log.Ltime|log.Lshortfile)
    ErrorLogger = log.New(errorWriter, "ERROR: ", log.Ldate|log.Ltime|log.Lshortfile)
    
    // Debug logger only active in debug mode
    if level == "DEBUG" {
        DebugLogger = log.New(debugWriter, "DEBUG: ", log.Ldate|log.Ltime|log.Lshortfile)
    } else {
        DebugLogger = log.New(io.Discard, "", 0)
    }
}

func Info(v ...interface{}) {
    InfoLogger.Println(v...)
}

func Warning(v ...interface{}) {
    WarningLogger.Println(v...)
}

func Error(v ...interface{}) {
    ErrorLogger.Println(v...)
}

func Debug(v ...interface{}) {
    if DebugLogger != nil {
        DebugLogger.Println(v...)
    }
}

Advanced Init Patterns

1. Conditional Initialization

package features

import (
    "fmt"
    "os"
    "strings"
)

var enabledFeatures map[string]bool

func init() {
    enabledFeatures = make(map[string]bool)
    
    // Initialize features based on environment
    initializeFeatures()
    
    // Initialize specific features
    if IsEnabled("analytics") {
        initializeAnalytics()
    }
    
    if IsEnabled("cache") {
        initializeCache()
    }
    
    if IsEnabled("metrics") {
        initializeMetrics()
    }
    
    fmt.Printf("Initialized features: %v\n", getEnabledFeatures())
}

func initializeFeatures() {
    // Default features
    enabledFeatures["logging"] = true
    enabledFeatures["health"] = true
    
    // Environment-based features
    if features := os.Getenv("ENABLED_FEATURES"); features != "" {
        featureList := strings.Split(features, ",")
        for _, feature := range featureList {
            feature = strings.TrimSpace(feature)
            enabledFeatures[feature] = true
        }
    }
    
    // Development-only features
    if os.Getenv("GO_ENV") == "development" {
        enabledFeatures["debug"] = true
        enabledFeatures["profiling"] = true
    }
}

func initializeAnalytics() {
    fmt.Println("Initializing analytics feature")
    // Initialize analytics service
}

func initializeCache() {
    fmt.Println("Initializing cache feature")
    // Initialize cache system
}

func initializeMetrics() {
    fmt.Println("Initializing metrics feature")
    // Initialize metrics collection
}

func IsEnabled(feature string) bool {
    return enabledFeatures[feature]
}

func getEnabledFeatures() []string {
    var features []string
    for feature, enabled := range enabledFeatures {
        if enabled {
            features = append(features, feature)
        }
    }
    return features
}

2. Plugin System with Init

package plugins

import (
    "fmt"
    "sort"
)

type Plugin interface {
    Name() string
    Version() string
    Initialize() error
    Shutdown() error
}

var (
    plugins    []Plugin
    pluginMap  = make(map[string]Plugin)
    initOrder  []string
)

func init() {
    fmt.Println("Plugin system initializing...")
    
    // Initialize all registered plugins in dependency order
    if err := initializePlugins(); err != nil {
        panic(fmt.Sprintf("Failed to initialize plugins: %v", err))
    }
    
    fmt.Printf("Plugin system initialized with %d plugins\n", len(plugins))
}

func Register(plugin Plugin) {
    name := plugin.Name()
    if _, exists := pluginMap[name]; exists {
        panic(fmt.Sprintf("Plugin %s already registered", name))
    }
    
    plugins = append(plugins, plugin)
    pluginMap[name] = plugin
    fmt.Printf("Registered plugin: %s v%s\n", name, plugin.Version())
}

func initializePlugins() error {
    // Sort plugins by name for deterministic order
    sort.Slice(plugins, func(i, j int) bool {
        return plugins[i].Name() < plugins[j].Name()
    })
    
    for _, plugin := range plugins {
        fmt.Printf("Initializing plugin: %s\n", plugin.Name())
        if err := plugin.Initialize(); err != nil {
            return fmt.Errorf("failed to initialize plugin %s: %w", plugin.Name(), err)
        }
        initOrder = append(initOrder, plugin.Name())
    }
    
    return nil
}

func GetPlugin(name string) (Plugin, bool) {
    plugin, exists := pluginMap[name]
    return plugin, exists
}

func ListPlugins() []string {
    var names []string
    for name := range pluginMap {
        names = append(names, name)
    }
    return names
}

func Shutdown() error {
    // Shutdown plugins in reverse order
    for i := len(initOrder) - 1; i >= 0; i-- {
        name := initOrder[i]
        if plugin, exists := pluginMap[name]; exists {
            fmt.Printf("Shutting down plugin: %s\n", name)
            if err := plugin.Shutdown(); err != nil {
                return fmt.Errorf("failed to shutdown plugin %s: %w", name, err)
            }
        }
    }
    return nil
}

3. Environment-Specific Initialization

package environment

import (
    "fmt"
    "os"
    "strings"
)

type Environment string

const (
    Development Environment = "development"
    Testing     Environment = "testing"
    Staging     Environment = "staging"
    Production  Environment = "production"
)

var Current Environment

func init() {
    Current = detectEnvironment()
    fmt.Printf("Detected environment: %s\n", Current)
    
    switch Current {
    case Development:
        initDevelopment()
    case Testing:
        initTesting()
    case Staging:
        initStaging()
    case Production:
        initProduction()
    }
}

func detectEnvironment() Environment {
    env := strings.ToLower(os.Getenv("GO_ENV"))
    if env == "" {
        env = strings.ToLower(os.Getenv("ENVIRONMENT"))
    }
    
    switch env {
    case "dev", "development":
        return Development
    case "test", "testing":
        return Testing
    case "stage", "staging":
        return Staging
    case "prod", "production":
        return Production
    default:
        return Development // Default to development
    }
}

func initDevelopment() {
    fmt.Println("Initializing development environment")
    // Enable debug logging
    // Set up hot reload
    // Configure local services
    os.Setenv("LOG_LEVEL", "debug")
    os.Setenv("ENABLE_PROFILING", "true")
}

func initTesting() {
    fmt.Println("Initializing testing environment")
    // Use in-memory databases
    // Mock external services
    // Disable certain features
    os.Setenv("USE_MOCK_SERVICES", "true")
    os.Setenv("LOG_LEVEL", "error")
}

func initStaging() {
    fmt.Println("Initializing staging environment")
    // Use staging databases
    // Enable monitoring
    // Use reduced cache TTL
    os.Setenv("LOG_LEVEL", "info")
    os.Setenv("ENABLE_MONITORING", "true")
}

func initProduction() {
    fmt.Println("Initializing production environment")
    // Use production databases
    // Enable all monitoring
    // Optimize for performance
    os.Setenv("LOG_LEVEL", "warn")
    os.Setenv("ENABLE_MONITORING", "true")
    os.Setenv("ENABLE_METRICS", "true")
}

func IsDevelopment() bool {
    return Current == Development
}

func IsProduction() bool {
    return Current == Production
}

func IsTesting() bool {
    return Current == Testing
}

Best Practices

1. Error Handling in Init Functions

package service

import (
    "fmt"
    "log"
)

var serviceClient *ServiceClient

func init() {
    var err error
    serviceClient, err = initializeService()
    if err != nil {
        // Log the error and panic (init functions can't return errors)
        log.Fatalf("Failed to initialize service: %v", err)
    }
}

func initializeService() (*ServiceClient, error) {
    // Perform initialization that might fail
    client := &ServiceClient{}
    
    if err := client.Connect(); err != nil {
        return nil, fmt.Errorf("failed to connect: %w", err)
    }
    
    if err := client.Authenticate(); err != nil {
        return nil, fmt.Errorf("failed to authenticate: %w", err)
    }
    
    return client, nil
}

type ServiceClient struct{}

func (sc *ServiceClient) Connect() error {
    // Connection logic
    return nil
}

func (sc *ServiceClient) Authenticate() error {
    // Authentication logic
    return nil
}

2. Avoid Heavy Operations

package heavy

import (
    "context"
    "fmt"
    "sync"
    "time"
)

var (
    dataCache     map[string]interface{}
    cacheMutex    sync.RWMutex
    cacheInitOnce sync.Once
)

func init() {
    // Don't do heavy operations in init
    // Initialize only lightweight structures
    dataCache = make(map[string]interface{})
    fmt.Println("Cache structure initialized")
}

// Use lazy initialization for heavy operations
func GetFromCache(key string) interface{} {
    cacheInitOnce.Do(func() {
        go initializeCacheAsync()
    })
    
    cacheMutex.RLock()
    defer cacheMutex.RUnlock()
    return dataCache[key]
}

func initializeCacheAsync() {
    fmt.Println("Starting cache initialization in background")
    
    // Simulate heavy initialization
    ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
    defer cancel()
    
    data := loadHeavyData(ctx)
    
    cacheMutex.Lock()
    for k, v := range data {
        dataCache[k] = v
    }
    cacheMutex.Unlock()
    
    fmt.Println("Cache initialization completed")
}

func loadHeavyData(ctx context.Context) map[string]interface{} {
    // Simulate loading data from external source
    time.Sleep(2 * time.Second)
    return map[string]interface{}{
        "key1": "value1",
        "key2": "value2",
    }
}

Testing Init Functions

Testing Strategies

package testable

import (
    "os"
    "testing"
)

var configValue string

func init() {
    configValue = getConfigValue()
}

func getConfigValue() string {
    if value := os.Getenv("TEST_CONFIG"); value != "" {
        return value
    }
    return "default"
}

func GetConfigValue() string {
    return configValue
}

// Test file
func TestInitialization(t *testing.T) {
    // Set environment variable before import
    os.Setenv("TEST_CONFIG", "test_value")
    
    // Note: init has already run, so we need to test indirectly
    // or use build tags for different test configurations
    
    expected := "default" // Since init already ran
    if actual := GetConfigValue(); actual != expected {
        t.Errorf("Expected %s, got %s", expected, actual)
    }
}

func TestWithBuildTags(t *testing.T) {
    // Use build tags to create test-specific init behavior
    // +build testing
}

FAQ

Q: Can init functions take parameters or return values? A: No, init functions cannot take parameters or return values. They must have the signature func init().

Q: When exactly do init functions run? A: Init functions run after all package-level variables are initialized, but before main() is called.

Q: Can I call init functions manually? A: No, init functions are called automatically by the Go runtime and cannot be called manually.

Q: What happens if an init function panics? A: If an init function panics, the program will terminate. This is why proper error handling is crucial.

Q: Are init functions executed in parallel? A: No, init functions within a package execute sequentially in the order they appear.

Q: Should I use init functions for all initialization? A: Use init functions for package-level setup that must happen automatically. For optional or configurable initialization, consider explicit initialization functions.

Conclusion

Init functions are a powerful feature in Go that enables automatic package initialization. When used correctly, they can simplify package setup and ensure dependencies are properly configured. However, they should be used judiciously to avoid making code harder to test and debug.

Key takeaways:

  • Use init functions for essential package setup
  • Keep init functions lightweight and fast
  • Handle errors appropriately (usually by panicking)
  • Be mindful of initialization order
  • Consider lazy initialization for heavy operations
  • Use environment variables for configuration
  • Make code testable despite automatic initialization

Master these patterns to build more robust and maintainable Go applications. Share your experiences with init functions and initialization patterns in the comments below!

Share this article

Add Comment

No comments yet. Be the first to comment!

More from Go