《Go web程式設計》ChitChat論壇

weixin_34232744發表於2017-12-11

《Go web程式設計》ChitChat論壇

本文摘自《Go web程式設計》

京東購書:https://item.jd.com/12252845.html

{:--}本文主要內容

  • <span style=“font-family: Times New Roman,楷體_GB2312”>使用Go進行Web程式設計的方法
  • <span style=“font-family: Times New Roman,楷體_GB2312”>設計一個典型的Go Web應用
  • <span style=“font-family: Times New Roman,楷體_GB2312”>編寫一個完整的Go Web應用
  • <span style=“font-family: Times New Roman,楷體_GB2312”>瞭解Go Web應用的各個組成部分

上一章在末尾展示了一個非常簡單的Go Web應用,但是因為該應用只是一個Hello World程式,所以它實際上並沒有什麼用處。在本章中,我們將會構建一個簡單的網上論壇Web應用,這個應用同樣非常基礎,但是卻有用得多:它允許使用者登入到論壇裡面,然後在論壇上釋出新帖子,又或者回復其他使用者發表的帖子。

雖然本章介紹的內容無法讓你一下子就學會如何編寫一個非常成熟的Web應用,但這些內容將教會你如何組織和開發一個Web應用。在閱讀完這一章之後,你將進一步地瞭解到使用Go進行Web應用開發的相關方法。

如果你覺得本章介紹的內容難度較大,又或者你覺得本章展示的大量程式碼看起來讓人覺得膽戰心驚,那也不必過於擔心:本章之後的幾章將對本章介紹的內容做進一步的解釋,在閱讀完本章並繼續閱讀後續章節時,你將會對本章介紹的內容有更加深入的瞭解。

2.1 ChitChat簡介

網上論壇無處不在,它們是網際網路上最受歡迎的應用之一,與舊式的電子公告欄(BBS)、新聞組(Usenet)和電子郵件一脈相承。雅虎公司和Google公司的群組(Groups)都非常流行,雅虎報告稱,他們總共擁有1000萬個群組以及1.15億個群組成員,其中每個群組都擁有一個自己的論壇;而全球最具人氣的網上論壇之一——Gaia線上——則擁有2300萬註冊使用者以及接近230億張帖子,並且這些帖子的數量還在以每天上百萬張的速度持續增長。儘管現在出現了諸如Facebook這樣的社交網站,但論壇仍然是人們在網上進行交流時最為常用的手段之一。作為例子,圖2-1展示了GoogleGroups的樣子。

圖2-1 一個網上論壇示例:GoogleGroups裡面的Go程式語言論壇

從本質上來說,網上論壇就相當於一個任何人都可以通過發帖來進行對話的公告板,公告板上面可以包含已註冊使用者以及未註冊的匿名使用者。論壇上的對話稱為<span style=“font-family: Times New Roman,楷體_GB2312”>帖子(thread),一個帖子通常包含了作者想要討論的一個主題,而其他使用者則可以通過回覆這個帖子來參與對話。比較複雜的論壇一般都會按層級進行劃分,在這些論壇裡面,可能會有多個討論特定型別主題的子論壇存在。大多數論壇都會由一個或多個擁有特殊許可權的使用者進行管理,這些擁有特殊許可權的使用者被稱為<span style=“font-family: Times New Roman,楷體_GB2312”>版主(moderator)。

在本章中,我們將會開發一個名為ChitChat的簡易網上論壇。為了讓這個例子保持簡單,我們只會為ChitChat實現網上論壇的關鍵特性:在這個論壇裡面,使用者可以註冊賬號,並在登入之後發表新帖子又或者回復已有的帖子;未註冊使用者可以檢視帖子,但是無法發表帖子或是回覆帖子。現在,讓我們首先來思考一下如何設計ChitChat這個應用。

{關於本章展示的程式碼!}

跟本書的其他章節不一樣,因為篇幅的關係,本章並不會展示ChitChat論壇的所有實現程式碼,但你可以在GitHub頁面https://github.com/sausheong/gwp找到這些程式碼。如果你打算在閱讀本章的同時實際瞭解一下這個應用,那麼這些完整的程式碼應該會對你有所幫助。

2.2 應用設計

正如第1章所說,Web應用的一般工作流程是客戶端向伺服器傳送請求,然後伺服器對客戶端進行響應(如圖2-2所示),ChitChat應用的設計也遵循這一流程。

圖2-2 Web應用的一般工作流程,客戶端向伺服器傳送請求,然後等待接收響應

ChitChat的應用邏輯會被編碼到伺服器裡面。伺服器會向客戶端提供HTML頁面,並通過頁面的超連結向客戶端表明請求的格式以及被請求的資料,而客戶端則會在傳送請求時向伺服器提供相應的資料,如圖2-3所示。

圖2-3 HTTP請求的URL格式

請求的格式通常是由應用自行決定的,比如,ChitChat的請求使用的是以下格式:http://<伺服器名><處理器名>?<引數>

<span style=“font-family: Times New Roman,楷體_GB2312”>伺服器名(server name)是ChitChat伺服器的名字,而<span style=“font-family: Times New Roman,楷體_GB2312”>處理器名(handler name)則是被呼叫的處理器的名字。處理器的名字是按層級進行劃分的:位於名字最開頭是被呼叫模組的名字,而之後跟著的則是被呼叫子模組的名字,以此類推,位於處理器名字最末尾的則是子模組中負責處理請求的處理器。比如,對/thread/read這個處理器名字來說,thread是被呼叫的模組,而read則是這個模組中負責讀取帖子內容的處理器。

該應用的<span style=“font-family: Times New Roman,楷體_GB2312”>引數(parameter)會以URL查詢的形式傳遞給處理器,而處理器則會根據這些引數對請求進行處理。比如說,假設客戶端要向處理器傳遞帖子的唯一ID,那麼它可以將URL的引數部分設定成id=123,其中123就是帖子的唯一ID。

如果chitchat就是ChitChat伺服器的名字,那麼根據上面介紹的URL格式規則,客戶端傳送給ChitChat伺服器的URL可能會是這樣的:http://chitchat/thread/read?id=123。

當請求到達伺服器時,<span style=“font-family: Times New Roman,楷體_GB2312”>多路複用器(multiplexer)會對請求進行檢查,並將請求重定向至正確的處理器進行處理。處理器在接收到多路複用器轉發的請求之後,會從請求中取出相應的資訊,並根據這些資訊對請求進行處理。在請求處理完畢之後,處理器會將所得的資料傳遞給模板引擎,而模板引擎則會根據這些資料生成將要返回給客戶端的HTML,整個過程如圖2-4所示。

圖2-4 伺服器在典型Web應用中的工作流程

2.3 資料模型

絕大多數應用都需要以某種方式與資料打交道。對ChitChat來說,它的資料將被儲存到關係式資料庫PostgreSQL裡面,並通過SQL與之互動。

ChitChat的資料模型非常簡單,只包含4種資料結構,它們分別是:

  • <span style=“font-family: Times New Roman,楷體_GB2312”>User——表示論壇的使用者資訊;
  • <span style=“font-family: Times New Roman,楷體_GB2312”>Session——表示論壇使用者當前的登入會話;
  • <span style=“font-family: Times New Roman,楷體_GB2312”>Thread——表示論壇裡面的帖子,每一個帖子都記錄了多個論壇使用者之間的對話;
  • <span style=“font-family: Times New Roman,楷體_GB2312”>Post——表示使用者在帖子裡面新增的回覆。

以上這4種資料結構都會被對映到關聯式資料庫裡面,圖2-5展示了這4種資料結構是如何與資料庫互動的。

ChitChat論壇允許使用者在登入之後釋出新帖子或者回復已有的帖子,未登入的使用者可以閱讀帖子,但是不能釋出新帖子或者回復帖子。為了對應用進行簡化,ChitChat論壇沒有設定版主這一職位,因此使用者在釋出新帖子或者新增新回覆的時候不需要經過稽核。

圖2-5 Web應用訪問資料儲存系統的流程

在瞭解了ChitChat的設計方案之後,現在可以開始考慮具體的實現程式碼了。在開始學習ChitChat的實現程式碼之前,請注意,如果你在閱讀本章展示的程式碼時遇到困難,又或者你是剛開始學習Go語言,那麼為了更好地理解本章介紹的內容,你可以考慮先花些時間閱讀一本Go語言的程式設計入門書,比如,由William Kennedy、Brian Ketelsen和Erik St. Martin撰寫的《Go語言實戰》就是一個很不錯的選擇。

除此之外,在閱讀本章時也請儘量保持耐性:本章只是從巨集觀的角度展示Go Web應用的樣子,並沒有對Web應用的細節作過多的解釋,而是將這些細節留到之後的章節再進一步說明。在有需要的情況下,本章也會在介紹某種技術的同時,說明在哪一章可以找到這一技術的更多相關資訊。

2.4 請求的接收與處理

請求的接收和處理是所有Web應用的核心。正如之前所說,Web應用的工作流程如下。

(1)客戶端將請求傳送到伺服器的一個URL上。

(2)伺服器的多路複用器將接收到的請求重定向到正確的處理器,然後由該處理器對請求進行處理。

(3)處理器處理請求並執行必要的動作。

(4)處理器呼叫模板引擎,生成相應的HTML並將其返回給客戶端。

讓我們先從最基本的根URL(/)來考慮Web應用是如何處理請求的:當我們在瀏覽器上輸入地址http://localhost的時候,瀏覽器訪問的就是應用的根URL。在接下來的幾個小節裡面,我們將會看到ChitChat是如何處理髮送至根URL的請求的,以及它又是如何通過動態地生成HTML來對請求進行響應的。

2.4.1 多路複用器

因為編譯後的二進位制Go應用總是以main函式作為執行的起點,所以我們在對Go應用進行介紹的時候也總是從包含main函式的主原始碼檔案(main source code file)開始。ChitChat應用的主原始碼檔案為main.go,程式碼清單2-1展示了它的一個簡化版本。

程式碼清單2-1 main.go檔案中的main函式,函式中的程式碼經過了簡化

package main

import (
 "net/http"
)

func main() {

 mux := http.NewServeMux()
 files := http.FileServer(http.Dir("/public"))
 mux.Handle("/static/", http.StripPrefix("/static/", files))

  mux.HandleFunc("/", index)

 server := &http.Server{
  Addr: "0.0.0.0:8080",
  Handler: mux,
 }
 server.ListenAndServe()
}
複製程式碼

main.go首先建立了一個多路複用器,然後通過一些程式碼將接收到的請求重定向到處理器。net/http標準庫提供了一個預設的多路複用器,這個多路複用器可以通過呼叫NewServeMux函式來建立:

mux := http.NewServeMux()
複製程式碼

為了將傳送至根URL的請求重定向到處理器,程式使用了HandleFunc函式:

mux.HandleFunc("/", index)
複製程式碼

HandleFunc函式接受一個URL和一個處理器的名字作為引數,並將針對給定URL的請求轉發至指定的處理器進行處理,因此對上述呼叫來說,當有針對根URL的請求到達時,該請求就會被重定向到名為index的處理器函式。此外,因為所有處理器都接受一個ResponseWriter和一個指向Request結構的指標作為引數,並且所有請求引數都可以通過訪問Request結構得到,所以程式並不需要向處理器顯式地傳入任何請求引數。

需要注意的是,前面的介紹模糊了處理器以及處理器函式之間的區別:我們剛開始談論的是處理器,而現在談論的卻是處理器函式。這是有意而為之的——儘管處理器和處理器函式提供的最終結果是一樣的,但它們實際上<span style=“font-family: Times New Roman,楷體_GB2312”>並不相同。本書的第3章將對處理器和處理器函式之間的區別做進一步的說明,但是現在讓我們暫時先忘掉這件事,繼續研究ChitChat應用的程式碼實現。

2.4.2 服務靜態檔案

除負責將請求重定向到相應的處理器之外,多路複用器還需要為靜態檔案提供服務。為了做到這一點,程式使用FileServer函式建立了一個能夠為指定目錄中的靜態檔案服務的處理器,並將這個處理器傳遞給了多路複用器的Handle函式。除此之外,程式還使用StripPrefix函式去移除請求URL中的指定字首:

files := http.FileServer(http.Dir("/public"))
mux.Handle("/static/", http.StripPrefix("/static/", files))
複製程式碼

當伺服器接收到一個以/static/開頭的URL請求時,以上兩行程式碼會移除URL中的/static/字串,然後在public目錄中查詢被請求的檔案。比如說,當伺服器接收到一個針對檔案http://localhost/static/css/bootstrap.min.css的請求時,它將會在public目錄中查詢以下檔案:

<application root>/css/bootstrap.min.css
複製程式碼

當伺服器成功地找到這個檔案之後,會把它返回給客戶端。

2.4.3 建立處理器函式

正如之前的小節所說,ChitChat應用會通過HandleFunc函式把請求重定向到處理器函式。正如程式碼清單2-2所示,處理器函式實際上就是一個接受ResponseWriterRequest指標作為引數的Go函式。

程式碼清單2-2 main.go檔案中的index處理器函式

func index(w http.ResponseWriter, r *http.Request) {
 files := []string{"templates/layout.html",
          "templates/navbar.html",
          "templates/index.html",}
 templates := template.Must(template.ParseFiles(files...))
 threads, err := data.Threads(); if err == nil {
  templates.ExecuteTemplate(w, "layout", threads)
 }
}
複製程式碼

index函式負責生成HTML並將其寫入ResponseWriter中。因為這個處理器函式會用到html/template標準庫中的Template結構,所以包含這個函式的檔案需  要在檔案的開頭匯入html/template庫。之後的小節將對生成HTML的方法做進一步的介紹。

除了前面提到過的負責處理根URL請求的index處理器函式,main.go檔案實際上還包含很多其他的處理器函式,如程式碼清單2-3所示。

程式碼清單2-3 ChitChat應用的main.go原始檔

package main

import (
 "net/http"
)

func main() {

 mux := http.NewServeMux()
 files := http.FileServer(http.Dir(config.Static))
 mux.Handle("/static/", http.StripPrefix("/static/", files))

 mux.HandleFunc("/", index)
 mux.HandleFunc("/err", err)

 mux.HandleFunc("/login", login)
 mux.HandleFunc("/logout", logout)
 mux.HandleFunc("/signup", signup)
 mux.HandleFunc("/signup_account", signupAccount)
 mux.HandleFunc("/authenticate", authenticate)

 mux.HandleFunc("/thread/new", newThread)
 mux.HandleFunc("/thread/create", createThread)
 mux.HandleFunc("/thread/post", postThread)
 mux.HandleFunc("/thread/read", readThread)

 server := &http.Server{
  Addr:      "0.0.0.0:8080",
  Handler:    mux,
 }
 server.ListenAndServe()
}
複製程式碼

main函式中使用的這些處理器函式並沒有在main.go檔案中定義,它們的定義在其他檔案裡面,具體請參考ChitChat專案的完整原始碼。

為了在一個檔案裡面引用另一個檔案中定義的函式,諸如PHP、Ruby和Python這樣的語言要求使用者編寫程式碼去包含(include)被引用函式所在的檔案,而另一些語言則要求使用者在編譯程式時使用特殊的連結(link)命令。

但是對Go語言來說,使用者只需要把位於相同目錄下的所有檔案都設定成同一個包,那麼這些檔案就會與包中的其他檔案分享彼此的定義。又或者,使用者也可以把檔案放到其他獨立的包裡面,然後通過匯入(import)這些包來使用它們。比如,ChitChat論壇就把連線資料庫的程式碼放到了獨立的包裡面,我們很快就會看到這一點。

2.4.4 使用cookie進行訪問控制

跟其他很多Web應用一樣,ChitChat既擁有任何人都可以訪問的公開頁面,也擁有使用者在登入賬號之後才能看見的私人頁面。

當一個使用者成功登入以後,伺服器必須在後續的請求中標示出這是一個已登入的使用者。為了做到這一點,伺服器會在響應的首部中寫入一個cookie,而客戶端在接收這個cookie之後則會把它儲存到瀏覽器裡面。程式碼清單2-4展示了authenticate處理器函式的實現程式碼,這個函式定義在route_auth.go檔案中,它的作用就是對使用者的身份進行驗證,並在驗證成功之後向客戶端返回一個cookie。

程式碼清單2-4 route_auth.go檔案中的authenticate處理器函式

func authenticate(w http.ResponseWriter, r *http.Request) {
 r.ParseForm()
 user, _ := data.UserByEmail(r.PostFormValue("email"))
 if user.Password == data.Encrypt(r.PostFormValue("password")) {
  session := user.CreateSession()
  cookie := http.Cookie{
   Name: "_cookie",
   Value: session.Uuid,
   HttpOnly: true,
  }
  http.SetCookie(w, &cookie)
  http.Redirect(w, r, "/", 302)
 } else {
  http.Redirect(w, r, "/login", 302)
 }
}
複製程式碼

注意,程式碼清單2-4中的authenticate函式使用了兩個我們尚未介紹過的函式,一個是data.Encrypt,而另一個則是data.UserbyEmail。因為本節關注的是ChitChat論壇的訪問控制機制而不是資料處理方法,所以本節將不會對這兩個函式的實現細節進行解釋,但這兩個函式的名字已經很好地說明了它們各自的作用:data.UserByEmail函式通過給定的電子郵件地址獲取與之對應的User結構,而data.Encrypt函式則用於加密給定的字串。本章稍後將會對data包作更詳細的介紹,但是在此之前,讓我們回到對訪問控制機制的討論上來。

在驗證使用者身份的時候,程式必須先確保使用者是真實存在的,並且提交給處理器的密碼在加密之後跟儲存在資料庫裡面的已加密使用者密碼完全一致。在核實了使用者的身份之後,程式會使用User結構的CreateSession方法建立一個Session結構,該結構的定義如下:

type Session struct {
 Id    int
 Uuid   string
 Email   string
 UserId  int
 CreatedAt time.Time
}
複製程式碼

Session結構中的Email欄位用於儲存使用者的電子郵件地址,而UserId欄位則用於記錄使用者表中儲存使用者資訊的行的ID。Uuid欄位儲存的是一個隨機生成的唯一ID,這個ID是實現會話機制的核心,伺服器會通過cookie把這個ID儲存到瀏覽器裡面,並把Session結構中記錄的各項資訊儲存到資料庫中。

在建立了Session結構之後,程式又建立了Cookie結構:

cookie := http.Cookie{
 Name:   "_cookie",
 Value:   session.Uuid,
 HttpOnly: true,
}
複製程式碼

cookie的名字是隨意設定的,而cookie的值則是將要被儲存到瀏覽器裡面的唯一ID。因為程式沒有給cookie設定過期時間,所以這個cookie就成了一個會話cookie,它將在瀏覽器關閉時自動被移除。此外,程式將HttpOnly欄位的值設定成了true,這意味著這個cookie只能通過HTTP或者HTTPS訪問,但是卻無法通過JavaScript等非HTTP API進行訪問。

在設定好cookie之後,程式使用以下這行程式碼,將它新增到了響應的首部裡面:

http.SetCookie(writer, &cookie)
複製程式碼

在將cookie儲存到瀏覽器裡面之後,程式接下來要做的就是在處理器函式裡面檢查當前訪問的使用者是否已經登入。為此,我們需要建立一個名為session的工具(utility)函式,並在各個處理器函式裡面複用它。程式碼清單2-5展示了session函式的實現程式碼,跟其他工具函式一樣,這個函式也是在util.go檔案裡面定義的。再提醒一下,雖然程式把工具函式的定義都放在了util.go檔案裡面,但是因為util.go檔案也隸屬於main包,所以這個檔案裡面定義的所有工具函式都可以直接在整個main包裡面呼叫,而不必像data.Encrypt函式那樣需要先引入包然後再呼叫。

程式碼清單2-5 util.go檔案中的session工具函式

func session(w http.ResponseWriter, r *http.Request)(sess data.Session, err
 error){
 cookie, err := r.Cookie("_cookie")
 if err == nil {
  sess = data.Session{Uuid: cookie.Value}
  if ok, _ := sess.Check(); !ok {
   err = errors.New("Invalid session")
  }
 }
 return
}
複製程式碼

為了從請求中取出cookie,session函式使用了以下程式碼:

cookie, err := r.Cookie("_cookie")
複製程式碼

如果cookie不存在,那麼很明顯使用者並未登入;相反,如果cookie存在,那麼session函式將繼續進行第二項檢查——訪問資料庫並核實會話的唯一ID是否存在。第二項檢查是通過data.Session函式完成的,這個函式會從cookie中取出會話並呼叫後者的Check方法:

sess = data.Session{Uuid: cookie.Value}
if ok, _ := sess.Check(); !ok {
 err = errors.New("Invalid session")
}
複製程式碼

在擁有了檢查和識別已登入使用者和未登入使用者的能力之後,讓我們來回顧一下之前展示的index處理器函式,程式碼清單2-6中被加粗的程式碼行展示了這個處理器函式是如何使用session函式的。

{--:}程式碼清單2-6 index處理器函式

func index(w http.ResponseWriter, r *http.Request) {
 threads, err := data.Threads(); if err == nil {
  , err := session(w, r)
  public_tmpl_files := []string{"templates/layout.html",
                 "templates/public.navbar.html",
                 "templates/index.html"}
  private_tmpl_files := []string{"templates/layout.html",
                  "templates/private.navbar.html",
                  "templates/index.html"}
  var templates *template.Template
  if err != nil {
   templates = template.Must(template.Parse-
 Files(private_tmpl_files...))
  } else {
   templates = template.Must(template.ParseFiles(public_tmpl_files...))
  }
  templates.ExecuteTemplate(w, "layout", threads)
 }
}複製程式碼

通過呼叫session函式可以取得一個儲存了使用者資訊的Session結構,不過因為index函式目前並不需要這些資訊,所以它使用<span style=“font-family: Times New Roman,楷體_GB2312”>空白識別符號(blank identifier)(_)忽略了這一結構。index函式真正感興趣的是err變數,程式會根據這個變數的值來判斷使用者是否已經登入,然後以此來選擇是使用public導航條還是使用private導航條。

好的,關於ChitChat應用處理請求的方法就介紹到這裡了。本章接下來會繼續討論如何為客戶端生成HTML,並完整地敘述之前沒有說完的部分。

2.5 使用模板生成HTML響應

index處理器函式裡面的大部分程式碼都是用來為客戶端生成HTML的。首先,函式把每個需要用到的模板檔案都放到了Go切片裡面(這裡展示的是私有頁面的模板檔案,公開頁面的模板檔案也是以同樣方式進行組織的):

private_tmpl_files := []string{"templates/layout.html",
                "templates/private.navbar.html",
                "templates/index.html"}
複製程式碼

跟Mustache和CTemplate等其他模板引擎一樣,切片指定的這3個HTML檔案都包含了特定的嵌入命令,這些命令被稱為<span style=“font-family: Times New Roman,楷體_GB2312”>動作(action),動作在HTML檔案裡面會被{{符號和}}符號包圍。

接著,程式會呼叫ParseFiles函式對這些模板檔案進行語法分析,並建立出相應的模板。為了捕捉語法分析過程中可能會產生的錯誤,程式使用了Must函式去包圍ParseFiles函式的執行結果,這樣當ParseFiles返回錯誤的時候,Must函式就會向使用者返回相應的錯誤報告:

templates := template.Must(template.ParseFiles(private_tmpl_files...))
複製程式碼

好的,關於模板檔案的介紹已經足夠多了,現在是時候來看看它們的廬山真面目了。

ChitChat論壇的每個模板檔案都定義了一個模板,這種做法並不是強制的,使用者也可以在一個模板檔案裡面定義多個模板,但模板檔案和模板一一對應的做法可以給開發帶來方便,我們在之後就會看到這一點。程式碼清單2-7展示了layout.html模板檔案的原始碼,原始碼中使用了define動作,這個動作通過檔案開頭的{{ define "layout" }}和檔案末尾的{{ end }},把被包圍的文字塊定義成了layout模板的一部分。

程式碼清單2-7 layout.html模板檔案

{{ define "layout" }}

<!DOCTYPE html>
<html lang="en">
 <head>
  <meta charset="utf-8">
  <meta http-equiv="X-UA-Compatible" content="IE=9">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>ChitChat</title>
  <link href="/static/css/bootstrap.min.css" rel="stylesheet">
  <link href="/static/css/font-awesome.min.css" rel="stylesheet">
 </head>
 <body>
  {{ template "navbar" . }}

  <div class="container">

   {{ template "content" . }}

  </div> <!-- /container -->

  <script src="/static/js/jquery-2.1.1.min.js"></script>
  <script src="/static/js/bootstrap.min.js"></script>
 </body>
</html>

{{ end }}
複製程式碼

除了define動作之外,layout.html模板檔案裡面還包含了兩個用於引用其他模板檔案的template動作。跟在被引用模板名字之後的點(.)代表了傳遞給被引用模板的資料,比如{{ template "navbar" . }}語句除了會在語句出現的位置引入navbar模板之外,還會將傳遞給layout模板的資料傳遞給navbar模板。

程式碼清單2-8展示了public.navbar.html模板檔案中的navbar模板,除了定義模板自身的define動作之外,這個模板沒有包含其他動作(嚴格來說,模板也可以不包含任何動作)。

程式碼清單2-8 public.navbar.html模板檔案

{{ define "navbar" }}

<div class="navbar navbar-default navbar-static-top" role="navigation">
 <div class="container">
  <div class="navbar-header">
   <button type="button" class="navbar-toggle collapsed"
   ➥ data-toggle="collapse" data-target=".navbar-collapse">
    <span class="sr-only">Toggle navigation</span>
    <span class="icon-bar"></span>
    <span class="icon-bar"></span>
    <span class="icon-bar"></span>
   </button>
   <a class="navbar-brand" href="/">
    <i class="fa fa-comments-o"></i>
    ChitChat
   </a>
  </div>
  <div class="navbar-collapse collapse">
   <ul class="nav navbar-nav">
    <li><a href="/">Home</a></li>
   </ul>
   <ul class="nav navbar-nav navbar-right">
    <li><a href="/login">Login</a></li>
   </ul>
  </div>
 </div>
</div>

{{ end }}
複製程式碼

最後,讓我們來看看定義在index.html模板檔案中的content模板,程式碼清單2-9展示了這個模板的原始碼。注意,儘管之前展示的兩個模板都與模板檔案擁有相同的名字,但實際上模板和模板檔案分別擁有不同的名字也是可行的。

程式碼清單2-9 index.html模板檔案

{{ define "content" }}

<p class="lead">
 <a href="/thread/new">Start a thread</a> or join one below!
</p>

{{ range . }}
 <div class="panel panel-default">
  <div class="panel-heading">
   <span class="lead"> <i class="fa fa-comment-o"></i> {{ .Topic }}</span>
  </div>
  <div class="panel-body">
   Started by {{ .User.Name }} - {{ .CreatedAtDate }} - {{ .NumReplies }}
 posts.
   <div class="pull-right">
    <a href="/thread/read?id={{.Uuid }}">Read more</a>
   </div>
  </div>
 </div>
{{ end }}

{{ end }}
複製程式碼

index.html檔案裡面的程式碼非常有趣,特別值得一提的是檔案裡面包含了幾個以點號(.)開頭的動作,比如{{ .User.Name }}{{ .CreatedAtDate }},這些動作的作用和之前展示過的index處理器函式有關:

threads, err := data.Threads(); if err == nil {
 templates.ExecuteTemplate(writer, "layout", threads)
}
複製程式碼

在以下這行程式碼中:

templates.ExecuteTemplate(writer, "layout", threads)
複製程式碼

程式通過呼叫ExecuteTemplate函式,執行(execute)已經經過語法分析的layout模板。執行模板意味著把模板檔案中的內容和來自其他渠道的資料進行合併,然後生成最終的HTML內容,具體過程如圖2-6所示。

圖2-6 模板引擎通過合併資料和模板來生成HTML

程式之所以對layout模板而不是navbar模板或者content模板進行處理,是因為layout模板已經引用了其他兩個模板,所以執行layout模板就會導致其他兩個模板也被執行,由此產生出預期的HTML。但是,如果程式只執行navbar模板或者content模板,那麼程式最終只會產生出預期的HTML的一部分。

現在,你應該已經明白了,點號(.)代表的就是傳入到模板裡面的資料(實際上還不僅如此,接下來的小節會對這方面做進一步的說明)。圖2-7展示了程式根據模板生成的ChitChat論壇的樣子。

圖2-7 ChitChat Web應用示例的主頁

整理程式碼

因為生成HTML的程式碼會被重複執行很多次,所以我們決定對這些程式碼進行一些整理,並將它們移到程式碼清單2-10所示的generateHTML函式裡面。

程式碼清單2-10 generateHTML函式

func generateHTML(w http.ResponseWriter, data interface{}, fn ...string) {
 var files []string
 for _, file := range fn {
  files = append(files, fmt.Sprintf("templates/%s.html", file))
 }
 templates := template.Must(template.ParseFiles(files...))
 templates.ExecuteTemplate(writer, "layout", data)
}
複製程式碼

generateHTML函式接受一個ResponseWriter、一些資料以及一系列模板檔案作為引數,然後對給定的模板檔案進行語法分析。data引數的型別為空介面型別(empty interface type),這意味著該引數可以接受任何型別的值作為輸入。剛開始接觸Go語言的人可能會覺得奇怪——Go不是靜態程式語言嗎,它為什麼能夠使用沒有型別限制的引數?

但實際上,Go程式可以通過介面(interface)機制,巧妙地繞過靜態程式語言的限制,並藉此獲得接受多種不同型別輸入的能力。Go語言中的介面由一系列方法構成,並且每個介面就是一種型別。一個空介面就是一個空集合,這意味著任何型別都可以成為一個空介面,也就是說任何型別的值都可以傳遞給函式作為引數。

generateHTML函式的最後一個引數以3個點(...)開頭,它表示generateHTML函式是一個<span style=“font-family: Times New Roman,楷體_GB2312”>可變引數函式(variadic function),這意味著這個函式可以在最後的可變引數中接受零個或任意多個值作為引數。generateHTML函式對可變引數的支援使我們可以同時將任意多個模板檔案傳遞給該函式。在Go語言裡面,可變引數必須是可變引數函式的最後一個引數。

在實現了generateHTML函式之後,讓我們回過頭來,繼續對index處理器函式進行整理。程式碼清單2-11展示了經過整理之後的index處理器函式,現在它看上去更整潔了。

程式碼清單2-11 index處理器函式的最終版本

func index(writer http.ResponseWriter, request *http.Request) {
 threads, err := data.Threads(); if err == nil {
  _, err := session(writer, request)
  if err != nil {
   generateHTML(writer, threads, "layout", "public.navbar", "index")
  } else {
   generateHTML(writer, threads, "layout", "private.navbar", "index")
  }
 }
}
複製程式碼

在這一節中,我們學習了很多關於模板的基礎知識,之後的第5章將對模板做更詳細的介紹。但是在此之前,讓我們先來了解一下ChitChat應用使用的資料來源(data source),並藉此瞭解一下ChitChat應用的資料是如何與模板一同生成最終的HTML的。

2.6 安裝PostgreSQL

在本章以及後續幾章中,每當遇到需要訪問關聯式資料庫的場景,我們都會使用PostgreSQL。在開始使用PostgreSQL之前,我們首先需要學習的是如何安裝並執行PostgreSQL,以及如何建立本章所需的資料庫。

2.6.1 在Linux或FreeBSD系統上安裝

www.postgresql.org/download為各種不同版本的Linux和FreeBSD都提供了預編譯的二進位制安裝包,使用者只需要下載其中一個安裝包,然後根據指示進行安裝就可以了。比如說,通過執行以下命令,我們可以在Ubuntu發行版上安裝Postgres:

sudo apt-get install postgresql postgresql-contrib
複製程式碼

這條命令除了會安裝postgres包之外,還會安裝附加的工具包,並在安裝完畢之後啟動PostgreSQL資料庫系統。

在預設情況下,Postgres會建立一個名為postgres的使用者,並將其用於連線伺服器。為了操作方便,你也可以使用自己的名字建立一個Postgres賬號。要做到這一點,首先需要登入Postgres賬號:

sudo su postgres
複製程式碼

接著使用createuser命令建立一個PostgreSQL賬號:

createuser –interactive
複製程式碼

最後,還需要使用createdb命令建立以你的賬號名字命名的資料庫:

createdb <YOUR ACCOUNT NAME>
複製程式碼

2.6.2 在Mac OS X系統上安裝

要在Mac OS X上安裝PostgreSQL,最簡單的方法是使用PostgresApp.com提供的Postgres應用:你只需要把網站上提供的zip壓縮包下載下來,解壓它,然後把Postgres.app檔案拖曳到自己的Applications資料夾裡面就可以了。啟動Postgres.app的方法跟啟動其他Mac OS X應用的方法完全一樣。Postgres.app在初次啟動的時候會初始化一個新的資料庫叢集,併為自己建立一個資料庫。因為命令列工具psql也包含在了Postgres.app裡面,所以在設定好正確的路徑之後,你就可以使用psql訪問資料庫了。設定路徑的工作可以通過在你的~/.profile檔案或者~/.bashrc檔案中新增以下程式碼行來完成[1]

export PATH=$PATH:/Applications/Postgres.app/Contents/Versions/9.4/bin
複製程式碼

2.6.3 在Windows系統上安裝

因為Windows系統上的很多PostgreSQL圖形安裝程式都會把一切安裝步驟佈置妥當,使用者只需要進行相應的設定就可以了,所以在Windows系統上安裝PostgreSQL也是非常簡單和直觀的。其中一個流行的安裝程式是由Enterprise DB提供的:www.enterprisedb.com/products- services-training/pgdownload。

除了PostgreSQL資料庫本身之外,安裝包還會附帶諸如pgAdmin等工具,以便使用者通過這些工具進行後續的配置。

2.7 連線資料庫

本章前面在展示ChitChat應用的設計方案時,曾經提到過ChitChat應用包含了4種資料結構。雖然把這4種資料結構放到主原始碼檔案裡面也是可以的,但更好的辦法是把所有與資料相關的程式碼都放到另一個包裡面——ChitChat應用的data包也因此應運而生。

為了建立data包,我們首先需要建立一個名為data的子目錄,並建立一個用於儲存所有帖子相關程式碼的thread.go檔案(在之後的小節裡面,我們還會建立一個用於儲存所有使用者相關程式碼的user.go檔案)。在此之後,每當程式需要用到data包的時候(比如處理器需要訪問資料庫的時候),程式都需要通過import語句匯入這個包:

import (
 "github.com/sausheong/gwp/Chapter_2_Go_ChitChat/chitchat/data"
)
複製程式碼

程式碼清單2-12展示了定義在thread.go檔案裡面的Thread結構,這個結構儲存了與帖子有關的各種資訊。

{--:}程式碼清單2-12 定義在thread.go檔案裡面的Thread結構

package data

import(
 "time"
)
type Thread struct {
 Id    int
 Uuid   string
 Topic   string
 UserId  int
 CreatedAt time.Time
}複製程式碼

正如程式碼清單2-12中加粗顯示的程式碼行所示,檔案的包名現在是data而不再是main了,這個包就是前面小節中我們曾經見到過的data包。data包除了包含與資料庫互動的結構和程式碼,還包含了一些與資料處理密切相關的函式。隸屬於其他包的程式在引用data包中定義的函式、結構或者其他東西時,必須在被引用元素的名字前面顯式地加上data這個包名。比如說,引用Thread結構就需要使用data.Thread這個名字,而不能僅僅使用Thread這個名字。

Thread結構應該與建立關聯式資料庫表threads時使用的<span style=“font-family: Times New Roman,楷體_GB2312”>資料定義語言(Data Definition Language,<SPAN STYLE=“FONT-FAMILY: TIMES NEW ROMAN,楷體_GB2312”>DDL)保持一致。因為threads表目前尚未存在,所以我們必須建立這個表以及容納該表的資料庫。建立chitchat資料庫的工作可以通過執行以下命令來完成:

createdb chitchat
複製程式碼

在建立資料庫之後,我們就可以通過程式碼清單2-13展示的setup.sql檔案為ChitChat論壇建立相應的資料庫表了。

程式碼清單2-13 用於在PostgreSQL裡面建立資料庫表的setup.sql檔案

create table users (
 id     serial primary key,
 uuid    varchar(64) not null unique,
 name    varchar(255),
 email   varchar(255) not null unique,
 password  varchar(255) not null,
 created_at timestamp not null
);

create table sessions (
 id     serial primary key,
 uuid    varchar(64) not null unique,
 email   varchar(255),
 user_id  integer references users(id),
 created_at timestamp not null
);

create table threads (
 id     serial primary key,
 uuid    varchar(64) not null unique,
 topic   text,
 user_id  integer references users(id),
 created_at timestamp not null
);

create table posts (
 id     serial primary key,
 uuid    varchar(64) not null unique,
 body    text,
 user_id  integer references users(id),
 thread_id integer references threads(id),
 created_at timestamp not null
);
複製程式碼

執行這個指令碼需要用到psql工具,正如上一節所說,這個工具通常會隨著PostgreSQL一同安裝,所以你只需要在終端裡面執行以下命令就可以了:

psql –f setup.sql –d chitchat
複製程式碼

如果一切正常,那麼以上命令將在chitchat資料庫中建立出相應的表。在擁有了表之後,程式就必須考慮如何與資料庫進行連線以及如何對錶進行操作了。為此,程式建立了一個名為Db的全域性變數,這個全域性變數是一個指標,指向的是代表資料庫連線池的sql.DB,而後續的程式碼則會使用這個Db變數來執行資料庫查詢操作。程式碼清單2-14展示了Db變數在data.go檔案中的定義,此外還展示了一個用於在Web應用啟動時對Db變數進行初始化的init函式。

程式碼清單2-14 data.go檔案中的Db全域性變數以及init函式

Var Db *sql.DB

func init() {
 var err error
 Db, err = sql.Open("postgres", "dbname=chitchat sslmode=disable")
 if err != nil {
  log.Fatal(err)
 }
 return
}
複製程式碼

現在程式已經擁有了結構、表以及一個指向資料庫連線池的指標,接下來要考慮的是如何連線(connect)Thread結構和threads表。幸運的是,要做到這一點並不困難:跟ChitChat應用的其他部分一樣,我們只需要建立能夠在結構和資料庫之間互動的函式就可以了。例如,為了從資料庫裡面取出所有帖子並將其返回給index處理器函式,我們可以使用thread.go檔案中定義的Threads函式,程式碼清單2-15給出了這個函式的定義。

程式碼清單2-15 threads.go檔案中定義的Threads函式

func Threads() (threads []Thread, err error){
 rows, err := Db.Query("SELECT id, uuid, topic, user_id, created_at FROM
 threads ORDER BY created_at DESC")
 if err != nil {
  return
 }
 for rows.Next() {
  th := Thread{}
  if err = rows.Scan(&th.Id, &th.Uuid, &th.Topic, &th.UserId,
  ➥&th.CreatedAt); err != nil {
   return
  }
  threads = append(threads, th)
 }
 rows.Close()
 return
}
複製程式碼

簡單來講,Threads函式執行了以下工作:

(1)通過資料庫連線池與資料庫進行連線;

(2)向資料庫傳送一個SQL查詢,這個查詢將返回一個或多個行作為結果;

(3)遍歷行,為每個行分別建立一個Thread結構,首先使用這個結構去儲存行中記錄的帖子資料,然後將儲存了帖子資料的Thread結構追加到傳入的threads切片裡面;

(4)重複執行步驟3,直到查詢返回的所有行都被遍歷完畢為止。

本書的第6章將對資料庫操作的細節做進一步的介紹。

在瞭解瞭如何將資料庫表儲存的帖子資料提取到Thread結構裡面之後,我們接下來要考慮的就是如何在模板裡面展示Thread結構儲存的資料了。在程式碼清單2-9中展示的index.html模板檔案,有這樣一段程式碼:

{{ range . }}
 <div class="panel panel-default">
  <div class="panel-heading">
   <span class="lead"> <i class="fa fa-comment-o"></i> {{ .Topic }}</span>
  </div>
  <div class="panel-body">
   Started by {{ .User.Name }} - {{ .CreatedAtDate }} - {{ .NumReplies }}
 posts.
   <div class="pull-right">
    <a href="/thread/read?id={{.Uuid }}">Read more</a>
   </div>
  </div>
 </div>
{{ end }}
複製程式碼

正如之前所說,模板動作中的點號(.)代表傳入模板的資料,它們會和模板一起生成最終的結果,而{{ range . }}中的.號代表的是程式在稍早之前通過Threads函式取得的threads變數,也就是一個由Thread結構組成的切片。

range動作假設傳入的資料要麼是一個由結構組成的切片,要麼是一個由結構組成的陣列,這個動作會遍歷傳入的每個結構,而使用者則可以通過欄位名訪問結構裡面的欄位,比如,動作{{ .Topic }}訪問的是Thread結構的Topic欄位。注意,在訪問欄位時必須在欄位名的前面加上點號,並且欄位名的首字母必須大寫。

使用者除可以在欄位名的前面加上點號來訪問結構中的欄位以外,還可以通過相同的方法呼叫一種名為<span style=“font-family: Times New Roman,楷體_GB2312”>方法(method)的特殊函式。比如,在上面展示的程式碼中,{{ .User.Name }}{{ .CreatedAtDate }}{{ .NumReplies }}這些動作的作用就是呼叫結構中的同名方法,而不是訪問結構中的欄位。

方法是隸屬於特定型別的函式,指標、介面以及包括結構在內的所有具名型別都可以擁有自己的方法。比如說,通過將函式與指向Thread結構的指標進行繫結,可以建立出一個針對Thread結構的方法,而傳入方法裡面的Thread結構則稱為<span style=“font-family: Times New Roman,楷體_GB2312”>接收者(receiver):方法可以訪問接收者,也可以修改接收者。

作為例子,程式碼清單2-16展示了NumReplies方法的實現程式碼。

程式碼清單2-16 thread.go檔案中的NumReplies方法

func (thread *Thread) NumReplies() (count int) {
 rows, err := Db.Query("SELECT count(*) FROM posts where thread_id = $1",
 thread.Id)
 if err != nil {
  return
 }
 for rows.Next() {
  if err = rows.Scan(&count); err != nil {
   return
  }
 }
 rows.Close()
 return
}
複製程式碼

NumReplies方法首先開啟一個指向資料庫的連線,接著通過執行一條SQL查詢來取得帖子的數量,並使用傳入方法裡面的count引數來記錄這個值。最後,NumReplies方法返回帖子的數量作為方法的執行結果,而模板引擎則使用這個值去代替模板檔案中出現的{{ .NumReplies }}動作。

通過為UserSessionThreadPost這4種資料結構建立相應的函式和方法,ChitChat最終在處理器函式和資料庫之間構建起了一個資料層,以此來避免處理器函式直接對資料庫進行訪問,圖2-8展示了這個資料層和資料庫以及處理器函式之間的關係。雖然有很多庫都可以達到同樣的效果,但親自構建資料層能夠幫助我們學習如何對資料庫進行基本的訪問,並藉此瞭解到實現這種訪問並不困難,只需要用到一些簡單直接的程式碼,這一點是非常有益的。

圖2-8 通過結構模型連線資料庫和處理器

2.8 啟動伺服器

在本章的最後,讓我們來看一下ChitChat應用是如何啟動伺服器並將多路複用器與伺服器進行繫結的。執行這一工作的程式碼是在main.go檔案裡面定義的:

server := &http.Server{
 Addr:   "0.0.0.0:8080",
 Handler: mux,
}
server.ListenAndServe()
複製程式碼

這段程式碼非常簡單,它所做的就是建立一個Server結構,然後在這個結構上呼叫ListenAndServe方法,這樣伺服器就能夠啟動了。

現在,我們可以通過執行以下命令來編譯並執行ChitChat應用:

go build
複製程式碼

這個命令會在當前目錄以及$GOPATH/bin目錄中建立一個名為chitchat的二進位制可執行檔案,它就是ChitChat應用的伺服器。接著,我們可以通過執行以下命令來啟動這個伺服器:

./chitchat
複製程式碼

如果你已經按照之前所說的方法,在資料庫裡面建立了ChitChat應用所需的資料庫表,那麼現在你只需要訪問http://localhost:8080/並註冊一個新賬號,然後就可以使用自己的賬號在論壇上釋出新帖子了。

2.9 Web應用運作流程回顧

在本章的各節中,我們對一個Go Web應用的不同組成部分進行了初步的瞭解和觀察。圖2-9對整個應用的工作流程進行了介紹,其中包括:

(1)客戶端向伺服器傳送請求;

(2)多路複用器接收到請求,並將其重定向到正確的處理器;

(3)處理器對請求進行處理;

(4)在需要訪問資料庫的情況下,處理器會使用一個或多個資料結構,這些資料結構都是根據資料庫中的資料建模而來的;

(5)當處理器呼叫與資料結構有關的函式或者方法時,這些資料結構背後的模型會與資料庫進行連線,並執行相應的操作;

(6)當請求處理完畢時,處理器會呼叫模板引擎,有時候還會向模板引擎傳遞一些通過模型獲取到的資料;

(7)模板引擎會對模板檔案進行語法分析並建立相應的模板,而這些模板又會與處理器傳遞的資料一起合併生成最終的HTML;

(8)生成的HTML會作為響應的一部分回傳至客戶端。

圖2-9 Web應用工作流程概覽

主要的步驟大概就是這些。在接下來的幾章中,我們會更加深入地學習這一工作流程,並進一步瞭解該流程涉及的各個元件。

2.10 小結

  • <span style=“font-family: Times New Roman,楷體_GB2312”>請求的接收和處理是所有Web應用的核心。
  • <span style=“font-family: Times New Roman,楷體_GB2312”>多路複用器會將HTTP請求重定向到正確的處理器進行處理,針對靜態檔案的請求也是如此。
  • <span style=“font-family: Times New Roman,楷體_GB2312”>處理器函式是一種接受ResponseWriter<span style=“font-family: Times New Roman,楷體_GB2312”>和Requeest<span style=“font-family: Times New Roman,楷體_GB2312”>指標作為引數的Go函式。
  • <span style=“font-family: Times New Roman,楷體_GB2312”>cookie可以用作一種訪問控制機制。
  • <span style=“font-family: Times New Roman,楷體_GB2312”>對模板檔案以及資料進行語法分析會產生相應的HTML,這些HTML會被用作返回給瀏覽器的響應資料。
  • <span style=“font-family: Times New Roman,楷體_GB2312”>通過使用sql<span style=“font-family: Times New Roman,楷體_GB2312”>包以及相應的SQL語句,使用者可以將資料持久地儲存在關聯式資料庫中。

[1] 在安裝Postgres.app時,你可能需要根據Postgres.app的版本對路徑的版本部分做相應的修改,比如,將其中的9.4修改為9.5或者9.6,諸如此類。——譯者注

相關文章