Skip to content

Latest commit

 

History

History
311 lines (212 loc) · 15.8 KB

File metadata and controls

311 lines (212 loc) · 15.8 KB

三、连接数据

在上一章中,我们探讨了如何获取 URL 并将其转换到 web 应用程序中的不同页面。在这样做的过程中,我们构建了动态的 URL,并从(非常简单的)net/http处理程序中得到动态响应。

通过从 Gorilla toolkit 中实现一个扩展的 mux 路由器,我们通过允许正则表达式扩展了内置路由器的功能,这给了我们的应用程序更大的灵活性。

这是一些最流行的 web 服务器特有的特性。例如,Apache 和 NGiNX 都提供了在路由中使用正则表达式的方法,并且与公共解决方案保持一致应该是我们的最小功能基线。

但是,这只是构建一个具有多种功能的健壮 web 应用程序的重要垫脚石。更进一步,我们需要考虑引入数据。

上一章中的示例依赖于从静态文件中获取的硬编码内容,这显然是过时的,不可扩展。任何在 CGI 之前的早期网络工作过的人都可以向你讲述需要对静态文件进行全面重组的网站更新故事,或者解释服务器端所包含的时代错误。

但幸运的是,网络在 20 世纪 90 年代末变得相当活跃,数据库开始统治世界。虽然 API、微服务和 NoSQL 在某些地方已经取代了这种架构,但它仍然是当今 Web 工作方式的主要部分。

因此,无需进一步麻烦,让我们获取一些动态数据。

在本章中,我们将介绍以下主题:

  • 连接到数据库
  • 对更漂亮的 URL 使用 GUID
  • 处理 404s

连接到数据库

当涉及到访问数据库时,Go 的 SQL 接口提供了一种非常简单和可靠的方式来连接到具有驱动程序的各种数据库服务器。

在这一点上,大多数大牌都包含了 MySQL、Postgres、SQLite、MSSQL,还有很多都有维护良好的驱动程序,这些驱动程序利用了 Go 提供的database/sql接口。

Go 通过标准化 SQL 接口处理此问题的最好方法是,您不必学习自定义 Go 库来与数据库交互。这并不排除需要了解数据库的 SQL 实现或其他功能的细微差别,但它确实消除了一个潜在的混淆区域。

在您进行更多操作之前,您需要确保通过go get命令为您选择的数据库安装了库和驱动程序。

Go 项目维护了一个包含所有当前 SQLDrivers 的 Wiki,是在上寻找适配器时的一个很好的起始参考点 https://github.com/golang/go/wiki/SQLDrivers

注意:本书中的各种示例都使用 MySQL 和 Postgres,但使用最适合您的解决方案。在任何 Nix、Windows 或 OSX 机器上安装 MySQL 和 Postgres 都是相当基本的。

MySQL 可从下载 https://www.mysql.com/ 虽然谷歌列出了一些驱动程序,但我们推荐 Go MySQL 驱动程序。虽然您不会对 go 项目中推荐的替代方案出错,但 go MySQL 驱动程序非常干净且经过良好测试。您可以在获取 https://github.com/go-sql-driver/mysql/ 对于 Postgres,从获取二进制或包管理器命令 http://www.postgresql.org/ 。这里选择的 Postgres 驱动程序是pq,可以通过github.com/lib/pq上的go get安装

创建 MySQL 数据库

你可以选择设计任何你想要的应用程序,但是对于这些例子,我们将看一个非常简单的博客概念。

我们的目标是在数据库中拥有尽可能少的博客条目,能够通过 GUID 直接从数据库中调用这些条目,并在特定请求的博客条目不存在时显示错误。

为此,我们将创建一个包含页面的 MySQL 数据库。它们将有一个内部的、自动递增的数字 ID、一个文本全局唯一标识符或 GUID,以及博客条目本身周围的一些元数据。

简单地说,我们将创建一个标题page_title、正文page_content和一个 Unix 时间戳page_date。您可以随意使用 MySQL 的内置日期字段之一;使用整数字段存储时间戳只是一个偏好问题,可以在查询中进行更详细的比较。

以下是 MySQL 控制台(或 GUI 应用程序)中创建数据库cms和必要表pages的 SQL:

CREATE TABLE `pages` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
  `page_guid` varchar(256) NOT NULL DEFAULT '',
  `page_title` varchar(256) DEFAULT NULL,
  `page_content` mediumtext,
  `page_date` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`),
  UNIQUE KEY `page_guid` (`page_guid`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=latin1;

如前所述,您可以通过任意数量的接口执行此查询。要连接到 MySQL,请选择您的数据库并尝试这些查询,您可以按照中的命令行文档进行操作 http://dev.mysql.com/doc/refman/5.7/en/connecting.html

注意page_guid上的UNIQUE KEY。这是非常重要的,因为如果我们碰巧允许重复的 guid,那么,我们有一个问题。全局唯一键的概念是它不能存在于其他地方,因为我们将依赖它来解析 URL,所以我们希望确保每个 GUID 只有一个条目。

正如您可能知道的,这是一个非常基本的博客数据库内容类型。我们有一个自动递增的 ID 值,一个标题,一个日期和页面的内容,而不是很多其他的事情。

虽然不是很多,但在 Go 中使用数据库接口演示动态页面就足够了。

只是为了确保pages表中有一些数据,添加以下查询来稍微填充一下:

INSERT INTO `pages` (`id`, `page_guid`, `page_title`, `page_content`, `page_date`) VALUES (NULL, 'hello-world', 'Hello, World', 'I\'m so glad you found this page!  It\'s been sitting patiently on the Internet for some time, just waiting for a visitor.', CURRENT_TIMESTAMP);

这将给我们一些开始。

现在,我们有了我们的 To.t0 结构和一些虚拟数据,让我们来看看我们如何连接到 MySQL,检索数据,并根据 URL 请求和大猩猩的 MUX 模式动态地服务它。

首先,让我们创建一个外壳,其中包含我们需要连接的内容:

package main

import (
  "database/sql"
  "fmt"
  _ "github.com/go-sql-driver/mysql"
  "log"
)

我们正在为所谓的副作用导入 MySQL 驱动程序包。这通常意味着该包是对另一个包的补充,并提供了不需要特别引用的各种接口。

您可以通过包导入之前的下划线_语法来注意这一点。您可能已经熟悉这是一种忽略方法返回值的实例化的快速而肮脏的方法。例如x, _ := something()允许您忽略第二个返回值。

当开发人员计划使用库,但尚未使用时,也经常使用它。通过以这种方式预先结束包,它允许导入声明保留,而不会导致编译器错误。虽然不赞成这样做,但在前面的方法中使用下划线或空白标识符作为副作用是相当常见的,并且通常是可以接受的。

不过,与往常一样,这完全取决于您使用标识符的方式和原因:

const (
  DBHost  = "127.0.0.1"
  DBPort  = ":3306"
  DBUser  = "root"
  DBPass  = "password!"
  DBDbase = "cms"
)

当然,请确保将这些值替换为与您的安装相关的值:

var database *sql.DB

通过将数据库连接引用保持为全局变量,我们可以避免大量重复代码。为了清楚起见,我们将在代码中对其进行相当高的定义。没有什么可以阻止您将其设置为常量,但我们已经将其保留为可更改的,以满足未来任何必要的灵活性,例如向单个应用程序添加多个数据库:

type Page struct {
  Title   string
  Content string
  Date    string
}

当然,这个struct与我们的数据库模式非常匹配,其中TitleContentDate表示表中的非 ID 值。正如我们将在本章稍后(以及下一章)中看到的,在一个精心设计的结构中描述我们的数据有助于展示 Go 的模板功能。在这一点上,请确保您的结构字段是可导出的或公共的,方法是保持它们的大小写。任何小写字段都不可导出,因此无法用于模板。稍后我们将对此进行更多讨论:

func main() {
  dbConn := fmt.Sprintf("%s:%s@tcp(%s)/%s", DBUser, DBPass, DBHost, DBDbase)
  db, err := sql.Open("mysql", dbConn)
  if err != nil {
    log.Println("Couldn't connect!")
    log.Println(err.Error)
  }
  database = db
}

正如我们前面提到的,这主要是脚手架。我们要做的就是确保能够连接到数据库。如果出现错误,请检查您的连接和Couldn't connect之后的日志条目输出。

如果您能够连接到这个脚本,我们可以继续创建一个通用路由,并从数据库中输出特定请求的 GUID 中的相关数据。

要做到这一点,我们需要重新实现 Gorilla,创建一条路由,然后实现一个处理程序,该处理程序生成一些非常简单的输出,与数据库中的[T1]匹配。

让我们来看看修改和补充,我们需要做的是允许这种情况发生:

package main

import (
  "database/sql"
  "fmt"
  _ "github.com/go-sql-driver/mysql"
  "github.com/gorilla/mux"
  "log"
  "net/http"
)

这里最大的变化是我们把 Gorilla 和net/http带回项目中。显然,我们需要这些服务页面:

const (
  DBHost  = "127.0.0.1"
  DBPort  = ":3306"
  DBUser  = "root"
  DBPass  = "password!"
  DBDbase = "cms"
  PORT    = ":8080"
)

我们添加了一个PORT常量,它表示我们的 HTTP 服务器端口。

请注意,如果您的主机是localhost/127.0.0.1,则无需指定DBPort,但我们在常量部分保留了这一行。我们在 MySQL 连接中不使用此处的主机:

var database *sql.DB

type Page struct {
  Title   string
  Content string
  Date    string
}

func ServePage(w http.ResponseWriter, r *http.Request) {
  vars := mux.Vars(r)
  pageID := vars["id"]
  thisPage := Page{}
  fmt.Println(pageID)
  err := database.QueryRow("SELECT page_title,page_content,page_date FROM pages WHERE id=?", pageID).Scan(&thisPage.Title, &thisPage.Content, &thisPage.Date)
  if err != nil {

    log.Println("Couldn't get page: +pageID")
    log.Println(err.Error)
  }
  html := `<html><head><title>` + thisPage.Title + `</title></head><body><h1>` + thisPage.Title + `</h1><div>` + thisPage.Content + `</div></body></html>`
  fmt.Fprintln(w, html)
}

ServePage是从mux.Vars获取id并查询我们数据库中的博客条目 ID 的功能。我们进行查询的方式有一些细微差别,值得注意;消除 SQL 注入漏洞的最简单方法是使用准备好的语句,如QueryQueryRowPrepare。使用其中任何一个,并包括要注入到准备好的语句中的变量变量,可以消除手工构造查询的固有风险。

然后,Scan方法获取查询结果并将其转换为结构;您需要确保结构与查询中请求的字段的顺序和数量相匹配。在本例中,我们将page_titlepage_contentpage_date映射到Page结构的TitleContentDate

func main() {
  dbConn := fmt.Sprintf("%s:%s@/%s", DBUser, DBPass, DBDbase)
  fmt.Println(dbConn)
  db, err := sql.Open("mysql", dbConn)
  if err != nil {
    log.Println("Couldn't connect to"+DBDbase)
    log.Println(err.Error)
  }
  database = db

  routes := mux.NewRouter()
  routes.HandleFunc("/page/{id:[0-9]+}", ServePage)
  http.Handle("/", routes)
  http.ListenAndServe(PORT, nil)

}

注意我们这里的正则表达式:它只是一个数字,由一个或多个数字组成,可以从我们的处理程序访问id变量。

还记得我们说过使用内置 GUID 吗?稍后我们将讨论这个问题,但现在让我们来看一下local``host:8080/page/1的输出:

Creating a MySQL database

在前面的示例中,我们可以看到数据库中的博客条目。这很好,但显然在很多方面都有所欠缺。

对更漂亮的 URL 使用 GUID

在本章前面,我们讨论了使用 GUID 作为所有请求的 URL 标识符。相反,我们从使用数值开始,从而自动增加表中的列。这是为了简单起见,但将其切换为字母数字 GUID 并不重要。

我们需要做的就是切换正则表达式,并在ServePage处理程序中更改生成的 SQL 查询。

如果我们只更改正则表达式,那么最后一个 URL 的页面仍然可以工作:

routes.HandleFunc("/page/{id:[0-9a-zA\\-]+}", ServePage)

当然,页面仍将传递给我们的处理程序。为了消除任何歧义,让我们为路由分配一个guid变量:

routes.HandleFunc("/page/{guid:[0-9a-zA\\-]+}", ServePage)

之后,我们更改结果调用和 SQL:

func ServePage(w http.ResponseWriter, r *http.Request) {
  vars := mux.Vars(r)
  pageGUID := vars["guid"]
  thisPage := Page{}
  fmt.Println(pageGUID)
  err := database.QueryRow("SELECT page_title,page_content,page_date FROM pages WHERE page_guid=?", pageGUID).Scan(&thisPage.Title, &thisPage.Content, &thisPage.Date)

完成此操作后,通过/pages/hello-worldURL 访问我们的页面将产生与通过/pages/1访问相同的页面内容。唯一的真正优势是美观,它创建了一个更美观的 URL,更易于阅读,并且可能对搜索引擎更有用:

Using GUID for prettier URLs

处理 404s

我们前面的代码有一个非常明显的问题,即它不能处理请求无效 ID(或 GUID)的场景。

实际上,一个请求,比如说,/page/999只会给用户带来一个空白页面,而在后台,**无法获取页面!**消息,如下图截图所示:

Handling 404s

通过传递适当的错误来解决这个问题非常简单。现在,在上一章中,我们探讨了定制404页面,您当然可以在这里实现其中一个,但最简单的方法是在找不到帖子时返回 HTTP 状态码,并允许浏览器处理演示。

在前面的代码中,我们有一个错误处理程序,除了将问题返回到日志文件之外,它没有做什么。让我们更具体地说:

  err := database.QueryRow("SELECT page_title,page_content,page_date FROM pages WHERE page_guid=?", pageGUID).Scan(&thisPage.Title, &thisPage.Content, &thisPage.Date)
  if err != nil {
    http.Error(w, http.StatusText(404), http.StatusNotFound)
    log.Println("Couldn't get page!")
  }

您将在下面的屏幕截图中看到输出。同样,用一个定制的404页面来替换这个页面是很简单的,但是现在我们希望通过对数据库进行验证来确保我们正在处理无效的请求:

Handling 404s

提供良好的错误消息有助于提高开发人员和其他用户的可用性。此外,这对 SEO 是有益的,因此使用 HTTP 标准中定义的 HTTP 状态代码是有意义的。

总结

在本章中,我们实现了从简单显示内容到使用数据库以可持续和可维护的方式显示内容的飞跃。虽然这使我们能够轻松地显示动态数据,但它只是实现全功能应用程序的一个核心步骤。

我们已经研究了如何创建一个数据库,然后从中检索数据以注入到路由中,同时保持查询参数的净化以防止 SQL 注入。

我们还通过为数据库中不存在的任何请求 GUID 返回404 Not Found状态,解释了具有无效 GUID 的潜在错误请求。我们还研究了按 ID 以及字母数字 GUID 请求数据。

不过,这只是我们应用程序的开始。

第 4 章使用模板中,我们将从 MySQL(和 Postgres)中获取数据,并将 Go 的一些模板语言应用到这些数据中,以给我们提供更大的前端灵活性。

到本章结束时,我们将有一个应用程序,允许直接从我们的应用程序中创建和删除页面。