32. Server web pertamamu

Server di Bab 31 memberikan jawaban yang sama untuk semua path. Server sungguhan punya jawaban berbeda untuk setiap path, dan menolak apa yang tidak bisa dia lakukan. Bab ini menambahkan rute, halaman HTML, header, dan kode status.

Rute

Rute menghubungkan pola dengan handler. Ini exercises/32/01-routes.go:

// Chapter 32 example: several routes with Go 1.22 patterns.
// Run with: go run exercises/32/01-routes.go

package main

import (
    "fmt"
    "net/http"
)

func handleHome(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintln(w, "This is the home page.")
}

func handleAbout(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintln(w, "This server was written in Go.")
}

func main() {
    http.HandleFunc("GET /{$}", handleHome)
    http.HandleFunc("GET /about", handleAbout)

    fmt.Println("Server running at http://localhost:8080")
    fmt.Println("Press Ctrl+C to stop it.")

    err := http.ListenAndServe("localhost:8080", nil)
    if err != nil {
        fmt.Println("Server stopped:", err)
    }
}

Jalankan dari go-work. Dengan curl.exe -i, kamu bisa memeriksa setiap rute. Mulai dari sini, buku ini hanya menampilkan baris pertama dan body, dan melewatkan header yang tidak penting untuk contohnya.

curl.exe -i http://localhost:8080/
HTTP/1.1 200 OK
This is the home page.

curl.exe -i http://localhost:8080/about
HTTP/1.1 200 OK
This server was written in Go.

curl.exe -i http://localhost:8080/nothing
HTTP/1.1 404 Not Found
404 page not found

Kamu tidak menulis kode apa pun untuk 404 itu. Router mengirimnya ketika tidak ada pola yang cocok.

Membaca pola

Go 1.22 menambahkan metode dan wildcard pada pola. "GET /about" punya dua bagian, dengan satu spasi di antaranya:

  • GET berarti rute itu hanya menjawab metode ini. Rute itu juga menjawab HEAD, sejenis GET yang hanya meminta header.
  • /about adalah path-nya.

Kirim metode lain ke path itu, dan Go memberikan status 405 dengan sendirinya. -X POST meminta curl.exe memakai POST:

curl.exe -i -X POST http://localhost:8080/about
HTTP/1.1 405 Method Not Allowed
Allow: GET, HEAD

Method Not Allowed

Allow adalah header yang mendaftar apa yang seharusnya berhasil.

"GET /{$}" tampak aneh, dan ada alasannya. Pola yang berakhir dengan / cocok dengan semua yang ada di bawahnya. Jadi "GET /" saja juga akan menangkap /nothing, dan menjawab dengan halaman depan dan status 200. {$} berarti "dan berhenti di sini", jadi pola itu cocok persis dengan /.

Ketika lebih dari satu pola cocok, yang paling spesifik yang menang, jadi urutan baris http.HandleFunc tidak penting.

Dua kesalahan ini tidak menghentikan program, jadi sulit ditemukan:

Yang kamu tulis Yang terjadi
"GET/about" Tanpa spasi, Go membaca GET sebagai nama komputer. /about memberikan 404.
"get /about" Metode membedakan huruf besar dan kecil. Browser mengirim GET, dan mendapat 405.

Mendaftarkan pola yang sama dua kali membuat program crash begitu dimulai:

panic: pattern "GET /about" (registered at C:\go-work\exercises\32\01-routes.go:22) conflicts with pattern "GET /about" (registered at C:\go-work\exercises\32\01-routes.go:21):
GET /about matches the same requests as GET /about

Buka exercises/32/01-routes.go. Tambahkan rute GET /hours yang menjawab dengan jam bukamu. Lalu coba /hours/, dengan garis miring di akhir, dan lihat statusnya.

Dua parameter

Handler selalu punya dua parameter yang sama.

r *http.Request adalah apa yang dikirim pengunjung. Sejauh ini kamu sudah memakai r.Method dan r.URL.Path. Bab 33 membaca sisanya.

w http.ResponseWriter adalah sisimu dalam percakapan ini. Dia adalah interface dengan tiga metode:

  • w.Write, yang mengirim sebagian body. fmt.Fprintf memanggilnya untukmu.
  • w.Header(), yang memberimu header, supaya kamu bisa mengaturnya.
  • w.WriteHeader(code), yang mengatur status. Kalau tidak dipanggil, statusnya 200.

Urutannya penting, karena response keluar dari atas ke bawah: status, header, lalu body. Jadi atur header dulu, lalu status, lalu tulis body. Begitu body dimulai, sudah terlambat. Memanggil WriteHeader sesudah menulis tidak mengubah status, dan Go mengeluh di terminal:

2026/11/05 01:33:24 http: superfluous response.WriteHeader call from main.handleCreated (03-status-codes.go:29)

Mengatur header sesudah menulis diabaikan begitu saja, tanpa pesan apa pun.

Mengirim HTML

Browser menampilkan teks biasa apa adanya. Untuk mendapatkan judul atau tautan, kirim HTML, yaitu teks dengan tag di dalam kurung sudut. Buku ini hanya mengandaikan sedikit: <h1> adalah judul besar, <p> adalah paragraf, <a href="..."> adalah tautan, dan <ul> serta <li> membuat daftar.

Browser perlu diberi tahu bahwa dia menerima HTML. Itulah gunanya header Content-Type. Ini bagian penting dari exercises/32/02-html-page.go:

const homePage = `<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Home</title>
</head>
<body>
<h1>Welcome</h1>
<p>This page came from a Go string.</p>
<p><a href="/about">About this server</a></p>
</body>
</html>
`

func handleHome(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "text/html; charset=utf-8")
    fmt.Fprint(w, homePage)
}

Halamannya berupa raw string di antara backtick, dari Bab 12, supaya bisa terdiri dari banyak baris dan memuat ". fmt.Fprint menuliskannya tanpa format dan tanpa akhir baris tambahan.

charset=utf-8 mengatakan bagaimana karakter disimpan, supaya teks seperti é tampil dengan benar. Selalu sertakan.

Kalau kamu melewatkan header itu, Go melihat awal body dan menebak. Di sini tebakannya benar. Tetapi tebakan bukan sesuatu yang bisa diandalkan, jadi aturlah sendiri.

Buka exercises/32/02-html-page.go. Jalankan, dan klik tautannya. Ubah text/html menjadi text/plain di satu handler, lalu mulai ulang. Apa yang ditampilkan browser sekarang?

Kode status

exercises/32/03-status-codes.go punya satu handler untuk setiap cara mengatur status:

func handleOld(w http.ResponseWriter, r *http.Request) {
    http.NotFound(w, r)
}

func handlePrivate(w http.ResponseWriter, r *http.Request) {
    http.Error(w, "You may not see this page.", http.StatusForbidden)
}

func handleBroken(w http.ResponseWriter, r *http.Request) {
    http.Error(w, "Something went wrong on the server.", http.StatusInternalServerError)
}

func handleCreated(w http.ResponseWriter, r *http.Request) {
    w.WriteHeader(http.StatusCreated)
    fmt.Fprintln(w, "This reply has the status 201 Created.")
}

Hasilnya:

curl.exe -i http://localhost:8080/old
HTTP/1.1 404 Not Found
404 page not found

curl.exe -i http://localhost:8080/private
HTTP/1.1 403 Forbidden
You may not see this page.

curl.exe -i http://localhost:8080/broken
HTTP/1.1 500 Internal Server Error
Something went wrong on the server.

curl.exe -i http://localhost:8080/created
HTTP/1.1 201 Created
This reply has the status 201 Created.
  • http.NotFound(w, r) mengirim 404 yang sama dengan router.
  • http.Error(w, text, code) mengirim status dan teksnya sebagai teks biasa. Pakai ini untuk sebagian besar error.
  • http.StatusForbidden dan kerabatnya adalah konstanta untuk angka-angka itu. Namanya mengatakan artinya, jadi pakailah sebagai ganti 403.

Kedua fungsi itu tidak mengakhiri handlermu. Keduanya menulis response, lalu kodemu berlanjut. Jadi tulis return sesudahnya, kalau masih ada kode yang mengikuti. Bab berikutnya menunjukkan apa yang terjadi kalau kamu lupa.

Menyajikan file dari folder

Kadang sebuah halaman butuh file apa adanya, seperti stylesheet. Go bisa menyajikan seluruh folder dengan dua baris. Contoh ini butuh foldernya, jadi dia berupa modul tersendiri, di exercises/32/04-static-files/:

04-static-files/
    go.mod
    main.go
    static/
        notes.txt
        style.css

Dua baris itu, di dalam main:

files := http.FileServer(http.Dir("static"))
http.Handle("GET /static/", http.StripPrefix("/static/", files))

http.FileServer adalah handler siap pakai yang menjawab dengan file dari sebuah folder. http.StripPrefix melepas /static/ dari path lebih dulu, jadi /static/notes.txt mencari notes.txt di dalam folder itu. Karena handler ini adalah nilai dengan metode dan bukan fungsimu sendiri, dia didaftarkan dengan http.Handle dan bukan http.HandleFunc.

Nama foldernya relatif terhadap tempat program berjalan, jadi jalankan dari foldernya sendiri:

cd exercises/32/04-static-files
go run .

Untuk kembali ke go-work sesudahnya:

cd ../../..

Buka http://localhost:8080, dan judulnya biru karena file itu. Halaman itu menautkan stylesheet-nya lewat path lokal. Semua yang dibutuhkan halaman berasal dari servermu sendiri. Request untuk file di luar folder itu tidak berhasil:

curl.exe -i http://localhost:8080/static/notes.txt
HTTP/1.1 200 OK
These notes are a file in the static folder.

curl.exe -i http://localhost:8080/main.go
HTTP/1.1 404 Not Found
404 page not found

File server menyajikan semua yang ada di foldernya, dan request untuk /static/ mendaftar nama-nama file. Taruh hanya file publik di sana. Bagian lain buku ini tidak membutuhkan file server.

PR

Starter PR ada di exercises/32/homework/. Masing-masing bisa langsung dijalankan dan menjawab dengan satu baris pengganti.

Soal 1 — Tiga halaman

Buka exercises/32/homework/01-three-pages.go. Buat tiga rute yang hanya menjawab GET: halaman depan di /, halaman di /about, dan halaman di /contact. Masing-masing menjawab dengan satu baris teks biasa. Path lain harus memberikan 404, termasuk /contact/extra.

Soal 2 — Halaman HTML

Buka exercises/32/homework/02-html-page.go. Jawab di / dengan halaman HTML yang punya judul, satu paragraf, dan daftar tiga hal yang kamu sukai. Simpan HTML-nya dalam konstanta, dan atur header Content-Type.

Soal 3 — Kode status

Buka exercises/32/homework/03-status-codes.go. Tambahkan tiga rute:

  • GET /ok menjawab dengan status 200 dan All good.
  • GET /gone menjawab dengan http.NotFound.
  • GET /locked menjawab dengan http.Error, teks Locked., dan status 403.

Periksa status masing-masing dengan curl.exe -i.

Tantangan — Situs kecil

Buka exercises/32/homework/04-small-site.go. Tulis fungsi pembantu:

func writePage(w http.ResponseWriter, status int, title string, body string)

Fungsi itu mengatur Content-Type, mengatur status, dan menulis seluruh halaman HTML dengan judul dan body tersebut. Pakai dari tiga handler: halaman depan, /about, dan satu halaman buatanmu. Setiap halaman punya tautan ke dua halaman lainnya. Lalu buat halaman 404-mu sendiri: pola "GET /" menangkap path GET apa pun yang tidak cocok dengan rute lain, jadi handler di sana bisa memanggil writePage dengan http.StatusNotFound.

Stuck atau sudah selesai? Buka halaman solusi PR.