Pendahuluan

Dalam tutorial ini kita akan belajar tentang:

  • Membuat sebuah struktur data dengan method-method untuk membaca dan menyimpan data

  • Menggunakan paket net/http untuk membangun aplikasi web

  • Menggunakan paket html/template untuk memroses templat HTML

  • Menggunakan paket regexp untuk validasi input dari pengguna

  • Menggunakan closure

Pengetahuan yang diperlukan:

  • Pengalaman pemrograman

  • Pemahaman dari dasar teknologi web (HTTP, HTML)

  • Pengetahuan tentang perintah pada UNIX/DOS

Memulai

Untuk memulai, kita membutuhkan mesin FreeBSD, Linux, OS X, atau Windows supaya dapat menjalankan Go. Kita akan menggunakan $ untuk merepresentasikan baris perintah.

Pasanglah Go (lihat Instruksi Pemasangan).

Buatlah sebuah direktori baru untuk tutorial ini di dalam GOPATH Anda dan pindah lah ke sana:

$ mkdir gowiki
$ cd gowiki

Buat sebuah berkas bernama wiki.go, sunting dengan menambahkan baris berikut:

package main

import (
	"fmt"
	"io/ioutil"
)

Kita mengimpor paket fmt dan ioutil dari pustaka standar Go. Nanti, saat kita mengimplementasikan fungsionalitas, kita akan menambahkan paket lain ke dalam deklarasi impor tersebut.

Struktur Data

Mari kita mulai dengan mendefinisikan struktur data. Sebuah wiki terdiri dari sekumpulan halaman yang saling terhubung, setiap halaman memiliki sebuah judul dan isi. Di sini, kita definisikan Page sebagai sebuah struct dengan dua field yang merepresentasikan judul (Title) dan isi (Body).

type Page struct {
	Title string
	Body  []byte
}

Tipe []byte artinya "potongan byte". (Lihat Slice: penggunaan dan internal untuk informasi lebih lanjut tentang slice). Elemen dari Body adalah []byte bukan string karena tipe tersebut yang diharapkan oleh pustaka io yang akan kita gunakan, seperti yang dapat kita lihat di bawah.

Struct Page menjelaskan bagaimana data dari sebuah halaman disimpan dalam memori. Lalu bagaimana dengan penyimpanan yang permanen? Kita dapat menyelesaikan masalah tersebut dengan membuat method save pada struct Page:

func (p *Page) save() error {
	filename := p.Title + ".txt"
	return ioutil.WriteFile(filename, p.Body, 0600)
}

Method tersebut dibaca: "Method ini bernama save dengan penerima p, sebuah pointer ke Page. Ia tidak menerima parameter, dan mengembalikan sebuah nilai bertipe error."

Method tersebut akan menyimpan Body (isi) dari Page (halaman) ke dalam berkas. Untuk memudahkan, kita akan menggunakan Title (judul) sebagai nama berkas.

Method save mengembalikan sebuah nilai error dari fungsi WriteFile (fungsi dari pustaka standar yang menulis slice byte ke dalam berkas). Method save mengembalikan nilai error tersebut, supaya aplikasi dapat menangani-nya bila ada kesalahan saat menulis ke berkas. Jika semua berjalan dengan lancar, Page.save() akan mengembalikan nil (sebuah nilai kosong untuk pointer, interface, dan beberapa tipe lainnya).

Nilai integer oktal 0600, yang dikirim sebagai parameter ketiga pada WriteFile, mengindikasikan bahwa berkas dibuat dengan akses baca-tulis untuk pengguna yang sekarang. (Lihat halaman manual Unix untuk open(2) untuk lebih detail.)

Selain menyimpan halaman, kita juga ingin membaca halaman dari berkas:

func loadPage(title string) *Page {
	filename := title + ".txt"
	body, _ := ioutil.ReadFile(filename)
	return &Page{Title: title, Body: body}
}

Fungsi loadPage membuat nama berkas dari parameter title (judul), membaca isi dari berkas ke dalam variabel body, dan mengembalikan sebuah pointer ke Page yang berisi nilai title dan body.

Fungsi dapat mengembalikan beberapa nilai. Fungsi io.ReadFile dari pustaka standar mengembalikan []byte dan error. Di dalam loadPage, eror belum ditangani; "pengidentifikasi kosong" direpresentasikan dengan simbol garis-bawah (_) digunakan untuk mengindahkan nilai kembalian (intinya, tidak mengisi nilai kembalian ke apa pun).

Tetapi apa yang terjadi bila ReadFile mendapatkan eror? Misalnya, berkas bisa saja tidak ada. Kita sebaiknya tidak mengindahkan eror tersebut. Mari kita ubah fungsi tersebut supaya mengembalikan *Page dan error.

func loadPage(title string) (*Page, error) {
	filename := title + ".txt"
	body, err := ioutil.ReadFile(filename)
	if err != nil {
		return nil, err
	}
	return &Page{Title: title, Body: body}, nil
}

Siapa pun yang memanggil fungsi ini dapat memeriksa nilai kembalian kedua; jika nil berarti sebuah Page telah sukses dibaca. Jika tidak, maka akan ada error yang harus ditangani oleh si pemanggil fungsi (lihat spesifikasi bahasa untuk lebih detail).

Sekarang kita telah memiliki sebuah struktur data sederhana dan kemampuan untuk menyimpan dan membaca dari berkas. Mari kita tulis sebuah fungsi main untuk menguji apa yang telah kita tulis:

func main() {
	p1 := &Page{Title: "TestPage", Body: []byte("This is a sample Page.")}
	p1.save()
	p2, _ := loadPage("TestPage")
	fmt.Println(string(p2.Body))
}

Setelah mengompilasi dan mengeksekusi kode tersebut, sebuah berkas bernama TestPage.txt akan dibuat, berisi nilai dari p1.Body. Berkas tersebut kemudian dibaca ke dalam struct p2, dengan elemen Body dicetak ke layar.

Anda dapat mengompilasi dan menjalankan program seperti berikut:

$ go build wiki.go
$ ./wiki
This is a sample Page.

(Jika Anda menggunakan sistem Windows, Anda harus mengetikan "wiki" tanpa "./" untuk menjalankan program.)

Memperkenalkan paket net/http

Berikut contoh peladen web yang sederhana:

// +build ignore

package main

import (
	"fmt"
	"log"
	"net/http"
)

func handler(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintf(w, "Hi there, I love %s!", r.URL.Path[1:])
}

func main() {
	http.HandleFunc("/", handler)
	log.Fatal(http.ListenAndServe(":8080", nil))
}

Fungsi main dimulai dengan memanggil ke http.HandleFunc, yang memberitahu paket http supaya mengirim semua permintaan dari path "/" ke fungsi handler.

Dalam pemanggilan http.ListenAndServe, kita menspesifikasikan bahwa peladen (server) akan mendengarkan permintaan pada port 8080 di semua jaringan (":8080"). Tidak perlu khawatir dengan parameter kedua, nil, untuk saat sekarang. Fungsi ini akan mem-blok sampai program dihentikan.

Fungsi ListenAndServe selalu mengembalikan sebuah nilai error yang tidak nil bila sebuah kesalahan tidak terduga terjadi. Supaya dapat mencatat kesalahan tersebut, kita membungkus pemanggilan fungsi dengan log.Fatal

Fungsi handler bertipe http.HandlerFunc. Ia menerima sebuah http.ResponseWriter dan sebuah http.Request.

Nilai dari http.ResponseWriter mengumpulkan respon untuk HTTP server; dengan menulis lewat nilai tersebut, kita mengirim data ke klien HTTP.

Sebuah http.Request adalah struktur data yang merepresentasikan permintaan dari klien HTTP. r.URL.Path adalah komponen path dari URL. Sintaksis [1:] pada akhir baris artinya "buat potongan slice pada Path dari karakter 1 sampai akhir." Perintah ini memotong awalan "/" pada nilai path.

Jika kita menjalankan program ini dan mengakses URL:

http://localhost:8080/monkeys

maka program akan menampilkan sebuah halaman berisi:

Hi there, I love monkeys!

Menggunakan net/http untuk melayani halaman wiki

Untuk menggunakan paket net/http, ia harus lah diimpor:

import (
	"fmt"
	"io/ioutil"
	"log"
	"net/http"
)

Mari kita buat sebuah fungsi viewHandler yang membolehkan pengguna untuk melihat sebuah halaman wiki. Fungsi tersebut akan menangani URL dengan prefiks "/view/".

func viewHandler(w http.ResponseWriter, r *http.Request) {
	title := r.URL.Path[len("/view/"):]
	p, _ := loadPage(title)
	fmt.Fprintf(w, "<h1>%s</h1><div>%s</div>", p.Title, p.Body)
}

Sekali lagi, perhatikan penggunaan _ untuk mengindahkan nilai kembalian eror dari loadPage. Hal ini kita lakukan supaya lebih simpel tetapi praktik yang buruk. Kita akan membahas hal ini nanti.

Pertama, fungsi tersebut mengekstrak judul halaman dari r.URL.Path, komponen path dari URL yang diminta. Nilai Path kemudian dipotong dengan [len("/view/"):] untuk memotong komponen "/view/" dari path. Hal ini karena path akan selalu dimulai dengan "/view/", yang bukan bagian dari judul halaman.

Fungsi tersebut kemudian memuat data halaman, mem-format halaman dengan sebuah HTML sederhana, dan menulisnya ke w, instan dari http.ResponseWriter.

Untuk menggunakan fungsi ini, kita tulis ulang fungsi main supaya menginisiasi http menggunakan viewHandler untuk menangani permintaan ke path "/view/"`.

func main() {
 	http.HandleFunc("/view/", viewHandler)
	log.Fatal(http.ListenAndServe(":8080", nil))
}

Mari kita buat sebuah halaman, test.txt, kompilasi kode, dan mencoba melayani halaman wiki.

Buka berkas test.txt, dan simpan string "Hello world" (tanpa tanda kutip) ke dalamnya.

$ go build wiki.go
$ ./wiki

(Jika Anda menggunakan Windows, Anda harus menulis "wiki" tanpa "./" untuk menjalakan program.)

Saat peladen web telah berjalan, membuka localhost:8080/view/test akan menampilkan sebuah halaman berjudul "test" berisi kata "Hello world".

Menyunting halaman

Sebuah aplikasi wiki bukanlah wiki bila tidak bisa menyunting halaman. Mari kita buat dua buah handler: satu bernama editHandler untuk menampilkan form menyunting halaman, dan yang lain bernama saveHandler untuk menyimpan data yang diinput pada form suntingan.

Pertama, kita tambahkan ke fungsi main():

func main() {
	http.HandleFunc("/view/", viewHandler)
	http.HandleFunc("/edit/", editHandler)
	http.HandleFunc("/save/", saveHandler)
	log.Fatal(http.ListenAndServe(":8080", nil))
}

Fungsi editHandler membaca halaman (atau, jika tidak ada, membuat sebuah struct Page yang kosong), dan menampilkan sebuah form HTML.

func editHandler(w http.ResponseWriter, r *http.Request) {
	title := r.URL.Path[len("/edit/"):]
	p, err := loadPage(title)
	if err != nil {
		p = &Page{Title: title}
	}
	fmt.Fprintf(w, "<h1>Editing %s</h1>"+
		"<form action=\"/save/%s\" method=\"POST\">"+
		"<textarea name=\"body\">%s</textarea><br>"+
		"<input type=\"submit\" value=\"Save\">"+
		"</form>",
		p.Title, p.Title, p.Body)
}

Fungsi ini bekerja, namun kode HTML yang ditulis sangat jelek. Tentu saja, ada cara yang lebih baik.

Paket html/template

Paket html/template adalah bagian dari pustaka standar Go. Kita dapat menggunakan html/template untuk menyimpan HTML pada berkas yang berbeda, membolehkan kita mengubah struktur HTML dari halaman sunting tanpa mengubah kode Go.

Pertama, kita impor html/template. Secara kita tidak menggunakan fmt lagi, jadi kita bisa hapus dari impor.

import (
	"html/template"
	"io/ioutil"
	"net/http"
)

Mari kita buat sebuah berkas templat yang berisi form HTML. Buat lah sebuah berkas bernama edit.hmtl, dan tambahkan baris berikut:

<h1>Editing {{.Title}}</h1>

<form action="/save/{{.Title}}" method="POST">
<div><textarea name="body" rows="20" cols="80">{{printf "%s" .Body}}</textarea></div>
<div><input type="submit" value="Save"></div>
</form>

Ubah editHandler supaya menggunakan templat tersebut:

func editHandler(w http.ResponseWriter, r *http.Request) {
	title := r.URL.Path[len("/edit/"):]
	p, err := loadPage(title)
	if err != nil {
		p = &Page{Title: title}
	}
	t, _ := template.ParseFiles("edit.html")
	t.Execute(w, p)
}

Fungsi template.ParseFiles akan membaca isi dari berkas edit.html dan mengembalikan *template.Template.

Method t.Execute mengeksekusi templat, menulis HTML hasil pembangkitan ke http.ResponseWriter. Variabel dengan awalan titik .Title dan .Body mengacu pada p.Title dan p.Body.

Direktif templat ditandai oleh kurung kurawal ganda. Perintah ‘printf "%s" .Body’ yaitu pemanggilan fungsi yang mencetak .Body sebagai string, sama seperti memanggil fmt.Printf. Paket html/template menjamin hanya HTML yang aman dan benar dibangkitkan oleh aksi templat. Misalnya, ia secara otomatis mengganti karakter '>' dengan &gt;, untuk memastikan data pengguna tidak merusak format HTML.

Secara kita sekarang bekerja dengan templat, mari buat sebuah templat lagi untuk viewHandler yang bernama view.html.

<h1>{{.Title}}</h1>

<p>[<a href="/edit/{{.Title}}">edit</a>]</p>

<div>{{printf "%s" .Body}}</div>

Ubah viewHandler menjadi:

func viewHandler(w http.ResponseWriter, r *http.Request) {
	title := r.URL.Path[len("/view/"):]
	p, _ := loadPage(title)
	t, _ := template.ParseFiles("view.html")
	t.Execute(w, p)
}

Perhatikan bahwa kita hampir menggunakan kode templat yang sama pada kedua handler. Mari kita coba hapus duplikasi ini dengan memindahkan kode templat ke fungsinya sendiri.

func renderTemplate(w http.ResponseWriter, tmpl string, p *Page) {
	t, _ := template.ParseFiles(tmpl + ".html")
	t.Execute(w, p)
}

Dan mengubah handler supaya menggunakan fungsi tersebut:

func viewHandler(w http.ResponseWriter, r *http.Request) {
	title := r.URL.Path[len("/view/"):]
	p, _ := loadPage(title)
	renderTemplate(w, "view", p)
}

func editHandler(w http.ResponseWriter, r *http.Request) {
	title := r.URL.Path[len("/edit/"):]
	p, err := loadPage(title)
	if err != nil {
		p = &Page{Title: title}
	}
	renderTemplate(w, "edit", p)
}

Jika kita tutup registrasi dari fungsi save yang belum diimplementasikan dalam fungsi main, kita dapat membangun dan menguji program kita. Klik di sini untuk melihat kode yang telah kita tulis sejauh ini.

Menangani halaman yang tidak ada

Apa yang terjadi bila kita mengunjungi /view/HalamanYangTidakAda? Kita akan melihat sebuah halaman yang berisi HTML. Hal ini karena kita mengindahkan error dari loadPage dan melanjutkan mencoba mengisi templat dengan data yang tidak ada. Jika halaman yang diminta tidak ada, aplikasi seharusnya mengalihkan klien ke halaman sunting supaya isinya bisa dibuat:

func viewHandler(w http.ResponseWriter, r *http.Request) {
	title := r.URL.Path[len("/view/"):]
	p, err := loadPage(title)
	if err != nil {
		http.Redirect(w, r, "/edit/"+title, http.StatusFound)
		return
	}
	renderTemplate(w, "view", p)
}

Fungsi http.Redirect men-set HTTP status kode http.StatusFound (302) dan header Location pada respon HTTP.

Menyimpan halaman

Fungsi saveHandler akan menangani penyimpan form dari halaman sunting. Setelah membuka komentar baris http.HandleFunc("/save/", saveHandler) pada fungsi main, mari kita implementasi fungsi tersebut:

func saveHandler(w http.ResponseWriter, r *http.Request) {
	title := r.URL.Path[len("/save/"):]
	body := r.FormValue("body")
	p := &Page{Title: title, Body: []byte(body)}
	p.save()
	http.Redirect(w, r, "/view/"+title, http.StatusFound)
}

Judul halaman (yang diberikan lewat URL) dan satu-satunya kolom pada form, Body, disimpan dalam Page yang baru. Method save() kemudian dipanggil untuk menulis data ke dalam sebuah berkas, dan klien dialihkan ke halaman "/view/".

Nilai yang dikembalikan oleh FormValue bertipe string. Kita harus mengonversi nilai tersebut ke []byte sebelum dapat disimpan dalam struct Page. Kita menggunakan []byte(body) untuk melakukan konversi.

Penanganan eror

Ada beberapa tempat dalam program kita yang mana eror diindahkan. Hal ini merupakan praktik yang buruk, karena saat eror terjadi program akan memiliki perilaku yang tidak terduga. Solusi yang lebih baik yaitu menangani eror dan mengembalikan pesan eror kepada pengguna. Dengan cara ini jika sesuatu kesalahan terjadi, peladen akan berfungsi seperti yang kita inginkan dan pengguna dapat diberi tahu.

Pertama, mari kita tangani eror dalam renderTemplate:

func renderTemplate(w http.ResponseWriter, tmpl string, p *Page) {
	t, err := template.ParseFiles(tmpl + ".html")
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
	err = t.Execute(w, p)
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
	}
}

Fungsi http.Error mengirim kode HTTP respon tertentu (dalam kasus ini "Internal Server Error") dan pesan eror.

Sekarang kita perbaiki saveHandler:

func saveHandler(w http.ResponseWriter, r *http.Request) {
	title := r.URL.Path[len("/save/"):]
	body := r.FormValue("body")
	p := &Page{Title: title, Body: []byte(body)}
	err := p.save()
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
	http.Redirect(w, r, "/view/"+title, http.StatusFound)
}

Setiap eror yang terjadi selama p.save() akan dilaporkan ke pengguna.

Tembolok templat

Kode kita ada yang tidak efisien: renderTemplate memanggil ParseFiles setiap kali sebuah halaman dibangkitkan. Pendekatan yang lebih bagus yaitu dengan memanggil ParseFiles sekali saat program diinisiasi, membaca semua berkas templat ke dalam sebuah *Template. Kemudian kita dapat menggunakan method ExecuteTemplate untuk menulis templat tertentu.

Pertama kita buat sebuah variabel global bernama templates dan menginisiasi-nya dengan ParseFiles.

var templates = template.Must(template.ParseFiles("edit.html", "view.html"))

Fungsi template.Must adalah pembungkus yang akan panic bila ada eror, dan mengembalikan *Template bila tidak ada eror. Sebuah panic cocok dilakukan untuk kasus ini; jika template tidak dapat dibaca satu-satunya hal yang masuk akal dilakukan yaitu menghentikan program.

Fungsi ParseFiles menerima berapa pun argumen string yang merujuk ke berkas templat, dan membaca berkas tersebut menjadi templat yang diberi nama sesuai dengan nama berkas. Jika kita ingin menambahkan templat baru ke program, kita tinggal tambah nama berkas ke argumen pada pemanggilan ParseFiles.

Kita kemudian mengubah fungsi renderTemplate untuk memanggil method templates.ExecuteTemplate dengan nama templat yang sesuai:

func renderTemplate(w http.ResponseWriter, tmpl string, p *Page) {
	err := templates.ExecuteTemplate(w, tmpl+".html", p)
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
	}
}

Nama templat yaitu nama berkas templat itu sendiri, jadi kita harus menambahkan ".html" ke argument tmpl.

Validasi

Jika kita perhatikan, program kita ini memiliki celah sekuriti: pengguna bisa memberikan path apa pun untuk dibaca/ditulis di server. Untuk menghindari hal ini, kita dapat menulis fungsi untuk validasi judul yang dikirim dengan sebuah regular expression.

Pertama, tambahkan paket "regexp" ke daftar impor. Kemudian kita buat variabel global untuk menyimpan validasi path:

var validPath = regexp.MustCompile("^/(edit|save|view)/([a-zA-Z0-9]+)$")

Fungsi regexp.MustCompile akan mengurai dan mengompilasi regular expression, dan mengembalikan regexp.Regexp. MustCompile berbeda dari Compile karena ia akan panic jika ekspresi kompilasi gagal, sementara Compile mengembalikan sebuah error pada parameter kedua.

Sekarang mari kita tulis sebuah fungsi yang menggunakan ekspresi pada validPath untuk memvalidasi path dan mengekstrak judul halaman:

func getTitle(w http.ResponseWriter, r *http.Request) (string, error) {
	m := validPath.FindStringSubmatch(r.URL.Path)
	if m == nil {
		http.NotFound(w, r)
		return "", errors.New("invalid Page Title")
	}
	return m[2], nil // The title is the second subexpression.
}

Jika judul yang diberikan valid, maka ia akan dikembalikan bersama dengan nilai nil untuk error. Jika judul tidak valid, fungsi tersebut akan mengirim error "404 Not Found" ke koneksi HTTP klien, dan mengembalikan sebuah error ke yang memanggil. Untuk membuat error yang baru, kita harus mengimpor paket errors.

Mari kita gunakan getTitle pada setiap handler:

func viewHandler(w http.ResponseWriter, r *http.Request) {
	title, err := getTitle(w, r)
	if err != nil {
		return
	}
	p, err := loadPage(title)
	if err != nil {
		http.Redirect(w, r, "/edit/"+title, http.StatusFound)
		return
	}
	renderTemplate(w, "view", p)
}

func editHandler(w http.ResponseWriter, r *http.Request) {
	title, err := getTitle(w, r)
	if err != nil {
		return
	}
	p, err := loadPage(title)
	if err != nil {
		p = &Page{Title: title}
	}
	renderTemplate(w, "edit", p)
}

func saveHandler(w http.ResponseWriter, r *http.Request) {
	title, err := getTitle(w, r)
	if err != nil {
		return
	}
	body := r.FormValue("body")
	p := &Page{Title: title, Body: []byte(body)}
	err = p.save()
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
	http.Redirect(w, r, "/view/"+title, http.StatusFound)
}

Memperkenalkan fungsi dan closure

Menangkap kondisi eror di setiap handler mengakibatkan banyaknya kode yang sama. Bagaimana jika seandainya kita dapat membungkus setiap handler tersebut dalam sebuah fungsi yang melakukan validasi dan melakukan pemeriksaan eror? Fungsi pada Go memiliki fungsionalitas abstraksi yang dapat membantu kita.

Pertama, kita tulis ulang definisi fungsi dari setiap handler untuk menerima string judul:

func viewHandler(w http.ResponseWriter, r *http.Request, title string)
func editHandler(w http.ResponseWriter, r *http.Request, title string)
func saveHandler(w http.ResponseWriter, r *http.Request, title string)

Selanjutnya kita definisikan sebuah fungsi pembungkus yang menerima sebuah fungsi dari tipe di atas, dan mengembalikan sebuah fungsi bertipe http.HandlerFunc (cocok untuk dikirim ke fungsi http.HandleFunc):

func makeHandler(fn func (http.ResponseWriter, *http.Request, string)) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		// Di sini kita akan mengekstrak judul halaman dari Request, dan
		// memanggil fungsi `fn`.
	}
}

Fungsi yang dikembalikan disebut dengan closure karena ia membungkus nilai yang didefinisikan di luar fungsi tersebut. Dalam kasus ini, variabel fn (satu-satunya argument pada fungsi makeHandler) dibungkus oleh closure. Variabel fn akan menjadi satu-satunya fungsi yang menangani penyimpanan, penyuntingan, dan melihat halaman wiki.

Selanjutnya kita bisa gunakan kode dari getTitle dan menggunakannya di sini (dengan sedikit modifikasi):

func makeHandler(fn func(http.ResponseWriter, *http.Request, string)) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		m := validPath.FindStringSubmatch(r.URL.Path)
		if m == nil {
			http.NotFound(w, r)
			return
		}
		fn(w, r, m[2])
	}
}

Closure yang dikembalikan oleh makeHandler adalah sebuah fungsi yang menerima http.ResponseWriter dan http.Request (dengan kata lain, sebuah http.HandlerFunc). Closure tersebut mengekstrak judul berdasarkan path, dan memvalidasinya dengan regexp validPath. Jika judul yang diterima tidak valid, sebuah eror akan ditulis ke ResponseWriter menggunakan fungsi http.NotFound. Jika judul valid, fungsi fn akan dipanggil dengan ResponseWriter, Request, dan judul sebagai argument.

Sekarang kita dapat membungkus fungsi-fungsi handler dengan makeHandler dari dalam main, sebelum diregistrasi lewat paket http:

func main() {
	http.HandleFunc("/view/", makeHandler(viewHandler))
	http.HandleFunc("/edit/", makeHandler(editHandler))
	http.HandleFunc("/save/", makeHandler(saveHandler))

	log.Fatal(http.ListenAndServe(":8080", nil))
}

Terakhir, kita hapus pemanggilan ke getTitle dari fungsi-fungsi handler, membuatnya lebih sederhana:

func viewHandler(w http.ResponseWriter, r *http.Request, title string) {
	p, err := loadPage(title)
	if err != nil {
		http.Redirect(w, r, "/edit/"+title, http.StatusFound)
		return
	}
	renderTemplate(w, "view", p)
}

func editHandler(w http.ResponseWriter, r *http.Request, title string) {
	p, err := loadPage(title)
	if err != nil {
		p = &Page{Title: title}
	}
	renderTemplate(w, "edit", p)
}

func saveHandler(w http.ResponseWriter, r *http.Request, title string) {
	body := r.FormValue("body")
	p := &Page{Title: title, Body: []byte(body)}
	err := p.save()
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
	http.Redirect(w, r, "/view/"+title, http.StatusFound)
}

Cobalah!

Kompilasi ulang kode, dan jalankan aplikasi:

$ go build wiki.go
$ ./wiki

Membuka halaman berikut localhost:8080/view/ANewPage seharusnya memperlihatkan halaman penyuntingan. Anda seharusnya bisa menginput teks, klik 'Save’, dan dialihkan ke halaman yang baru dibuat.

Pekerjaan tambahan

Berikut beberapa pekerjaan yang bisa Anda tambahkan sendiri:

  • Menyimpan templat dalam tmpl/ dan halaman wiki dalam data/.

  • Membuat sebuah handler untuk mengalihkan halaman depan ke /view/FrontPage.

  • Mengembangkan halaman templat supaya menjadi HTML yang valid dan menambahkan beberapa aturan CSS.

  • Mengimplementasikan penautan antar-halaman dengan mengonversi teks [PageName] ke <a href="/view/PageName">PageName</a>. (petunjuk: Anda dapat menggunakan regexp.ReplaceAllFunc untuk melakukan hal ini).