go語言編寫Web程序


 

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 {
  	title	string
  	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. 驗證

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

首先,添加"regexp"到導入列表。然后創建一個全局變量存儲我們的驗證正則表達式:

函數regexp.MustCompile解析並且編譯正則表達式,返回一個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達到這個效果)


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM