《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所示,處理器函式實際上就是一個接受ResponseWriter
和Request
指標作為引數的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 }}
動作。
通過為User
、Session
、Thread
和Post
這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
,諸如此類。——譯者注