Error listen tcp :3000: bind: address already in use merupakan masalah klasik saat mengonfigurasi live reload aplikasi Go Fiber menggunakan Air. Masalah ini muncul karena Air mematikan proses lama secara paksa tanpa memberikan waktu bagi Fasthttp untuk menutup file descriptor TCP socket, atau proses kompilasi binary baru selesai lebih cepat daripada waktu rilis port oleh sistem operasi. Dampaknya, proses lama menggantung (zombie/orphan process) dan port target terkunci.

Solusinya membutuhkan penanganan dua arah: konfigurasi sinyal terminasi yang benar pada .air.toml dan implementasi graceful shutdown di sisi kode Go Fiber.

Akar Masalah: Mengapa Port Tertahan?

Secara default, jika tidak dikonfigurasi, tool watcher seperti Air akan menghentikan proses lama menggunakan sinyal SIGKILL langsung saat ada modifikasi file. Sinyal SIGKILL tidak dapat ditangkap oleh aplikasi. Akibatnya, server Fasthttp milik Go Fiber tidak sempat menjalankan siklus pembersihan koneksi.

Selain itu, sistem operasi membutuhkan waktu dalam fase TIME_WAIT untuk membersihkan socket. Jika Air langsung mengeksekusi binary baru hasil go build sebelum soket dilepas, proses baru gagal melakukan bind ke port yang sama.

Implementasi Graceful Shutdown pada Go Fiber

Gunakan package standar os/signal dan context untuk menangkap sinyal interupsi (SIGINT, SIGTERM). Gunakan method ShutdownWithContext() milik Fiber agar listener berhenti menerima traffic baru dan menyelesaikan request yang sedang berjalan sebelum proses benar-benar berhenti.

package main

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

	"github.com/gofiber/fiber/v2"
)

func main() {
	app := fiber.New()

	app.Get("/health", func(c *fiber.Ctx) error {
		return c.SendString("OK")
	})

	// Jalankan Fiber di goroutine terpisah
	go func() {
		if err := app.Listen(":3000"); err != nil && !errors.Is(err, http.ErrServerClosed) {
			log.Fatalf("Listen error: %v", err)
		}
	}()

	// Tangkap sinyal terminasi OS
	ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
	defer stop()

	<-ctx.Done()
	log.Println("Menerima sinyal stop, mematikan server Fiber...")

	// Beri batas waktu shutdown maksimal 5 detik
	shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()

	if err := app.ShutdownWithContext(shutdownCtx); err != nil {
		log.Fatalf("Server paksa berhenti: %v", err)
	}

	log.Println("Server selesai dibersihkan, port dilepas.")
}

Konfigurasi Optimal .air.toml

Inisialisasi file konfigurasi standar Air dengan perintah air init jika belum ada, lalu sesuaikan nilai-nilai kritis berikut:

root = "."
test_data_dir = "testdata"
tmp_dir = "tmp"

[build]
  # Gunakan flag -ldflags="-s -w" untuk memangkas debug info demi kompilasi lokal yang lebih cepat
  cmd = "go build -ldflags='-s -w' -o ./tmp/main ."
  bin = "tmp/main"
  full_bin = ""
  
  # Pantau ekstensi yang relevan saja
  include_ext = ["go", "tpl", "tmpl", "html"]
  
  # Exclude direktori kerja dan cache agar tidak memicu build loop tanpa henti
  exclude_dir = ["assets", "tmp", "vendor", "testdata", ".git"]
  include_dir = []
  exclude_file = []
  exclude_regex = ["_test\\.go$"]
  exclude_unchanged = false
  follow_symlink = false
  
  # Jeda debounce sebelum kompilasi dimulai (dalam milidetik)
  delay = 500
  
  # Hentikan eksekusi binary lama jika build error
  stop_on_error = true
  
  # Kirim SIGINT alih-alih SIGKILL agar ditangkap oleh signal.NotifyContext
  send_interrupt = true
  
  # Beri jeda waktu bagi server untuk menyelesaikan ShutdownWithContext sebelum dipaksa mati
  kill_delay = "1000ms"

  rerun = false
  rerun_delay = 500

[log]
  time = true

[color]
  main = "magenta"
  watcher = "cyan"
  build = "yellow"
  runner = "green"

[misc]
  clean_on_exit = true

Penjelasan Parameter Kritis:

  • send_interrupt = true: Mengubah aksi Air saat mendeteksi perubahan file. Air mengirimkan os.Interrupt (SIGINT) ke proses lama, bukan langsung mengirim SIGKILL. Ini mengaktifkan blok <-ctx.Done() di aplikasi.
  • kill_delay = "1000ms": Menentukan toleransi waktu tunggu sebelum Air mengirim sinyal pembunuhan paksa jika proses lama macet saat shutdown. Waktu 1 detik sudah mencukupi untuk menutup socket lokal Fasthttp.
  • delay = 500: Debounce timer. Mencegah Air mengeksekusi kompilasi ganda jika editor menyimpan beberapa file sekaligus (misal auto-save atau formatting on save).
  • -ldflags='-s -w': Menghapus symbol table dan DWARF debugging info. Mengurangi ukuran binary dan mempercepat fase linking pada proses kompilasi berulang.

Tips Debugging Socket Gantung

Jika proses tetap terkunci saat pertama kali mengonfigurasi Air, cari dan matikan proses zombie yang masih memegang socket secara manual.

Linux / macOS:

# Cari PID yang mendengarkan port 3000
lsof -i :3000

# Atau langsung hentikan proses pada port tersebut
fuser -k 3000/tcp

Windows (PowerShell):

# Cari PID berdasarkan port
netstat -ano | findstr :3000

# Matikan proses berdasarkan PID
taskkill /F /PID <PID_DITEMUKAN>

Kombinasi send_interrupt = true, alokasi kill_delay, dan app.ShutdownWithContext() menyelesaikan masalah tumpang tindih alokasi port. Siklus reload berjalan mulus tanpa perlu intervensi manual membunuh proses.