go語言編寫Web程式

來源:互聯網
上載者:User
 1. 簡介

這個例子涉及到的技術:

  • 建立一個資料類型,含有load和save函數
  • 基於http包建立web程式
  • 基於template包的html模板技術
  • 使用regexp包驗證使用者輸入
  • 使用閉包

假設讀者有以下知識:

  • 基本的編程經驗
  • web程式的基礎技術(HTTP, HTML)
  • UNIX 命令列
2. 開始

首先,要有一個Linux, OS X, or FreeBSD系統,可以運行go程式。如果沒有的話,可以安裝一個虛擬機器(如VirtualBox)或者 Virtual Private Server。

安裝Go環境: (請參考 Installation Instructions).

建立一個新的目錄,並且進入該目錄:

  $ mkdir ~/gowiki  $ cd ~/gowiki

建立一個wiki.go檔案,用你喜歡的編輯器開啟,然後添加以下代碼:

  package main    import (  "fmt"  "io/ioutil"  "os"  )

我們從go的標準庫匯入fmt, ioutil 和 os包。 以後,當實現其他功能時,我們會根據需要匯入更多包。

3. 資料結構

我們先定義一個結構類型,用於儲存資料。wiki系統由一組互聯的wiki頁面組成,每個wiki頁麵包含內容和標題。我們定義wiki頁面為結構page, 如下:

  type page struct {  titlestring  body[]byte  }

類型[]byte表示一個byte slice。(參考Effective Go瞭解slices的更多資訊) 成員body之所以定義為[]byte而不是string類型,是因為[]byte可以直接使用io包的功能。

結構體page描述了一個頁面在記憶體中的儲存方式。但是,如果要將資料儲存到磁碟的話,還需要給page類型增加save方法:

  func (p *page) save() os.Error {  filename := p.title + ".txt"  return ioutil.WriteFile(filename, p.body, 0600)  }

類型方法的簽名可以這樣解讀:“save為page類型的方法,方法的調用者為page類型的指標變數p。該成員函數沒有參數,傳回值為os.Error,表示錯誤資訊。”

該方法會將page結構的body部分儲存到文字檔中。為了簡單,我們用title作為文字檔的名字。

方法save的傳回值類型為os.Error,對應WriteFile(標準庫函數,將byte slice寫到檔案中)的傳回值。通過返回os.Error值,可以判斷髮生錯誤的類型。如果沒有錯誤,那麼返回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根據頁面標題從對應檔案讀取頁面的內容,並且構造一個新的 page變數——對應一個頁面。

go中函數(以及成員方法)可以返回多個值。標準庫中的io.ReadFile在返回[]byte的同時還返回os.Error類型的錯誤資訊。前面的代碼中我們用底線“_”丟棄了錯誤資訊。

但是ReadFile可能會發生錯誤,例如請求的檔案不存在。因此,我們給函數的傳回值增加一個錯誤資訊。

  func loadPage(title string) (*page, os.Error) {  filename := title + ".txt"  body, err := ioutil.ReadFile(filename)  if err != nil {  return nil, err  }  return &page{title: title, body: body}, nil  }

現在調用者可以檢測第二個傳回值,如果為nil就表示成功裝載頁面。否則,調用者可以得到一個os.Error對象。(關於錯誤的更多資訊可以參考os package documentation)

現在,我們有了一個簡單的資料結構,可以儲存到檔案中,或者從檔案載入。我們建立一個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,並且將p2的值列印到 螢幕。

可以用類似以下命令編譯運行程式:

  $ 8g wiki.go  $ 8l wiki.8  $ ./8.out  This is a sample page.

(命令8g和8l對應GOARCH=386。如果是amd64系統,可以用6g和6l)

點擊這裡查看我們當前的代碼。

4. 使用http包

下面是一個完整的web server例子:

  package main    import (  "fmt"  "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設定所有對根目錄請求的處理函數為handler。

然後調用http.ListenAndServe,在8080連接埠開始監聽(第二個參數暫時可以忽略)。然後程式將阻塞,直到退出。

函數handler為http.HandlerFunc類型,它包含http.Conn和http.Request兩個類型的參數。

其中http.Conn對應伺服器的http串連,我們可以通過它向用戶端發送資料。

類型為http.Request的參數對應一個用戶端請求。其中r.URL.Path 為請求的地址,它是一個string類型變數。我們用[1:]在Path上建立 一個slice,對應"/"之後的路徑名。

啟動該程式後,通過瀏覽器訪問以下地址:

  http://localhost:8080/monkeys

會看到以下輸出內容:

  Hi there, I love monkeys!
5. 基於http提供wiki頁面

要使用http包,先將其匯入:

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

然後建立一個用於瀏覽wiki的函數:

  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(請求URL的path部分)中解析頁面標題。全域常量lenPath儲存"/view/"的長度,它是請求路徑的首碼部分。Path總是以"/view/"開頭,去掉前面的6個字元就可以得到頁面標題。

然後載入頁面資料,格式化為簡單的HTML字串,寫到c中,c是一個http.Conn類型的參數。

注意這裡使用底線“_”忽略loadPage的os.Error傳回值。 這不是一種好的做法,此處是為了保持簡單。我們將在後面考慮這個問題。

為了使用這個處理函數(handler),我們建立一個main函數。它使用viewHandler初始化http,把所有以/view/開頭的請求轉寄給viewHandler處理。

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

點擊這裡查看我們當前的代碼。

讓我們建立一些頁面資料(例如as test.txt),編譯,運行。

  $ echo "Hello world" > test.txt  $ 8g wiki.go  $ 8l wiki.8  $ ./8.out

當伺服器啟動並執行時候,訪問http://localhost:8080/view/test將顯示一個頁面,標題為“test”,內容為“Hello world”。

6. 編輯頁面

編輯功能是wiki不可缺少的。現在,我們建立兩個新的處理函數(handler):editHandler顯示"edit page"表單(form),saveHandler儲存表單(form)中的資料。

首先,將他們添加到main()函數中:

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

函數editHandler載入頁面(或者,如果頁面不存在,建立一個空page 結構)並且顯示為一個HTML表單(form)。

  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非常醜陋。當然,我們有更好的辦法。

7. template包

template包是GO語言標準庫的一個部分。我們使用template將HTML存放在一個單獨的檔案中,可以更改編輯頁面的布局而不用修改相關的GO代碼。

首先,我們必須將template添加到匯入列表:

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

建立一個包含HTML表單的模板檔案。開啟一個名為edit.html的新檔案,添加下面的行:

  <h1>Editing {title}</h1>    <form action="/save/{title}" method="POST">  <div><textarea name="body" rows="20" cols="80">{body|html}</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.ParseFile("edit.html", nil)  t.Execute(p, w)  }

函數template.ParseFile讀取edit.html的內容,返回*template.Template類型的資料。

方法t.Execute用p.title和p.body的值替換模板中所有的{title}和{body},並且把結果寫到http.Conn。

注意,在上面的模板中我們使用{body|html}。|html部分請求模板引擎在輸出body的值之前,先將它傳到html格式化器(formatter),轉義HTML字元(比如用>替換>)。 這樣做,可以阻止使用者資料破壞表單HTML。

既然我們刪除了fmt.Sprintf語句,我們可以刪除匯入列表中的"fmt"。

使用模板技術,我們可以為viewHandler建立一個模板,命名為view.html。

  <h1>{title}</h1>    <p>[<a href="/edit/{title}">edit</a>]</p>    <div>{body}</div>

修改viewHandler:

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

注意,在兩個處理函數(handler)中使用了幾乎完全相同的模板處理代碼,我們可以把模板處理代碼寫成一個單獨的函數,以消除重複。

  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.ParseFile(tmpl+".html", nil)  t.Execute(p, w)  }

現在,處理函數(handler)代碼更短、更加簡單。

8. 處理不存在的頁面

當你訪問/view/APageThatDoesntExist的時候會發生什嗎?程式將會崩潰。因為我們忽略了loadPage返回的錯誤。請求頁不存在的時候,應該重新導向用戶端到編輯頁,這樣新的頁面將會建立。

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

函數http.Redirect添加HTTP狀態代碼http.StatusFound (302)和前序Location到HTTP響應。

9. 儲存頁面

函數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中)和表單中唯一的欄位,body,儲存在一個新的page中。然後調用save()方法將資料寫到檔案中,並且將客戶重新導向到/view/頁面。

FormValue傳回值的類型是string,在將它添加到page結構前,我們必須將其轉換為[]byte類型。我們使用[]byte(body)執行轉換。

10. 錯誤處理

在我們的程式中,有幾個地方的錯誤被忽略了。這是一種很糟糕的方式,特別是在錯誤發生後,程式會崩潰。更好的方案是處理錯誤並返回錯誤訊息給使用者。這樣做,當錯誤發生後,伺服器可以繼續運行,使用者也會得到通知。

首先,我們處理renderTemplate中的錯誤:

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

函數http.Error發送一個特定的HTTP響應碼(在這裡表示“Internal Server Error”)和錯誤訊息。

現在,讓我們修複saveHandler:

  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.String(), http.StatusInternalServerError)  return  }  http.Redirect(w, r, "/view/"+title, http.StatusFound)  }

p.save()中發生的任何錯誤都將報告給使用者。

11. 模板緩衝

代碼中有一個低效率的地方:每次顯示一個頁面,renderTemplate都要調用ParseFile。更好的做法是在程式初始化的時候對每個模板調用ParseFile一次,將結果儲存為*Template類型的值,在以後使用。

首先,我們建立一個全域map,命名為templates。templates用於儲存*Template類型的值,使用string索引。

然後,我們建立一個init函數,init函數會在程式初始化的時候調用,在main函數之前。函數template.MustParseFile是ParseFile的一個封裝,它不返回錯誤碼,而是在錯誤發生的時候拋出(panic)一個錯誤。拋出錯誤(panic)在這裡是合適的,如果模板不能載入,程式唯一能做的有意義的事就是退出。

  func init() { for _, tmpl := range []string{"edit", "view"} { templates[tmpl] = template.MustParseFile(tmpl+".html", nil) } }

使用帶range語句的for逐一查看一個常量數組中的每一個元素,這個常量數組中包含了我們想要載入的所有模板的名稱。如果我們想要添加更多的模板,只要把模板名稱添加的數組中就可以了。

修改renderTemplate函數,在templates中相應的Template上調用Execute方法:

  func renderTemplate(w http.ResponseWriter, tmpl string, p *page) {  err := templates[tmpl].Execute(p, w)  if err != nil {  http.Error(w, err.String(), http.StatusInternalServerError)  }  }
12. 驗證

你可能已經發現,程式中有一個嚴重的安全性漏洞:使用者可以提供任意的路徑在伺服器上執行讀寫操作。為了消除這個問題,我們使用Regex驗證頁面的標題。

首先,添加"regexp"到匯入列表。然後建立一個全域變數儲存我們的驗證Regex:

函數regexp.MustCompile解析並且編譯Regex,返回一個regexp.Regexp對象。和template.MustParseFile類似,當運算式編譯錯誤時,MustCompile拋出一個錯誤,而Compile在它的第二個返回參數中返回一個os.Error。

現在,我們編寫一個函數,它從請求URL解析中解析頁面標題,並且使用titleValidator進行驗證:

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

如果標題有效,它返回一個nil錯誤值。如果無效,它寫"404 Not Found"錯誤到HTTP串連中,並且返回一個錯誤對象。

修改所有的處理函數,使用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.String(), http.StatusInternalServerError)  return  }  http.Redirect(w, r, "/view/"+title, http.StatusFound)  }
13. 函數文本和閉包

處理函數(handler)中捕捉錯誤是一些類似的重複代碼。如果我們想將捕捉錯誤的代碼封裝成一個函數,應該怎麼做?GO的函數文本提供了強大的抽象能力,可以幫我們做到這點。

首先,我們重寫每個處理函數的定義,讓它們接受標題字串:

定義一個封裝函數,接受上面定義的函數類型,返回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是我們的處理函數,save、edit、或view。

我們可以把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.Conn和http.Request(因此,它是http.HandlerFunc)。閉包從請求路徑解析title,使用titleValidator驗證標題。如果title無效,使用函數http.NotFound將錯誤寫到Conn。如果title有效,封裝的處理函數fn將被調用,參數為Conn, Request, 和title。

在main函數中,我們用makeHandler封裝所有處理函數:

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

最後,我們可以刪除處理函數中的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.String(), http.StatusInternalServerError)  return  }  http.Redirect(w, r, "/view/"+title, http.StatusFound)  }
14. 試試!

點擊這裡查看最終的代碼

重新編譯代碼,運行程式:

  $ 8g wiki.go  $ 8l wiki.8  $ ./8.out

訪問http://localhost:8080/view/ANewPage將會出現一個編輯表單。你可以輸入一些文版,點擊“Save”,重新導向到新的頁面。

15. 其他任務

這裡有一些簡單的任務,你可以自己解決:

  • 把模板檔案存放在tmpl/目錄,頁面資料存放在data/目錄。
  • 增加一個處理函數(handler),將對根目錄的請求重新導向到/view/FrontPage。
  • 修飾頁面模板,使其成為有效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.