Restful Web Services With Go (5)
文章目录
学习目标:
- REST API 框架 go-restful
- SQLite3 基础以及CRUD操作
- 使用go-restful 创建 MEtro Rail API
- 通过 Gin 框架创建RESTful API
go-restful 简介
-
首先安装需要用到的数据库包
1 2 3 4
# Ubuntu apt-get install sqlite3 libsqlite3-dev # MAC OS X brew install sqlite3
-
安装 go-restful 包
1
go get github.com/emicklei/go-restful
接下来我们通过一个实现一个简单的 ping server 来简单的了解一下 go-restful 的使用
-
创建项目
1 2 3
mkdir basicExample && cd basicExample touch main.go go mod init example
-
创建一个函数,将服务器时间作为返回响应。
1 2 3 4
func pingTime(req *restful.Request, resp *restful.Response){ // write to the response io.WriteString(resp, fmt.Sprintf("%s", time.Now())) }
-
主要逻辑
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
package main import ( "fmt" "io" "net/http" "time" "github.com/emicklei/go-restful" ) 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) }
-
运行并验证
1 2 3 4
go run main.go ➜ ~ curl -X GET "http://localhost:8000/ping" 2020-03-22 14:55:00.553986 +0800 CST m=+24.083065640
可以看到 go-restful 从创建到添加路由都很简单,并且 http.ListenAdnServer 中没有添加 ServeMux ,框架已经帮我们处理好了。
SQLite3 基本操作
SQLite3 是一个轻量级,以文件为基础的SQL数据库
Go语言中对SQLite3的调用我们可以使用 go-sqlite3 这个库
|
|
接下来我们通过例子,来学习简单使用
-
创建项目目录
1 2 3
mkdir sqliteExample && sqliteExample touch main.go go mod init example
-
通多代码创建数据库
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
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") 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() dbOperations(db) }
这里首先需要注意的是我们导入了
go-sqlite3
但是却忽略了它,真正使用的还是Go自带的database/sql
。这相当于Go自带的 sql 是一个接口,我们实际需要的的数据库操作为对它的实现;这样就可以统一数据库操作的接口,方便切换调用不同数据库引擎。同时这里用到了数据库创建以及表格操作,使用 exec 方法实际执行操作。
-
上面还有一个自定义的函数 dbOperations 这是对数据库的一些具体操作,比如创建一本书,读取,更新,删除等一系列操作
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
func dbOperations (db *sql.DB) { // 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("双城记", 1) log.Printf("Successfully updated the book in database!") // Read Again rows, _ = db.Query("SELECT id, name, author FROM books") var tempBook2 Book for rows.Next() { rows.Scan(&tempBook2.id, &tempBook2.name, &tempBook2.author) log.Printf("ID: %d, Book:%s, Author:%s\n", tempBook2.id, tempBook2.name, tempBook2.author) } //Delete statement, _ = db.Prepare("delete from books where id=?") statement.Exec(1) log.Println("Successfully deleted the book in database!") }
-
运行并验证
1 2 3 4 5 6 7
➜ sqliteExample go run main.go 2020/03/22 15:54:26 Successfully created table books! 2020/03/22 15:54:26 Inserted the book into database! 2020/03/22 15:54:26 ID: 1, Book:A Tale of Two Cities, Author:Charles Dickens 2020/03/22 15:54:26 Successfully updated the book in database! 2020/03/22 15:54:26 ID: 1, Book:双城记, Author:Charles Dickens 2020/03/22 15:54:26 Successfully deleted the book in database!
从代码中我们看到,每次执行SQL语句都先调用 Prepare 函数,然后再传入需要的值;这样做的好处是避免应传入非法的字符串而导致SQL注入,因为数据库引擎默认已经做了一些防注入的措施。
使用go-restful 创建 Metro Rail API
API 设计说明
HTTP verb | Path | Action | Resource |
---|---|---|---|
POST | /v1/train (details as JSON body) | Create | Train |
POST | /v1/station (details as JSON body) | Create | Station |
GET | /v1/train/id | Read | Train |
GET | /v1/station/id | Read | Station |
POST | /v1/schedule (source and destination) | Create | Route |
除了这些,如果有需要还可以添加 UPDATE 和 DELETE 方法。
创建数据库 models
数据库需要创建的表为 train
, station
, route
。项目目录结构为:
|
|
- 我们先编写 models.go
|
|
-
接下来是数据库的初始化操作, 编辑 init-tables.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
package dbutils import ( "database/sql" "log" ) 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!") }
上面的代码创建了三张表,主函数调用的时候需要向函数传入相应的数据库驱动。
-
接下来编辑主程序 main.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
package main import ( "database/sql" "log" "railapi/dbutils" _ "github.com/mattn/go-sqlite3" ) 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) }
-
运行程序
1 2
➜ railAPI go run main.go 2020/03/22 17:39:06 All tables created/initialized successfully!
接下来就是根据之前的API设计说明文档来编写路由以及处理函数了
-
导入必要的包
1 2 3 4 5 6 7 8 9 10 11 12 13 14
package main import ( "database/sql" "encoding/json" "log" "net/http" "time" "github.com/emicklei/go-restful" _ "github.com/mattn/go-sqlite3" "railapi/dbutils" )
-
创建接收数据库信息的结构体
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
// 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 infomation about locations type StationResource struct { ID int Nmae 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 }
-
对每个资源编写路由及处理函数操作,并注册到 container
1 2 3 4 5 6 7 8 9
// Register adds paths and routes to a new service instance func (t *TrainResource) Register( container *restful.Container) { ws := new(restful.WebService) ws.Path("/v1/trains").Consumes(restful.MIME_JSON).Produces(restful.MIME_JSON) 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) }
这段代码创建了一个 server 并向它添加了路由与操作函数的对应。这里需要注意的是ws.Path这个方法,请求必须为json格式,如果不是会报错
415--Media Not Supported
。这里响应也返回了 json 格式,但是也可以自定义为其他比如说 XML。 -
定义上面提到的路由处理函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46
// 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) // write a struct as JSON to a response } } // 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, OPRATING_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()) } }
-
实现主函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
func main() { var err error // Connect to Database DB, err = sql.Open("sqlite3", "./railapi.db") if err != nil { log.Println("Driver creation failed") } // Create tables 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()) }
-
运行测试
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
go run main.go curl -X POST http://127.0.0.1:8000/v1/trains -H 'cache-control: no-cache' -H 'content-type: application/json' -d '{"driverName": "Veronica", "operatingStatus": true}' { "ID": 1, "DriverName": "Veronica", "OperatingStatus": true } curl -X GET "http://localhost:8000/v1/trains/1" { "ID": 1, "DriverName": "Veronica", "OperatingStatus": true } curl -X DELETE "http://localhost:8000/v1/trains/1" curl -X GET "http://localhost:8000/v1/trains/1" Train could not be found.
这样我们就完成了对 TrainResource 的增删改查操作,类似的剩下的 Station 和 Schedule 表的操作请自行尝试完成。
go-restful 同样提供内置的功能支持 swagger ,用于生成API文档。具体使用可以查看文档:https://github.com/emicklei/go-restful-swagger12
使用Gin框架编写RESTful API
Gin-Gonic 框架基于 httprouter
,这个路由管理器的一大特点就是快。Gin 提供了更高阶的API,这使得创建REST services 更加高效。
安装Gin包的命令是:
|
|
接下来通过看看Gin的简单使用:
-
创建项目,初始化
1 2 3
mkdir ginExample && cd ginExample touch main.go go mod init example
-
编写 main.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
package main import ( "time" "github.com/gin-gonic/gin" ) func main() { r := gin.Default() 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") }
-
运行并检测
1 2 3 4 5 6 7 8 9 10 11 12 13 14
➜ ginExample go run main.go [GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached. [GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production. - using env: export GIN_MODE=release - using code: gin.SetMode(gin.ReleaseMode) [GIN-debug] GET /pingTime --> main.main.func1 (3 handlers) [GIN-debug] Listening and serving HTTP on :8000 [GIN] 2020/03/23 - 16:11:32 | 200 | 101.572µs | 127.0.0.1 | GET "/pingTime" curl http://localhost:8000/pingTime {"serverTime":"2020-03-23T08:11:32.719289Z"}
我们再来写一遍之前的 Metro Rail API 来看看换不同框架是怎么的体验。结构目录跟之前的一样 railAPIGin 目录,包含dbutils包
-
导入需要使用到的包
1 2 3 4 5 6 7 8 9 10 11
import ( "database/sql" "log" "net/http" "example/dbutils" "github.com/gin-gonic/gin" _ "github.com/mattn/go-sqlite3" )
-
定义数据库驱动,表示 station 的 struct
1 2 3 4 5 6 7 8 9 10
// 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"` }
StationResource 有两个作用,可以接收通过HTTP POST过来的请求数据,第二种是接收来自数据的数据
-
接下来分别实现基于 GET, POST, DELETE 的路由函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55
// 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 resource 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 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, "") } }
第一个函数作用于GET请求,其中
c.Param
用于提取路径参数 station_id,我们用这个ID值去数据库取值;这里的原生SQL语句使用到了 CAST ,它的作用是将 SQL TIME字段转换为Go语言的字符串,否则会报类型错误。第二个函数对应POST请求,这需要向数据库插入数据。使用
c.BindJSON
方法从 request body 中取出数据,将它加载到传入的参数的结构体中。第三个函数对应DELETE请求,使用了SQL语言的 DELETE 语句。
-
最后完成主函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
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") }
-
运行并检测
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
curl -X POST http://127.0.0.1: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"}} curl -X GET "http://localhost:8000/v1/stations/1" {"result":{"id":1,"name":"Brooklyn","opening_time":"8:12:00","closing_time":"18:23:00"}} curl -X DELETE "http://localhost:8000/v1/stations/1" ➜ ginExample go run main.go 2020/03/23 21:02:43 All tables created/initialized successfully! [GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached. [GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production. - using env: export GIN_MODE=release - using code: gin.SetMode(gin.ReleaseMode) [GIN-debug] GET /v1/stations/:station_id --> main.GetStation (3 handlers) [GIN-debug] POST /v1/stations --> main.CreateStation (3 handlers) [GIN-debug] DELETE /v1/stations/:station_id --> main.RemoveStation (3 handlers) [GIN-debug] Listening and serving HTTP on :8000 [GIN] 2020/03/23 - 21:05:01 | 200 | 913.033µs | 127.0.0.1 | POST "/v1/stations" [GIN] 2020/03/23 - 21:06:11 | 200 | 176.74µs | 127.0.0.1 | GET "/v1/stations/1" [GIN] 2020/03/23 - 21:07:06 | 200 | 111.08µs | 127.0.0.1 | DELETE "/v1/stations/1"