Cara Implementasi Graceful Shutdown di Golang

Tutorial praktis graceful shutdown di Go. Simak cara menangani sinyal, implementasi dengan berbagai framework, dan konfigurasi PM2 untuk zero-downtime deployment

Cara Implementasi Graceful Shutdown di Golang

Di environment production, restart aplikasi pasti terjadi—baik karena deployment, scaling, maupun maintenance. Tanpa penanganan yang tepat, restart bisa menyebabkan user kehilangan request yang sedang diproses, koneksi database terputus, atau bahkan data corrupt.

Artikel ini akan membahas cara implementasi graceful shutdown di Go secara lengkap, mulai dari signal handling hingga integrasi dengan PM2 untuk zero-downtime deployment.

Daftar Isi

Apa Itu Graceful Shutdown?

Secara singkat, graceful shutdown adalah proses untuk menghentikan aplikasi secara perlahan dan terstruktur. Berbeda dengan forced shutdown yang langsung matikan aplikasi, graceful shutdown akan memberikan waktu bagi koneksi yang sedang aktif untuk menyelesaikan tugasnya terlabih dulu.

Ada 4 hal penting yang dijamin dengan graceful shutdown:

  1. Request yang sedang diproses diselesaikan – Server tunggu hingga semua request selesai sebelum menutup.
  2. Koneksi database ditutup dengan benar – Tidak ada orphan connection atau transaksi yang terputus.
  3. Resource dibebaskan secara proper – File, memory, dan koneksi jaringan ditutup dengan rapi.
  4. Tidak ada kehilangan data – Data yang sedang diproses disimpan dulu.

Analoginya seperti restoran yang akan tutup. Staff tidak langsung usir pelanggan, tetapi akan memberitahu restoran mau tutup dan memberikan waktu untuk semua pelanggan untuk menyelesaikan makanannya. Setelah semua selesai, baru deh tutup. Nah, graceful shutdown prinsipnya sama.

Mengapa Graceful Shutdown Penting?

Di production, aplikasi pasti sering restart karena:

  • Deployment – Update versi aplikasi baru
  • Scaling – Tambah atau kurang instance
  • Maintenance – Perbaikan sistem atau server
  • Auto-restart – PM2 atau systemd restart aplikasi yang crash

Tanpa graceful shutdown, setiap restart bisa menyebabkan:

  • User kehilangan request yang sedang diproses
  • Data belum tersimpan menjadi corrupt
  • Koneksi database jadi orphan
  • Logging tidak lengkap untuk debugging
  • User experience jadi buruk

Signal Handling di Go

Go punya package os/signal untuk menangkap sinyal dari sistem operasi. Yang paling umum ditangani di aplikasi server adalah sebagai berikut:

SinyalDeskripsiKapan Terjadi
SIGTERMTermination requestPM2, systemd, atau proses lain
SIGINTInterrupt dari keyboardCtrl+C di terminal
SIGQUITQuit dengan core dumpUntuk debugging
SIGHUPHangup signalReload konfigurasi

Contoh Dasar Signal Handling

package main

import (
	"fmt"
	"os"
	"os/signal"
	"syscall"
)

func main() {
	// Buat channel untuk terima sinyal
	sigChan := make(chan os.Signal, 1)

	// Listen kepada sinyal yang diharapkan
	signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)

	fmt.Println("Aplikasi jalan. Tekan Ctrl+C untuk stop.")

	// Tunggu sinyal
	sig := <-sigChan
	fmt.Printf("Terima sinyal: %v\n", sig)
	fmt.Println("Menutup aplikasi...")
}

Timeout untuk Graceful Shutdown

Di aplikasi nyata, untuk memastikan aplikasi nanti akan tetap dimatikan, maka kita haru menyertakan timeout, agar aplikasi tidak ditunggu selamanya:

package main

import (
	"fmt"
	"os"
	"os/signal"
	"syscall"
	"time"
)

func main() {
	sigChan := make(chan os.Signal, 1)
	signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)

	fmt.Println("Aplikasi jalan...")

	sig := <-sigChan
	fmt.Printf("Terima sinyal: %v\n", sig)

	// Channel untuk tandain shutdown selesai
	done := make(chan struct{})

	go func() {
		// Tambah cleanup logic di sini
		fmt.Println("Lagi cleanup...")
		time.Sleep(2 * time.Second)
		fmt.Println("Cleanup beres")
		close(done)
	}()

	// Beri timeout 10 detik
	select {
	case <-done:
		fmt.Println("Aplikasi berhenti aman")
	case <-time.After(10 * time.Second):
		fmt.Println("Timeout! Aplikasi dipaksa stop")
	}

	os.Exit(0)
}

Implementasi HTTP Server dengan Graceful Shutdown

Package net/http di Go punya method Shutdown() yang sangat berguna untuk ini. Berikut ini adalah salah satu contoh implementasi graceful shutdown:

Contoh Lengkap HTTP Server

package main

import (
	"context"
	"log"
	"net/http"
	"os"
	"os/signal"
	"syscall"
	"time"
)

func main() {
	// Inisialisasi router
	mux := http.NewServeMux()
	mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		w.Write([]byte("Hello, World!"))
	})
	mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
		w.WriteHeader(http.StatusOK)
		w.Write([]byte(`{"status":"healthy"}`))
	})
	mux.HandleFunc("/slow", func(w http.ResponseWriter, r *http.Request) {
		time.Sleep(5 * time.Second)
		w.Write([]byte("Selesai setelah 5 detik"))
	})

	// Konfigurasi server
	server := &http.Server{
		Addr:         ":8080",
		Handler:      mux,
		ReadTimeout:  15 * time.Second,
		WriteTimeout: 15 * time.Second,
		IdleTimeout:  60 * time.Second,
	}

	// Jalanin server dalam goroutine
	go func() {
		log.Println("Server started on :8080")
		if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
			log.Fatalf("Server error: %v", err)
		}
	}()

	// Tunggu sinyal shutdown
	quit := make(chan os.Signal, 1)
	signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
	<-quit

	log.Println("Menerima sinyal shutdown...")

	// Context dengan timeout 30 detik
	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
	defer cancel()

	// Shutdown server
	if err := server.Shutdown(ctx); err != nil {
		log.Printf("Server shutdown error: %v", err)
	}

	log.Println("Server berhenti aman")
}

Tambahkan Cleanup untuk Database

Pastikan untuk menutup semua koneksi ke database, agar koneksi tidak dangling, yang bisa mengurangi jatah koneksi:

package main

import (
	"context"
	"database/sql"
	"log"
	"net/http"
	"os"
	"os/signal"
	"syscall"
	"time"

	_ "github.com/lib/pq"
)

type App struct {
	server *http.Server
	db     *sql.DB
}

func main() {
	db, err := sql.Open("postgres", "user=postgres dbname=mydb sslmode=disable")
	if err != nil {
		log.Fatal(err)
	}
    // hilangkan defer, karena sudah ada close di akhir aplikasi
	//defer db.Close()

	app := &App{db: db}

	mux := http.NewServeMux()
	mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		w.Write([]byte("OK"))
	})

	server := &http.Server{
		Addr:    ":8080",
		Handler: mux,
	}
	app.server = server

	go func() {
		if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
			log.Fatalf("Server error: %v", err)
		}
	}()

	quit := make(chan os.Signal, 1)
	signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
	<-quit

	log.Println("Shutting down...")

	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
	defer cancel()

	if err := server.Shutdown(ctx); err != nil {
		log.Printf("Server shutdown error: %v", err)
	}

	if err := db.Close(); err != nil {
		log.Printf("Database close error: %v", err)
	} else {
		log.Println("Database connection closed")
	}

	log.Println("Aplikasi berhenti aman")
}

Graceful Shutdown dengan Framework Populer

Banyak developer Go yang menggunakan framework seperti Gin, Fiber, atau Echo. Berikut ini adalah cara implementasi graceful shutdown di masing-masing framework:

Gin

Gin adalah framework Go yang paling populer. Implementasinya:

package main

import (
	"context"
	"log"
	"net/http"
	"os"
	"os/signal"
	"syscall"
	"time"

	"github.com/gin-gonic/gin"
)

func main() {
	gin.SetMode(gin.ReleaseMode)

	router := gin.New()
	router.Use(gin.Recovery())
	router.Use(gin.Logger())

	router.GET("/", func(c *gin.Context) {
		c.String(http.StatusOK, "Hello from Gin!")
	})

	router.GET("/health", func(c *gin.Context) {
		c.JSON(http.StatusOK, gin.H{"status": "healthy"})
	})

	srv := &http.Server{
		Addr:         ":8080",
		Handler:      router,
		ReadTimeout:  15 * time.Second,
		WriteTimeout: 15 * time.Second,
		IdleTimeout:  60 * time.Second,
	}

	go func() {
		log.Println("Gin server starting on :8080")
		if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
			log.Fatalf("Server error: %v", err)
		}
	}()

	quit := make(chan os.Signal, 1)
	signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
	<-quit

	log.Println("Shutting down Gin server...")

	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
	defer cancel()

	if err := srv.Shutdown(ctx); err != nil {
		log.Printf("Server shutdown error: %v", err)
	}

	log.Println("Gin server stopped gracefully")
}

Catatan:

  • Gin sebenarnya menggunakan http.Server secara internal, jadi bisa lengsung menggunakan srv.Shutdown()
  • Wajib menggunakan gin.SetMode(gin.ReleaseMode) di production
  • Middleware gin.Recovery() wajib untuk meng-handle panic

Fiber

Fiber adalah framework Express-like yang sangat cepat karena menggunakan fasthttp. Berikut ini adalah contoh implementasinya:

package main

import (
	"log"
	"os"
	"os/signal"
	"syscall"
	"time"

	"github.com/gofiber/fiber/v2"
	"github.com/gofiber/fiber/v2/middleware/logger"
	"github.com/gofiber/fiber/v2/middleware/recover"
)

func main() {
	app := fiber.New(fiber.Config{
		ReadTimeout:       15 * time.Second,
		WriteTimeout:      15 * time.Second,
		IdleTimeout:       60 * time.Second,
		GracefulShutdown:  true,
	})

	app.Use(recover.New())
	app.Use(logger.New())

	app.Get("/", func(c *fiber.Ctx) error {
		return c.SendString("Hello from Fiber!")
	})

	app.Get("/health", func(c *fiber.Ctx) error {
		return c.JSON(fiber.Map{"status": "healthy"})
	})

	errChan := make(chan error, 1)

	go func() {
		log.Println("Fiber server starting on :8080")
		if err := app.Listen(":8080"); err != nil {
			errChan <- err
		}
	}()

	quit := make(chan os.Signal, 1)
	signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)

	select {
	case sig := <-quit:
		log.Printf("Received signal: %v", sig)
	case err := <-errChan:
		log.Printf("Server error: %v", err)
	}

	log.Println("Shutting down Fiber server gracefully...")

	if err := app.ShutdownWithTimeout(30 * time.Second); err != nil {
		log.Printf("Server shutdown error: %v", err)
	}

	log.Println("Fiber server stopped gracefully")
}

Catatan:

  • Fiber memiliki konfigurasi GracefulShutdown: true yang mempermudah proses shutdown
  • Gunakan app.ShutdownWithTimeout() untuk membatasi waktu shutdown
  • Fiber sangat cepat tapi ada beberapa perbedaan dengan net/http

Echo

Echo adalah framework yang ringan dan fleksibel. Berikut ini implementasinya:

package main

import (
	"context"
	"log"
	"net/http"
	"os"
	"os/signal"
	"syscall"
	"time"

	"github.com/labstack/echo/v4"
	"github.com/labstack/echo/v4/middleware"
)

func main() {
	e := echo.New()

	e.Use(middleware.Logger())
	e.Use(middleware.Recover())
	e.Use(middleware.CORS())

	e.GET("/", func(c echo.Context) error {
		return c.String(http.StatusOK, "Hello from Echo!")
	})

	e.GET("/health", func(c echo.Context) error {
		return c.JSON(http.StatusOK, map[string]string{
			"status": "healthy",
		})
	})

	srv := &http.Server{
		Addr:         ":8080",
		Handler:      e,
		ReadTimeout:  15 * time.Second,
		WriteTimeout: 15 * time.Second,
		IdleTimeout:  60 * time.Second,
	}

	go func() {
		log.Println("Echo server starting on :8080")
		if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
			log.Fatalf("Server error: %v", err)
		}
	}()

	quit := make(chan os.Signal, 1)
	signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
	<-quit

	log.Println("Shutting down Echo server...")

	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
	defer cancel()

	if err := srv.Shutdown(ctx); err != nil {
		log.Printf("Server shutdown error: %v", err)
	}

	log.Println("Echo server stopped gracefully")
}

Catatan:

  • Echo juga menggunakan http.Server jadi mirip net/http standar
  • Echo punya middleware yang berguna seperti CORS, Logger, dan Recover
  • Echo sangat fleksibel dan cocok untuk API yang kompleks

Perbandingan Framework

AspekGinFiberEcho
Enginenet/httpfasthttpnet/http
Graceful Shutdownsrv.Shutdown()app.ShutdownWithTimeout()srv.Shutdown()
PerformaSangat cepatTercepatCepat

Integrasi dengan PM2 untuk Zero-Downtime Reload

PM2 adalah process manager yang populer buat manage aplikasi Go di production. Dengan konfigurasi yang tepat, PM2 bisa zero-downtime reload yang bekerja sama sama graceful shutdown di Go.

Konfigurasi PM2 Ecosystem File

module.exports = {
    apps: [
        {
            name: "graceful-go-app",
            script: "./myapp",
            exec_mode: "fork",
            instances: 1,
            env: {
                PORT: 8080,
                LOG_LEVEL: "info",
            },
            // Pengaturan untuk graceful shutdown
            kill_timeout: 30000, // 30 detik timeout
            wait_ready: true, // Tunggu aplikasi siap
            listen_timeout: 5000, // Timeout untuk siap
            // Pengaturan restart
            max_restarts: 10,
            min_uptime: "10s",
            // Log
            out_file: "~/.pm2/logs/graceful-go-app-out.log",
            error_file: "~/.pm2/logs/graceful-go-app-error.log",
            log_date_format: "YYYY-MM-DD HH:mm:ss Z",
            merge_logs: true,
        },
    ],
};

Penjelasan Konfigurasi

OpsiNilaiFungsi
kill_timeout30000Waktu buat graceful shutdown sebelum PM2 kill proses
wait_readytruePM2 tunggu aplikasi kirim sinyal siap
listen_timeout5000Timeout buat terima sinyal ready

Perintah PM2 untuk Zero-Downtime Reload

# Build aplikasi Go
go build -o myapp

# Jalankan dengan PM2
pm2 start ecosystem.config.js

# Zero-downtime reload
pm2 reload graceful-go-app

# Atau untuk restart dengan delay
pm2 restart graceful-go-app

# Lihat status
pm2 status
pm2 logs graceful-go-app
Deploy Aplikasi Go dengan PM2: Panduan Lengkap
Pelajari cara mudah deploy aplikasi Go menggunakan PM2. Ikuti panduan lengkap dengan contoh kode, konfigurasi log rotasi, clustering, dan zero‑downtime reload

Kesimpulan

Graceful shutdown wajib ada di aplikasi zonba production. Dengan implementasi yang tepat, maka bisa dipastikan:

  • Tidak ada kehilangan data – Request yang sedang diproses akan ditunggu sampai selesai dulu
  • User experience tetap baik – User tidak dapat error tiba-tiba
  • Resource bersih – Koneksi database dan resource lain ditutup dengan benar
  • Zero-downtime deployment – Aplikasi bisa di-update tanpa mengganggu user

Kunci utamanya:

  1. Tangkap sinyal SIGINT dan SIGTERM
  2. Pakai context dengan timeout
  3. Tutup semua resource dalam urutan yang benar
  4. Konfigurasi PM2 dengan kill_timeout yang sesuai

Langkah selanjutnya:
Silakan coba implementasikan graceful shutdown di aplikasi Go Anda. Jika Anda menggunakan PM2 untuk deployment, silahkan baca juga panduan Deploy Aplikasi Go dengan PM2 untuk konfigurasi lengkapnya.

Deploy Aplikasi Go dengan PM2: Panduan Lengkap
Pelajari cara mudah deploy aplikasi Go menggunakan PM2. Ikuti panduan lengkap dengan contoh kode, konfigurasi log rotasi, clustering, dan zero‑downtime reload