چگونگی نوشتن وب اپلیکیشن با استفاده از زبان گو در این درس آموزش داده میشود.
مواردی که در این آموزش پوشش داده شدهاند:
-
ایجاد یک ساختار داده (Data Structure) با متدهای بارگذاری (Load) و ذخیره (Save)
-
استفاده از پکیج
net/httpبرای ساخت برنامههای وب -
استفاده از پکیج
html/templateجهت پردازش قالبهای HTML -
استفاده از پکیج
regexpبرای اعتبارسنجی ورودیهای کاربر -
استفاده از کلوژرها (Closures)
پیشنیازها و دانش فرضشده:
-
تجربه برنامهنویسی
-
درک فناوریهای پایه وب (HTTP, HTML)
-
آشنایی نسبی با خط فرمان (Command-line) در محیطهای UNIX یا DOS
شروع کار
در حال حاضر برای اجرای زبان Go به سیستمی با یکی از سیستمعاملهای FreeBSD، لینوکس، macOS یا ویندوز نیاز دارید. در این آموزش برای نشان دادن خط فرمان از علامت $ استفاده میکنیم.
۱. زبان Go را نصب کنید (به راهنمای نصب یا Installation Instructions مراجعه کنید). ۲. یک پوشه جدید برای این آموزش در داخل GOPATH خود بسازید و وارد آن شوید:
$ mkdir gowiki
$ cd gowiki
۳. فایلی به نام wiki.go ایجاد کنید، آن را در ویرایشگر متن دلخواه خود باز کنید و خطوط زیر را در آن قرار دهید:
package main
import (
"fmt"
"os"
)
ما پکیجهای fmt و os را از کتابخانه استاندارد Go وارد (Import) میکنیم. بعدها با پیادهسازی قابلیتهای بیشتر، پکیجهای دیگری را نیز به این بخش اضافه خواهیم کرد.
ساختارهای داده (Data Structures)
بیایید کار را با تعریف ساختارهای داده شروع کنیم. یک ویکی شامل مجموعهای از صفحات بههمپیوسته است که هر کدام دارای یک عنوان (Title) و یک بدنه یا محتوا (Body) هستند. در اینجا Page را بهعنوان یک struct با دو فیلد برای نگهداری عنوان و بدنه تعریف میکنیم:
type Page struct {
Title string
Body []byte
}
نوع دادهای []byte به معنای «اسلایس بایت» (Byte Slice) است (برای اطلاعات بیشتر درباره اسلایسها به مبحث Slices: usage and internals مراجعه کنید). عنصر Body از نوع []byte در نظر گرفته شده است، زیرا کتابخانههای مربوط به ورودی/خروجی (io) که در ادامه استفاده خواهیم کرد، خروجی یا ورودی را در این قالب انتظار دارند.
ساختار Page نحوه ذخیرهسازی دادههای صفحه در حافظه را توصیف میکند. اما ذخیرهسازی پایدار (Persistent Storage) روی دیسک چطور؟ با ایجاد یک متد save روی Page میتوانیم این مسئله را حل کنیم:
func (p *Page) save() error {
filename := p.Title + ".txt"
return os.WriteFile(filename, p.Body, 0600)
}
امضای (Signature) این متد به این شکل خوانده میشود: "این یک متد به نام save است که گیرنده (Receiver) آن p (اشارهگری به Page) است. این متد هیچ پارامتری دریافت نمیکند و مقداری از نوع error بازمیگرداند."
این متد محتوای Body صفحه را در یک فایل متنی ذخیره میکند. برای سادگی، از Title بهعنوان نام فایل استفاده میکنیم.
متد save یک مقدار خطا (error) برمیگرداند؛ زیرا این نوع بازگشتی تابع WriteFile است (تابعی از کتابخانه استاندارد که یک اسلایس بایت را در فایل مینویسد). متد save خطا را برمیگرداند تا اگر حین نوشتن فایل مشکلی پیش آمد، برنامه بتواند آن را مدیریت کند. اگر همه چیز به خوبی پیش برود، Page.save() مقدار nil را برمیگرداند (مقدار صفر برای اشارهگرها، اینترفیسها و برخی انواع دیگر).
عدد مبنای هشت (Octal) 0600 که بهعنوان پارامتر سوم به WriteFile پاس داده میشود، نشان میدهد که فایل باید تنها با دسترسی خواندن و نوشتن برای کاربر فعلی ایجاد شود (برای جزئیات بیشتر به راهنمای یونیکس open(2) مراجعه کنید).
علاوه بر ذخیره صفحات، مسلماً به بارگذاری (Load) آنها نیز نیاز داریم:
func loadPage(title string) *Page {
filename := title + ".txt"
body, _ := os.ReadFile(filename)
return &Page{Title: title, Body: body}
}
تابع loadPage نام فایل را از پارامتر title میسازد، محتوای فایل را در متغیری جدید به نام body میخواند و در نهایت اشارهگری به یک نمونه از Page که با مقادیر درستِ عنوان و بدنه ساخته شده است را برمیگرداند.
توابع در Go میتوانند چندین مقدار را بازگردانند. تابع کتابخانه استاندارد os.ReadFile مقادیر []byte و error را برمیگرداند. در تابع loadPage فعلاً خطای خروجی مدیریت نشده است؛ «شناسگر خالی» (Blank Identifier) که با علامت زیرخط (_) نشان داده میشود، برای دور ریختن مقدار خطای بازگشتی استفاده میشود (در واقع مقدار را به هیچکدام اختصاص نمیدهد).
اما اگر ReadFile با خطا مواجه شود چه اتفاقی میافتد؟ برای مثال، ممکن است فایل وجود نداشته باشد. ما نباید چنین خطاهایی را نادیده بگیریم. بیایید تابع را اصلاح کنیم تا *Page و error را با هم برگرداند:
func loadPage(title string) (*Page, error) {
filename := title + ".txt"
body, err := os.ReadFile(filename)
if err != nil {
return nil, err
}
return &Page{Title: title, Body: body}, nil
}
فراخوانندگان این تابع اکنون میتوانند پارامتر دوم را بررسی کنند؛ اگر مقدار آن nil باشد، یعنی صفحه با موفقیت بارگذاری شده است. در غیر این صورت، خطایی رخ داده است که فراخواننده میتواند آن را مدیریت کند.
در این مرحله ما یک ساختار داده ساده و قابلیت ذخیره و بارگذاری از روی فایل را داریم. بیایید یک تابع main برای تست کدهایی که نوشتهایم بنویسیم:
func main() {
p1 := &Page{Title: "TestPage", Body: []byte("This is a sample Page.")}
p1.save()
p2, _ := loadPage("TestPage")
fmt.Println(string(p2.Body))
}
پس از کامپایل و اجرای این کد، فایلی به نام TestPage.txt ایجاد میشود که حاوی محتوای p1 است. سپس فایل در ساختار داده p2 خوانده شده و عنصر Body آن روی صفحه چاپ میشود.
میتوانید برنامه را به این شکل کامپایل و اجرا کنید:
$ go build wiki.go
$ ./wiki
This is a sample Page.
💡 نکته: اگر از ویندوز استفاده میکنید، برای اجرای برنامه باید دستور
wikiرا بدون./تایپ کنید.
آشنایی با پکیج net/http (میانپرده)
در ادامه یک نمونه کاملاً کاربردی از یک وبسرور ساده را مشاهده میکنید:
//go: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))
}
تابع main با فراخوانی http.HandleFunc شروع میشود که به پکیج http میگوید تمام درخواستهای ارسالی به ریشه وب ("/") را با تابع handler مدیریت کند.
سپس تابع http.ListenAndServe را فراخوانی میکند و مشخص میکند که سرور باید روی پورت 8080 روی هر اینترفیسی گوش دهد (":8080"). (فعلاً نگران پارامتر دوم آن یعنی nil نباشید). این تابع تا زمانی که برنامه خاتمه یابد، مسدودکننده (Blocking) باقی میماند.
تابع ListenAndServe همیشه یک خطا برمیگرداند، زیرا تنها زمانی بازمیگردد که یک خطای غیرمنتظره رخ دهد. برای ثبت و لاگ کردن آن خطا، فراخوانی تابع را با log.Fatal پوشش میدهیم.
تابع handler از نوع http.HandlerFunc است. این تابع دو آرگومان به نامهای http.ResponseWriter و http.Request میپذیرد.
-
مقدار
http.ResponseWriterپاسخ سرور HTTP را مونتاژ میکند؛ با نوشتن در آن، دادهها را به کلاینت HTTP ارسال میکنیم. -
مقدار
http.Requestساختار دادهای است که درخواست HTTP کلاینت را نشان میدهد. عبارتr.URL.Pathمؤلفه مسیر (Path) در URL درخواست است. عبارت[1:]در انتها به این معناست: "یک زیر-اسلایس از Path از کاراکتر اول تا انتها بساز" که باعث حذف اسلش پیشتاز (/) از ابتدای نام مسیر میشود.
اگر این برنامه را اجرا کنید و به آدرس زیر بروید: http://localhost:8080/monkeys برنامه صفحهای شامل متن زیر را نمایش خواهد داد: Hi there, I love monkeys!
استفاده از پکیج net/http برای سرویسدهی به صفحات ویکی
برای استفاده از پکیج net/http، باید آن را Import کنید:
import (
"fmt"
"os"
"log"
"net/http"
)
بیایید یک کنترلکننده (Handler) به نام viewHandler بسازیم که به کاربران اجازه دهد یک صفحه ویکی را مشاهده کنند. این تابع URLهای پیشوندشده با "/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)
}
مجدداً به استفاده از _ برای نادیده گرفتن خطای بازگشتی از loadPage توجه کنید. این کار در اینجا برای سادگی انجام شده و بهطور کلی یک شیوه بد (Bad Practice) محسوب میشود که بعداً به آن خواهیم پرداخت.
ابتدا، این تابع عنوان صفحه را از r.URL.Path استخراج میکند. مسیر مجدداً با [len("/view/"):] اسلایس میشود تا مؤلفه پیشتاز "/view/" حذف شود؛ زیرا این بخش هرگز جزئی از عنوان صفحه نیست.
سپس تابع، دادههای صفحه را بارگذاری کرده، آن را با چند تگ ساده HTML قالببندی میکند و در w (یعنی http.ResponseWriter) مینویسد.
برای استفاده از این Handler، تابع main خود را بازنویسی میکنیم تا پکیج HTTP را مقداردهی اولیه کرده و از viewHandler برای مدیریت درخواستهای زیر مسیر /view/ استفاده کند:
func main() {
http.HandleFunc("/view/", viewHandler)
log.Fatal(http.ListenAndServe(":8080", nil))
}
بیایید مقداری داده برای صفحه ایجاد کنیم (بهعنوان test.txt)، کد خود را کامپایل کرده و تلاش کنیم یک صفحه ویکی را سرویس دهیم. فایل test.txt را در ویرایشگر خود باز کنید و رشته Hello world (بدون نقلقول) را در آن ذخیره کنید.
$ go build wiki.go
$ ./wiki
با اجرای این وبسرور، مراجعه به آدرس http://localhost:8080/view/test باید صفحهای با عنوان test و حاوی عبارت Hello world را نمایش دهد.
ویرایش صفحات (Editing Pages)
یک ویکی بدون قابلیت ویرایش صفحات، ویکی نیست! بیایید دو Handler جدید بسازیم: یکی به نام editHandler برای نمایش فرم «ویرایش صفحه» و دیگری به نام saveHandler برای ذخیره دادههای وارد شده از طریق فرم.
ابتدا آنها را به main() اضافه میکنیم:
func main() {
http.HandleFunc("/view/", viewHandler)
http.HandleFunc("/edit/", editHandler)
http.HandleFunc("/save/", saveHandler)
log.Fatal(http.ListenAndServe(":8080", nil))
}
تابع editHandler صفحه را بارگذاری میکند (یا اگر وجود نداشته باشد، یک ساختار دادهی Page خالی میسازد) و یک فرم 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)
}
این تابع به خوبی کار خواهد کرد، اما تمام آن کدهای سختکد شده (Hard-coded) HTML ظاهر زیبایی ندارند. مسلماً راه بهتری وجود دارد.
پکیج html/template
پکیج html/template بخشی از کتابخانه استاندارد Go است. ما میتوانیم از html/template برای نگهداری کدهای HTML در یک فایل جداگانه استفاده کنیم؛ این کار به ما اجازه میدهد بدون تغییر کدهای زیربنایی Go، ظاهر صفحه ویرایش را تغییر دهیم.
ابتدا باید html/template را به لیست پکیجهای واردشده اضافه کنیم. از آنجا که دیگر از fmt استفاده نخواهیم کرد، آن را حذف میکنیم.
import (
"html/template"
"os"
"net/http"
)
بیایید یک فایل قالب (Template) حاوی فرم HTML بسازیم. فایل جدیدی به نام edit.html ایجاد کنید و خطوط زیر را در آن قرار دهید:
<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>
حالا تابع editHandler را طوری تغییر دهید که بهجای 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}
}
t, _ := template.ParseFiles("edit.html")
t.Execute(w, p)
}
تابع template.ParseFiles محتوای edit.html را میخواند و یک *template.Template برمیگرداند.
متد t.Execute قالب را اجرا کرده و HTML تولیدشده را در http.ResponseWriter مینویسد. شناسگرهای نقطهدار .Title و .Body به ترتیب به مقادیر p.Title و p.Body اشاره میکنند.
دستورات قالب درون دو آکولاد ({{ }}) قرار میگیرند. دستور printf "%s" .Body یک فراخوانی تابع است که .Body را بهصورت یک رشته (String) و نه یک جریان بایت (Byte Stream) خروجی میدهد؛ دقیقاً مشابه عملکرد تابع fmt.Printf. پکیج html/template کمک میکند تا مطمئن شویم فقط کدهای HTML امن و صحیح توسط دستورات قالب تولید میشوند. برای مثال، این پکیج به طور خودکار هر علامت بزرگتری (>) را با > جایگزین میکند تا مطمئن شود دادههای ورودی کاربر فرم HTML را خراب نمیکنند.
از آنجا که اکنون با قالبها کار میکنیم، بیایید یک قالب هم برای viewHandler به نام view.html بسازیم:
<h1>{{.Title}}</h1>
<p>[<a href="/edit/{{.Title}}">edit</a>]</p>
<div>{{printf "%s" .Body}}</div>
و viewHandler را به شکل زیر بهروزرسانی کنید:
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)
}
مشاهده میکنید که ما تقریباً از کد قالببندی کاملاً یکسانی در هر دو Handler استفاده کردهایم. بیایید با منتقل کردن کدهای قالببندی به یک تابع مستقل، این تکرار را حذف کنیم:
func renderTemplate(w http.ResponseWriter, tmpl string, p *Page) {
t, _ := template.ParseFiles(tmpl + ".html")
t.Execute(w, p)
}
و سپس Handlerها را برای استفاده از این تابع اصلاح میکنیم:
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)
}
اگر ثبت (Registration) مربوط به save handler پیادهسازینشده را در تابع main کامنت کنیم، یکبار دیگر میتوانیم برنامهمان را کامپایل و تست کنیم.
مدیریت صفحات ناموجود
اگر به آدرس /view/APageThatDoesntExist بروید چه اتفاقی میافتد؟ صفحهای حاوی کدهای خام HTML را مشاهده خواهید کرد. دلیل این امر آن است که برنامه مقدار خطای برگشتی از loadPage را نادیده میگیرد و به تلاش برای پر کردن قالب بدون هیچ دادهای ادامه میدهد. در عوض، اگر صفحه درخواستی وجود نداشته باشد، سرور باید کلاینت را به صفحه ویرایش هدایت کند تا محتوای آن ساخته شود:
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)
}
تابع http.Redirect یک کد وضعیت HTTP برابر با http.StatusFound (۳۰۲) و یک هدر Location به پاسخ HTTP اضافه میکند.
ذخیره صفحات (Saving Pages)
تابع saveHandler ارسال فرمهای واقع در صفحات ویرایش را مدیریت خواهد کرد. پس از از حالت کامنت خارج کردن خط مربوط به آن در تابع main، این Handler را پیادهسازی میکنیم:
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)
}
عنوان صفحه (ارائهشده در URL) و تنها فیلد فرم یعنی Body، در یک Page جدید ذخیره میشوند. سپس متد save() برای نوشتن دادهها روی فایل فراخوانی میشود و کلاینت به صفحه /view/ هدایت میگردد.
مقدار بازگردانده شده توسط FormValue از نوع string است. پیش از اینکه بتوانیم آن را در ساختار Page قرار دهیم، باید این مقدار را به []byte تبدیل کنیم. برای این کار از عبارت []byte(body) استفاده میکنیم.
مدیریت خطا (Error Handling)
در برنامهی ما چند بخش وجود دارد که خطاها در آنها نادیده گرفته میشوند. این یک روش نامناسب است، بهویژه به این دلیل که هنگام بروز خطا، برنامه رفتار پیشبینینشدهای از خود نشان خواهد داد. راه حل بهتر این است که خطاها را مدیریت کرده و یک پیام خطا به کاربر برگردانیم. به این ترتیب اگر مشکلی پیش بیاید، سرور دقیقا همانطور که میخواهیم کار میکند و کاربر نیز مطلع خواهد شد.
ابتدا خطاها را در 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)
}
}
تابع http.Error یک کد پاسخ HTTP مشخص (در اینجا "Internal Server Error") و پیام خطا را ارسال میکند. تصمیمگیری برای قرار دادن این منطق در یک تابع جداگانه، هماکنون نتیجهخش خود را نشان میدهد.
اکنون 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)
}
هر خطایی که حین اجرای p.save() رخ دهد، به کاربر گزارش خواهد شد.
کش کردن قالبها (Template Caching)
در این کد یک ناکارآمدی (Inefficiency) وجود دارد: renderTemplate در هر بار رندر شدن صفحه، تابع ParseFiles را فراخوانی میکند. رویکرد بهتر این است که ParseFiles یکبار در زمان راهاندازی برنامه فراخوانی شود و تمام قالبها در یک شیء واحدِ *Template پارس شوند. سپس میتوانیم از متد ExecuteTemplate برای رندر کردن یک قالب خاص استفاده کنیم.
ابتدا یک متغیر سراسری به نام templates میسازیم و آن را با ParseFiles مقداردهی اولیه میکنیم:
var templates = template.Must(template.ParseFiles("edit.html", "view.html"))
تابع template.Must یک تابع کمکی (Wrapper) است که اگر مقدار خطایی غیر از nil دریافت کند، دچار پنیک (panic) میشود و در غیر این صورت همان *Template را بدون تغییر برمیگرداند. وقوع panic در اینجا کاملاً مناسب است؛ اگر قالبها نتوانند بارگذاری شوند، تنها کار عاقلانه خروج از برنامه است.
تابع ParseFiles هر تعداد آرگومان رشتهای که فایلهای قالب ما را شناسایی کنند میپذیرد و آن فایلها را به قالبهایی تبدیل میکند که بر اساس نام پایه فایل نامگذاری شدهاند. اگر قالبهای بیشتری به برنامهمان اضافه کنیم، نام آنها را به آرگومانهای فراخوانی ParseFiles اضافه خواهیم کرد.
سپس تابع renderTemplate را طوری تغییر میدهیم که متد templates.ExecuteTemplate را با نام قالب مناسب فراخوانی کند:
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)
}
}
توجه داشته باشید که نام قالب همان نام فایل قالب است، بنابراین باید پسوند ".html" را به آرگومان tmpl اضافه کنیم.
اعتبارسنجی (Validation)
همانطور که احتمالاً متوجه شدهاید، این برنامه یک نقص امنیتی جدی دارد: کاربر میتواند یک مسیر دلخواه برای خواندن یا نوشتن روی سرور ارسال کند. برای کاهش این ریسک، میتوانیم تابعی بنویسیم تا عنوان صفحه را با یک عبارت منظم (Regular Expression) اعتبارسنجی کند.
ابتدا regexp را به لیست پکیجهای Import شده اضافه کنید. سپس میتوانیم یک متغیر سراسری برای ذخیره عبارت اعتبارسنجی بسازیم:
var validPath = regexp.MustCompile("^/(edit|save|view)/([a-zA-Z0-9]+)$")
تابع regexp.MustCompile عبارت منظم را تجزیه و کامپایل کرده و یک regexp.Regexp برمیگرداند. تفاوت MustCompile با Compile در این است که اگر کامپایل عبارت با خطا مواجه شود، MustCompile دچار panic میشود، در حالی که Compile خطا را به عنوان دومین پارامتر برمیگرداند.
حالا تابعی بنویسیم که از عبارت validPath برای اعتبارسنجی مسیر و استخراج عنوان صفحه استفاده کند:
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 // عنوان صفحه، دومین زیرعبارت (Subexpression) است.
}
اگر عنوان معتبر باشد، همراه با یک خطای nil بازگردانده میشود. اگر عنوان نامعتبر باشد، تابع خطای «۴۰۴ پیدا نشد» را در اتصال HTTP مینویسد و یک خطا به Handler برمیگرداند. برای ساخت یک خطای جدید، باید پکیج errors را Import کنیم.
بیایید فراخوانی getTitle را در هر یک از 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)
}
معرفی لترالهای تابع و کلوژرها (Function Literals and Closures)
گرفتن شرایط خطا در هر Handler کدهای تکرارشده زیادی ایجاد میکند. چه میشد اگر میتوانستیم هر یک از Handlerها را در تابعی بپیچیم (Wrap کنیم) که این اعتبارسنجی و بررسی خطا را انجام دهد؟ لترالهای تابع در Go ابزاری قدرتمند برای انتزاع قابلیتها فراهم میکنند که در اینجا به ما کمک میکنند.
ابتدا، تعریف تابع هر کدام از Handlerها را بازنویسی میکنیم تا یک رشتهی عنوان (Title) را هم بپذیرند:
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)
اکنون یک تابع Wrapper تعریف میکنیم که تابعی با امضای بالا را میگیرد و تابعی از نوع http.HandlerFunc برمیگرداند (مناسب برای ارسال به تابع http.HandleFunc):
func makeHandler(fn func (http.ResponseWriter, *http.Request, string)) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
// در اینجا عنوان صفحه را از Request استخراج میکنیم،
// و تابع دریافت شده یعنی 'fn' را فراخوانی میکنیم.
}
}
تابع بازگردانده شده یک کلوژر (Closure) نامیده میشود؛ زیرا مقادیر تعریفشده در خارج از خود را محصور (Enclose) میکند. در اینجا، متغیر fn (تنها آرگومان ورودی makeHandler) توسط کلوژر محصور شده است. متغیر fn یکی از save، edit یا view handlerهای ما خواهد بود.
حالا میتوانیم کد getTitle را برداریم و در اینجا (با کمی تغییرات جزئی) استفاده کنیم:
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])
}
}
کلوژر بازگردانده شده توسط makeHandler تابعی است که یک http.ResponseWriter و http.Request میپذیرد (به عبارت دیگر، یک http.HandlerFunc است). این کلوژر عنوان را از مسیر درخواست استخراج کرده و آن را با عبارت منظم validPath اعتبارسنجی میکند. اگر عنوان نامعتبر باشد، با استفاده از تابع http.NotFound یک خطا در ResponseWriter نوشته میشود. اگر عنوان معتبر باشد، تابع Handler محصور شدهی fn با آرگومانهای ResponseWriter، Request و title فراخوانی خواهد شد.
اکنون میتوانیم توابع Handler را پیش از ثبت در پکیج http، در تابع main با makeHandler پوشش دهیم:
func main() {
http.HandleFunc("/view/", makeHandler(viewHandler))
http.HandleFunc("/edit/", makeHandler(editHandler))
http.HandleFunc("/save/", makeHandler(saveHandler))
log.Fatal(http.ListenAndServe(":8080", nil))
}
در نهایت، فراخوانیهای getTitle را از توابع Handler حذف میکنیم تا کدهای آنها بسیار سادهتر شوند:
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)
}
امتحان کنید!
کد را مجدداً کامپایل کرده و برنامه را اجرا کنید:
$ go build wiki.go
$ ./wiki
مراجعه به آدرس http://localhost:8080/view/ANewPage باید فرم ویرایش صفحه را به شما نمایش دهد. سپس باید بتوانید مقداری متن وارد کنید، روی دکمه 'Save' کلیک کنید و به صفحه تازه ایجاد شده هدایت شوید.
وظایف تکمیلی
در اینجا چند وظیفه ساده آورده شده است که میتوانید برای تمرین بیشتر روی آنها کار کنید:
-
ذخیره قالبها در پوشه
tmpl/و دادههای صفحه در پوشهdata/. -
افزودن یک Handler برای هدایت (Redirect) ریشه وب به
/view/FrontPage. -
زیباتر کردن قالبهای صفحه با تبدیل آنها به HTML معتبر و اضافه کردن برخی قوانین CSS.
-
پیادهسازی لینکدهی بین صفحات با تبدیل نمونههای
[PageName]به<a href="/view/PageName">PageName</a>. (نکته: میتوانید ازregexp.ReplaceAllFuncبرای این کار استفاده کنید).
این محتوا کاملا رایگان توسط تیم کدلپر ترجمه شده و در اختیار شما کاربران عزیز قرار گرفته است، هر گونه کپی برداری برای مقاصد غیر رایگان و بدون ذکر منبع، مورد پیگیری قانونی قرار میگیرد.
ترجمه شده از منبع: منبع مستندات