接下来的内容,我们将重点介绍如何为GET /v1/movies接口构建高级功能,该接口将以JSON数组的形式返回多个movie详细信息。
Method | URL | 动作 |
---|---|---|
GET | /v1/healthcheck | 显示应用程序运行状况和版本信息 |
POST | /v1/movies | 添加新的电影 |
GET | /v1/movies/:id | 根据id查询特定电影 |
PUT | /v1/movies/:id | 更新特定电影 |
DELETE | /v1/movies/:id | 删除特定电影 |
GET | /v1/movies | 查询电影详情列表 |
我们将逐步为这个接口开发额外功能,首先返回所有影片的数据,然后通过添加过滤、排序和分页功能逐渐使其变得更实用。
在后面的内容中你将学到:
- 在一个JSON响应中返回多个资源的详细信息。
- 接受并应用可选参数来缩小返回的数据集。
- 使用PostgreSQL的内置功能在数据库字段上实现全文搜索。
- 接受并安全地使用排序参数来对数据库查询结果排序。
- 开发一个实用、可重用的模式来支持大数据集分页,并在JSON响应中返回分页元数据。
下面开始第一部分内容:解析API请求中的查询参数。
解析查询参数
在接下来的几节中,我们将配置GET /v1/movies接口,以便客户端能够通过查询字符串参数返回所需电影数据。例如:
/v1/movies?title=godfather&genres=crime,drama&page=1&page_size=5&sort=-year
如果客户端发送以上查询请求,意味着向接口传递的信息是:请返回电影名称包含"godfather",电影类型是crime和drama类型,根据年份降序排序的前5条数据。
在sort参数中使用“-“符号表示结果降序排序。例如,参数sort=title指的是根据电影名称按字母升序排序,而sort=-title就是降序。
首先,需要做的就是如何将这些参数解析到Go代码中。在Go中可以使用r.URL.Query()函数来解析查询参数。该函数返回url.Values()类型,是一个包含查询参数的map类型。我们可以使用Get()方法提取查询参数,如果参数值存在就返回否则返回空字符串。
在我们的示例中,还需要对其中一些查询字符串值执行额外的处理。具体地说:
- genre参数可能会包含多个用逗号隔开的值,例如:genres=crime,drama。我们需要将这些值分开并存放在一个[]string切片中。
- page和page_size参数值是数字,需要将字符串转为int类型。
除此之外:
- 还需要对这些参数做校验,例如page和page_size不能是负数。
- 如果page,page_size和sort客户端没有提供值的话,需要设置默认值。
创建帮助函数
为此,我们将创建三个新的帮助函数:readString()、readInt()和readCSV()。我们将使用这些帮助函数从查询字符串中提取和解析值,或者在必要时返回一个默认值。
在cmd/api/helpers.go文件添加以下代码:
// readString 从查询字符串中返回一个字符串值,如果没有匹配的key就返回默认值
func (app *application)readString(qs url.Values, key string, defaultValue string) string {
s := qs.Get(key)
if s == ""{
return defaultValue
}
return s
}
// readCSV 从查询中读取一个字符串并根据逗号分割,返回一个字符串切片
func (app *application)readCSV(qs url.Values, key string, defaultValue []string) []string {
csv := qs.Get(key)
if csv == "" {
return defaultValue
}
return strings.Split(csv, ",")
}
// readInt 从查询字符串中读取值,并将字符串值转为int类型
func (app *application)readInt(qs url.Values, key string, defaultValue int, v *validator.Validator) int {
s := qs.Get(key)
if s == "" {
return defaultValue
}
i, err := strconv.Atoi(s)
if err != nil {
v.AddError(key, "must be integer value")
return defaultValue
}
return i
}
添加API处理程序和路由
接下来,我们为GET /v1/movies接口创建新的API处理程序:listMoviesHandler。为了演示请求参数解析,当前这个处理程序仅使用帮助函数来解析请求中的查询参数,将解析出来的参数返回给客户端。
如果你跟随本系列文章操作的话,接下来创建listMoviesHandler,如下所示:
File: cmd/api/movies.go
package main
...
func (app *application)listMoviesHandler(w http.ResponseWriter, r *http.Request) {
var input struct{
Title string
Genres []string
Page int
PageSize int
Sort string
}
v := validator.New()
qs := r.URL.Query()
input.Title = app.readString(qs, "title", "")
input.Genres = app.readCSV(qs, "genres", []string{})
input.Page = app.readInt(qs, "page", 1, v)
input.PageSize = app.readInt(qs, "page_size", 20, v)
input.Sort = app.readString(qs, "sort", "id")
//检查校验是否通过
if !v.Valid(){
app.failedValidationResponse(w, r, v.Errors)
return
}
//将读取数据返回给客户端
fmt.Fprintf(w, "%+v\n", input)
}
然后我们需要为这个处理程序添加路由,在cmd/api/routes.go文件中添加以下代码:
File: cmd/api/routes.go
func (app *application) routes() http.Handler {
router := httprouter.New()
router.NotFound = http.HandlerFunc(app.notFoundResponse)
router.MethodNotAllowed = http.HandlerFunc(app.methodNotAllowedResponse)
router.HandlerFunc(http.MethodGet, "/v1/healthcheck", app.healthcheckHandler)
// 为GET /v1/movies接口添加路由
router.HandlerFunc(http.MethodGet, "/v1/movies", app.listMoviesHandler)
router.HandlerFunc(http.MethodPost, "/v1/movies", app.createMovieHandler)
router.HandlerFunc(http.MethodGet, "/v1/movies/:id", app.showMovieHandler)
router.HandlerFunc(http.MethodPatch, "/v1/movies/:id", app.updateMovieHandler)
router.HandlerFunc(http.MethodDelete, "/v1/movies/:id", app.deleteMovieHandler)
return app.recoverPanic(app.rateLimit(router))
}
重启服务后,就可以向GET /v1/movies接口发送带有查询参数的请求。如下所示:
使用curl发送带查询参数请求时,url必须使用双引号。
$ curl "localhost:4000/v1/movies?title=godfather&genres=crime,drama&page=1&page_size=5&sort=year"
{Title:godfather Genres:[crime drama] Page:1 PageSize:5 Sort:year}
看起来不错,请求中的查询参数都正确解析成Go结构体类型字段。还可以尝试不使用查询字符串参数发出请求。在本例中,您应该看到input结构中的值采用了我们在listMoviesHandler代码中指定的默认值。像这样:
$ curl localhost:4000/v1/movies
{Title: Genres:[] Page:1 PageSize:20 Sort:id}
创建Filters结构体
page,page_size和sort查询参数比较常用,在其他API查询接口中也需要使用。因此,为了查询简单,我们将其放在一个可复用的Filters结构体中。
创建internal/data/filters.go文件:
touch internal/data/filters.go
添加以下代码:
package data
type Filters struct {
Page int
PageSize int
Sort string
}
完成以上操作后,回到listMoviesHandler处理程序,更新代码使用Filters结构体:
File: cmd/api/movies.go
package main
....
func (app *application) listMoviesHandler(w http.ResponseWriter, r *http.Request) {
var input struct {
Title string
Genres []string
data.Filters
}
v := validator.New()
qs := r.URL.Query()
input.Title = app.readString(qs, "title", "")
input.Genres = app.readCSV(qs, "genres", []string{})
input.Filters.Page = app.readInt(qs, "page", 1, v)
input.Filters.PageSize = app.readInt(qs, "page_size", 20, v)
input.Filters.Sort = app.readString(qs, "sort", "id")
//检查校验是否通过
if !v.Valid() {
app.failedValidationResponse(w, r, v.Errors)
return
}
//将读取数据返回给客户端
fmt.Fprintf(w, "%+v\n", input)
}
此时,您应该能够再次运行API,并且一切都应该像前面一样正常工作。