32. Server web pertamamu — Solusi PR

File solusi .go ada di exercises/32/homework/solutions/.

Soal 1 — Tiga halaman

Soal. Tiga rute untuk GET, dan 404 untuk yang lainnya.

Cara berpikir. Satu handler untuk setiap halaman, dan satu baris di main untuk setiap handler. Halaman depan butuh "GET /{$}", karena "GET /" akan cocok dengan semua path, dan /contact/extra akan mendapat halaman depan. "GET /contact" tidak punya garis miring di akhir, jadi hanya cocok dengan path itu persis.

Solusi yang dikerjakan.

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 has three pages.")
}

func handleContact(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintln(w, "Write to Keiko at the front desk.")
}

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

    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)
    }
}

Hasilnya:

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

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

Method Not Allowed

Kesalahan umum.

  • Menulis "GET /" untuk halaman depan. Maka /nothing mendapat status 200.
  • Menulis "GET /contact/", dengan garis miring di akhir. Itu cocok dengan semua yang ada di bawahnya, jadi /contact/extra mendapat halaman itu.
  • Menulis handleAbout() di baris http.HandleFunc. Itu memanggil fungsinya dan memberikan hasilnya. Berikan namanya, tanpa ().

Soal 2 — Halaman HTML

Soal. Halaman dengan judul, paragraf, dan daftar.

Cara berpikir. Halamannya tidak berubah, jadi konstanta bisa menyimpannya. Halaman itu punya beberapa baris dan beberapa karakter ", jadi raw string cocok. Atur header sebelum menulis body.

Solusi yang dikerjakan.

package main

import (
    "fmt"
    "net/http"
)

const homePage = `<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Things I like</title>
</head>
<body>
<h1>Things I like</h1>
<p>Here are three of them.</p>
<ul>
<li>Fried rice</li>
<li>Long walks</li>
<li>Small programs</li>
</ul>
</body>
</html>
`

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

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

    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)
    }
}

Kesalahan umum.

  • Memakai fmt.Fprintf(w, homePage). Itu berjalan sampai halamannya memuat %, yang dianggap Fprintf sebagai awal verb. fmt.Fprint tidak punya masalah itu.
  • Mengatur header sesudah fmt.Fprint. Header itu diabaikan, tanpa pesan.
  • Melewatkan charset=utf-8, sehingga teks seperti é bisa tampil sebagai karakter aneh.

Soal 3 — Kode status

Soal. Tiga rute, masing-masing dengan statusnya sendiri.

Cara berpikir. Yang pertama tidak melakukan apa-apa yang khusus, karena 200 adalah yang kamu dapat kalau tidak mengatur status. Dua lainnya adalah dua fungsi pembantu, masing-masing satu baris.

Solusi yang dikerjakan.

package main

import (
    "fmt"
    "net/http"
)

func handleHome(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintln(w, "Try /ok, /gone and /locked.")
}

func handleOK(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintln(w, "All good.")
}

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

func handleLocked(w http.ResponseWriter, r *http.Request) {
    http.Error(w, "Locked.", http.StatusForbidden)
}

func main() {
    http.HandleFunc("GET /{$}", handleHome)
    http.HandleFunc("GET /ok", handleOK)
    http.HandleFunc("GET /gone", handleGone)
    http.HandleFunc("GET /locked", handleLocked)

    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)
    }
}

Hasilnya:

curl.exe -i http://localhost:8080/ok
HTTP/1.1 200 OK
All good.

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

curl.exe -i http://localhost:8080/locked
HTTP/1.1 403 Forbidden
Locked.

Kesalahan umum.

  • Menulis 403 sebagai ganti http.StatusForbidden. Itu berjalan, tetapi namanya memberi tahu pembaca berikutnya apa artinya.
  • Tertukar urutan http.Error. Urutannya writer, lalu teks, lalu status.

Tantangan — Situs kecil

Soal. Fungsi pembantu yang menulis seluruh halaman, dipakai oleh beberapa handler, dan halaman 404-mu sendiri.

Cara berpikir. Halamannya selalu sama kecuali dua bagian, jadi bisa berupa string format dengan %s di dalamnya, yang diisi dengan fmt.Fprintf. Fungsi pembantu itu melakukan tiga langkah sesuai urutan keluarnya response: header, status, body. Itulah sebabnya status menjadi salah satu parameternya: handler 404 tidak bisa mengatur status lebih dulu lalu memanggil fungsi yang mengatur header.

"GET /" menangkap semua path GET yang tidak cocok dengan yang lebih spesifik, jadi itulah tempat untuk halaman 404. Di sini pola itu lebih baik daripada "/": metode lain pada /about tetap mendapat 405, karena router melihat bahwa path itu ada untuk GET.

Solusi yang dikerjakan.

package main

import (
    "fmt"
    "net/http"
)

const pageFormat = `<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>%s</title>
</head>
<body>
<h1>%s</h1>
<p>%s</p>
<p><a href="/">Home</a> | <a href="/about">About</a> | <a href="/games">Games</a></p>
</body>
</html>
`

// writePage sends a whole page. The title and the body must be text from this
// program, never text from a visitor, because they go into the HTML as they are.
func writePage(w http.ResponseWriter, status int, title string, body string) {
    w.Header().Set("Content-Type", "text/html; charset=utf-8")
    w.WriteHeader(status)
    fmt.Fprintf(w, pageFormat, title, title, body)
}

func handleHome(w http.ResponseWriter, r *http.Request) {
    writePage(w, http.StatusOK, "Home", "Welcome to a small site.")
}

func handleAbout(w http.ResponseWriter, r *http.Request) {
    writePage(w, http.StatusOK, "About", "This site is one Go file.")
}

func handleGames(w http.ResponseWriter, r *http.Request) {
    writePage(w, http.StatusOK, "Games", "Number Guess and Monster Battle.")
}

func handleNotFound(w http.ResponseWriter, r *http.Request) {
    writePage(w, http.StatusNotFound, "Not found", "There is no page at this address.")
}

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

    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)
    }
}

Komentar pada writePage itu penting. Fungsi ini menaruh argumennya ke dalam HTML apa adanya. Itu tidak masalah untuk teks di dalam programmu sendiri, dan tidak aman untuk teks dari pengunjung. Bab berikutnya menunjukkan alasannya, dan Bab 34 menunjukkan alat yang tepat.

Kesalahan umum.

  • Memanggil w.WriteHeader lebih dulu lalu mengatur Content-Type di fungsi pembantu. Header-nya diabaikan.
  • Menulis w.WriteHeader(http.StatusNotFound) sesudah fmt.Fprintf. Halaman keluar dengan status 200.
  • Mendaftarkan halaman 404 sebagai "/". Maka POST /about mendapat halaman 404-mu, bukan 405.