在构建微服务时,事实上的方法是使用HTTP(作为协议)和REST(作为表示资源的方式)来做。HTTP实际上是万维网的基础,它被多个程序以某种方式支持,所以使用HTTP是不费吹灰之力的,因为它可以被任何前端和后端代码使用,几乎没有外部依赖性。
REST是Roy Fielding在其博士论文中提出的REpresentational State Transfer的缩写,它是一种架构风格,定义了构建网络服务时的准则,它不是一个标准,但它确实使用了一些标准,如HTTP(及其表示行动的动词)和有效载荷/消息格式(如JSON或XML),等等。
HTTP处理程序介绍
Go在其标准库中包含了实现Webservices的所有必要构件,包括HTTP服务器、不同的多路复用器和定义HTTP处理程序的方法,以及以不同的编码(如JSON或XML)呈现内容的方法,类似于以下创建HTTP服务器的做法。
package main
import (
"fmt"
"net/http"
)
func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Path: %s!", r.URL.Path[1:])
})
http.ListenAndServe(":8080", nil)
}
使用curl 来访问它。
❯ curl -X GET http://localhost:8080/resources
Path: resources!
使用标准库来定义我们的HTTP资源肯定是可行的,然而在使用标准库时,最大的困难之一是需要添加额外的代码来构建恰好具有相同基本路径的处理程序,或者需要变量作为模式匹配。
例如,假设我们需要为/resources,/resources/{id} 和/resources/{id}/values 定义资源,使用标准库将要求我们在处理完/resources**后,**定义代码来识别/resources/{id} 和/resources/{id}/values 。
这在请求这样的东西时可以得到更好的解释。
❯ curl -X GET http://localhost:8080/resources/1234/values
Path: resources/1234/values!
在这种情况下,我们实现的处理程序需要从路径本身确定这些参数。
对于这样的情况,我推荐使用一个第三方包,叫做 github.com/gorilla/mux.
使用github.com/gorilla/mux
github.com/gorilla/mux 是一个强大的URL路由器和调度器,它可以让我们实现旨在由不同选项匹配的处理程序,包括路径、方案或查询值,仅举几例。为了解决我们之前的问题,即处理程序需要我们定义代码来确定其他资源,我们可以写一些类似的东西。
package main
import (
"fmt"
"log"
"net/http"
"time"
"github.com/gorilla/mux"
)
func main() {
router := mux.NewRouter()
router.HandleFunc("/resources", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "/resources")
})
router.HandleFunc("/resources/{id:[0-9]+}", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "/resources/{id:[0-9]+}: %s!", mux.Vars(r)["id"])
})
router.HandleFunc("/resources/{id:[0-9]+}/values", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "/resources/{id:[0-9]+}/values: %s", mux.Vars(r)["id"])
})
srv := &http.Server{
Handler: router,
Addr: ":8080",
WriteTimeout: 15 * time.Second,
ReadTimeout: 15 * time.Second,
}
log.Fatal(srv.ListenAndServe())
}
然后我们就可以像以前那样使用curl ,但这次三个不同的资源和明确的独立处理。
❯ curl http://localhost:8080/resources
/resources
❯ curl http://localhost:8080/resources/123
/resources/{id:[0-9]+}: 123!
❯ curl http://localhost:8080/resources/456/values
/resources/{id:[0-9]+}/values: 456
实现资源
我之前在《领域驱动设计和项目布局》一文中提到了六边形架构,以它为基础,我们可以定义一个新的类型 TaskHandler来定义代表我们资源的所有不同的名词。
func (t *TaskHandler) Register(r *mux.Router) {
r.HandleFunc("/tasks", t.create).Methods(http.MethodPost)
r.HandleFunc(fmt.Sprintf("/tasks/{taskId:%s}", uuidRegEx), t.task).Methods(http.MethodGet)
r.HandleFunc(fmt.Sprintf("/tasks/{taskId:%s}", uuidRegEx), t.update).Methods(http.MethodPut)
}
这些资源的命名通常被定义为集合,在我们的例子中是/tasks ,根据需要,我们可以定义子集合来表示与属于我们父集合的实体相关的集合,所以一个例子是一个假想的资源,叫做/tasks/{taskId}/categories 。
在我们的 "To Do Microservice "领域的背景下,我们定义了三个方法,它们正好映射到三个不同的HTTP动词和我们的TaskService 应用服务中定义的三个不同的动作,这些HTTP动词通常映射到CRUD操作。
- POST -> 创建
- GET -> 读取
- PUT -> 更新
- DELETE -> 删除
因此,我们上面定义的类型是这样映射的。
POST /tasks-> 用来创建任务。(*TaskHandler).createGET /tasks/{id}-> 用来按id检索任务,以及(*TaskHandler).taskPUT /tasks/{id}-> 用来按id更新任务(*TaskHandler).update- 这次我们没有实现
DELETE /tasks/{id},但在未来的提交中我们会看到它出现。
实现所需的资源还需要我们定义处理请求和响应的具体类型,我喜欢推荐的实现方式是定义具有以下命名的类型<MethodName><ResourceName><Request|Response> ,例如,代表通过create 创建Task 的有效载荷的类型将是。
type CreateTasksRequest struct {
Description string `json:"description"`
}
而来自task 的响应类型将是。
type GetTasksResponse struct {
Task Task `json:"task"`
}
虽然有点重复,但为请求和响应定义具体的类型可以让我们在定义OpenAPI文档时对这些值定义更多的规则,这将在以后的文章中讨论。
总结
通过这篇文章,我们拉开了描述构建REST APIs步骤的序幕,在使用标准库时,构建基于HTTP的webservices相对简单,但是当试图处理嵌套或参数化的资源时,事情会变得困难,在这些情况下,使用第三方包(如github.com/gorilla/mux )是有意义的。
因为我们正在使用领域驱动设计和六边形架构,我们可以明确指出代表我们试图建模的资源的类型,将HTTP动词映射到具体的方法,并为请求和响应定义特定的类型,让我们为我们需要通过OpenAPI 3添加文档的变化铺平道路。