如何使用Go创建您的第一个Web应用程序

哈Ha! 我向您呈现Ayooluwa Isaiah撰写的文章“如何使用Go构建您的第一个Web应用程序”的翻译。


这是您第一个Go Web应用程序的指南。 我们将创建一个新闻应用程序 ,该新闻应用程序使用News API接收有关特定主题的新闻文章,并最终将其部署到生产服务器。


您可以在此GitHub存储库中找到用于本教程的完整代码。


要求条件


此任务的唯一要求是Go已安装在您的计算机上,并且您对它的语法和构造有点熟悉。 在撰写本文时,我用来创建应用程序的Go版本也是最新的: 1.12.9 。 要查看Go的安装版本,请使用go version命令。


如果您觉得这项工作对您来说太难了,请转到我上一门入门语言 ,这应该可以帮助您入门。


因此,让我们开始吧!


我们在GitHub上克隆启动文件存储库,cd到创建的目录中。 我们有三个主要文件:在main.go文件中main.go我们将为此任务编写所有Go代码。 index.html文件是将发送到浏览器的模板,该应用程序的assets/styles.css


创建一个基本的Web服务器


让我们开始创建一个核心服务器,该服务器在对服务器根目录执行GET请求时将文本“ Hello World!”发送到浏览器。 更改您的main.go文件,如下所示:


 package main import ( "net/http" "os" ) func indexHandler(w http.ResponseWriter, r *http.Request) { w.Write([]byte("<h1>Hello World!</h1>")) } func main() { port := os.Getenv("PORT") if port == "" { port = "3000" } mux := http.NewServeMux() mux.HandleFunc("/", indexHandler) http.ListenAndServe(":"+port, mux) } 

package main的第一行-声明main.go文件中的代码main.go main软件包。 之后,我们导入了net/http包,该包提供了供我们的应用程序使用的HTTP客户端和服务器实现。 该软件包是标准库的一部分,并且随每个Go安装一起提供。


main函数中, http.NewServeMux()创建一个新的HTTP请求多路复用器,并将其分配给mux变量。 本质上,请求多路复用器将传入请求的URL与注册路径列表进行匹配,并在找到匹配项时为该路径调用适当的处理程序。


接下来,我们为根路径/注册我们的第一个处理函数。 此处理函数是HandleFunc的第二个参数,并且始终具有签名func (w http.ResponseWriter, r * http.Request)


如果查看indexHandler函数,您将看到它具有这样的签名,这使其成为HandleFunc的有效第二个参数。 w参数是我们用来发送对HTTP请求的响应的结构。 它实现Write()方法,该方法采用一个字节片并将组合的数据作为HTTP响应的一部分写入。


另一方面,参数r表示从客户端收到的HTTP请求。 这就是我们访问服务器上Web浏览器发送的数据的方式。 我们现在还没有使用它,但是以后一定会使用。


最后,我们有http.ListenAndServe()方法,如果环境未设置端口,则它将在端口3000上启动服务器。 如果您的计算机上使用了3000,请随意使用其他端口。


然后编译并执行您刚刚编写的代码:


 go run main.go 

如果在浏览器中转到http://本地主机:3000 ,则应看到文本“ Hello World!”。


勇敢的浏览器显示Hello World文本


转到模板


让我们看一下Go语言中模板制作的基本知识。 如果您熟悉其他语言的模板,这应该很容易理解。


模板提供了一种简便的方法,可以根据路由自定义Web应用程序的输出,而不必在不同的地方编写相同的代码。 例如,我们可以为导航栏创建模板,并在网站的所有页面上使用它,而无需重复代码。 另外,我们也有机会向我们的网页添加一些基本逻辑。


Go在其标准库中提供了两个模板库: text/templatehtml/template 。 两者都提供相同的接口,但是html/template包用于生成受代码注入保护的HTML输出,因此我们将在这里使用它。


将此包导入到main.go文件中,并按以下方式使用它:


 package main import ( "html/template" "net/http" "os" ) var tpl = template.Must(template.ParseFiles("index.html")) func indexHandler(w http.ResponseWriter, r *http.Request) { tpl.Execute(w, nil) } func main() { port := os.Getenv("PORT") if port == "" { port = "3000" } mux := http.NewServeMux() mux.HandleFunc("/", indexHandler) http.ListenAndServe(":"+port, mux) } 

tpl是程序包级别的变量,用于指示所提供文件中模板的定义。 template.ParseFiles调用将分析项目目录根目录中的index.html文件并检查其有效性。


我们将template.ParseFiles调用包装在template.ParseFiles中,以便代码在发生错误时引起恐慌。 我们在这里恐慌而不是尝试处理该错误的原因是,如果模板无效,继续执行代码是没有意义的。 这是在尝试重新启动服务器之前需要解决的问题。


indexHandler函数中indexHandler我们通过提供两个参数来执行先前创建的模板:我们要在其中写入输出的内容和我们要传递给模板的数据。


在上述情况下,我们将输出写入ResponseWriter接口,由于目前没有数据可传递到模板,因此将nil作为第二个参数传递。


使用Ctrl-C停止终端中正在运行的进程,然后使用go run main.go再次启动它,然后刷新浏览器。 您应该在页面上看到文本“ News App Demo”,如下所示:


勇敢的的浏览器显示新闻应用程序程序演示文本


在页面上添加导航栏


替换index.html文件中<body>的内容,如下所示:


 <main> <header> <a class="logo" href="/">News Demo</a> <form action="/search" method="GET"> <input autofocus class="search-input" value="" placeholder="Enter a news topic" type="search" name="q"> </form> <a href="https://github.com/freshman-tech/news" class="button github-button">View on Github</a> </header> </main> 

然后重新启动服务器并刷新浏览器。 您应该看到类似以下内容:


浏览器显示无样式的导航栏


处理静态文件


请注意,尽管我们已经在文档的<head>指定了样式,但上面添加的导航栏没有样式。


这是因为路径/实际上匹配在其他地方未处理的所有路径。 因此,如果转到http://本地主机:3000 / asset / style.css ,则仍会获得News Demo主页而不是CSS文件,因为尚未明确声明/assets/style.css路由。


但是需要声明所有静态文件的显式处理程序是不现实的,并且无法扩展。 幸运的是,我们可以创建一个处理程序来服务所有静态资源。


要做的第一件事是创建文件服务器对象的实例,并传入我们所有静态文件所在的目录:


 fs := http.FileServer(http.Dir("assets")) 

接下来,我们需要告诉路由器将这个文件服务器对象用于所有以/assets/前缀开头的路径:


 mux.Handle("/assets/", http.StripPrefix("/assets/", fs)) 

现在一起:


 // main.go //   func main() { port := os.Getenv("PORT") if port == "" { port = "3000" } mux := http.NewServeMux() //     fs := http.FileServer(http.Dir("assets")) mux.Handle("/assets/", http.StripPrefix("/assets/", fs)) mux.HandleFunc("/", indexHandler) http.ListenAndServe(":"+port, mux) } 

重新启动服务器并刷新浏览器。 样式应如下所示打开:


勇敢的浏览器显示样式化的导航栏



让我们创建一个处理新闻文章搜索查询的路由。 我们将使用News API来处理请求,因此您需要在此处注册以获取免费的API密钥。


此路由需要两个查询参数: q表示用户的查询, page用于滚动结果。 此page参数是可选的。 如果URL中未包含该字符,我们仅假设结果的页码设置为“ 1”。


indexHandler下将以下处理程序添加到main.go文件中:


 func searchHandler(w http.ResponseWriter, r *http.Request) { u, err := url.Parse(r.URL.String()) if err != nil { w.WriteHeader(http.StatusInternalServerError) w.Write([]byte("Internal server error")) return } params := u.Query() searchKey := params.Get("q") page := params.Get("page") if page == "" { page = "1" } fmt.Println("Search Query is: ", searchKey) fmt.Println("Results page is: ", page) } 

上面的代码从请求URL中提取qpage参数,并将它们都显示在终端中。


然后将searchHandler函数注册为/search路径处理程序,如下所示:


 func main() { port := os.Getenv("PORT") if port == "" { port = "3000" } mux := http.NewServeMux() fs := http.FileServer(http.Dir("assets")) mux.Handle("/assets/", http.StripPrefix("/assets/", fs)) // Add the next line mux.HandleFunc("/search", searchHandler) mux.HandleFunc("/", indexHandler) http.ListenAndServe(":"+port, mux) } 

请记住从上面导入fmtnet/url包:


 import ( "fmt" "html/template" "net/http" "net/url" "os" ) 

现在重新启动服务器,在搜索字段中输入查询并检查终端。 您应该在终端上看到您的请求,如下所示:




创建数据模型


当我们向News API/everything端点发出请求时,我们期望以下格式的json响应:


 { "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中使用此数据,我们需要生成一个结构,该结构在解码响应主体时会反映数据。 当然,您可以手动执行此操作,但是我更喜欢使用JSON-to-Go网站,这使此过程非常容易。 它生成一个Go结构(带有标签),该结构适用于此JSON。


您要做的就是复制JSON对象并将其粘贴到标有JSON的字段中,然后复制输出并将其粘贴到您的代码中。 这是上面的JSON对象得到的结果:


 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转工具


我通过将Articles片段分为其自己的结构并更新了结构名称,对AutoGenerated结构进行了几处更改。 将以下tpl变量声明粘贴到main.go ,并将time包添加到您的导入中:


 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要求结构中所有导出的字段都以大写字母开头。 但是,习惯上使用camelCasesnake_case表示JSON字段,它们不以大写字母开头。


因此,我们使用诸如json:"id"类的结构字段标记在JSON字段中显式显示结构字段,如上所示。 如果需要,它还允许您为结构字段和相应的json字段使用完全不同的名称。


最后,让我们为每个搜索查询创建不同类型的结构。 将其添加到main.go的“ Results结构main.go


 type Search struct { SearchKey string NextPage int TotalPages int Results Results } 

该结构表示用户进行的每个搜索查询。 SearchKey是查询本身, NextPage字段使NextPage可以滚动浏览结果, TotalPages查询结果的总页数,以及TotalPages查询结果的当前页。


使用News API发送请求并呈现结果


现在我们有了应用程序的数据模型,让我们继续并向News API发出请求,然后在页面上呈现结果。


由于News API需要API密钥,因此我们需要找到一种无需在代码中进行硬编码即可在应用程序中传递它的方法。 环境变量是一种常见的方法,但是我决定改用命令行标志。 Go提供了一个flag包,它支持命令行标志的基本分析,这就是我们将在此处使用的flag包。


首先在tpl变量下声明一个新的apiKey变量:


 var apiKey *string 

然后在main功能中使用它,如下所示:


 func main() { apiKey = flag.String("apikey", "", "Newsapi.org access key") flag.Parse() if *apiKey == "" { log.Fatal("apiKey must be set") } //    } 

在这里,我们调用flag.String()方法,该方法允许我们定义一个字符串标志。 此方法的第一个参数是标志名称,第二个是默认值,第三个是用法说明。


定义所有标志后,您需要调用flag.Parse()来实际flag.Parse()它们。 最后,由于apikey是此应用程序的必需组件,因此,如果在程序执行期间未设置此标志,我们将确保程序崩溃。


确保将flag包添加到导入中,然后重新启动服务器并传递所需的apikey标志,如下所示:


 go run main.go -apikey=<your newsapi access key> 

接下来,让我们继续并更新searchHandler以便将用户的搜索查询发送到newsapi.org ,并将结果显示在模板中。


用以下代码替换对searchHandler函数结尾处的fmt.Println()方法的两次调用:


 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 { w.WriteHeader(http.StatusInternalServerError) } } 

首先,我们创建一个Search结构的新实例,并将SearchKey字段的值设置为HTTP请求中URL参数q的值。


之后,我们将page变量转换为整数,并将结果分配给NextPage search变量的NextPage字段。 然后,我们创建pageSize变量并将其值设置为20。此pageSize变量表示新闻API将在其响应中返回的结果数。 该值的范围是0到100。


然后,我们使用fmt.Sprintf()创建端点,并对其进行GET请求。 如果News API的响应不是200 OK ,我们将向客户端返回一般服务器错误。 否则,将在search.Results解析响应主体。


然后,我们通过将TotalResults字段除以pageSize来计算总页数。 例如,如果一个查询返回100个结果,而我们一次只查看20个结果,则需要滚动浏览五个页面以查看该查询的所有100个结果。


之后,我们渲染模板并将search变量作为数据接口传递。 如您所见,这使我们能够从模板中的JSON对象访问数据。


在继续进行index.html之前,请确保如下所示更新导入:


 import ( "encoding/json" "flag" "fmt" "html/template" "log" "math" "net/http" "net/url" "os" "strconv" "time" ) 

让我们继续,通过如下更改index.html文件,在页面上显示结果。 将其添加到<header>


 <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> 

要访问模板中的结构字段,我们使用点运算符。 该运算符引用一个结构对象(在这种情况下为search ),然后在模板内部我们简单地指定字段名称(如{{.Results}} )。


range块使我们可以在Go中的一个切片上进行迭代,并为该切片中的每个元素输出一些HTML。 在这里,我们遍历Articles字段中包含的Article结构的一部分,并在每次迭代时显示HTML。


重新启动服务器,刷新浏览器并搜索有关热门主题的新闻。 您应该获得每页20条结果的列表,如下面的屏幕快照所示。


浏览器显示新闻列表


将搜索查询保存在外部


请注意,当页面刷新结果时,搜索查询将从输入中消失。 理想情况下,应保留查询,直到用户执行新搜索为止。 例如,以下是Google搜索的工作方式。


我们可以通过如下方式更新index.html文件中input标签的value属性来轻松解决此问题:


 <input autofocus class="search-input" value="{{ .SearchKey }}" placeholder="Enter a news topic" type="search" name="q"> 

重新启动浏览器并执行新搜索。 搜索查询将被保存,如下所示:



格式发布日期


如果您查看每篇文章中的日期,将会发现它的可读性很差。 当前输出显示News API如何返回文章的发布日期。 但是我们可以通过在Article结构中添加一种方法并使用它来格式化日期而不是使用默认值来轻松地更改此设置。


让我们在main.goArticle结构下添加以下代码:


 func (a *Article) FormatPublishedDate() string { year, month, day := a.PublishedAt.Date() return fmt.Sprintf("%v %d, %d", month, day, year) } 

在这里,在Article结构中创建了一个新的FormatPublishedDate方法,该方法格式化ArticlePublishedAt字段,并以以下格式返回字符串: 10 2009


要在模板中使用此新方法,请在index.html文件.FormatPublishedDate替换为.FormatPublishedDate 。 然后重新启动服务器并重复上一个搜索查询。 这将以正确的格式输出结果,如下所示:


勇敢的的浏览器显示正确格式的日期


显示结果总数。


让我们通过在页面顶部指示结果总数来改善新闻应用程序的用户界面,然后在未针对特定查询找到结果的情况下显示一条消息。


您所需要做的就是将以下代码作为.container的子项添加到index.html文件中.container .search-results元素的上方:


 <div class="result-count"> {{ if (gt .Results.TotalResults 0)}} <p>About <strong>{{ .Results.TotalResults }}</strong> results were found.</p> {{ else if (ne .SearchKey "") and (eq .Results.TotalResults 0) }} <p>No results found for your query: <strong>{{ .SearchKey }}</strong>.</p> {{ end }} </div> 

Go , . gt , , TotalResults Results . , .


, SearchKey ( (ne .SearchKey "") ) TotalResults ( (eq .Results.TotalResults 0) ), «No results found».


, . «No results found».


浏览器显示未找到结果消息


. , :


Browser showing results count at the top of the page



20 , , .


Next , . , , Search main.go :


 func (s *Search) IsLastPage() bool { return s.NextPage >= s.TotalPages } 

, NextPage , TotalPages Search . , NextPage , . :


 func searchHandler(w http.ResponseWriter, r *http.Request) { //   search.TotalPages = int(math.Ceil(float64(search.Results.TotalResults / pageSize))) //   if  if ok := !search.IsLastPage(); ok { search.NextPage++ } //    } 

, , . .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> 

, Next .


, href /search q , NextPage page .


Previous . , 1. , CurrentPage() Search , . IsLastPage :


 func (s *Search) CurrentPage() int { if s.NextPage == 1 { return s.NextPage } return s.NextPage - 1 } 

NextPage - 1 , , NextPage 1. , 1 . :


 func (s *Search) PreviousPage() int { return s.CurrentPage() - 1 } 

, Previous , 1. .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> 

. , :





, , , , .


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> 

, , .


Browser showing current page


Heroku


, , Heroku. , , . . freshman-news .


, Heroku . heroku login , Heroku.


, git- . , git init , , heroku git-. freshman-news .


 heroku git:remote -a freshman-news 

Procfile ( touch Procfile ) :


 web: bin/news-demo -apikey $NEWS_API_KEY 

GitHub Go, , go.mod , . , , .


 module github.com/freshman-tech/news-demo go 1.12.9 

Settings Heroku Reveal Config Vars . NEWS_API_KEY , .


Heroku config variables


, Heroku :


 git add . git commit -m "Initial commit" git push heroku master 

https://__.herokuapp.com , .


结论


News Go -. , Heroku.


, . - , , .


感谢您的阅读!

Source: https://habr.com/ru/post/zh-CN475390/


All Articles