33. Request dan formulir — Solusi PR
File solusi .go ada di
exercises/33/homework/solutions/.
Soal 1 — Sapaan dari query string
Soal. Baca nama dari query string, dan tolak yang kosong.
Cara berpikir. Baca, periksa, kerjakan.
Get mengembalikan "" untuk nama yang tidak ada
maupun yang kosong, jadi satu pemeriksaan mencakup keduanya. Pemeriksaan
itu diakhiri return. Jawabannya memuat input pengunjung,
jadi berupa teks biasa, dan browser tidak akan menjalankan apa pun di
dalamnya.
Solusi yang dikerjakan.
package main
import (
"fmt"
"net/http"
)
func handleGreet(w http.ResponseWriter, r *http.Request) {
name := r.URL.Query().Get("name")
if name == "" {
http.Error(w, "Please give a name.", http.StatusBadRequest)
return
}
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
fmt.Fprintf(w, "Good morning, %s.\n", name)
}
func main() {
http.HandleFunc("GET /greet", handleGreet)
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/greet?name=Keiko"
HTTP/1.1 200 OK
Good morning, Keiko.
curl.exe -i http://localhost:8080/greet
HTTP/1.1 400 Bad Request
Please give a name.
Kesalahan umum.
- Melewatkan
returnsesudahhttp.Error. Jawabannya lalu memuatGood morning, .sesudah error. - Melewatkan
Content-Type. Go menebak dari byte pertama body, dan itu bukan sesuatu yang bisa diandalkan untuk teks pengunjung.
Soal 2 — Kelipatan dua dari path
Soal. Baca angka dari path, dan periksa.
Cara berpikir. Ada dua hal yang bisa salah, jadi ada
dua pemeriksaan, berurutan. Yang pertama bertanya apakah itu angka. Yang
kedua bertanya apakah angkanya dalam rentang, yang juga berarti
n*2 tidak bisa overflow. Pola /double/{n}
butuh sesuatu sesudah garis miring, jadi /double/
memberikan 404 dari router.
Solusi yang dikerjakan.
package main
import (
"fmt"
"net/http"
"strconv"
)
func handleDouble(w http.ResponseWriter, r *http.Request) {
n, err := strconv.Atoi(r.PathValue("n"))
if err != nil {
http.Error(w, "The path must end in a whole number.", http.StatusBadRequest)
return
}
if n < -1000000 || n > 1000000 {
http.Error(w, "The number must be from -1000000 to 1000000.", http.StatusBadRequest)
return
}
fmt.Fprintf(w, "%d doubled is %d\n", n, n*2)
}
func main() {
http.HandleFunc("GET /double/{n}", handleDouble)
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/double/21
HTTP/1.1 200 OK
21 doubled is 42
curl.exe -i http://localhost:8080/double/abc
HTTP/1.1 400 Bad Request
The path must end in a whole number.
curl.exe -i http://localhost:8080/double/2000000
HTTP/1.1 400 Bad Request
The number must be from -1000000 to 1000000.
Kesalahan umum.
- Memakai angka sesudah Atoi gagal. Nilainya
0, yang tampak seperti jawaban sungguhan. - Menulis
r.PathValue("number")padahal polanya{n}. Namanya harus cocok. Kalau tidak cocok,PathValuemengembalikan"", jadi setiap request mendapat 400.
Soal 3 — Formulir penilaian
Soal. Baca nilai dari POST, periksa, dan redirect.
Cara berpikir. Langkah yang sama lagi, dan langkah
terakhirnya redirect. Nilai yang tidak ada sampai ke Atoi sebagai
"", yang gagal, jadi tidak perlu pemeriksaan tersendiri.
Redirect itulah yang membuat muat ulang aman.
Solusi yang dikerjakan.
package main
import (
"fmt"
"net/http"
"strconv"
)
const formPage = `<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Rate this book</title>
</head>
<body>
<h1>Rate this book</h1>
<form action="/rating" method="post">
<p><label>Rating from 1 to 5: <input type="text" name="rating"></label></p>
<p><button type="submit">Send</button></p>
</form>
</body>
</html>
`
func handleForm(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
fmt.Fprint(w, formPage)
}
func handleRating(w http.ResponseWriter, r *http.Request) {
rating, err := strconv.Atoi(r.FormValue("rating"))
if err != nil {
http.Error(w, "The rating must be a whole number.", http.StatusBadRequest)
return
}
if rating < 1 || rating > 5 {
http.Error(w, "The rating must be from 1 to 5.", http.StatusBadRequest)
return
}
fmt.Println("Rating received:", rating)
http.Redirect(w, r, "/thanks", http.StatusSeeOther)
}
func handleThanks(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "Thank you for your rating.")
}
func main() {
http.HandleFunc("GET /{$}", handleForm)
http.HandleFunc("POST /rating", handleRating)
http.HandleFunc("GET /thanks", handleThanks)
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 -d "rating=4" http://localhost:8080/rating
HTTP/1.1 303 See Other
Location: /thanks
curl.exe -i -d "rating=9" http://localhost:8080/rating
HTTP/1.1 400 Bad Request
The rating must be from 1 to 5.
curl.exe -i -d "rating=abc" http://localhost:8080/rating
HTTP/1.1 400 Bad Request
The rating must be a whole number.
curl.exe -i http://localhost:8080/rating
HTTP/1.1 405 Method Not Allowed
Allow: POST
Method Not Allowed
Kesalahan umum.
- Memercayai formulir. Formulir bisa saja bertuliskan
min="1", dan request daricurl.exetetap bisa datang dengan9. Hanya pemeriksaan di handler yang berarti. - Memakai
http.StatusMovedPermanentlyuntuk redirect. Browser mengingat redirect jenis itu, dan mungkin tidak pernah bertanya lagi ke servermu. Pakai 303. - Mencoba membaca
r.FormValuedi dalamhandleThanks. Redirect adalah request baru, dan nilai dari POST bukan bagian darinya.
Tantangan — Kalkulator
Soal. Formulir, empat operasi, dan error yang berguna.
Cara berpikir. Pecah menjadi potongan yang bisa
dibaca sendiri, seperti di Bab 05. readNumber mengubah satu
nilai dan memeriksa rentangnya, dan dipanggil dua kali.
calculate mengetahui keempat operasi, dan mengembalikan
error untuk operasi yang tidak dikenal atau pembagian dengan nol.
Handler hanya menghubungkannya, dan mengubah setiap error menjadi status
400.
Formulir ini memakai GET karena menghitung tidak mengubah apa-apa. Jawabannya bisa dimuat ulang dan disimpan sebagai bookmark, dan tidak ada yang rusak kalau diminta dua kali. Alamatnya lalu menampilkan seluruh pertanyaan.
Solusi yang dikerjakan.
package main
import (
"errors"
"fmt"
"net/http"
"strconv"
)
const formPage = `<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Calculator</title>
</head>
<body>
<h1>Calculator</h1>
<form action="/calc" method="get">
<p><label>a: <input type="text" name="a"></label></p>
<p><label>op:
<select name="op">
<option>add</option>
<option>sub</option>
<option>mul</option>
<option>div</option>
</select>
</label></p>
<p><label>b: <input type="text" name="b"></label></p>
<p><button type="submit">Calculate</button></p>
</form>
</body>
</html>
`
const limit = 1000000
// readNumber converts one form value, and checks its range.
func readNumber(label string, text string) (int, error) {
n, err := strconv.Atoi(text)
if err != nil {
return 0, fmt.Errorf("%s must be a whole number", label)
}
if n < -limit || n > limit {
return 0, fmt.Errorf("%s must be from %d to %d", label, -limit, limit)
}
return n, nil
}
// calculate returns the answer, or an error that is safe to show the visitor.
func calculate(a int, op string, b int) (int, error) {
switch op {
case "add":
return a + b, nil
case "sub":
return a - b, nil
case "mul":
return a * b, nil
case "div":
if b == 0 {
return 0, errors.New("cannot divide by zero")
}
return a / b, nil
}
return 0, errors.New("op must be add, sub, mul or div")
}
func handleForm(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
fmt.Fprint(w, formPage)
}
func handleCalc(w http.ResponseWriter, r *http.Request) {
a, err := readNumber("a", r.FormValue("a"))
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
b, err := readNumber("b", r.FormValue("b"))
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
op := r.FormValue("op")
answer, err := calculate(a, op, b)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
fmt.Fprintf(w, "%d %s %d = %d\n", a, op, b, answer)
}
func main() {
http.HandleFunc("GET /{$}", handleForm)
http.HandleFunc("GET /calc", handleCalc)
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/calc?a=6&op=div&b=3"
HTTP/1.1 200 OK
6 div 3 = 2
curl.exe -i "http://localhost:8080/calc?a=6&op=div&b=0"
HTTP/1.1 400 Bad Request
cannot divide by zero
curl.exe -i "http://localhost:8080/calc?a=1&op=pow&b=1"
HTTP/1.1 400 Bad Request
op must be add, sub, mul or div
curl.exe -i "http://localhost:8080/calc?a=2000000&op=mul&b=2"
HTTP/1.1 400 Bad Request
a must be from -1000000 to 1000000
http.Error butuh string, dan err.Error()
adalah metode yang memberikan teks sebuah error. Ini aman di sini karena
setiap pesan error ditulis oleh program ini. Jawabannya mencetak
op hanya sesudah calculate menerimanya, jadi
nilainya salah satu dari empat kata yang dikenal.
Kesalahan umum.
- Lupa pemeriksaan
b == 0. Pembagian bilangan bulat dengan nol membuat handler crash. Server pulih dan berjalan terus, tetapi pengunjung tidak mendapat response. - Memercayai
<select>. Request bisa mengirimopapa saja, jadiswitchbutuh jalan keluar untuk yang tidak dikenal.