使用Go語言開發(fā)Web系統(tǒng)
我們將構(gòu)建一個(gè)新聞應(yīng)用程序,該新聞應(yīng)用程序利用News API來展示有關(guān)特定主題的新聞文章,并將其最終部署到生產(chǎn)服務(wù)器中。
本教程將帶您進(jìn)入第一個(gè)使用Go的Web應(yīng)用程序。我們將構(gòu)建一個(gè)新聞應(yīng)用程序,該新聞應(yīng)用程序利用News API來展示有關(guān)特定主題的新聞文章,并將其最終部署到生產(chǎn)服務(wù)器中。
您可以在GitHub存儲庫中找到本教程使用的完整代碼。
先決條件
本教程的唯一要求是您已經(jīng)在計(jì)算機(jī)上安裝了Go,并且對它的語法和構(gòu)造隱約熟悉。我在構(gòu)建應(yīng)用程序時(shí)使用的Go版本也是撰寫本文時(shí)的最新版本:1.12.9。要查看已安裝的Go版本,請使用go version命令。
如果您覺得本教程對您來說太高級了,請轉(zhuǎn)至我之前的 入門教程,以掌握該語言。
開始吧
在GitHub上克隆啟動程序文件倉庫,并cd進(jìn)入創(chuàng)建的目錄。我們有三個(gè)主要文件:在該main.go文件中,我們將編寫本教程的所有Go代碼。該index.html文件是將發(fā)送到瀏覽器的模板,而styles該應(yīng)用程序位于中assets/styles.css。
創(chuàng)建一個(gè)基本的Web服務(wù)器
首先創(chuàng)建一個(gè)發(fā)送“ Hello World!”的基本服務(wù)器。向服務(wù)器根目錄發(fā)出GET請求時(shí),將文本發(fā)送到瀏覽器。修改您的main.go文件,如下所示:

第一行package main聲明main.go文件中的代碼屬于主程序包。之后,我們導(dǎo)入了該net/http 軟件包,該軟件包提供了HTTP客戶端和服務(wù)器實(shí)現(xiàn),供我們的應(yīng)用程序使用。該軟件包是標(biāo)準(zhǔn)庫的一部分,并且隨Go的所有安裝一起提供。
在該main函數(shù)中,http.NewServeMux()創(chuàng)建一個(gè)新的HTTP請求多路復(fù)用器,并將其分配給該mux變量。本質(zhì)上,請求多路復(fù)用器將傳入請求的URL與注冊路徑列表進(jìn)行匹配,并在找到匹配項(xiàng)時(shí)為該路徑調(diào)用關(guān)聯(lián)的處理程序。
接下來,我們?yōu)楦窂阶晕覀兊牡谝粋€(gè)處理函數(shù)/。該處理函數(shù)是HandleFunc簽名的第二個(gè)參數(shù),始終是簽名的 func(w http.ResponseWriter, r *http.Request)。
如果您看一下indexHandler函數(shù),您會發(fā)現(xiàn)它具有這個(gè)確切的簽名,從而使其成為的有效第二個(gè)參數(shù)HandleFunc。該w參數(shù)是我們用來發(fā)送響應(yīng)HTTP請求的結(jié)構(gòu)。它強(qiáng)加了一個(gè)Write()方法,該方法接受一個(gè)字節(jié)片段并將數(shù)據(jù)作為HTTP響應(yīng)的一部分寫入連接。
另一方面,該r參數(shù)表示從客戶端收到的HTTP請求。這就是我們訪問服務(wù)器上Web瀏覽器發(fā)送的數(shù)據(jù)的方式。我們在這里還沒有使用它,但是稍后我們一定會使用它。
最后,http.ListenAndServe()如果環(huán)境中未設(shè)置服務(wù)器,則可以使用方法在端口3000上啟動服務(wù)器。如果您的計(jì)算機(jī)上使用的是3000,請隨意使用其他端口。
接下來,編譯并執(zhí)行您剛編寫的代碼:

如果在瀏覽器中轉(zhuǎn)到http:// localhost:3000,則應(yīng)該看到文本“ Hello World!”。顯示在屏幕上。

Go中的模板
讓我們研究一下Go中的模板基礎(chǔ)知識。如果您熟悉其他語言的模板,那么它應(yīng)該足夠容易理解。
模板提供了一種簡便的方法,可以根據(jù)路由自定義Web應(yīng)用程序的輸出,而不必在許多地方編寫相同的代碼。例如,我們可以為導(dǎo)航欄創(chuàng)建一個(gè)模板,并在網(wǎng)站的所有頁面上使用它,而無需重復(fù)代碼。最重要的是,我們還能夠向網(wǎng)頁添加一些基本邏輯。
Go在其標(biāo)準(zhǔn)庫中提供了兩個(gè)模板庫:text/template和html/template。兩者都提供相同的接口,但是該html/template包用于生成可防止代碼注入的HTML輸出,因此我們將在這里使用它。
將此包導(dǎo)入main.go文件中,并按以下方式使用:

tpl是程序包級別的變量,指向提供的文件中的模板定義。用來template.ParseFiles解析index.html文件的調(diào)用位于項(xiàng)目目錄的根目錄中,并對其進(jìn)行驗(yàn)證。
我們將template.ParseFileswith的調(diào)用包裝起來,以template.Must使代碼在出現(xiàn)錯(cuò)誤時(shí)會慌張。我們在這里恐慌而不是嘗試處理該錯(cuò)誤的原因是,如果模板無效,繼續(xù)執(zhí)行代碼毫無意義。在嘗試重新啟動服務(wù)器之前,應(yīng)該解決此問題。
在該indexHandler函數(shù)中,我們通過提供兩個(gè)參數(shù)來執(zhí)行先前創(chuàng)建的模板:我們要將輸出寫入到的位置以及要傳遞給模板的數(shù)據(jù)。
在上述情況下,我們將輸出寫入ResponseWriter接口,并且由于此時(shí)沒有任何數(shù)據(jù)可傳遞到模板,nil因此將其作為第二個(gè)參數(shù)傳遞。
使用停止終端中正在運(yùn)行的進(jìn)程,然后使用ctrl-c再次啟動它go run main.go,然后刷新瀏覽器。您應(yīng)該在頁面上看到文本“ News App Demo”,如下所示:

在頁面上添加導(dǎo)航欄
替換文件中<body>標(biāo)記的內(nèi)容,index.html如下所示:

然后重新啟動服務(wù)器并刷新瀏覽器。您應(yīng)該看到如下所示的內(nèi)容:

服務(wù)靜態(tài)文件
請注意,盡管我們已經(jīng)在<head>文檔的中鏈接了樣式表,但上面添加的導(dǎo)航欄并未設(shè)置樣式。
這是因?yàn)樵?路徑實(shí)際上與未在其他位置處理的所有路徑匹配。因此,如果您轉(zhuǎn)到http:// localhost:3000 / assets / style.css,則仍會獲得News Demo主頁,而不是CSS文件,因?yàn)樵?assets/style.css路由未明確聲明。
但是必須為所有靜態(tài)文件聲明顯式處理程序是不現(xiàn)實(shí)的,并且無法擴(kuò)展。幸運(yùn)的是,我們可以創(chuàng)建一個(gè)處理程序來處理所有靜態(tài)資產(chǎn)。
要做的第一件事是通過傳遞放置所有靜態(tài)文件的目錄來實(shí)例化文件服務(wù)器對象:

接下來,我們需要告訴路由器將這個(gè)文件服務(wù)器對象用于所有以/assets/前綴開頭的路徑:


重新啟動服務(wù)器,然后刷新瀏覽器。樣式應(yīng)如下所示:

創(chuàng)建/搜索路線
讓我們創(chuàng)建一條處理新聞搜索請求的路由。我們將使用News API來處理查詢,因此您需要在此處注冊免費(fèi)的API密鑰。
該路由需要兩個(gè)查詢參數(shù):q代表用戶的查詢,并 page用于分頁搜索結(jié)果。此page參數(shù)是可選的。如果網(wǎng)址中未包含該網(wǎng)址,則我們僅假設(shè)該網(wǎng)頁為 1。
indexHandler在main.go文件中添加以下處理程序:

上面的代碼從請求URL中提取q和page參數(shù),并將它們都打印到終端。
接下來,將searchHandler函數(shù)注冊為/search路徑的處理程序,如下所示:

不要忘記在頂部導(dǎo)入fmt和net/url軟件包:
import (
"fmt"
"html/template"
"net/http"
"net/url"
"os"
)
現(xiàn)在重新啟動服務(wù)器,在搜索輸入中鍵入查詢,然后檢查終端。您應(yīng)該以如下所示的方式在終端中看到查詢打?。?/span>
創(chuàng)建數(shù)據(jù)模型
當(dāng)我們向News API的/everything端點(diǎn)發(fā)出請求時(shí),我們期望以下格式的json響應(yīng):
{
"status": "ok",
"totalResults": 4661,
"articles": [
{
"source": {
"id": null,
"name": "Gizmodo.com"
},
"author": "Jennings Brown",
"title": "World's Dumbest Bitcoin Scammer Tries to Scam Bitcoin Educator, Gets Scammed in The Process",
"description": "Ben Perrin is a Canadian cryptocurrency enthusiast and educator who hosts a bitcoin show on YouTube. This is immediately apparent after a quick a look at all his social media. Ten seconds of viewing on of his videos will show that he is knowledgeable about di…",
"url": "https://gizmodo.com/worlds-dumbest-bitcoin-scammer-tries-to-scam-bitcoin-ed-1837032058",
"urlToImage": "https://i.kinja-img.com/gawker-media/image/upload/s--uLIW_Oxp--/c_fill,fl_progressive,g_center,h_900,q_80,w_1600/s4us4gembzxlsjrkmnbi.png",
"publishedAt": "2019-08-07T16:30:00Z",
"content": "Ben Perrin is a Canadian cryptocurrency enthusiast and educator who hosts a bitcoin show on YouTube. This is immediately apparent after a quick a look at all his social media. Ten seconds of viewing on of his videos will show that he is knowledgeable about..."
}
]
}
為了在Go中使用此數(shù)據(jù),我們需要生成一個(gè)在解碼響應(yīng)主體時(shí)鏡像數(shù)據(jù)的結(jié)構(gòu)。您當(dāng)然可以手動執(zhí)行此操作,但是我的首選方法是使用JSON-to-Go網(wǎng)站,該過程非常容易。它將生成適用于該JSON的Go結(jié)構(gòu)(帶有標(biāo)簽)。
您需要做的就是復(fù)制JSON對象并將其粘貼到標(biāo)記為JSON的字段中 ,然后復(fù)制輸出并將其粘貼到您的代碼中。這是上面的JSON對象得到的結(jié)果:
type AutoGenerated struct {
Status string `json:"status"`
TotalResults int `json:"totalResults"`
Articles []struct {
Source struct {
ID interface{} `json:"id"`
Name string `json:"name"`
} `json:"source"`
Author string `json:"author"`
Title string `json:"title"`
Description string `json:"description"`
URL string `json:"url"`
URLToImage string `json:"urlToImage"`
PublishedAt time.Time `json:"publishedAt"`
Content string `json:"content"`
} `json:"articles"`
}
勇敢的瀏覽器顯示JSON to Go工具
AutoGenerated通過將的切片分割A(yù)rticles成自己的結(jié)構(gòu)并更新結(jié)構(gòu)名稱,對結(jié)構(gòu) 進(jìn)行了一些更改。將以下內(nèi)容粘貼到tpl變量聲明下面,main.go然后將time 包添加到導(dǎo)入中:
type Source struct {
ID interface{} `json:"id"`
Name string `json:"name"`
}
type Article struct {
Source Source `json:"source"`
Author string `json:"author"`
Title string `json:"title"`
Description string `json:"description"`
URL string `json:"url"`
URLToImage string `json:"urlToImage"`
PublishedAt time.Time `json:"publishedAt"`
Content string `json:"content"`
}
type Results struct {
Status string `json:"status"`
TotalResults int `json:"totalResults"`
Articles []Article `json:"articles"`
}
您可能知道,Go要求結(jié)構(gòu)中的所有導(dǎo)出字段以大寫字母開頭。但是,習(xí)慣上用Camel大小寫或Snake大小寫名稱表示JSON字段,這些名稱不以大寫字母開頭。
因此,我們利用結(jié)構(gòu)字段標(biāo)簽,例如json:"id"將結(jié)構(gòu)字段顯式映射到JSON字段,如上所示。這也使得有可能在結(jié)構(gòu)字段和相應(yīng)的json字段中使用競爭不同的名稱(如果需要)。
最后,讓我們?yōu)槊總€(gè)搜索查詢創(chuàng)建另一個(gè)結(jié)構(gòu)類型。將其添加到以下Results結(jié)構(gòu)中main.go:
type Search struct {
SearchKey string
NextPage int
TotalPages int
Results Results
}
該結(jié)構(gòu)表示用戶進(jìn)行的每個(gè)搜索查詢。該SearchKey是查詢本身的NextPage領(lǐng)域允許我們通過頁面的結(jié)果, TotalPages是結(jié)果頁面的查詢的總數(shù),Results是結(jié)果查詢當(dāng)前頁面。
發(fā)送請求到News API并呈現(xiàn)結(jié)果
現(xiàn)在我們有了應(yīng)用程序的數(shù)據(jù)模型,讓我們繼續(xù)向News API發(fā)出請求,然后在頁面上呈現(xiàn)結(jié)果。
由于News API需要API密鑰,因此我們需要找出一種無需在代碼中進(jìn)行硬編碼即可在應(yīng)用程序中傳遞該密鑰的方法。環(huán)境變量是一種常見的方法,但是我選擇使用命令行標(biāo)志。Go提供了一個(gè)flag支持基本命令行標(biāo)志解析的軟件包,這就是我們將在此處使用的軟件包。
首先,在apiKey變量下聲明一個(gè)新tpl變量:
var apiKey *string
然后在main函數(shù)中使用它,如下所示:
func main() {
apiKey = flag.String("apikey", "", "Newsapi.org access key")
flag.Parse()
if *apiKey == "" {
log.Fatal("apiKey must be set")
}
// rest of the function
}
在這里,我們調(diào)用flag.String()允許我們定義字符串標(biāo)志的方法。此方法的第一個(gè)參數(shù)是標(biāo)志名稱,第二個(gè)參數(shù)是默認(rèn)值,而第三個(gè)參數(shù)是用法說明。
定義所有標(biāo)志后,您需要調(diào)用flag.Parse()以實(shí)際解析它們。最后,由于apikey是該應(yīng)用程序的必需組件,因此,如果在執(zhí)行程序時(shí)未設(shè)置此標(biāo)志,我們將確保程序崩潰。
確保已將flag軟件包添加到導(dǎo)入中,然后重新啟動服務(wù)器并傳遞所需的apikey標(biāo)志,如下所示:
go run main.go -apikey=<your newsapi access key>
接下來,讓我們繼續(xù)進(jìn)行更新,searchHandler以便將用戶的搜索查詢發(fā)送到newsapi.org,并將結(jié)果呈現(xiàn)在我們的模板中。
用 以下代碼替換函數(shù)fmt.Println()末尾的兩個(gè)方法調(diào)用searchHandler:
func searchHandler(w http.ResponseWriter, r *http.Request) {
// beginning of the function
search := &Search{}
search.SearchKey = searchKey
next, err := strconv.Atoi(page)
if err != nil {
http.Error(w, "Unexpected server error", http.StatusInternalServerError)
return
}
search.NextPage = next
pageSize := 20
endpoint := fmt.Sprintf("https://newsapi.org/v2/everything?q=%s&pageSize=%d&page=%d&apiKey=%s&sortBy=publishedAt&language=en", url.QueryEscape(search.SearchKey), pageSize, search.NextPage, *apiKey)
resp, err := http.Get(endpoint)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
w.WriteHeader(http.StatusInternalServerError)
return
}
err = json.NewDecoder(resp.Body).Decode(&search.Results)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
search.TotalPages = int(math.Ceil(float64(search.Results.TotalResults / pageSize)))
err = tpl.Execute(w, search)
if err != nil {
log.Println(err)
}
}
首先,我們創(chuàng)建該Search結(jié)構(gòu)的新實(shí)例,并將SearchKey 該實(shí)例上的字段設(shè)置q為HTTP請求中URL參數(shù)的值。
之后,我們將page變量轉(zhuǎn)換為整數(shù),并將結(jié)果分配給變量的NextPage字段search。然后,我們創(chuàng)建一個(gè) pageSize變量并將其值設(shè)置為20。此pageSize變量表示News API將在其響應(yīng)中返回的結(jié)果數(shù)。該值的范圍是0到100。
接下來,我們使用構(gòu)建端點(diǎn),fmt.Sprintf()并對其進(jìn)行GET請求。如果News API的響應(yīng)不是200 OK,我們將向客戶端返回一個(gè)通用服務(wù)器錯(cuò)誤。否則,響應(yīng)主體將解析為search.Results。
然后,我們將TotalResults字段除以來計(jì)算總頁數(shù)pageSize。例如,如果一個(gè)查詢返回100個(gè)結(jié)果,而我們一次只查看20個(gè)結(jié)果,則我們將需要分頁瀏覽五頁以查看該查詢的所有100個(gè)結(jié)果。
之后,我們執(zhí)行模板并將search變量作為數(shù)據(jù)接口傳遞。如您所見,這使我們能夠從模板中的JSON對象訪問數(shù)據(jù)。
切換到之前index.html,請確保如下所示更新導(dǎo)入:
import (
"encoding/json"
"flag"
"fmt"
"html/template"
"log"
"math"
"net/http"
"net/url"
"os"
"strconv"
"time"
)
讓我們繼續(xù)進(jìn)行以下修改index.html,以將結(jié)果呈現(xiàn)到頁面上。將此添加到<header>標(biāo)簽下面:
<section class="container">
<ul class="search-results">
{{ range .Results.Articles }}
<li class="news-article">
<div>
<a target="_blank" rel="noreferrer noopener" href="{{.URL}}">
<h3 class="title">{{.Title }}</h3>
</a>
<p class="description">{{ .Description }}</p>
<div class="metadata">
<p class="source">{{ .Source.Name }}</p>
<time class="published-date">{{ .PublishedAt }}</time>
</div>
</div>
<img class="article-image" src="{{ .URLToImage }}">
</li>
{{ end }}
</ul>
</section>
要訪問模板中的struct字段,我們使用點(diǎn)運(yùn)算符。該運(yùn)算符引用struct對象(search在這種情況下),然后在模板內(nèi)部,我們僅指定字段名稱(如{{ .Results }})。
該range塊使我們可以遍歷Go中的一個(gè)切片,并為該切片中的每個(gè)項(xiàng)目輸出一些HTML。在這里,我們遍歷Article該Articles字段中包含的結(jié)構(gòu)片,并在每次迭代中輸出一些HTML。
重新啟動服務(wù)器,刷新瀏覽器并搜索有關(guān)熱門主題的新聞。您應(yīng)該在頁面上獲得20條結(jié)果的列表,如下面的GIF所示。
瀏覽器顯示新聞列表
在輸入中保留搜索查詢
請注意,頁面刷新結(jié)果后,搜索查詢?nèi)绾螐妮斎胫邢?。理想情況下,查詢應(yīng)一直保留到用戶進(jìn)行新搜索為止。例如,這就是Google搜索的工作方式。
我們可以通過如下方式更新文件中標(biāo)記的value屬性來輕松解決此問題:inputindex.html
<input autofocus class="search-input" value="{{ .SearchKey }}" placeholder="Enter a news topic" type="search" name="q">
重新啟動瀏覽器,然后進(jìn)行新的搜索。搜索查詢將被保留,如下所示:
格式化發(fā)布日期
如果您查看每篇文章中的日期,您會發(fā)現(xiàn)它的可讀性不高。當(dāng)前輸出是News API如何返回文章的發(fā)布日期。但是我們可以通過在Article 結(jié)構(gòu)中添加一個(gè)方法并使用該方法格式化日期而不是使用默認(rèn)值來輕松更改此方法。
繼續(xù),在Articlestruct中的 下面添加以下代碼main.go:
func (a *Article) FormatPublishedDate() string {
year, month, day := a.PublishedAt.Date()
return fmt.Sprintf("%v %d, %d", month, day, year)
}
此處,F(xiàn)ormatPublishedDate在Articlestruct上創(chuàng)建了一個(gè)新方法,該方法格式化上的PublishedAt字段,Article并以以下格式返回字符串:January 10, 2009。
要在模板中使用此新方法,請?jiān)谖募刑鎿Q.PublishedAt為 。然后重新啟動服務(wù)器并重復(fù)上一個(gè)搜索查詢。這將以正確的時(shí)間格式輸出結(jié)果,如下所示:.FormatPublishedDateindex.html
顯示結(jié)果總數(shù)
讓我們通過在頁面頂部顯示結(jié)果總數(shù)來改善新聞應(yīng)用程序的UI,然后在未發(fā)現(xiàn)特定查詢結(jié)果的情況下顯示一條消息。
您需要做的就是將以下代碼作為的子代添加.container,.search-results位于index.html文件元素上方:
<div class="result-count">
{{ if (gt .Results.TotalResults 0)}}
<p>About <strong>{{ .Results.TotalResults }}</strong> results were found.</p>
{{ else if and (ne .SearchKey "") (eq .Results.TotalResults 0) }}
<p>No results found for your query: <strong>{{ .SearchKey }}</strong>.</p>
{{ end }}
</div>
Go模板支持多種比較功能,上面已使用其中一些功能。該gt函數(shù)用于檢查結(jié)構(gòu)的TotalResults字段Results是否大于零。如果是,結(jié)果總數(shù)將打印在頁面頂部。
否則,如果SearchKey不等于空字符串((ne .SearchKey ""))且TotalResults等于零((eq .Results.TotalResults 0)),則輸出“未找到結(jié)果”消息。
重新啟動服務(wù)器,然后在搜索輸入中鍵入一些亂碼,以便找不到您要查詢的新聞項(xiàng)目。您應(yīng)該No results found在屏幕上看到該消息。
瀏覽器顯示未找到結(jié)果消息
之后,這次再次搜索熱門主題。結(jié)果數(shù)將在頁面頂部輸出,如下所示:
瀏覽器將結(jié)果顯示在頁面頂部
分頁
由于我們一次只顯示20個(gè)結(jié)果,因此我們需要一種讓用戶隨時(shí)查看結(jié)果的下一頁或上一頁的方法。
首先,如果尚未到達(dá)結(jié)果的最后一頁,我們將在結(jié)果底部添加一個(gè)“下一步”按鈕。要確定是否已到達(dá)結(jié)果的最后一頁,請?jiān)赟earchstruct定義下創(chuàng)建此新方法main.go:
func (s *Search) IsLastPage() bool {
return s.NextPage >= s.TotalPages
}
此方法檢查NextPage字段是否大于實(shí)例TotalPages上的字段Search。但是,要使此方法起作用,我們需要在NextPage每次呈現(xiàn)新的結(jié)果頁面時(shí)增加。這就是這樣做的方法:
func searchHandler(w http.ResponseWriter, r *http.Request) {
// start of the function
search.TotalPages = int(math.Ceil(float64(search.Results.TotalResults / pageSize)))
// Add this if block
if ok := !search.IsLastPage(); ok {
search.NextPage++
}
// rest of the function
}
最后,讓我們添加一個(gè)按鈕,該按鈕將允許用戶轉(zhuǎn)到結(jié)果的下一頁。以下內(nèi)容應(yīng)放在.search-results您的 index.html文件中。
<div class="pagination">
{{ if (ne .IsLastPage true) }}
<a href="/search?q={{ .SearchKey }}&page={{ .NextPage }}" class="button next-page">Next</a>
{{ end }}
</div>
只要尚未到達(dá)該查詢的最后一頁,“下一步”按鈕將呈現(xiàn)在結(jié)果列表的底部。
如您所見,href上面的anchor標(biāo)簽的指向/search路線,并在使用q參數(shù)中的值的同時(shí) 將當(dāng)前搜索查詢保留NextPage在page參數(shù)中。
讓我們也輸入上一個(gè)按鈕。僅當(dāng)當(dāng)前頁面大于1時(shí)才應(yīng)顯示此按鈕。為此,我們需要在其上創(chuàng)建一個(gè)新CurrentPage()方法Search來幫助我們做到這一點(diǎn)。在IsLastPage方法下面添加:
func (s *Search) CurrentPage() int {
if s.NextPage == 1 {
return s.NextPage
}
return s.NextPage - 1
}
當(dāng)前頁面僅是NextPage - 1if(如果NextPage為1)。要獲得上一頁,只需從當(dāng)前頁面減去1。以下方法可以做到這一點(diǎn):
func (s *Search) PreviousPage() int {
return s.CurrentPage() - 1
}
因此,僅當(dāng)當(dāng)前頁面大于1時(shí),我們才能添加以下代碼以呈現(xiàn)“上一步”按鈕。如下修改.pagination您的元素index.html:
<div class="pagination">
{{ if (gt .NextPage 2) }}
<a href="/search?q={{ .SearchKey }}&page={{ .PreviousPage }}" class="button previous-page">Previous</a>
{{ end }}
{{ if (ne .IsLastPage true) }}
<a href="/search?q={{ .SearchKey }}&page={{ .NextPage }}" class="button next-page">Next</a>
{{ end }}
</div>
現(xiàn)在,重新啟動服務(wù)器,并進(jìn)行新的搜索查詢。您應(yīng)該能夠分頁顯示結(jié)果,如下所示:
顯示當(dāng)前頁面
不僅顯示查詢的結(jié)果總數(shù),還有助于用戶查看該查詢的總頁數(shù)以及他當(dāng)前所處的頁面。
為此,我們只需要index.html如下修改文件:
<div class="result-count">
{{ if (gt .Results.TotalResults 0)}}
<p>About <strong>{{ .Results.TotalResults }}</strong> results were found. You are on page <strong>{{ .CurrentPage }}</strong> of <strong> {{ .TotalPages }}</strong>.</p>
{{ else if (ne .SearchKey "") and (eq .Results.TotalResults 0) }}
<p>No results found for your query: <strong>{{ .SearchKey }}</strong>.</p>
{{ end }}
</div>
重新啟動服務(wù)器并進(jìn)行新搜索后,將在頁面頂部顯示當(dāng)前頁面和總頁數(shù)以及總結(jié)果計(jì)數(shù)。
瀏覽器顯示當(dāng)前頁面
妥善處理錯(cuò)誤
目前,如果對News API的請求失敗,我們只會收到500 Internal Server錯(cuò)誤,而沒有添加任何信息。讓我們通過在響應(yīng)正文中顯示從請求接收到的錯(cuò)誤消息來解決此問題。
在函數(shù)下面添加以下結(jié)構(gòu)indexHandler。此結(jié)構(gòu)表示每當(dāng)請求失敗時(shí)從News API接收到的JSON響應(yīng)。
type NewsAPIError struct {
Status string `json:"status"`
Code string `json:"code"`
Message string `json:"message"`
}
讓我們繼續(xù)更新該searchHandler函數(shù)中的錯(cuò)誤處理 。
查找函數(shù)的以下部分:
if resp.StatusCode != 200 {
w.WriteHeader(http.StatusInternalServerError)
return
}
并將其替換為以下代碼段:
if resp.StatusCode != 200 {
newError := &NewsAPIError{}
err := json.NewDecoder(resp.Body).Decode(newError)
if err != nil {
http.Error(w, "Unexpected server error", http.StatusInternalServerError)
return
}
http.Error(w, newError.Message, http.StatusInternalServerError)
return
}
現(xiàn)在,一旦請求失敗,我們就會收到一條簡短的消息,說明原因。
新聞API錯(cuò)誤
部署到Heroku
現(xiàn)在我們的應(yīng)用程序已經(jīng)完成功能,讓我們繼續(xù)將其部署到Heroku。注冊一個(gè)免費(fèi)帳戶,然后點(diǎn)擊此鏈接創(chuàng)建一個(gè)新應(yīng)用。給它起一個(gè)唯一的名字。我打電話給我的新生新聞。
接下來,按照此處的說明在計(jì)算機(jī)上安裝Heroku CLI。然后heroku login在終端中運(yùn)行命令以登錄到您的Heroku帳戶。
確保已為項(xiàng)目初始化了一個(gè)git存儲庫。如果不是,請git init在您的項(xiàng)目目錄的根目錄下運(yùn)行命令,然后運(yùn)行以下命令以將heroku設(shè)置為git repo的遠(yuǎn)程目錄。用freshman-news您的應(yīng)用程序名稱替換。
heroku git:remote -a freshman-news
接下來,在您的項(xiàng)目目錄(touch Procfile)的根目錄中創(chuàng)建一個(gè)Procfile,然后粘貼以下內(nèi)容:
web: bin/news-demo -apikey $NEWS_API_KEY
然后,為您的項(xiàng)目指定GitHub存儲庫,并在go.mod文件中指定您正在使用的Go版本,如下所示。如果項(xiàng)目根目錄中尚不存在此文件,則創(chuàng)建它。
module github.com/freshman-tech/news-demo
go 1.12.9
在部署應(yīng)用程序之前,請轉(zhuǎn)到Heroku儀表板中的“設(shè)置”標(biāo)簽,然后點(diǎn)擊“顯示配置變量”。我們需要設(shè)置NEWS_API_KEY環(huán)境變量,以便在啟動服務(wù)器時(shí)可以將其傳遞給二進(jìn)制文件。
Heroku配置變量
最后,使用以下命令提交代碼并將其推送到Heroku遠(yuǎn)程服務(wù)器:
git add .
git commit -m "Initial commit"
git push heroku master
部署過程完成后,您可以打開https://<your-app-name>.herokuapp.com以查看和測試項(xiàng)目。

結(jié)論
在本文中,我們成功創(chuàng)建了一個(gè)新聞應(yīng)用程序,并學(xué)習(xí)了使用Go進(jìn)行Web開發(fā)的基礎(chǔ)知識。我們還介紹了如何將完成的應(yīng)用程序部署到Heroku。
