Kebutuhan lintas handler muncul begitu aplikasi tumbuh: setiap request perlu dicatat, endpoint rahasia perlu diperiksa tokennya, dan panic handler tidak boleh menjatuhkan seluruh layanan. Menulis ulang logika tersebut di setiap handler jelas tidak masuk akal. Middleware menyelesaikannya dengan cara membungkus handler — satu fungsi menerima handler lama dan mengembalikan handler baru yang menambahkan perilaku. Pada chapter ini kita akan membangun middleware logging, recover, dan auth, lalu menyusunnya menjadi rantai berurutan yang diuji lewat httptest.
Middleware Adalah Fungsi yang Membungkus Handler #
Bentuk tipenya hanya satu baris:
type Middleware func(http.Handler) http.HandlerSebuah middleware menerima handler next lalu mengembalikan handler baru — biasanya sebuah closure — yang menjalankan kode sebelum dan sesudah memanggil next.ServeHTTP(w, r):
func contohMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// kode sebelum handler inti
next.ServeHTTP(w, r)
// kode setelah handler inti
})
}Pemanggilan next.ServeHTTP(w, r) adalah titik penyerahan ke handler berikutnya. Kode sebelum pemanggilan tersebut berjalan dalam perjalanan masuk, sedangkan kode setelahnya berjalan dalam perjalanan keluar — persis seperti lapisan bawang.
Tiga Middleware Praktis #
Mencatat Log dengan statusRecorder #
Middleware logging ingin mencatat status code akhir, padahal http.ResponseWriter tidak menyediakan pengambil status. Solusinya: bungkus w dengan struct yang merekam setiap WriteHeader:
type statusRecorder struct {
http.ResponseWriter
status int
}
func (rec *statusRecorder) WriteHeader(code int) {
rec.status = code
rec.ResponseWriter.WriteHeader(code)
}Struct statusRecorder menempelkan http.ResponseWriter secara anonymous embedding, lalu menimpa method WriteHeader agar status tersimpan sebelum diteruskan ke w yang asli. Middleware logging kemudian mencetak baris seperti log: GET /items -> 200 setelah handler selesai.
Menangkap Panic dengan recover #
Middleware recover membungkus handler dalam defer berisi recover():
defer func() {
if err := recover(); err != nil {
http.Error(w, fmt.Sprintf("kesalahan internal: %v", err),
http.StatusInternalServerError)
}
}()Panic dari handler ditangkap di sini, lalu klien menerima respons 500 yang terkontrol. Server net/http sebenarnya sudah menangkap panic bawaan per koneksi — request lain tidak ikut mati — tetapi tangkapan bawaan memutus koneksi tanpa respons; middleware recover memberi Anda kendali penuh atas bentuk respons gagal.
Memeriksa Token di authMiddleware #
Middleware auth memeriksa header Authorization sebelum meneruskan request. Token yang tidak cocok dijawab 401 tanpa perlu handler inti ikut terpanggil:
if r.Header.Get("Authorization") != "Bearer token-demo" {
http.Error(w, "token tidak valid", http.StatusUnauthorized)
return
}Urutan Rantai Itu Penting #
Penyusunan rantai dilakukan dari dalam ke luar: handler inti dibungkus middleware terakhir, lalu hasilnya dibungkus middleware sebelumnya, demikian seterusnya:
func chain(h http.Handler, mws ...Middleware) http.Handler {
for i := len(mws) - 1; i >= 0; i-- {
h = mws[i](h)
}
return h
}Hasil pemanggilan chain(final, loggingMiddleware, recoverMiddleware, authMiddleware) berlapis seperti diagram berikut:
Logging berada paling luar agar baris log mencatat status akhir — termasuk 401 dari auth dan 500 dari recover. Recover berada di dalam logging, sehingga panic yang ditangkap tetap menghasilkan respons 500 yang tercatat, bukan koneksi putus tanpa jejak. Menukar urutan kedua lapisan tersebut membuat panic melewati logging sebelum tertangkap, dan baris log pun hilang. Pada aplikasi nyata, lapisan serupa juga menyetor request ID unik ke header atau context agar satu request bisa ditelusuri lintas layanan.
Program Utuh: Rantai Teruji #
Berkas middleware-demo.go pada bagian bawah halaman membangun ketiga middleware di atas, menyusunnya dengan chain, lalu menguji tiga skenario: request sah, request tanpa token, dan request yang memicu panic.
package main
import (
"fmt"
"net/http"
"net/http/httptest"
"strings"
)
// Middleware membungkus satu handler menjadi handler baru.
type Middleware func(http.Handler) http.Handler
// statusRecorder membungkus ResponseWriter untuk merekam status code.
type statusRecorder struct {
http.ResponseWriter
status int
}
func (rec *statusRecorder) WriteHeader(code int) {
rec.status = code
rec.ResponseWriter.WriteHeader(code)
}
func loggingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
rec := &statusRecorder{ResponseWriter: w, status: http.StatusOK}
next.ServeHTTP(rec, r)
fmt.Printf("log: %s %s -> %d\n", r.Method, r.URL.Path, rec.status)
})
}
func recoverMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if err := recover(); err != nil {
http.Error(w, fmt.Sprintf("kesalahan internal: %v", err),
http.StatusInternalServerError)
}
}()
next.ServeHTTP(w, r)
})
}
func authMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Header.Get("Authorization") != "Bearer token-demo" {
http.Error(w, "token tidak valid", http.StatusUnauthorized)
return
}
next.ServeHTTP(w, r)
})
}
// chain menyusun middleware: m0 membungkus m1, m1 membungkus m2, dst.
func chain(h http.Handler, mws ...Middleware) http.Handler {
for i := len(mws) - 1; i >= 0; i-- {
h = mws[i](h)
}
return h
}
func main() {
final := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/ledak" {
panic("boom dari handler")
}
fmt.Fprintln(w, "selamat datang di area terproteksi")
})
// Urutan bungkus: logging -> recover -> auth -> final.
app := chain(final, loggingMiddleware, recoverMiddleware, authMiddleware)
uji(app, "GET", "/items", "dengan token")
uji(app, "GET", "/items", "tanpa token")
uji(app, "GET", "/ledak", "dengan token")
}
func uji(h http.Handler, method, path, keterangan string) {
fmt.Printf("== %s %s (%s)\n", method, path, keterangan)
req := httptest.NewRequest(method, path, nil)
if keterangan == "dengan token" {
req.Header.Set("Authorization", "Bearer token-demo")
}
rec := httptest.NewRecorder()
h.ServeHTTP(rec, req)
fmt.Printf(" status: %d | body: %s\n", rec.Code, strings.TrimSpace(rec.Body.String()))
}Jalankan berkas tersebut dengan perintah berikut dari direktori chapter:
go run middleware-demo.goOutput yang Dihasilkan:
== GET /items (dengan token)
log: GET /items -> 200
status: 200 | body: selamat datang di area terproteksi
== GET /items (tanpa token)
log: GET /items -> 401
status: 401 | body: token tidak valid
== GET /ledak (dengan token)
log: GET /ledak -> 500
status: 500 | body: kesalahan internal: boom dari handlerMari kita telaah baris demi baris program di atas:
- Baris 11 (
type Middleware ...): tipe fungsi yang menjadi kontrak seluruh middleware — menerima handler, mengembalikan handler. - Baris 14-22 (
type statusRecorder ...dst.): pembungkusResponseWriteryang menyimpan status lewat penimpaan methodWriteHeader. - Baris 24-30 (
func loggingMiddleware(...)): memanggil handler inti denganstatusRecorder, lalu mencetak method, path, dan status akhir. - Baris 32-42 (
func recoverMiddleware(...)): menangkap panic lewatdefer recoverdan mengubahnya menjadi respons500. - Baris 44-52 (
func authMiddleware(...)): menolak request tanpa token sah menggunakan respons401sebelum handler inti terpanggil. - Baris 55-60 (
func chain(...)): menyusun rantai dari dalam ke luar; iterasi mundur memastikan middleware pertama menjadi lapisan terluar. - Baris 63-68 (
final := ...): handler inti yang menyapa, sekaligus sengaja panic pada path/ledakuntuk menguji recover. - Baris 71 (
app := chain(...)): memaketkan urutanlogging -> recover -> auth -> finalmenjadi satu handler aplikasi. - Baris 73-75 (
uji(app, ...)): tiga skenario uji; perhatikan bahwa barislog:tercetak sebelum barisstatus:pada setiap skenario karena logging berjalan paling luar.
Middleware yang Anda bangun di sini adalah versi mini dari pola produksi. Proyek Web Server menyusun middleware serupa untuk logging terstruktur, autentikasi, dan penanganan error secara lebih lengkap, dan pengaturan timeout klien serta server menyambung pembahasan pada chapter berikutnya.
Referensi #
- Dokumentasi http.Handler — interface yang dibungkus seluruh middleware.
- Dokumentasi package net/http
—
ResponseWriter,HandlerFunc, dan helper respons. - Tutorial: Web service with Gin — contoh middleware pada framework pihak ketiga.
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
middleware-demo.go
Unduh berkaspackage main
import (
"fmt"
"net/http"
"net/http/httptest"
"strings"
)
// Middleware membungkus satu handler menjadi handler baru.
type Middleware func(http.Handler) http.Handler
// statusRecorder membungkus ResponseWriter untuk merekam status code.
type statusRecorder struct {
http.ResponseWriter
status int
}
func (rec *statusRecorder) WriteHeader(code int) {
rec.status = code
rec.ResponseWriter.WriteHeader(code)
}
func loggingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
rec := &statusRecorder{ResponseWriter: w, status: http.StatusOK}
next.ServeHTTP(rec, r)
fmt.Printf("log: %s %s -> %d\n", r.Method, r.URL.Path, rec.status)
})
}
func recoverMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if err := recover(); err != nil {
http.Error(w, fmt.Sprintf("kesalahan internal: %v", err),
http.StatusInternalServerError)
}
}()
next.ServeHTTP(w, r)
})
}
func authMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Header.Get("Authorization") != "Bearer token-demo" {
http.Error(w, "token tidak valid", http.StatusUnauthorized)
return
}
next.ServeHTTP(w, r)
})
}
// chain menyusun middleware: m0 membungkus m1, m1 membungkus m2, dst.
func chain(h http.Handler, mws ...Middleware) http.Handler {
for i := len(mws) - 1; i >= 0; i-- {
h = mws[i](h)
}
return h
}
func main() {
final := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/ledak" {
panic("boom dari handler")
}
fmt.Fprintln(w, "selamat datang di area terproteksi")
})
// Urutan bungkus: logging -> recover -> auth -> final.
app := chain(final, loggingMiddleware, recoverMiddleware, authMiddleware)
uji(app, "GET", "/items", "dengan token")
uji(app, "GET", "/items", "tanpa token")
uji(app, "GET", "/ledak", "dengan token")
}
func uji(h http.Handler, method, path, keterangan string) {
fmt.Printf("== %s %s (%s)\n", method, path, keterangan)
req := httptest.NewRequest(method, path, nil)
if keterangan == "dengan token" {
req.Header.Set("Authorization", "Bearer token-demo")
}
rec := httptest.NewRecorder()
h.ServeHTTP(rec, req)
fmt.Printf(" status: %d | body: %s\n", rec.Code, strings.TrimSpace(rec.Body.String()))
}