Middleware paling dikenal lewat framework web, tetapi masalah yang diselesaikannya bersifat umum: sekelompok perlakuan yang sama — pencatatan, autentikasi, penyesuaian harga — harus diterapkan pada setiap item tanpa menumpuknya di satu fungsi raksasa. Pola intinya hanyalah fungsi pembungkus: middleware menerima handler, mengembalikan handler baru yang menambahkan perlakuan sebelum dan sesudah handler asli berjalan. Ganti tipe handler dari http.Handler menjadi fungsi pemroses data apa pun, dan pola yang sama tetap utuh. Chapter ini membangun pipeline pemrosesan pesanan lengkap dengan log, autentikasi, dan diskon, tanpa satu baris kode HTTP.

Middleware Generik: func(Handler) Handler #

Dua definisi tipe menjadi tulang punggung pola ini. Handler adalah fungsi yang mengubah satu data menjadi data baru, dan Middleware adalah fungsi yang menerima handler lalu mengembalikan handler lain:

go
// Handler memproses satu Pesanan menjadi Pesanan hasil.
type Handler func(Pesanan) Pesanan

// Middleware membungkus Handler dengan perlakuan tambahan.
type Middleware func(Handler) Handler

Bentuk ini adalah generalisasi langsung dari middleware HTTP func(http.Handler) http.Handler. Data yang mengalir boleh tipe apa saja — potongan byte, event, ataupun struct pesanan seperti di chapter ini. Middleware bekerja lewat closure: kode sebelum pemanggilan next berjalan saat data masuk, dan kode setelahnya berjalan saat hasil keluar.

Tiga Perlakuan untuk Pesanan #

Demo memakai tiga middleware. catat mencatat pesanan saat masuk dan hasilnya saat keluar. diskonMember memotong total belanja minimal seratus ribu rupiah. autentikasi memeriksa token dan berhak menolak — inilah kemampuan khas middleware yang membedakannya dari dekorator biasa:

go
func autentikasi(next Handler) Handler {
	return func(p Pesanan) Pesanan {
		if p.Token != "token-budi" {
			p.Ditolak = "token tidak valid"
			return p // next tidak dipanggil: pipeline berhenti di sini
		}
		return next(p)
	}
}

Ketika next tidak dipanggil, sisa pipeline di bawahnya tidak pernah dieksekusi dan data mengalir kembali keluar melewati lapisan yang sudah dilalui. Field Ditolak menjadi penanda penolakan yang terbaca oleh pemanggil. Di pipeline produksi, handler lazim mengembalikan (Pesanan, error) agar tiap lapisan bisa membungkus error — idiom yang dibahas tulisan Errors are values.

Melipat Middleware dengan chain #

Tiga middleware masih bisa disusun manual sebagai catat(autentikasi(diskonMember(10)(proses))); sepuluh middleware dengan tanda kurung bersarang semacam itu tidak terbaca lagi. Helper chain merapikannya dengan melipat daftar dari belakang, sehingga middleware yang ditulis lebih dulu menjadi lapisan terluar:

go
// chain(a, b, c)(h) == a(b(c(h))): item pertama jadi lapisan terluar.
func chain(mw ...Middleware) Middleware {
	return func(final Handler) Handler {
		for i := len(mw) - 1; i >= 0; i-- {
			final = mw[i](final)
		}
		return final
	}
}

Pemanggilan chain(catat, autentikasi, diskonMember(10))(proses) kini terbaca sebagai daftar perlakuan berurutan. Loop membangun tumpukan dari handler inti ke luar, lalu mengembalikan satu handler gabungan siap pakai.

Demo: Pipeline Pesanan #

Berkas middleware-demo.go pada bagian bawah halaman menjalankan pipeline lengkap atas dua pesanan: P-001 membawa token sah dan total 250000, sedangkan P-002 membawa token salah. Kode lengkapnya adalah sebagai berikut:

go
package main

import "fmt"

// Pesanan adalah data yang mengalir melalui pipeline.
type Pesanan struct {
	ID        string
	Pelanggan string
	Token     string
	Total     int
	Ditolak   string
}

// Handler adalah satu tahap pemrosesan: menerima Pesanan, mengembalikan Pesanan.
type Handler func(Pesanan) Pesanan

// Middleware generik: membungkus Handler menjadi Handler baru.
// Bentuknya sama dengan middleware HTTP, hanya tipenya diganti.
type Middleware func(Handler) Handler

// catat mencatat pesanan saat masuk dan hasilnya saat keluar.
func catat(next Handler) Handler {
	return func(p Pesanan) Pesanan {
		fmt.Printf("[log]    masuk  : %s (total=%d)\n", p.ID, p.Total)
		hasil := next(p)
		fmt.Printf("[log]    keluar : %s (total=%d)\n", hasil.ID, hasil.Total)
		return hasil
	}
}

// autentikasi memeriksa token sebelum pesanan diteruskan.
// Token salah: next tidak dipanggil, pipeline berhenti di lapisan ini.
func autentikasi(next Handler) Handler {
	return func(p Pesanan) Pesanan {
		if p.Token != "token-budi" {
			fmt.Printf("[auth]   tolak  : %s (token tidak valid)\n", p.ID)
			p.Ditolak = "token tidak valid"
			return p
		}
		fmt.Printf("[auth]   lolos  : %s\n", p.ID)
		return next(p)
	}
}

// diskonMember memotong total untuk pembelanjaan minimal 100000.
func diskonMember(persen int) Middleware {
	return func(next Handler) Handler {
		return func(p Pesanan) Pesanan {
			if p.Total >= 100000 {
				p.Total -= p.Total * persen / 100
				fmt.Printf("[diskon] potong : %s (%d persen)\n", p.ID, persen)
			}
			return next(p)
		}
	}
}

// proses adalah handler inti yang dibungkus middleware.
func proses(p Pesanan) Pesanan {
	fmt.Printf("[core]   proses: %s untuk %s\n", p.ID, p.Pelanggan)
	return p
}

// chain melipat daftar middleware menjadi satu:
// chain(a, b, c)(h) menghasilkan a(b(c(h))).
func chain(mw ...Middleware) Middleware {
	return func(final Handler) Handler {
		for i := len(mw) - 1; i >= 0; i-- {
			final = mw[i](final)
		}
		return final
	}
}

func main() {
	pipeline := chain(catat, autentikasi, diskonMember(10))(proses)

	pesanan := []Pesanan{
		{ID: "P-001", Pelanggan: "Budi", Token: "token-budi", Total: 250000},
		{ID: "P-002", Pelanggan: "anonim", Token: "salah", Total: 50000},
	}
	for i, p := range pesanan {
		if i > 0 {
			fmt.Println()
		}
		hasil := pipeline(p)
		if hasil.Ditolak != "" {
			fmt.Printf("=> %s gagal : %s\n", hasil.ID, hasil.Ditolak)
		} else {
			fmt.Printf("=> %s selesai, total=%d\n", hasil.ID, hasil.Total)
		}
	}
}

Output yang Dihasilkan:

text
[log]    masuk  : P-001 (total=250000)
[auth]   lolos  : P-001
[diskon] potong : P-001 (10 persen)
[core]   proses: P-001 untuk Budi
[log]    keluar : P-001 (total=225000)
=> P-001 selesai, total=225000

[log]    masuk  : P-002 (total=50000)
[auth]   tolak  : P-002 (token tidak valid)
[log]    keluar : P-002 (total=50000)
=> P-002 gagal : token tidak valid

Mari kita telaah baris demi baris program di atas:

  1. Baris 6–13 (type Pesanan struct): data yang mengalir; field Ditolak menjadi kanal penolakan yang dibawa sampai ke pemanggil.
  2. Baris 15 dan 19 (Handler serta Middleware): dua definisi tipe pendek — seluruh pola hanya berdiri di atas keduanya.
  3. Baris 22–30 (catat): mencetak baris masuk, memanggil next, lalu mencetak hasil keluar dengan total terbaru.
  4. Baris 33–44 (autentikasi): membandingkan token; token salah membuat next dilewati dan Ditolak terisi.
  5. Baris 46–57 (diskonMember): factory middleware — menerima persen, mengembalikan Middleware; closure mengikat nilai potongan.
  6. Baris 66–72 (chain): melipat daftar dari belakang ke depan; urutan argumen menentukan urutan lapisan.
  7. Baris 76 (pipeline): hasil akhir lipatan — satu handler berlapis tiga, dibangun sekali lalu dipakai berulang.
  8. Baris 82–92 (loop main): dua pesanan dibandingkan; P-001 menembus sampai handler inti, P-002 berhenti di lapisan autentikasi.

Urutan Eksekusi: Model Bawang #

Alur eksekusi middleware berbentuk bolak-balik seperti lapisan bawang: data masuk menembus lapisan luar ke dalam, hasil keluar menempuh jalur yang sama secara terbalik.

Urutan lapisan menentukan hasil. Karena catat berada paling luar, pesanan yang ditolak tetap tercatat — audit lengkap. Karena autentikasi berada di atas diskonMember, diskon hanya diberikan setelah token diperiksa; menukar keduanya berarti memberi potongan harga kepada pesanan tak dikenal.

Versi HTTP dari pola ini — lengkap dengan statusRecorder dan middleware recover — dibahas tuntas di chapter Middleware pada HTTP . Selanjutnya kita berpindah dari data yang mengalir ke data yang disimpan: Repository Pattern menyembunyikan detail penyimpanan di balik satu interface.

Referensi #

  • Effective Go — komposisi fungsi dan closure yang menjadi dasar pola middleware.
  • Errors are values — idiom penanganan error bertingkat untuk pipeline produksi.

Berkas Sumber

Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.

middleware-demo.go

Unduh berkas
go
package main

import "fmt"

// Pesanan adalah data yang mengalir melalui pipeline.
type Pesanan struct {
	ID        string
	Pelanggan string
	Token     string
	Total     int
	Ditolak   string
}

// Handler adalah satu tahap pemrosesan: menerima Pesanan, mengembalikan Pesanan.
type Handler func(Pesanan) Pesanan

// Middleware generik: membungkus Handler menjadi Handler baru.
// Bentuknya sama dengan middleware HTTP, hanya tipenya diganti.
type Middleware func(Handler) Handler

// catat mencatat pesanan saat masuk dan hasilnya saat keluar.
func catat(next Handler) Handler {
	return func(p Pesanan) Pesanan {
		fmt.Printf("[log]    masuk  : %s (total=%d)\n", p.ID, p.Total)
		hasil := next(p)
		fmt.Printf("[log]    keluar : %s (total=%d)\n", hasil.ID, hasil.Total)
		return hasil
	}
}

// autentikasi memeriksa token sebelum pesanan diteruskan.
// Token salah: next tidak dipanggil, pipeline berhenti di lapisan ini.
func autentikasi(next Handler) Handler {
	return func(p Pesanan) Pesanan {
		if p.Token != "token-budi" {
			fmt.Printf("[auth]   tolak  : %s (token tidak valid)\n", p.ID)
			p.Ditolak = "token tidak valid"
			return p
		}
		fmt.Printf("[auth]   lolos  : %s\n", p.ID)
		return next(p)
	}
}

// diskonMember memotong total untuk pembelanjaan minimal 100000.
func diskonMember(persen int) Middleware {
	return func(next Handler) Handler {
		return func(p Pesanan) Pesanan {
			if p.Total >= 100000 {
				p.Total -= p.Total * persen / 100
				fmt.Printf("[diskon] potong : %s (%d persen)\n", p.ID, persen)
			}
			return next(p)
		}
	}
}

// proses adalah handler inti yang dibungkus middleware.
func proses(p Pesanan) Pesanan {
	fmt.Printf("[core]   proses: %s untuk %s\n", p.ID, p.Pelanggan)
	return p
}

// chain melipat daftar middleware menjadi satu:
// chain(a, b, c)(h) menghasilkan a(b(c(h))).
func chain(mw ...Middleware) Middleware {
	return func(final Handler) Handler {
		for i := len(mw) - 1; i >= 0; i-- {
			final = mw[i](final)
		}
		return final
	}
}

func main() {
	pipeline := chain(catat, autentikasi, diskonMember(10))(proses)

	pesanan := []Pesanan{
		{ID: "P-001", Pelanggan: "Budi", Token: "token-budi", Total: 250000},
		{ID: "P-002", Pelanggan: "anonim", Token: "salah", Total: 50000},
	}
	for i, p := range pesanan {
		if i > 0 {
			fmt.Println()
		}
		hasil := pipeline(p)
		if hasil.Ditolak != "" {
			fmt.Printf("=> %s gagal : %s\n", hasil.ID, hasil.Ditolak)
		} else {
			fmt.Printf("=> %s selesai, total=%d\n", hasil.ID, hasil.Total)
		}
	}
}