Episode ini fokus pada sisi respons: helper JSON yang rapi, rendering HTML, redirect, dan streaming respons. Kalian juga belajar melayani file statis dengan http.FileServer, menangani upload file, dan menulis custom response writer untuk kasus khusus.

Handler yang baik tidak hanya membaca request — ia juga mengirimkan respons yang benar. Episode 7 mengajarkan seluruh sisi output: JSON yang konsisten, HTML, redirect, streaming, dan file statis.
Banyak aplikasi Go gagal karena respons JSON-nya tidak seragam: kadang status salah, kadang content type lupa di-set. Episode ini memberikan pola yang konsisten untuk dipakai di seluruh project kalian.
Menulis empat baris JSON encoding di setiap handler mengundang inkonsistensi. Buat helper sekali, pakai di mana-mana:
func writeJSON(w http.ResponseWriter, status int, data any) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
json.NewEncoder(w).Encode(data)
}writeJSON(w, http.StatusCreated, user) men-set header, status, lalu encode dalam satu panggilan. Parameter status int dan data any membuat helper ini menangani sukses maupun error.
func createUserHandler(w http.ResponseWriter, req *http.Request) {
var input User
if err := json.NewDecoder(req.Body).Decode(&input); err != nil {
writeJSON(w, http.StatusBadRequest,
map[string]string{"error": "body tidak valid"})
return
}
writeJSON(w, http.StatusCreated, input)
}json.NewDecoder(req.Body).Decode(&input) membaca body JSON dari request, lalu helper mengirimkan respons 201 Created dengan data hasil decode.
Untuk halaman sederhana, gunakan html/template:
tpl := template.Must(template.ParseFiles("templates/home.html"))
r.Get("/", func(w http.ResponseWriter, req *http.Request) {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
tpl.Execute(w, map[string]string{"Nama": "Budi"})
})tpl.Execute(w, data) menulis HTML hasil template langsung ke ResponseWriter. Hindari fmt.Fprintf untuk data tak terpercaya karena rawan injection — html/template meng-escape otomatis.
r.Get("/old", func(w http.ResponseWriter, req *http.Request) {
http.Redirect(w, req, "/new", http.StatusMovedPermanently)
})http.Redirect(w, req, "/new", http.StatusMovedPermanently) mengirim status 301 dan header Location: /new. Browser dan curl akan mengikuti otomatis.
Untuk data yang terus mengalir, aktifkan flush:
r.Get("/stream", func(w http.ResponseWriter, req *http.Request) {
flusher, ok := w.(http.Flusher)
if !ok {
http.Error(w, "streaming tidak didukung",
http.StatusInternalServerError)
return
}
for i := 0; i < 5; i++ {
fmt.Fprintf(w, "chunk %d\n", i)
flusher.Flush()
time.Sleep(500 * time.Millisecond)
}
})flusher.Flush() memaksa data terkirim ke klien sebelum handler selesai — pola dasar Server-Sent Events yang akan kita bangun di episode 16.
Layani direktori statis dengan kombinasi klasik:
fileServer := http.FileServer(http.Dir("./static"))
r.Handle("/static/*", http.StripPrefix("/static", fileServer))http.StripPrefix("/static", fileServer) membuang prefix /static dari request sehingga /static/css/app.css membaca file ./static/css/app.css. Pola r.Handle("/static/*", ...) menangkap seluruh sub-path.
Pendekatan yang setara dan lebih ringkas:
r.Mount("/static", http.StripPrefix("/static",
http.FileServer(http.Dir("./static"))))r.Mount("/static", handler) menempelkan file server pada prefix — hasilnya sama dengan pola Handle, hanya ditulis lebih pendek.
Terima file dari klien dengan ParseMultipartForm:
r.Post("/upload", func(w http.ResponseWriter, req *http.Request) {
req.ParseMultipartForm(10 << 20)
file, handler, err := req.FormFile("foto")
if err != nil {
http.Error(w, "file tidak ditemukan", http.StatusBadRequest)
return
}
defer file.Close()
dst, _ := os.Create("./uploads/" + handler.Filename)
defer dst.Close()
io.Copy(dst, file)
writeJSON(w, http.StatusOK,
map[string]string{"message": "upload sukses"})
})req.FormFile("foto") mengambil file dari field foto pada body multipart. Batas 10 << 20 membatasi ukuran body maksimal 10 MiB.
Kadang kalian perlu mengintip apa yang ditulis handler — misalnya untuk status code logging:
type statusWriter struct {
http.ResponseWriter
status int
}
func (sw *statusWriter) WriteHeader(code int) {
sw.status = code
sw.ResponseWriter.WriteHeader(code)
}statusWriter membungkus ResponseWriter asli dan merekam status code. chi bahkan menyediakan middleware.WrapResponseWriter untuk tujuan ini — kita akan pakai lagi di episode 11 dan 20.
Inti yang harus dibawa pulang:
writeJSON membuat respons JSON konsisten di semua handler.html/template meng-escape otomatis; http.Redirect untuk redirect.flusher.Flush() membuka jalan streaming.http.FileServer plus StripPrefix melayani static files.req.FormFile dan batasan ukuran body.Di episode 8 selanjutnya kita naik satu level: struktur project dan clean architecture — memisahkan handler, service, dan repository, memakai layout internal, dependency injection, dan mendefinisikan router sebagai fungsi untuk kemudahan testing.