Skip to content

Latest commit

 

History

History
974 lines (768 loc) · 43.5 KB

File metadata and controls

974 lines (768 loc) · 43.5 KB

四、使用流行的 Go 框架简化 RESTful 服务

在本章中,我们将介绍与使用框架简化楼宇 REST 服务相关的主题。首先,我们将快速了解 go restful,一个 REST API 创建框架,然后讨论一个名为Gin的框架。在本章中,我们将尝试构建 Metro Rail API。我们将要讨论的框架是成熟的 web 框架,它也可以用于在短时间内创建 RESTAPI。在本章中,我们将大量讨论资源和 REST 动词。我们将尝试将一个名为Sqlite3的小型数据库与我们的 API 集成。最后,我们将检查Revel.go并了解如何使用它来原型化 RESTAPI。

总的来说,我们将在本章中介绍的主题如下:

  • 如何在 Go 中使用 SQLite3
  • 使用 GoRESTful 包创建 RESTAPI
  • 介绍用于创建 RESTAPI 的 Gin 框架
  • 介绍 Revel.go 以创建 REST API
  • 构建 CRUD 操作的基础知识

获取代码

您可以从获取本章的代码示例 https://github.com/narenaryan/gorestful/tree/master/chapter4 。本章的示例以项目的形式出现,而不是以单个程序的形式出现。因此,将相应的目录复制到您的GOPATH以正确运行代码示例。

go restful,一个创建 REST API 的框架

go-restful是一个用于在 Go 中构建 REST 风格 web 服务的包。正如我们在上一节中讨论的,REST 要求开发人员遵循一组设计协议。我们已经讨论了 REST 动词应该如何定义以及它们对资源的作用。

使用go-restful,我们可以分离 API 处理程序的逻辑并附加 REST 谓词。这样做的好处是,通过查看代码可以清楚地告诉我们正在创建什么 API。在开始一个示例之前,我们需要为 RESTAPI 安装一个名为 SQLite3 的数据库,并使用go-restful。安装步骤如下:

  • 在 Ubuntu 上,运行以下命令:
 apt-get install sqlite3 libsqlite3-dev
  • 在 OS X 上,您可以使用brew命令安装 SQLite3:
 brew install sqlite3
  • 现在,使用以下get命令安装go-restful包:
 go get github.com/emicklei/go-restful

我们准备好出发了。首先,让我们编写一个简单的程序,展示go-restful在几行代码中可以做什么。让我们创建一个简单的 ping 服务器,将服务器时间回传给客户端:

package main
import (
    "fmt"
    "github.com/emicklei/go-restful"
    "io"
    "net/http"
    "time"
)
func main() {
    // Create a web service
    webservice := new(restful.WebService)
    // Create a route and attach it to handler in the service
    webservice.Route(webservice.GET("/ping").To(pingTime))
    // Add the service to application
    restful.Add(webservice)
    http.ListenAndServe(":8000", nil)
}
func pingTime(req *restful.Request, resp *restful.Response) {
    // Write to the response
   io.WriteString(resp, fmt.Sprintf("%s", time.Now()))
}

如果我们运行此程序:

go run basicExample.go

服务器将在本地主机的端口8000上运行。因此,我们可以发出 curl 请求,也可以使用浏览器查看GET请求输出:

curl -X GET "http://localhost:8000/ping"
2017-06-06 07:37:26.238146296 +0530 IST

在前面的程序中,我们导入了go-restful库,并使用restful.WebService结构的新实例创建了一个新服务。接下来,我们可以使用以下语句创建 REST 谓词:

**```go webservice.GET("/ping")


我们可以附加一个函数处理程序来执行这个动词;`pingTime`就是这样一种功能。这些链接函数被传递给`Route`**函数以创建路由器。然后是以下重要声明:**

 **```go
restful.Add(webservice)

这会将新创建的webservice注册到go-restful。如果您观察到,我们没有向http.ListenServe函数传递任何ServeMux对象;go-restful会处理好的。这里的主要概念是使用go-restful中基于资源的 REST API 创建。从基本示例开始,让我们构建一些实用的东西。

以一个场景为例,您所在的城市正在修建一条新的地铁,您需要开发一个 RESTAPI,供其他开发人员使用并创建相应的应用程序。在本章中,我们将创建一个这样的 API,并使用各种框架来展示实现。在此之前,对于创建、读取、更新、删除CRUD操作,我们应该知道如何使用 Go 代码查询或插入 SQLite DB。

CRUD 操作和 SQLite3 基础

所有 SQLite3 操作都将使用名为go-sqlite3的库来完成。我们可以使用以下命令安装该软件包:

**```go go get github.com/mattn/go-sqlite3


这个库的特殊之处在于它使用了 Go 的内部`sql`包。我们通常导入`database/sql`并使用`sql`对数据库执行数据库查询(这里是 SQLite3):

```go
import "database/sql"

现在,我们可以创建一个数据库驱动程序,然后使用名为Query的方法在其上执行 SQL 命令:

sqliteFundamentals.go

package main
import (
    "database/sql"
    "log"
    _ "github.com/mattn/go-sqlite3"
)
// Book is a placeholder for book
type Book struct {
    id int
    name string
    author string
}
func main() {
    db, err := sql.Open("sqlite3", "./books.db")
    log.Println(db)
    if err != nil {
        log.Println(err)
    }
    // Create table
    statement, err := db.Prepare("CREATE TABLE IF NOT EXISTS books (id
INTEGER PRIMARY KEY, isbn INTEGER, author VARCHAR(64), name VARCHAR(64) NULL)")
    if err != nil {
        log.Println("Error in creating table")
    } else {
        log.Println("Successfully created table books!")
    }
    statement.Exec()
    // Create
    statement, _ = db.Prepare("INSERT INTO books (name, author, isbn) VALUES (?, ?, ?)")
    statement.Exec("A Tale of Two Cities", "Charles Dickens", 140430547)
    log.Println("Inserted the book into database!")
    // Read
    rows, _ := db.Query("SELECT id, name, author FROM books")
    var tempBook Book
    for rows.Next() {
        rows.Scan(&tempBook.id, &tempBook.name, &tempBook.author)
        log.Printf("ID:%d, Book:%s, Author:%s\n", tempBook.id,
tempBook.name, tempBook.author)
    }
    // Update
    statement, _ = db.Prepare("update books set name=? where id=?")
    statement.Exec("The Tale of Two Cities", 1)
    log.Println("Successfully updated the book in database!")
    //Delete
    statement, _ = db.Prepare("delete from books where id=?")
    statement.Exec(1)
    log.Println("Successfully deleted the book in database!")
}

这个程序解释了如何在 SQL 数据库上执行 CRUD 操作。目前,数据库是 SQLite3。让我们使用以下命令运行此操作:

go run sqliteFundamentals.go

输出如下所示,打印所有日志语句:

2017/06/10 08:04:31 Successfully created table books!
2017/06/10 08:04:31 Inserted the book into database!
2017/06/10 08:04:31 ID:1, Book:A Tale of Two Cities, Author:Charles Dickens
2017/06/10 08:04:31 Successfully updated the book in database!
2017/06/10 08:04:31 Successfully deleted the book in database!

这个程序在 Windows 和 Linux 上运行没有任何问题。在低于 1.8.1 的 Go 版本中,您可能会看到 macOS X 上出现问题,例如信号被终止这是因为 Xcode 版本;请记住这一点。

**在节目中,我们首先导入database/sqlgo-sqlite3。然后,我们使用sql.Open()函数在文件系统上打开一个db文件。它有两个参数,数据库类型和文件名。如果出现错误,或者数据库驱动程序出错,则返回错误。在sql库中,为了规避 SQL 注入漏洞,包在数据库驱动上提供了一个名为Prepare的函数:

****```go statement, err := db.Prepare("CREATE TABLE IF NOT EXISTS books (id INTEGER PRIMARY KEY, isbn INTEGER, author VARCHAR(64), name VARCHAR(64) NULL)")


前面的语句只创建了一个语句,没有填写任何细节。传递给 SQL 查询的实际数据在语句中使用了一个`Exec`函数。例如,在前面的代码片段中,我们使用了:

```go
statement, _ = db.Prepare("INSERT INTO books (name, author, isbn) VALUES (?, ?, ?)")
statement.Exec("A Tale of Two Cities", "Charles Dickens", 140430547)

如果传递不正确的值,例如导致 SQL 注入的字符串,则驱动程序会立即拒绝 SQL 操作。要从数据库中获取数据,请使用Query方法。它返回一个迭代器,该迭代器使用Next方法返回匹配查询的所有行。我们应该在循环中使用该迭代器进行处理,如以下代码所示:

****```go rows, _ := db.Query("SELECT id, name, author FROM books") var tempBook Book for rows.Next() { rows.Scan(&tempBook.id, &tempBook.name, &tempBook.author) log.Printf("ID:%d, Book:%s, Author:%s\n", tempBook.id, tempBook.name, tempBook.author) }


如果我们需要通过`SELECT`**语句的标准,该怎么办?然后,您应该准备一个语句,然后将通配符(?)数据传递给它。**

 **# 使用 go restful 构建 Metro Rail API

让我们利用上一节中获得的知识,为上一节中提到的城市地铁项目创建一个 API。路线图如下:

1.  设计一个 RESTAPI 文档。
2.  为数据库创建模型。
3.  实现 API 逻辑。

# 设计规范

在创建任何 API 之前,我们应该知道 API 的规范是以文档的形式出现的。我们在前面的章节中展示了一些示例,包括 URL shortener API 设计文档。让我们试着为这个地铁项目创建一个。请看下表:

| **HTTP 动词** | **路径** | **动作** | **资源** |
| `POST` | `/v1/train`(详见正文) | 创造 | 火车 |
| `POST` | `/v1/station`(详见正文) | 创造 | 火车站 |
| `GET` | `/v1/train/id` | 阅读 | 火车 |
| `GET` | `/v1/station/id` | 阅读 | 火车站 |
| `POST` | `/v1/schedule`(来源和目的地) | 创造 | 路线 |

我们还可以包括`UPDATE`和`DELETE`方法。通过实现前面的设计,显然用户可以自己实现它们。

# 创建数据库模型

让我们编写一些 SQL 字符串,用于为前面的火车、车站和路线资源创建表。我们将为此 API 创建一个项目布局。项目布局将如以下屏幕截图所示:

![](img/bd044302-d60b-436b-b223-7ea7454c6d0e.png)

我们在`$GOPATH/src/github.com/user/`中创建我们的项目。这里,用户是`narenaryan`,`railAPI`是我们的项目源,`dbutils`是我们自己处理数据库初始化实用程序功能的包。让我们从`dbutils/models.go`文件开始。我将在`models.go`文件中为列车、车站和时刻表分别添加三个模型:

```go
package dbutils

const train = `
      CREATE TABLE IF NOT EXISTS train (
           ID INTEGER PRIMARY KEY AUTOINCREMENT,
           DRIVER_NAME VARCHAR(64) NULL,
           OPERATING_STATUS BOOLEAN
        )
`

const station = `
        CREATE TABLE IF NOT EXISTS station (
          ID INTEGER PRIMARY KEY AUTOINCREMENT,
          NAME VARCHAR(64) NULL,
          OPENING_TIME TIME NULL,
          CLOSING_TIME TIME NULL
        )
`
const schedule = `
        CREATE TABLE IF NOT EXISTS schedule (
          ID INTEGER PRIMARY KEY AUTOINCREMENT,
          TRAIN_ID INT,
          STATION_ID INT,
          ARRIVAL_TIME TIME,
          FOREIGN KEY (TRAIN_ID) REFERENCES train(ID),
          FOREIGN KEY (STATION_ID) REFERENCES station(ID)
        )
`

这些是由反勾(```go字符分隔的普通多行字符串。时刻表保存在给定时间到达特定车站的列车信息。在这里,火车和车站是时刻表的外键。对于 train,与之相关的详细信息为列。包名为dbutils当我们提到包名时,该包中的所有 Go 程序都可以共享导出的变量和函数,而无需实际导入。

**现在,让我们添加代码来初始化init-tables.go文件中的(创建表)数据库:

package dbutils
import "log"
import "database/sql"
func Initialize(dbDriver *sql.DB) {
    statement, driverError := dbDriver.Prepare(train)
    if driverError != nil {
        log.Println(driverError)
    }
    // Create train table
    _, statementError := statement.Exec()
    if statementError != nil {
        log.Println("Table already exists!")
    }
    statement, _ = dbDriver.Prepare(station)
    statement.Exec()
    statement, _ = dbDriver.Prepare(schedule)
    statement.Exec()
    log.Println("All tables created/initialized successfully!")
}
```go

我们正在导入`database/sql`以在函数中传递参数类型。函数中的所有其他语句与前面代码中给出的 SQLite3 示例类似。它只是在 SQLite3 数据库中创建了三个表。我们的主程序应该将数据库驱动程序传递给这个函数。如果你在这里观察,我们不是在输入火车、车站和时刻表。但是,由于该文件位于`db utils`包中,因此可以在此处访问`models.go`中的变量。

现在我们的初始包完成了。您可以使用以下命令为此包生成目标代码:

go build github.com/narenaryan/dbutils

直到我们创建并运行主程序它才有用因此让我们编写一个简单的主程序`dbutils`包导入`Initialize`函数我们将该文件称为`main.go`

package main

import ( "database/sql" "log"

_ "github.com/mattn/go-sqlite3"
"github.com/narenaryan/dbutils"

)

func main() { // Connect to Database db, err := sql.Open("sqlite3", "./railapi.db") if err != nil { log.Println("Driver creation failed!") } // Create tables dbutils.Initialize(db) }

并使用以下命令从`railAPI`目录运行程序

go run main.go

您看到的输出应该如下所示

2017/06/10 14:05:36 All tables created/initialized successfully!

在前面的程序中我们添加了创建数据库驱动程序的代码并将表创建任务从`dbutils`包传递给`Initialize`函数我们可以直接在主程序中这样做但最好将逻辑分解为多个包和组件现在我们将扩展这个简单的布局使用`go-restful`包创建一个 APIAPI 应该实现 API 设计文档中的所有功能一旦我们运行主程序就会创建前面目录树图片中的`railapi.db`文件如果数据库文件不存在SQLite3 将负责创建数据库文件SQLite3 数据库是简单的文件您可以使用`$ sqlite3 file_name`命令进入 SQLite shell让我们把主程序修改成一个新程序在本例中我们将逐步了解如何使用`go-restful`构建 REST 服务首先向程序添加必要的导入

package main import ( "database/sql" "encoding/json" "log" "net/http" "time" "github.com/emicklei/go-restful" _ "github.com/mattn/go-sqlite3" "github.com/narenaryan/dbutils" )

我们需要两个外部包`go-restful``go-sqlite3`来构建 API 逻辑第一个包用于处理程序第二个包用于添加持久性特性`dbutils`**是我们之前创建的`time``net/http`软件包用于一般用途任务**

 **尽管 SQLite 数据库表中的列有具体的名称但在 GO 编程中我们需要一些结构来处理进出数据库的数据所有模型都应该有数据保持器因此我们将在下一步定义它们请看以下代码段

// DB Driver visible to whole program var DB *sql.DB // TrainResource is the model for holding rail information type TrainResource struct { ID int DriverName string OperatingStatus bool } // StationResource holds information about locations type StationResource struct { ID int Name string OpeningTime time.Time ClosingTime time.Time } // ScheduleResource links both trains and stations type ScheduleResource struct { ID int TrainID int StationID int ArrivalTime time.Time }

分配了`DB`**变量来保存全局数据库驱动程序以上所有结构都是 SQL 中数据库模型的精确表示Go `time.Time`结构类型实际上可以保存数据库中的`TIME`字段**

 **现在是实际的`go-restful`实现我们需要在`go-restful`中为我们的 API 创建一个容器然后我们应该将 web 服务注册到该容器中让我们编写`Register`函数如下代码片段所示

// Register adds paths and routes to container func (t *TrainResource) Register(container *restful.Container) { ws := new(restful.WebService) ws. Path("/v1/trains"). Consumes(restful.MIME_JSON). Produces(restful.MIME_JSON) // you can specify this per route as well ws.Route(ws.GET("/{train-id}").To(t.getTrain)) ws.Route(ws.POST("").To(t.createTrain)) ws.Route(ws.DELETE("/{train-id}").To(t.removeTrain)) container.Add(ws) }

`go-restful`中的 Web 服务主要基于资源工作在这里我们在`TrainResource`上定义一个名为`Register`的函数将容器作为参数我们创建一个新的`WebService`并为其添加路径路径是 URL 端点路由是附加到函数处理程序的路径参数或查询参数`ws`**是为服务`Train`资源而创建的 web 服务**我们将`GET``POST``DELETE`三种 REST 方法分别附加到三个函数处理程序`getTrain``createTrain``removeTrain`****

 ****```
Path("/v1/trains").
Consumes(restful.MIME_JSON).
Produces(restful.MIME_JSON)
```go

这些语句表示 API 在请求中将只接受`Content-Type`作为 application/JSON对于所有其他类型它会自动返回 415--Media Not Supported 错误返回的响应将自动转换为漂亮的 JSON我们还可以有 XMLJSON 等格式的列表`go-restful`提供了这一开箱即用的功能现在让我们定义函数处理程序

// GET http://localhost:8000/v1/trains/1 func (t TrainResource) getTrain(request *restful.Request, response *restful.Response) { id := request.PathParameter("train-id") err := DB.QueryRow("select ID, DRIVER_NAME, OPERATING_STATUS FROM train where id=?", id).Scan(&t.ID, &t.DriverName, &t.OperatingStatus) if err != nil { log.Println(err) response.AddHeader("Content-Type", "text/plain") response.WriteErrorString(http.StatusNotFound, "Train could not be found.") } else { response.WriteEntity(t) } } // POST http://localhost:8000/v1/trains func (t TrainResource) createTrain(request *restful.Request, response *restful.Response) { log.Println(request.Request.Body) decoder := json.NewDecoder(request.Request.Body) var b TrainResource err := decoder.Decode(&b) log.Println(b.DriverName, b.OperatingStatus) // Error handling is obvious here. So omitting... statement, _ := DB.Prepare("insert into train (DRIVER_NAME, OPERATING_STATUS) values (?, ?)") result, err := statement.Exec(b.DriverName, b.OperatingStatus) if err == nil { newID, _ := result.LastInsertId() b.ID = int(newID) response.WriteHeaderAndEntity(http.StatusCreated, b) } else { response.AddHeader("Content-Type", "text/plain") response.WriteErrorString(http.StatusInternalServerError, err.Error()) } } // DELETE http://localhost:8000/v1/trains/1 func (t TrainResource) removeTrain(request *restful.Request, response *restful.Response) { id := request.PathParameter("train-id") statement, _ := DB.Prepare("delete from train where id=?") _, err := statement.Exec(id) if err == nil { response.WriteHeader(http.StatusOK) } else { response.AddHeader("Content-Type", "text/plain") response.WriteErrorString(http.StatusInternalServerError, err.Error()) } }

所有这些 REST 方法都是在`TimeResource`结构的实例上定义的来到`GET`处理程序它正在传递`Request``Response`作为其参数可通过`request.PathParameter`**功能获取`path`参数传递给它的参数将与我们在前面的代码段中添加的路由一致也就是说`train-id`**将被返回到处理程序中以便我们可以剥离它并将其用作从 SQLite 数据库中获取记录的标准****

 ****`POST`处理函数中我们使用 JSON 包的`NewDecoder`**函数解析请求主体`go-restful`没有解析客户端发布的原始数据的功能有一些函数可用于剥离查询参数和表单参数但缺少这一个因此我们编写了自己的逻辑来剥离和解析 JSON 主体并使用这些结果将数据插入 SQLite 数据库该处理程序正在使用请求中提供的详细信息为列车创建`db`记录**

 **如果您了解前两个处理程序`DELETE`函数非常明显我们正在使用`DB.Prepare`**生成`DELETE`SQL 命令并返回 201 状态 OK告诉我们删除操作成功否则我们将实际错误作为服务器错误发回现在让我们编写主函数处理程序它是我们程序的入口点**

 **```
func main() {
    var err error
    DB, err = sql.Open("sqlite3", "./railapi.db")
    if err != nil {
        log.Println("Driver creation failed!")
    }
    dbutils.Initialize(DB)
    wsContainer := restful.NewContainer()
    wsContainer.Router(restful.CurlyRouter{})
    t := TrainResource{}
    t.Register(wsContainer)
    log.Printf("start listening on localhost:8000")
    server := &http.Server{Addr: ":8000", Handler: wsContainer}
    log.Fatal(server.ListenAndServe())
}
```go

这里的前四行执行与数据库相关的内务管理然后我们使用`restful.NewContainer`创建一个新的容器**然后我们正在为我们的容器使用名为`CurlyRouter`**允许我们在设置路由时在路径中使用`{train_id}`语法的路由器然后我们创建了一个`TimeResource`结构的实例并将该容器传递给`Register`方法该容器实际上可以充当 HTTP 处理程序因此我们可以轻松地将其传递到`http.Server`********

 ******使用`request.QueryParameter`**`go-restful`处理程序中的 HTTP 请求中获取查询参数**

 **此代码在 GitHub repo 中提供现在当我们在`$GOPATH/src/github.com/narenaryan`目录中运行`main.go`文件时我们看到

go run railAPI/main.go

并发出 curl`POST`请求创建一列

curl -X POST
http://localhost:8000/v1/trains
-H 'cache-control: no-cache'
-H 'content-type: application/json'
-d '{"driverName": "Menaka", "operatingStatus": true}'

这将创建一列包含驾驶员和运行状态详细信息的新列车响应为分配列车`ID`的新创建资源

{ "ID": 1, "DriverName": "Menaka", "OperatingStatus": true }

现在让我们做一个卷曲请求来检查`GET`

CURL -X GET "http://localhost:8000/v1/trains/1"

您将看到 JSON 输出如下所示

{ "ID": 1, "DriverName": "Menaka", "OperatingStatus": true }

我们可以对发布数据和返回的 JSON 使用相同的名称但是为了显示两个操作之间的差异使用了不同的变量名称现在使用`DELETE`API 调用删除我们在前面代码段中创建的资源

CURL -X DELETE "http://localhost:8000/v1/trains/1"

它不会返回任何响应体如果操作成功则返回`Status 200 ok`现在如果我们尝试在`ID`1 列车上执行`GET`那么它会返回以下响应

Train could not be found.

这些实现可以扩展到`PUT``PATCH.`我们需要在`Register`方法中向 web 服务添加两个以上的路由并定义各自的处理程序这里我们为`Train`资源创建了一个 web 服务以类似的方式可以创建 web 服务来在`Station`**`Schedule`表上执行 CRUD 操作这项任务留给读者去探索**

**`go-restful` is a lightweight library that is powerful in creating RESTful services in an elegant way. The main theme is to convert resources (models) into consumable APIs. Using other heavy frameworks may speed up the development, but the API can end up slower because of the wrapping of code. `go-restful` is a lean and low-level package for API creation.

`go-restful`还通过**招摇过市** REST API 的文档化提供内置支持**是一个运行并生成模板的工具用于记录我们构建的 REST API通过将其与基于`go-restful` web 服务集成我们可以动态生成文档欲了解更多信息请访问[https://github.com/emicklei/go-restful-swagger12](https://github.com/emicklei/go-restful-swagger12) 。**

 **# 使用 Gin 框架构建 RESTful API

`Gin-gonic`是基于`httprouter`的框架**我们在[第二章](04.html)中了解了`httprouter`*为我们的休息服务*处理路由它是一个类似 Gorilla Mux  HTTP 多路复用器但速度更快`Gin`允许高级 API 以干净的方式创建 REST 服务`Gin`将自己与另一个名为`martini` web 框架进行比较除了服务创建之外所有 web 框架都允许我们做更多的事情比如模板和 web 服务器设计使用以下命令安装`Gin`软件包**

 **```
go get gopkg.in/gin-gonic/gin.v1
```go

让我们在`Gin`中编写一个简单的 hello world 程序以熟悉`Gin`结构该文件为`ginBasic.go`

package main import ( "time" "github.com/gin-gonic/gin" ) func main() { r := gin.Default() /* GET takes a route and a handler function Handler takes the gin context object */ r.GET("/pingTime", func(c *gin.Context) { // JSON serializer is available on gin context c.JSON(200, gin.H{ "serverTime": time.Now().UTC(), }) }) r.Run(":8000") // Listen and serve on 0.0.0.0:8080 }

这个简单的服务器尝试实现一个向客户端提供 UTC 服务器时间的服务我们在[ 3 ](03.html)中使用中间件和 RPC 实现了一个这样的服务但是在这里如果你看`Gin`允许你用几行代码做很多事情所有的样板细节都被删除了在前面的程序中我们正在创建一个具有`gin.Default`**功能的路由器然后我们在`go-restful`中用 REST 动词附加 routes到函数处理程序的路由然后我们通过传递要运行的端口来调用`Run`**函数默认端口为`8080`****

 ****`c`是保存个人请求信息的`gin.Context`**我们可以先将数据序列化为 JSON然后再使用`context.JSON`函数将其发送回客户端现在如果我们运行并看到前面的程序**

 **```
go run ginExamples/ginBasic.go
```go

提出卷曲请求

CURL -X GET "http://localhost:8000/pingTime"

Output

{"serverTime":"2017-06-11T03:59:44.135062688Z"}

同时我们运行`Gin`服务器的控制台漂亮地显示了调试消息![](img/54d2bbb0-6c1c-466d-b187-1828e5283490.png)

它是 Apache 风格的调试日志显示端点请求的延迟和 REST 方法为了在生产模式下运行`Gin`设置`GIN_MODE = release`环境变量然后控制台输出将被静音日志文件可用于监视日志现在让我们在`Gin`中编写 Rail API展示如何使用`Gin`框架实现完全相同的东西我将使用相同的项目布局将我的新项目命名为`railAPIGin`并按原样使用`dbutils`首先让我们为我们的项目准备导入

package main import ( "database/sql" "log" "net/http" "github.com/gin-gonic/gin" _ "github.com/mattn/go-sqlite3" "github.com/narenaryan/dbutils" )

我们为数据库相关操作导入了`sqlite3``dbutils`我们导入了`gin`**来创建我们的 API 服务器`net/http`**有助于提供随响应一起发送的直观状态代码请看以下代码段****

 ****```
// DB Driver visible to whole program
var DB *sql.DB
// StationResource holds information about locations
type StationResource struct {
    ID int `json:"id"`
    Name string `json:"name"`
    OpeningTime string `json:"opening_time"`
    ClosingTime string `json:"closing_time"`
}
```go

我们创建了一个可用于所有处理程序函数的数据库驱动程序。`StationResource`是我们的 JSON 的占位符,它从请求体和来自数据库的数据中解码。如果您注意到,它是从`go-restful`的示例中稍微修改的。现在,让我们为`station`资源编写实现`GET`、`POST`和`DELETE`方法的处理程序:

// GetStation returns the station detail func GetStation(c *gin.Context) { var station StationResource id := c.Param("station_id") err := DB.QueryRow("select ID, NAME, CAST(OPENING_TIME as CHAR), CAST(CLOSING_TIME as CHAR) from station where id=?", id).Scan(&station.ID, &station.Name, &station.OpeningTime, &station.ClosingTime) if err != nil { log.Println(err) c.JSON(500, gin.H{ "error": err.Error(), }) } else { c.JSON(200, gin.H{ "result": station, }) } } // CreateStation handles the POST func CreateStation(c *gin.Context) { var station StationResource // Parse the body into our resrource if err := c.BindJSON(&station); err == nil { // Format Time to Go time format statement, _ := DB.Prepare("insert into station (NAME, OPENING_TIME, CLOSING_TIME) values (?, ?, ?)") result, _ := statement.Exec(station.Name, station.OpeningTime, station.ClosingTime) if err == nil { newID, _ := result.LastInsertId() station.ID = int(newID) c.JSON(http.StatusOK, gin.H{ "result": station, }) } else { c.String(http.StatusInternalServerError, err.Error()) } } else { c.String(http.StatusInternalServerError, err.Error()) } } // RemoveStation handles the removing of resource func RemoveStation(c *gin.Context) { id := c.Param("station-id") statement, _ := DB.Prepare("delete from station where id=?") _, err := statement.Exec(id) if err != nil { log.Println(err) c.JSON(500, gin.H{ "error": err.Error(), }) } else { c.String(http.StatusOK, "") } }

`GetStation`我们使用`c.Param`**剥离`station_id`路径参数之后我们将使用该 ID  SQLite3 station 表中获取数据库记录如果仔细观察SQL 查询会有点不同我们使用`CAST`方法将 SQL`TIME`字段作为字符串检索以便正确使用如果删除强制转换将引发紧急错误因为我们试图在运行时将`TIME`字段加载到 Go 字符串中让您了解一下`TIME`字段类似于*8:00:00**17:31:12*等等接下来如果没有错误我们将使用`gin.H`**方法返回结果****

 ****`CreateStation`**我们正在尝试执行插入查询但在此之前为了从`POST`请求的主体中获取数据我们使用了一个名为`c.BindJSON`的函数该函数将数据加载到作为参数传递的结构中这意味着 station 结构将加载 body 提供的数据这就是为什么`StationResource` JSON 推断字符串来告诉期望的键值例如这是带有推断字符串的`StationResource`结构的此类字段**

 **```
ID int `json:"id"`
```go

在收集数据之后,我们准备一个数据库 insert 语句并执行它。结果是插入记录的 ID。我们正在使用该 ID 将站点详细信息发送回客户端。在`RemoveStation`、**中,我们正在执行一个`DELETE`SQL 查询。如果操作成功,我们将返回 200 OK 状态。否则,我们将发送 500 内部服务器错误的适当原因。**

 **现在是主程序,它首先运行数据库逻辑以确保创建表。然后,它尝试创建一个`Gin`路由器并向其中添加路由:

func main() { var err error DB, err = sql.Open("sqlite3", "./railapi.db") if err != nil { log.Println("Driver creation failed!") } dbutils.Initialize(DB) r := gin.Default() // Add routes to REST verbs r.GET("/v1/stations/:station_id", GetStation) r.POST("/v1/stations", CreateStation) r.DELETE("/v1/stations/:station_id", RemoveStation) r.Run(":8000") // Default listen and serve on 0.0.0.0:8080 }

我们正在向`Gin`路由器注册`GET``POST``DELETE`路由然后我们将路由和处理程序传递给它们最后我们使用 Gin `Run`**功能启动服务器`8000`为端口按如下方式运行前面的程序**

 **```
go run railAPIGin/main.go
```go

现在我们可以通过执行`POST`请求来插入新记录

curl -X POST
http://localhost:8000/v1/stations
-H 'cache-control: no-cache'
-H 'content-type: application/json'
-d '{"name":"Brooklyn", "opening_time":"8:12:00", "closing_time":"18:23:00"}'

它返回

{"result":{"id":1,"name":"Brooklyn","opening_time":"8:12:00","closing_time":"18:23:00"}}

现在尝试使用`GET`获取详细信息

CURL -X GET "http://10.102.78.140:8000/v1/stations/1"

Output

{"result":{"id":1,"name":"Brooklyn","opening_time":"8:12:00","closing_time":"18:23:00"}}

我们还可以使用以下命令删除站点记录

CURL -X DELETE "http://10.102.78.140:8000/v1/stations/1"

它返回 200 OK 状态确认资源已成功删除正如我们已经讨论过的`Gin`在控制台上提供了直观的调试显示了附加的处理程序并用颜色突出显示了延迟和 REST 动词![](img/c1f2942f-5dfc-4fda-b9d3-7a9470ca687d.png)

例如`200`为绿色`404`为黄色`DELETE`为红色依此类推`Gin`提供许多其他功能如路由分类重定向和中间件功能

如果您正在快速原型化 REST web 服务请使用`Gin`框架您还可以将其用于其他许多事情如静态文件服务等请记住它是一个成熟的 web 框架要获取 Gin 中的查询参数请在`Gin`上下文对象上使用以下方法`c.Query("param")`。

# 使用 Revel.go 构建 RESTful API

Revel.go 也是一个像 Python  Django 一样成熟的 web 框架它比 Gin 更古老被称为高生产率的 web 框架它是一个异步模块化和无状态的框架与我们自己创建项目的`go-restful``Gin`框架不同Revel 生成了一个直接工作的脚手架使用以下命令安装`Revel.go`

go get github.com/revel/revel

为了运行 scaffold 工具我们应该再安装一个补充包

go get github.com/revel/cmd/revel

确保`$GOPATH/bin`在您的`PATH`变量中一些外部软件包将二进制文件安装在`$GOPATH/bin`目录中如果它在路径中我们可以在系统范围内访问可执行文件在这里Revel 安装了一个名为`revel`的二进制文件 Ubuntu  macOS X 您可以使用以下命令执行此操作

export PATH=$PATH:$GOPATH/bin

将前一行添加到`~/.bashrc`以保存设置 Windows 您需要根据其位置直接调用可执行文件现在我们准备从狂欢开始让我们在`github.com/narenaryan`中创建一个名为`railAPIRevel`的新项目

revel new railAPIRevel

这将创建一个项目脚手架而无需编写一行代码这就是 web 框架为快速原型化抽象事物的方式Revel 项目布局树如下所示
conf/             Configuration directory
    app.conf      Main app configuration file
    routes        Routes definition file

app/              App sources
    init.go       Interceptor registration
    controllers/  App controllers go here
    views/        Templates directory

messages/         Message files

public/           Public static assets
    css/          CSS files
    js/           Javascript files
    images/       Image files

tests/            Test suites
在所有这些样板目录中有三件事对于创建 API 非常重要这些是*   `app/controllers`
*   `conf/app.conf`
*   `conf/routes`

控制器是执行 API 逻辑的逻辑容器`app.conf`允许我们设置`host``port``dev`模式/生产模式等`routes`定义端点REST 谓词和函数处理程序此处为控制器的函数的三元组这意味着在控制器中定义函数并将其附加到 routes 文件中的 route让我们使用与`go-restful`相同的示例为列车创建 API但是在这里由于冗余我们将删除数据库逻辑我们将很快了解如何使用 Revel  API 构建`GET``POST``DELETE`操作现在 routes 文件修改为

Routes Config

This file defines all application routes (Higher priority routes first)

module:testrunner

module:jobs

GET /v1/trains/:train-id App.GetTrain POST /v1/trains App.CreateTrain DELETE /v1/trains/:train-id App.RemoveTrain

语法可能看起来有点新这是一个配置文件我们仅在其中以以下格式定义路由

VERB END_POINT HANDLER

我们还没有定义处理程序在端点中使用`:param`**符号访问路径参数这意味着对于文件中的`GET`请求`train-id`将作为`path`参数传递现在移动到`controllers`文件夹并将`app.go`文件中的现有控制器修改为**

 **```
package controllers
import (
    "log"
    "net/http"
    "strconv"
    "github.com/revel/revel"
)
type App struct {
    *revel.Controller
}
// TrainResource is the model for holding rail information
type TrainResource struct {
    ID int `json:"id"`
    DriverName string `json:"driver_name"`
    OperatingStatus bool `json:"operating_status"`
}
// GetTrain handles GET on train resource
func (c App) GetTrain() revel.Result {
    var train TrainResource
    // Getting the values from path parameters.
    id := c.Params.Route.Get("train-id")
    // use this ID to query from database and fill train table....
    train.ID, _ = strconv.Atoi(id)
    train.DriverName = "Logan" // Comes from DB
    train.OperatingStatus = true // Comes from DB
    c.Response.Status = http.StatusOK
    return c.RenderJSON(train)
}
// CreateTrain handles POST on train resource
func (c App) CreateTrain() revel.Result {
    var train TrainResource
    c.Params.BindJSON(&train)
    // Use train.DriverName and train.OperatingStatus to insert into train table....
    train.ID = 2
    c.Response.Status = http.StatusCreated
    return c.RenderJSON(train)
}
// RemoveTrain implements DELETE on train resource
func (c App) RemoveTrain() revel.Result {
    id := c.Params.Route.Get("train-id")
    // Use ID to delete record from train table....
    log.Println("Successfully deleted the resource:", id)
    c.Response.Status = http.StatusOK
    return c.RenderText("")
}
```go

我们在文件`app.go`中创建了 API 处理程序。这些处理程序名称应该与我们在 routes 文件中提到的名称匹配。我们可以使用以`*revel.Controller`为成员的结构创建 Revel 控制器。然后,我们可以将任意数量的处理程序附加到它。控制器保存传入 HTTP 请求的信息,以便我们可以在处理程序中使用查询参数、路径参数、JSON 正文、表单数据等信息。

我们将`TrainResource`**定义为数据持有者。在`GetTrain`、**中,我们使用`c.Params.Route.Get`**函数获取路径参数。该函数的参数是我们在路由文件中指定的路径参数(此处为`train-id`)。该值将是一个字符串。我们需要将其转换为`Int`类型以将其映射到`train.ID`。**然后,我们使用`c.Response.Status`变量(非函数)将响应状态设置为`200 OK`。`c.RenderJSON`获取一个结构并将其转换为 JSON 主体********

 ******在`CreateTrain,`**中,我们添加了`POST`请求逻辑。我们正在创建一个新的`TrainResource`结构,并将其传递给一个名为`c.Params.BindJSON`的函数。**`BindJSON`**所做的是从 JSON`POST`主体中提取参数,并尝试在结构中找到匹配字段并填充。当我们将 Go 结构封送到 JSON 时,字段名将按原样转换为键。但是,如果我们将``jason:"id"``字符串格式附加到任何结构字段,它明确表示从该结构封送的 JSON 应该具有键`id`,而不是**ID**。在使用 JSON 时,这是 Go 中的一个良好实践。然后,我们将在 HTTP 响应中添加状态 201 created。我们将返回 train 结构,它将在内部转换为 JSON。******

 ****`RemoveTrain`**处理程序逻辑与`GET`类似。一个微妙的区别是身体里没有任何东西。如前所述,前面的示例中省略了数据库 CRUD 逻辑。读者可以通过观察我们在`go-restful`和`Gin`部分中所做的工作来尝试添加 SQLite3 逻辑。**

 **最后,Revel 服务器运行的默认端口是`9000`。更改端口号的配置在`conf/app.conf`**文件中。让我们遵循在`8000`上运行应用程序的传统。因此,将文件的`http`端口部分修改为以下内容。这会告诉 Revel 服务器在不同的端口上运行:**

 **```
......
# The IP address on which to listen.
http.addr =

# The port on which to listen.
http.port = 8000 # Change from 9000 to 8000 or any port

# Whether to use SSL or not.
http.ssl = false
......
```go

现在,我们可以使用以下命令运行 Revel API 服务器:

revel run github.com/narenaryan/railAPIRevel

我们的应用服务器从`http://localhost:8000`开始现在让我们提出几个 API 请求

CURL -X GET "http://10.102.78.140:8000/v1/trains/1"

Output

{ "id": 1, "driver_name": "Logan", "operating_status": true }

`POST`请求

curl -X POST
http://10.102.78.140:8000/v1/trains
-H 'cache-control: no-cache'
-H 'content-type: application/json'
-d '{"driver_name":"Magneto", "operating_status": true}'

Output

{ "id": 2, "driver_name": "Magneto", "operating_status": true }


`DELETE`与`GET`相同,但未返回尸体。这里,对代码进行了说明,以说明如何处理请求和响应。记住,Revel 不仅仅是一个简单的 API 框架。它是一个成熟的 web 框架,类似于 Django(Python)或 RubyonRails。我们有模板,测试,和更多的内在狂欢。

确保您为`GOPATH/user`创建了一个新的 Revel 项目。否则,您的 Revel 命令行工具在运行项目时可能找不到该项目。

我们在本章中看到的所有 web 框架都支持中间件。`go-restful`**将其中间件命名为`Filters`,而`Gin`将其命名为定制中间件。Revel 称其中间件为拦截器。中间件分别在函数处理程序之前和之后读取或写入请求和响应。在[第 3 章](03.html)中*使用中间件和 RPC*时,我们将进一步讨论中间件。**

 **# 总结

在本章中,我们试图借助 Go 中提供的一些 web 框架构建 Metro Rail API。最受欢迎的是`go-restful`、`Gin Gonic`和`Revel.go`。我们从学习 Go 应用程序中的第一个数据库集成开始。我们选择了 SQLite3,并尝试使用`go-sqlite3`库编写一个示例应用程序。

接下来,我们探索了`go-restful`并详细研究了如何创建路由和处理程序。`go-restful`具有在资源之上构建 API 的概念。它提供了一种直观的方法来创建可以使用和生成各种格式(如 XML 和 JSON)的 API。我们将列车用作一种资源,并构建了一个 API 来对数据库执行 CRUD 操作。我们解释了为什么`go-restful`是轻量级的,可以用来创建低延迟 API。接下来,我们看到了`Gin`框架,并尝试重复相同的 API,但围绕站点资源创建了一个 API。我们了解了如何在 SQL 数据库时间字段中存储时间。我们建议`Gin`快速原型化您的 API。

最后,我们尝试在 train 资源上创建另一个 API,但这次使用的是`Revel.go`web 框架。我们开始创建一个项目,检查目录结构,然后继续编写一些服务(没有`db`集成)。我们还了解了如何使用配置文件运行应用程序和更改端口。

本章的主题是向您介绍一些用于创建 RESTful API 的精彩框架。每个框架可能会做不同的事情,请选择一个您熟悉的框架。需要端到端 web 应用程序(模板和 UI)时使用`Revel.go`,快速创建 REST 服务时使用`Gin`,API 性能关键时使用`go-rest`****************************************************************************************