本文摘自《Go web編程》 css
京東購書:https://item.jd.com/12252845.htmlhtml
{:--}本文主要內容jquery
上一章在末尾展現了一個很是簡單的Go Web應用,可是由於該應用只是一個Hello World程序,因此它實際上並無什麼用處。在本章中,咱們將會構建一個簡單的網上論壇Web應用,這個應用一樣很是基礎,可是卻有用得多:它容許用戶登陸到論壇裏面,而後在論壇上發佈新帖子,又或者回復其餘用戶發表的帖子。git
雖然本章介紹的內容沒法讓你一會兒就學會如何編寫一個很是成熟的Web應用,但這些內容將教會你如何組織和開發一個Web應用。在閱讀完這一章以後,你將進一步地瞭解到使用Go進行Web應用開發的相關方法。github
若是你以爲本章介紹的內容難度較大,又或者你以爲本章展現的大量代碼看起來讓人以爲膽戰心驚,那也沒必要過於擔憂:本章以後的幾章將對本章介紹的內容作進一步的解釋,在閱讀完本章並繼續閱讀後續章節時,你將會對本章介紹的內容有更加深刻的瞭解。web
網上論壇無處不在,它們是互聯網上最受歡迎的應用之一,與舊式的電子公告欄(BBS)、新聞組(Usenet)和電子郵件一脈相承。雅虎公司和Google公司的羣組(Groups)都很是流行,雅虎報告稱,他們總共擁有1000萬個羣組以及1.15億個羣組成員,其中每一個羣組都擁有一個本身的論壇;而全球最具人氣的網上論壇之一——Gaia在線——則擁有2300萬註冊用戶以及接近230億張帖子,而且這些帖子的數量還在以天天上百萬張的速度持續增加。儘管如今出現了諸如Facebook這樣的社交網站,但論壇仍然是人們在網上進行交流時最爲經常使用的手段之一。做爲例子,圖2-1展現了GoogleGroups的樣子。sql
圖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這個應用。bootstrap
{關於本章展現的代碼!}
跟本書的其餘章節不同,由於篇幅的關係,本章並不會展現ChitChat論壇的全部實現代碼,但你能夠在GitHub頁面https://github.com/sausheong/gwp找到這些代碼。若是你打算在閱讀本章的同時實際瞭解一下這個應用,那麼這些完整的代碼應該會對你有所幫助。
正如第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應用中的工做流程
絕大多數應用都須要以某種方式與數據打交道。對ChitChat來講,它的數據將被存儲到關係式數據庫PostgreSQL裏面,並經過SQL與之交互。
ChitChat的數據模型很是簡單,只包含4種數據結構,它們分別是:
以上這4種數據結構都會被映射到關係數據庫裏面,圖2-5展現了這4種數據結構是如何與數據庫交互的。
ChitChat論壇容許用戶在登陸以後發佈新帖子或者回復已有的帖子,未登陸的用戶能夠閱讀帖子,可是不能發佈新帖子或者回復帖子。爲了對應用進行簡化,ChitChat論壇沒有設置版主這一職位,所以用戶在發佈新帖子或者添加新回覆的時候不須要通過審覈。
圖2-5 Web應用訪問數據存儲系統的流程
在瞭解了ChitChat的設計方案以後,如今能夠開始考慮具體的實現代碼了。在開始學習ChitChat的實現代碼以前,請注意,若是你在閱讀本章展現的代碼時遇到困難,又或者你是剛開始學習Go語言,那麼爲了更好地理解本章介紹的內容,你能夠考慮先花些時間閱讀一本Go語言的編程入門書,好比,由William Kennedy、Brian Ketelsen和Erik St. Martin撰寫的《Go語言實戰》就是一個很不錯的選擇。
除此以外,在閱讀本章時也請儘可能保持耐性:本章只是從宏觀的角度展現Go Web應用的樣子,並無對Web應用的細節做過多的解釋,而是將這些細節留到以後的章節再進一步說明。在有須要的狀況下,本章也會在介紹某種技術的同時,說明在哪一章能夠找到這一技術的更多相關信息。
請求的接收和處理是全部Web應用的核心。正如以前所說,Web應用的工做流程以下。
(1)客戶端將請求發送到服務器的一個URL上。
(2)服務器的多路複用器將接收到的請求重定向到正確的處理器,而後由該處理器對請求進行處理。
(3)處理器處理請求並執行必要的動做。
(4)處理器調用模板引擎,生成相應的HTML並將其返回給客戶端。
讓咱們先從最基本的根URL(/
)來考慮Web應用是如何處理請求的:當咱們在瀏覽器上輸入地址http://localhost
的時候,瀏覽器訪問的就是應用的根URL。在接下來的幾個小節裏面,咱們將會看到ChitChat是如何處理髮送至根URL的請求的,以及它又是如何經過動態地生成HTML來對請求進行響應的。
由於編譯後的二進制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應用的代碼實現。
除負責將請求重定向到相應的處理器以外,多路複用器還須要爲靜態文件提供服務。爲了作到這一點,程序使用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
複製代碼
當服務器成功地找到這個文件以後,會把它返回給客戶端。
正如以前的小節所說,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論壇就把鏈接數據庫的代碼放到了獨立的包裏面,咱們很快就會看到這一點。
跟其餘不少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,並完整地敘述以前沒有說完的部分。
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的。
在本章以及後續幾章中,每當遇到須要訪問關係數據庫的場景,咱們都會使用PostgreSQL。在開始使用PostgreSQL以前,咱們首先須要學習的是如何安裝並運行PostgreSQL,以及如何建立本章所需的數據庫。
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>
複製代碼
要在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
複製代碼
由於Windows系統上的不少PostgreSQL圖形安裝程序都會把一切安裝步驟佈置穩當,用戶只須要進行相應的設置就能夠了,因此在Windows系統上安裝PostgreSQL也是很是簡單和直觀的。其中一個流行的安裝程序是由Enterprise DB提供的:www.enterprisedb.com/products- services-training/pgdownload。
除了PostgreSQL數據庫自己以外,安裝包還會附帶諸如pgAdmin等工具,以便用戶經過這些工具進行後續的配置。
本章前面在展現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 經過結構模型鏈接數據庫和處理器
在本章的最後,讓咱們來看一下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/並註冊一個新帳號,而後就可使用本身的帳號在論壇上發佈新帖子了。
在本章的各節中,咱們對一個Go Web應用的不一樣組成部分進行了初步的瞭解和觀察。圖2-9對整個應用的工做流程進行了介紹,其中包括:
(1)客戶端向服務器發送請求;
(2)多路複用器接收到請求,並將其重定向到正確的處理器;
(3)處理器對請求進行處理;
(4)在須要訪問數據庫的狀況下,處理器會使用一個或多個數據結構,這些數據結構都是根據數據庫中的數據建模而來的;
(5)當處理器調用與數據結構有關的函數或者方法時,這些數據結構背後的模型會與數據庫進行鏈接,並執行相應的操做;
(6)當請求處理完畢時,處理器會調用模板引擎,有時候還會向模板引擎傳遞一些經過模型獲取到的數據;
(7)模板引擎會對模板文件進行語法分析並建立相應的模板,而這些模板又會與處理器傳遞的數據一塊兒合併生成最終的HTML;
(8)生成的HTML會做爲響應的一部分回傳至客戶端。
圖2-9 Web應用工做流程概覽
主要的步驟大概就是這些。在接下來的幾章中,咱們會更加深刻地學習這一工做流程,並進一步瞭解該流程涉及的各個組件。
ResponseWriter
<span style=「font-family: Times New Roman,楷體_GB2312」>和Requeest
<span style=「font-family: Times New Roman,楷體_GB2312」>指針做爲參數的Go函數。sql
<span style=「font-family: Times New Roman,楷體_GB2312」>包以及相應的SQL語句,用戶能夠將數據持久地存儲在關係數據庫中。[1] 在安裝Postgres.app
時,你可能須要根據Postgres.app
的版本對路徑的版本部分作相應的修改,好比,將其中的9.4
修改成9.5
或者9.6
,諸如此類。——譯者注