Go 使用流行框架简化RESTful 服务详解

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

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

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

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。

所有 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 创建一个项目布局。项目布局将如以下屏幕截图所示:

![](/github/go/rest/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`包创建一个 API。API 应该实现 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。我们还可以有 XML、JSON 等格式的列表。`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, OPERATINGSTATUS 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, OPERATINGSTATUS) 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`的框架。**我们在[第二章](go-rest-tutorial-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 章](go-rest-tutorial-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"

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


同时,我们运行`Gin`服务器的控制台漂亮地显示了调试消息:

![](/github/go/rest/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, CLOSINGTIME) 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"

{"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 动词:

![](/github/go/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 文件修改为:

module:testrunner

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"

{ "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}'

{ "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 章](go-rest-tutorial-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`****************************************************************************************

教程来源于Github,感谢apachecn大佬的无私奉献,致敬!

技术教程推荐

趣谈网络协议 -〔刘超〕

设计模式之美 -〔王争〕

动态规划面试宝典 -〔卢誉声〕

物联网开发实战 -〔郭朝斌〕

容量保障核心技术与实战 -〔吴骏龙〕

深入浅出分布式技术原理 -〔陈现麟〕

计算机基础实战课 -〔彭东〕

LangChain 实战课 -〔黄佳〕

PPT设计进阶 · 从基础操作到高级创意 -〔李金宝(Bobbie)〕