用Go語言開發Web程式[翻譯]

來源:互聯網
上載者:User
這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
  • 英文: http://golang.org/doc/articles/wiki/

簡介

本教程將討論:

  • 建立一個支援載入和儲存的資料結構
  • 使用 net/http 包來構建web應用程式
  • 使用 html/template 包來處理HTML模板
  • 使用 regexp 包來驗證使用者輸入
  • 使用閉包

基本知識:

  • 有一定的編程經驗
  • 瞭解基本的web技術(HTTP、HTML)
  • 一些UNIX/DOS命令列知識

開始

目前、你需要一個運行FreeBSD、Linux、OS X 或 Windows的機器。 我們將使用 $ 來代表命令提示字元。

安裝Go語言環境(參考 安裝說明)。

為本教程建立一個目錄,將建立目錄添加到GOPATH環境變數,然後命令列切換到建立目錄:

$ mkdir gowiki$ cd gowiki

建立一個名為wiki.go的源檔案,使用你喜歡的編輯器開啟,並添加以下代碼:

package mainimport (    "fmt"    "io/ioutil")

我們從標準庫匯入了fmt和ioutil包。 後面我們將實現更多的功能,到時候我們會添加更多的包到import聲明。

資料結構

我們現定義資料結構。一個wiki通常有一些列相互關聯的頁面組成,每個頁面有一個標題和一個主體(頁面的內容)。 在這裡,我們定的Page結構體包含標題和主體兩個成員。

type Page struct {    Title string    Body  []byte}

類型 []byte 表示“一個byte切片”。 (參見 Go切片:用法和本質) 我們將Body成員定義為 []byte 而不是 string 類型, 因為我們希望類型和 io 庫很好的配合,在後面會看到。

Page描述的頁面內容只是儲存在記憶體中。但是如何進行持久儲存呢? 我們可以為Page類型建立一個save方法:

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

方法的簽名這樣讀:“這是一個方法,名字叫save, 方法的接收者p是一個指向Page類型結構體的指標。 方法沒有參數,但有一個error類型的傳回值。”

該方法會將Page的Body成員的值儲存到一個文字檔。 為了簡化,我們使用Title成員的值作為檔案的名字。

save方法返回的error值和WriteFile函數的傳回型別 一致(將byte切片寫入檔案的標準庫函數)。程式可以通過save方法返回的 error值判斷寫檔案時是否遇到錯誤。如果寫檔案一切正常,Page.save() 將返回nil(對應指標、介面等類型的零值)。

傳遞給WriteFile函數的第三個參數0600是一個八進位整數面值, 表示新建立的檔案只對目前使用者是讀寫權限。(更多資訊請參考Unix手冊 open(2))

除了儲存頁面,我們還需要載入頁面:

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

函數loadPage從title參數構造檔案名稱,然後讀取檔案的內容到 新的變數body,最後返回兩個值:一個指向由title和body構造的 Page面值並且錯誤傳回值為nil。

函數可以返回多個值。標準庫函數io.ReadFile返回[]byte和error。 在loadPage函數中,錯誤資訊被丟失了;“空白標識符”所代表的底線(_) 符號用於扔掉錯誤傳回值(本質上沒喲分配任何值)。

但是如果ReadFile遇到錯誤怎麼辦?對於這個例子,檔案可能還不存在。我們不能忽略 類似的錯誤。我們修改函數返回*Page和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}

這個函數的調用者可以檢測第二個返回參數;如果是nil表示成功載入頁面。否則, error可以被調用者截獲(更多資訊請參考語言規範)。

現在我們有了一個簡單的資料結構,並且可以儲存到檔案和從檔案載入頁面。讓我們寫一個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$ ./wikiThis is a sample page.

(如果是使用Windows系統則不需要“wiki”前面的“./”。)

點擊這裡瀏覽完整代碼。

瞭解net/http包(插曲)

這裡是一個簡要Web伺服器的完整代碼:

package mainimport (    "fmt"    "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)    http.ListenAndServe(":8080", nil)}

main函數開始調用http.HandleFunc,告訴http包用handler函數處理所以針對跟目錄的訪問請求(“/“)。

然後調用http.ListenAndServe,指定監聽連接埠為8080(“:8080”)。 (目前先忽略第二個參數nil。)這個函數會阻塞直到程式終止。

函數handler的類型是http.HandlerFunc。它的參數是 一個http.ResponseWriter和一個http.Request

參數http.ResponseWriter匯總HTTP伺服器的響應;向它寫入的資料會發送 到HTTP客服端。

參數http.Request是用戶端請求資料對應的資料結構。 r.URL.Path表示用戶端請求的URL地址。後面的[1:]含義是 “從Path的第一個字元到 末尾建立一個子切片。” 這樣可以忽略URL路徑中的開始的“/”字元。

如果你運行程式並訪問一些URL地址:

http://localhost:8080/monkeys

程式會返回一個包含以下內容的頁面:

Hi there, I love monkeys!

基於net/http包提供wiki頁面

使用前需要匯入net/http包:

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

然後我們建立一個viewHandler函數,用於處理瀏覽wiki頁面。它會處理所有以”/view/“為首碼的URL地址。

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

首先,該函數從r.URL.Path中取出要瀏覽頁面的標題。 全域常量lenPath是URL首碼”/view/“的長度。 Path的切片[lenPath:]用於忽略前面的6個字元。 這是因為URL地址是以”/view/“為首碼,它們不是頁面標題的組成部分。

接著載入頁面資料,然後格式化為一個簡單的HTML頁面,寫入到http.ResponseWriter類型的w參數。

這裡又一次使用了_來忽略loadPage返回的錯誤error。 這裡只是為了簡化代碼,它並不是好的編程實踐。稍後我們會繼續完善這個部分。

要使用這個函數,我們需要修改main函數中的http初始化代碼, 使用viewHandler函數處理對應/view/地址的請求。

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

點擊這裡瀏覽完整代碼。

我們建立一些測試頁面(例如test.txt),然後嘗試提供一個wiki頁面:

使用編輯器開啟test.txt檔案,輸入“Hello world”內容並儲存(忽略雙引號)。

$ go build wiki.go$ ./wiki

如果是使用Windows系統則不需要“wiki”前面的“./”。

啟動web伺服器後,瀏覽http://localhost:8080/view/test 將顯示一個標題為“test”內容為“Hello world”的頁面。

編輯頁面

沒有編輯能力的wiki就不是真正的wiki了。我們繼續建立了兩個函數: 一個editHandler用於顯示編輯頁面的介面,另一個saveHandler 用於儲存編輯後的頁面內容。

我們先將它們加入到main()函數:

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

函數editHandler載入頁面,然後顯示一個HTML編輯頁面。

func editHandler(w http.ResponseWriter, r *http.Request) {    title := r.URL.Path[lenPath:]    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)}

這個函數只是可工作,但是那些HTML相關的代碼比較醜陋。 當然,還有更好的實現方式。

使用html/template包

html/template是標準庫中的包。我們使用html/template 包可以將HTML代碼分離到一個檔案,然後我們可以在不改變底層代碼前提下調整和完善編輯頁面。

首先,我們匯入html/template包。現在我們已經不再使用fmt包了, 因此需要刪除它。

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

我們需要為編輯頁面建立一個模板檔案。建立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[lenPath:]    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.Titlep.Body替換。

模板的驅動語句是被雙花括弧包括的部分. printf "%s" .Body表示將.Body輸出位字串
而不是位元組串, 類似fmt.Printf函數的效果. html/template可以保證輸出有效HTML字串,
對於(>)之類的特殊符號會自動替換為>等對應編碼, 保證不會破壞原先的HTML結構.

需要注意的是我們移除了fmt.Fprintf語句, 因此也移除了"fmt"包的匯入語句.

現在我們已經是基於模板方式的, 可以針對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[lenPath:]    p, _ := loadPage(title)    t, _ := template.ParseFiles("view.html")    t.Execute(w, p)}

觀察可以發現前面是否模板的方式非常相似. 因此我們將模板獨立大一個函數:

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

現在的處理函數更加清晰簡短.

處理不存在的頁面

如果訪問/view/APageThatDoesntExist會發生什麼情況? 程式會崩潰掉.
這是因為程式忽略了loadPage返回的錯誤資訊. 為了處理頁面不存在的情況,
程式會重新導向到一個新頁面的編輯頁面:

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

http.Redirect函數會添加http.StatusFound (302)狀態, 並且重新置放.

儲存頁面

函數saveHandler用於處理提交的表單.

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

頁面的標題(URL提供)和表單的內容將作為一個新頁面儲存.
調用save()方法將頁面寫到檔案, 然後重新導向到/view/頁面.

FormValue方法返回的傳回值是字串類型. 我們需要先轉換為[]byte, 然後填充到Page
結構體. 我們通過[]byte(body)語句做強制轉換.

錯誤處理

前面的代碼基本都是忽略了錯誤處理. 這不是好的處理方式, 因為發生錯誤的話會導致程式崩潰.
好的處理方式是截獲錯誤, 並給使用者顯示錯誤相關的資訊. 這樣即使發生錯誤, 伺服器也
可以正常運行, 使用者也可以收到錯誤提示資訊.

首先, 我先處理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函數返回一個具體的錯誤碼(這裡是屬於"伺服器錯誤"類型)和錯誤資訊.
看來剛才決定將模板處理獨立到一個函數是一個正確的決定.

下面是修複後的saveHandler:

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

p.save()時發生的錯誤資訊也將報告給使用者.

緩衝模板

前面的實現有一個效能缺陷: renderTemplate每次都會調用ParseFiles函數.
更好的最佳化思路是只在初始化的使用調用一次ParseFiles, 將全部要處理的模板
放到一個*Template中. 然後可以使用ExecuteTemplate渲染指定的模板.

首先建立一個名位templates全域變數, 然後用ParseFiles進行初始化.

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

template.Must只是一個簡便的封裝, 當傳遞非nil的錯誤是拋出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"尾碼名.

驗證

你可能以及發現, 這個程式有嚴重的安全缺陷: 使用者可以在伺服器上讀寫任意獨立路徑.
為了降低這種風險, 我們編寫一個函數以Regex的方式在驗證標題的合法性.

首先, 要匯入"regexp"包. 然後建立一個全域變數儲存用於驗證的Regex:

var titleValidator = regexp.MustCompile("^[a-zA-Z0-9]+$")

函數regexp.MustCompile將分析和編譯Regex, 返回regexp.Regexp.
MustCompileCompile有些不同, MustCompile遇到錯誤時會拋出panic異常,
Compile在遇到錯誤時通過第二個傳回值返回錯誤.

現在, 讓我們寫一個函數getTitle, 從請求的URL提取標題, 並且測試是否是有效運算式:

func getTitle(w http.ResponseWriter, r *http.Request) (title string, err error) {    title = r.URL.Path[lenPath:]    if !titleValidator.MatchString(title) {        http.NotFound(w, r)        err = errors.New("Invalid Page Title")    }    return}

如果標題是有效, 將返回nil錯誤值. 如果標題無效, 函數會輸出"404 Not Found"錯誤.

讓我們將getTitle應用到每個處理常式:

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

函數字面值和閉包

每個處理函數為了增加錯誤錯誤引入了很多重複的代碼. 如果是否可以將每個處理函數的
錯誤處理封裝到一個函數? Go語言的閉包函數提供的強有力的手段, 剛好可以用在這裡.

第一步, 我們重寫每個處理函數, 增加一個標題字串參數:

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)

然後, 我們頂一個封裝函數, 參數類型和前面定義的處理函數類型一致, 最後返回
http.HandlerFunc(用於適配http.HandleFunc的參數類型):

func makeHandler(fn func (http.ResponseWriter, *http.Request, string)) http.HandlerFunc {    return func(w http.ResponseWriter, r *http.Request) {        // Here we will extract the page title from the Request,        // and call the provided handler 'fn'    }}

這裡返回的函數就是一個閉包, 因為它引用了在它外部定義的局部變數的值.
在這裡情況下, 變數fn(makeHandler函數的唯一參數)被閉包函數持有.
fn變數將對應我們的儲存, 編輯 和 查看 的處理函數.

現在我們可以將getTitle的代碼移到這裡(還有一些細節的改動):

func makeHandler(fn func(http.ResponseWriter, *http.Request, string)) http.HandlerFunc {    return func(w http.ResponseWriter, r *http.Request) {        title := r.URL.Path[lenPath:]        if !titleValidator.MatchString(title) {            http.NotFound(w, r)            return        }        fn(w, r, title)    }}

makeHandler返回的是一個持有http.ResponseWriterhttp.Request參數的閉包函數
(其實就是http.HandlerFunc類型). 閉包函數提取頁面的標題, 並通過TitleValidator驗證
標題是否符合Regex. 如果是無效的標題, 那麼將使用http.NotFound輸出錯誤的響應.
如果是有效標題, 那麼fn處理函數將會被調用.

現在我們可以在main函數註冊的時候使用makeHandler封裝具體的處理函數:

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

Finally we remove the calls to getTitle from the handler functions, making them much simpler:

最後我們刪除處理函數對getTitle的調用, 處理代碼變得更加簡單:

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/目錄.
  • 增加一個根目錄的處理函數, 重新導向到/view/FrontPage.
  • Spruce up the page templates by making them valid HTML and adding some CSS rules.
  • 完善頁面模板, 讓它們輸出有效HTML, 並且添加一些CSS規則。
  • 通過將[PageName]轉換位<a href="/view/PageName">PageName</a>實現頁面之間的連結.
    (提示: 可以使用regexp.ReplaceAllFunc實現該功能)
相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.