开发人员曾经使用单片机架构构建基于云的应用程序,他们通常将整个应用逻辑嵌入一个进程,并在单个服务器计算机内运行。但是,单体架构模式为现代网络应用程序后端带来了扩展挑战和可维护性问题。
现在,几乎所有的开发者都使用微服务架构来避免这些问题。我们可以通过使用Go来做到这一点,Go是一种快速、简单、通用的、对开发者友好的编程语言。
我们还可以使用Gin框架,它为你提供了构建RESTful现代微服务所需的所有功能。在本教程中,我将解释如何用Gin在Go中构建微服务。
Gin的突出特点
Gin是Go生态系统中的一个功能齐全、高性能的HTTP网络框架。由于以下特点,它在Gophers(Go开发者)中每天都变得越来越受欢迎。
性能方面
Gin带有一个非常快速和轻量级的Go HTTP路由库(见详细的基准测试)。它使用了一个自定义版本的轻量级HttpRouter路由库,它使用了快速的、基于Radix树的路由算法。
灵活、可扩展、对开发者友好的API
Gin的中间件系统可以让你随心所欲地扩展该框架。它还允许你根据你的需要定制HTTP服务器实例。Gin为开发者提供了一个富有成效的API,其功能包括路由分组、结构绑定、内置验证器等。
其他内置的功能
- XML/JSON/YAML/ProtoBuf渲染
- 错误管理和日志记录
- JSON验证
- 静态文件服务功能
Gin与其他流行软件包的比较
Gin提供了一个具有竞争力的快速HTTP路由实现。Gin比其他流行的路由库和网络框架更快。它由许多开源贡献者积极维护,经过良好的测试,并且API被锁定。因此,未来的Gin版本不会破坏你现有的微服务。
我们也可以使用内置的Gonet/http
包来构建微服务,但它不提供参数化路由。你可以使用Gorilla mux作为你的路由库,但与Gin相比,Gorilla mux并不是一个功能全面的网络框架,它只是一个HTTP请求复用器。Gorilla mux不提供内置的数据渲染、JSON绑定或验证,也不像Gin那样提供预建的中间件。
Gin为你提供预建的中间件,用于CORS、超时、缓存、认证和会话管理。
开始使用Gin框架
让我们创建一个简单的微服务来开始使用该框架。首先,我们需要设置好我们的开发环境。
设置开发环境
确保你的电脑已经有Go ≥ v1.13。你可以在任何时候从官方的Go二进制版本中安装最新的稳定版本。
现在,我们需要初始化一个新的Go项目以使用远程依赖,并下载Gin框架包。输入以下命令来初始化一个新项目。
mkdir simpleservice
cd simpleservice
go mod init simpleservice
复制代码
现在,下载并引用Gin框架。
go get -u github.com/gin-gonic/gin
复制代码
构建一个简单的微服务
将下面的代码添加到main.go
源文件中,就可以开始了。
package main
import (
"runtime"
"github.com/gin-gonic/gin"
)
func main() {
router := gin.Default()
router.GET("/hello", func(c *gin.Context) {
c.JSON(200, gin.H{
"message": "Hello World!",
})
})
router.GET("/os", func(c *gin.Context) {
c.String(200, runtime.GOOS)
})
router.Run(":5000")
}
复制代码
上面的代码定义了两个HTTPGET
端点:/hello
和/os
。/hello
端点返回一个JSON格式的消息。/os
端点返回纯文本格式的当前操作系统名称。
在定义了端点和处理程序后,我们需要通过Run()
函数调用来启动HTTP服务器实例。
用下面的命令运行这个样本微服务。
go run main.go
复制代码
通过从你的网络浏览器导航到以下URL来测试它。
http://localhost:5000/hello
http://localhost:5000/os
复制代码
用Postman测试微服务
刚才,我们用网络浏览器发送了一个HTTP GET请求。我们还可以使用cURL命令行工具来测试基于HTTP的微服务。
像Postman这样的API测试应用程序提供了测试微服务所需的所有功能。我将在接下来的演示中使用Postman工具。如果你是Postman的新手,可以测试一下微服务的例子来入门。
用路由构建微服务
我们可以创建一个只有一个端点的微服务来执行一个动作,就像众所周知的无服务器概念。但我们经常让微服务执行多个动作。例如,你可以建立一个微服务来获取产品细节,添加新产品,并删除现有产品。这种方法被称为RESTful模式。
看一下下面的RESTful路线。
/products
/products/:productId/reviews
开发人员通常为每个路由创建多个端点。例如,可以在/products
路线下使用以下端点。
GET /products
- 列出几个产品GET /products/:productId
- 获取一个产品的详细信息POST /products
- 添加一个新产品PUT /products/:productId
- 更新一个产品DELETE /products/:productId
- 删除一个产品
Gin为我们提供了API功能,通过创建多个端点来构造我们的微服务。此外,我们还可以对路由进行分组,以提高可维护性。
请看下面的示例代码。
package main
import (
"github.com/gin-gonic/gin"
)
func endpointHandler(c *gin.Context) {
c.String(200, "%s %s", c.Request.Method, c.Request.URL.Path)
}
func main() {
router := gin.Default()
router.GET("/products", endpointHandler)
router.GET("/products/:productId", endpointHandler)
// Eg: /products/1052
router.POST("/products", endpointHandler)
router.PUT("/products/:productId", endpointHandler)
router.DELETE("/products/:productId", endpointHandler)
router.Run(":5000")
}
复制代码
上面的代码定义了五个端点来对产品执行CRUD操作。在这里,代码使用了一个名为endpointHandler
的通用端点处理程序,但你可以使用Gin上下文引用创建不同的处理程序来执行不同的操作。
如果你的RESTful API有多个版本,你可以使用Gin的路由分组功能来编写干净的API代码。请看下面的例子。
package main
import (
"github.com/gin-gonic/gin"
)
func v1EndpointHandler(c *gin.Context) {
c.String(200, "v1: %s %s", c.Request.Method, c.Request.URL.Path)
}
func v2EndpointHandler(c *gin.Context) {
c.String(200, "v2: %s %s", c.Request.Method, c.Request.URL.Path)
}
func main() {
router := gin.Default()
v1 := router.Group("/v1")
v1.GET("/products", v1EndpointHandler)
// Eg: /v1/products
v1.GET("/products/:productId", v1EndpointHandler)
v1.POST("/products", v1EndpointHandler)
v1.PUT("/products/:productId", v1EndpointHandler)
v1.DELETE("/products/:productId", v1EndpointHandler)
v2 := router.Group("/v2")
v2.GET("/products", v2EndpointHandler)
v2.GET("/products/:productId", v2EndpointHandler)
v2.POST("/products", v2EndpointHandler)
v2.PUT("/products/:productId", v2EndpointHandler)
v2.DELETE("/products/:productId", v2EndpointHandler)
router.Run(":5000")
}
复制代码
接受、处理和响应
每个RESTful微服务都会执行三个关键动作。
- 接受数据
- 加工/处理数据
- 返回数据
微服务通常向外部环境发送响应,如Web或移动应用程序,但它们也可以相互通信。开发人员使用不同的数据格式进行微服务通信,如JSON、XML或YAML。
通过URL参数接受数据3
我们在之前的端点中使用了:productId
,但我们也可以在URL中提供:productId
以外的值。URL参数是一个很好的选择,可以接受对微服务的简短输入。
让我们写一个带有两个URL参数的简单计算器。在main.go
文件中添加以下代码,并启动服务器。
package main
import (
"fmt"
"strconv"
"github.com/gin-gonic/gin"
)
func add(c *gin.Context) {
x, _ := strconv.ParseFloat(c.Param("x"), 64)
y, _ := strconv.ParseFloat(c.Param("y"), 64)
c.String(200, fmt.Sprintf("%f", x + y))
}
func main() {
router := gin.Default()
router.GET("/add/:x/:y", add)
router.Run(":5000")
}
复制代码
上面的代码实现了一个GET
资源,让我们通过URL参数发送两个数字。当它收到两个数字时,它将以这些数字的总和作为回应。例如,GET /add/10/5
将返回15
,如下图所示。
接受来自HTTP消息体的数据
由于各种原因,我们通常不会用URL参数发送大量的数据--URL可能会变得很冗长,我们可能会遇到通用的RESTful模式违反,等等。HTTP消息体是发送任何大型输入的最佳位置。
但URL参数仍然是发送过滤器和模型标识符的最佳方式,比如短数据,如customerId
,productId
, 等等。
让我们通过使用HTTP消息体接受数据来重构之前的计算器端点。
package main
import (
"github.com/gin-gonic/gin"
)
type AddParams struct {
X float64 `json:"x"`
Y float64 `json:"y"`
}
func add(c *gin.Context) {
var ap AddParams
if err := c.ShouldBindJSON(&ap); err != nil {
c.JSON(400, gin.H{"error": "Calculator error"})
return
}
c.JSON(200, gin.H{"answer": ap.X + ap.Y})
}
func main() {
router := gin.Default()
router.POST("/add", add)
router.Run(":5000")
}
复制代码
我们新的计算器实现有一个POST
端点,接受JSON格式的数据。我们不需要在Gin处理程序中手动解密JSON有效载荷--相反,Gin框架提供了内置的函数来将JSON结构与内部Go结构绑定。上面的代码将传入的JSON有效载荷绑定到AddParams
结构中。
使用Postman测试上述示例代码,将以下JSON有效载荷发送至POST /add
{
"x": 10,
"y": 5
}
复制代码
返回JSON、YAML和XML格式的数据
正如我们之前讨论的,微服务使用各种数据格式进行通信。几乎所有的现代微服务都使用JSON进行数据交换,但你可以根据你的需要使用YAML和XML数据交换格式。你可以从Gin路由器中序列化各种数据格式,如下所示。
package main
import (
"github.com/gin-gonic/gin"
)
type Product struct {
Id int `json:"id" xml:"Id" yaml:"id"`
Name string `json:"name" xml:"Name" yaml:"name"`
}
func main() {
router := gin.Default()
router.GET("/productJSON", func(c *gin.Context) {
product := Product{1, "Apple"}
c.JSON(200, product)
})
router.GET("/productXML", func(c *gin.Context) {
product := Product{2, "Banana"}
c.XML(200, product)
})
router.GET("/productYAML", func(c *gin.Context) {
product := Product{3, "Mango"}
c.YAML(200, product)
})
router.Run(":5000")
}
复制代码
上面的代码有三个端点,以三种不同的数据格式返回数据。JSON、XML和YAML。你可以传递一个Go结构实例,让Gin根据结构标签自动序列化数据。运行上面的代码片段,用Postman进行测试,如下图所示。
<h2″>验证传入的请求
微服务可以处理各种传入的请求。假设你正在实现一个微服务,通过与打印设备的通信,在纸上实际打印数字文档。如果你需要限制一个打印作业的页数怎么办?如果请求不包含启动一个新的打印作业所需的输入,怎么办?那么你就必须验证请求,并对每个错误信息做出相应的回应。
Gin提供了一个基于结构标签的验证功能,可以用更少的代码实现验证。请看下面的源代码。
package main
import (
"fmt"
"github.com/gin-gonic/gin"
)
type PrintJob struct {
JobId int `json:"jobId" binding:"required,gte=10000"`
Pages int `json:"pages" binding:"required,gte=1,lte=100"`
}
func main() {
router := gin.Default()
router.POST("/print", func(c *gin.Context) {
var p PrintJob
if err := c.ShouldBindJSON(&p); err != nil {
c.JSON(400, gin.H{"error": "Invalid input!"})
return
}
c.JSON(200, gin.H{"message":
fmt.Sprintf("PrintJob #%v started!", p.JobId)})
})
router.Run(":5000")
}
复制代码
我们需要使用binding
结构标签来定义我们在PrintJob
结构内的验证规则。Gin使用 go-playground/validator
用于内部绑定验证器的实现。上述验证定义接受基于以下规则的输入。
JobId
: Required, x ≥ 10000- 页面。需要,100≥x≥1
上述微服务将接受基于验证定义的输入,如下图所示。
用中间件扩展Gin
中间件指的是作用于两个相连的软件组件之间的组件。Gin社区在这个GitHub资源库中维护着几个通用的中间件。
Gin的中间件系统让开发者可以修改HTTP消息并执行常见的动作,而不需要在端点处理程序内编写重复的代码。当你用gin.Default()
功能创建一个新的Gin路由器实例时,它会自动附加日志和恢复中间件。
例如,你可以用下面的代码片断在微服务中启用CORS。
package main
import (
"github.com/gin-gonic/gin"
"github.com/gin-contrib/cors"
)
func main() {
router := gin.Default()
router.Use(cors.Default())
router.GET("/", func(c *gin.Context) {
c.JSON(200, gin.H{"message": "CORS works!"})
})
router.Run(":5000")
}
复制代码
也可以用Gin的中间件API构建你自己的中间件。例如,下面的自定义中间件拦截并打印(记录到控制台)每个HTTP请求的User-Agent
头的值。
package main
import (
"log"
"github.com/gin-gonic/gin"
)
func FindUserAgent() gin.HandlerFunc {
return func(c *gin.Context) {
log.Println(c.GetHeader("User-Agent"))
// Before calling handler
c.Next()
// After calling handler
}
}
func main() {
router := gin.Default()
router.Use(FindUserAgent())
router.GET("/", func(c *gin.Context) {
c.JSON(200, gin.H{"message": "Middleware works!"})
})
router.Run(":5000")
}
复制代码
微服务与微服务之间的通信
外部应用程序客户端通常直接或通过类似API网关的服务与微服务连接和通信。软件架构师根据他们的架构要求使用各种服务间的消息传递协议--一些软件开发团队实现了RESTful服务间通信,而其他团队则使用RabbitMQ等消息代理实现了异步的、基于消息传递的服务间通信。
Gin框架是专门为用RESTful模式构建微服务而建立的。因此,我们可以用Gin快速构建同步的、基于HTTP的服务间通信。
让我们建立两个微服务:InvoiceGenerator
和PrinterService
。InvoiceGenerator
微服务将负责生成发票。一旦它生成了一张新的发票,它就要求PrinterService
,通过服务间通信启动一个新的打印作业。
请注意,这些微服务用控制台消息模拟发票生成和打印文件。换句话说,这些微服务只演示了同步的服务间通信,而不是实际的发票生成和打印。
首先,将以下代码添加到printer_service.go
package main
import (
"math/rand"
"time"
"log"
"github.com/gin-gonic/gin"
)
type PrintJob struct {
Format string `json:"format" binding:"required"`
InvoiceId int `json:"invoiceId" binding:"required,gte=0"`
JobId int `json:"jobId" binding:"gte=0"`
}
func main() {
router := gin.Default()
router.POST("/print-jobs", func(c *gin.Context) {
var p PrintJob
if err := c.ShouldBindJSON(&p); err != nil {
c.JSON(400, gin.H{"error": "Invalid input!"})
return
}
log.Printf("PrintService: creating new print job from invoice #%v...", p.InvoiceId)
rand.Seed(time.Now().UnixNano())
p.JobId = rand.Intn(1000)
log.Printf("PrintService: created print job #%v", p.JobId)
c.JSON(200, p)
})
router.Run(":5000")
}
复制代码
运行上述代码,并用Postman进行测试--当你通过Postman提出POST
请求时,它模拟了打印作业的创建。
现在我们要创建InvoiceGenerator
微服务,它负责根据价格、客户细节和购买描述创建发票。
我们需要从InvoiceGenerator
中调用PrinterService
。因此,我们的项目中需要一个HTTP客户端。用以下命令安装Go的restyHTTP客户端库。
go get -u github.com/go-resty/resty/v2
复制代码
现在将下面的代码添加到invoice_generator.go
package main
import (
"math/rand"
"time"
"log"
"github.com/gin-gonic/gin"
"github.com/go-resty/resty/v2"
)
type Invoice struct {
InvoiceId int `json:"invoiceId"`
CustomerId int `json:"customerId" binding:"required,gte=0"`
Price int `json:"price" binding:"required,gte=0"`
Description string `json:"description" binding:"required"`
}
type PrintJob struct {
JobId int `json:"jobId"`
InvoiceId int `json:"invoiceId"`
Format string `json:"format"`
}
func createPrintJob(invoiceId int) {
client := resty.New()
var p PrintJob
// Call PrinterService via RESTful interface
_, err := client.R().
SetBody(PrintJob{Format: "A4", InvoiceId: invoiceId}).
SetResult(&p).
Post("http://localhost:5000/print-jobs")
if err != nil {
log.Println("InvoiceGenerator: unable to connect PrinterService")
return
}
log.Printf("InvoiceGenerator: created print job #%v via PrinterService", p.JobId)
}
func main() {
router := gin.Default()
router.POST("/invoices", func(c *gin.Context) {
var iv Invoice
if err := c.ShouldBindJSON(&iv); err != nil {
c.JSON(400, gin.H{"error": "Invalid input!"})
return
}
log.Println("InvoiceGenerator: creating new invoice...")
rand.Seed(time.Now().UnixNano())
iv.InvoiceId = rand.Intn(1000)
log.Printf("InvoiceGenerator: created invoice #%v", iv.InvoiceId)
createPrintJob(iv.InvoiceId) // Ask PrinterService to create a print job
c.JSON(200, iv)
})
router.Run(":6000")
}
复制代码
上面的代码实现了POST /invoices
端点,它根据JSON输入的有效载荷创建了一张新的发票。创建新发票后,它与PrinterService
微服务同步通信,以创建一个新的打印作业,并在控制台打印作业标识符。
通过创建一个新的发票和检查控制台日志来测试服务间的通信。确保在通过Postman发送HTTP请求之前启动两个微服务。发送以下JSON有效载荷到POST /invoices
。
{
"customerId": 10,
"description": "Computer repair",
"price": 150
}
复制代码
现在检查InvoiceGenerator
的日志。你会注意到,它显示了从另一个微服务收到的新的打印作业标识符。
如果你检查PrinterService
的日志,你会注意到同样的打印作业标识符。我们还可以从两个日志中看到相同的发票标识符,这意味着我们的服务间通信实现工作得很好。
项目结构化和微服务最佳实践
程序员使用不同的策略来编写可维护的代码库,通常基于REST模式的微服务开发活动的REST设计最佳实践。
我们可以遵循MVC模式的原则来结构我们的代码。另外,我们可以尝试使用大多数Go开发者接受和使用的通用做法。当你使用基于Gin的微服务时,请验证以下检查表。
- 如果你的微服务执行CRUD操作。为每个实体控制器创建一个源文件,并为每个CRUD操作实现单独的函数
- 例如,你可以创建
controllers/product.go
,为每个CRUD操作添加处理程序
- 例如,你可以创建
- 使用
net/http
包中的状态代码,而不是硬编码的整数状态代码--为了简化演示,我在例子中使用了硬编码的值- 例如,使用
http.StatusOK
,而不是200
- 例如,使用
- 如果你觉得你在端点处理程序里面写了重复的代码,实现自定义中间件总是好的
- 使用
gin.H
捷径直接操作JSON会产生重复的代码--如果可能的话,尽量使用结构。- 例如,
gin.H
只是一个简短的类型定义。map[string]interface{}
- 例如,
- 确保在服务间通信过程中正确处理错误;否则,你将不能轻易追踪连接问题
- 在日志文件中写入关键情况
你也可以从以下已经使用REST最佳实践的模板项目开始。此外,还可以从这些项目中继承一些设计模式到你自己的代码中,而不必使用整个模板代码。
- 带有CRUD API和SQL连接的Gin启动项目:Gin-boilerplate
- 带有CRUD API和DynamoDB连接的Gin启动项目:Go-gin-boilerplate
总结
在本教程中,我们学习了如何使用Gin web框架在Go中创建微服务。我们还用Postman工具测试了我们的微服务实例。
在现实中,我们通常通过Web应用、移动应用和物联网框架来消费微服务。然而,现代后端开发人员通常不会直接调用微服务,因为存在扩展问题和网络安全问题。因此,在将你的微服务暴露在互联网上之前,开发者会将它们连接到API网关或负载平衡器。
大多数现代软件开发项目让Kubernetes容器编排器自动管理和扩展微服务实例。由于Docker等容器服务,我们还可以在各种部署环境和云服务提供商之间轻松转移微服务。
但迁移到一个新的HTTP网络框架需要耗费时间的代码重构。因此,考虑用Gin这样一个包含电池的网络框架来开始你的RESTful微服务。
The postBuilding microservices in Go with Ginappeared first onLogRocket Blog.