在Go中用傻瓜式方法构建RESTful服务
Go,也被称为goLang,是Rob Pike、Robert Griesemer和Ken Thompson的心血结晶。2007年开始在谷歌开发,2009年开源,2012年3月发布了1.0版本。
截至本文撰写之时,最新版本为go1.17。
Go是
一种静态类型的语言。
它与C语言相似,但有垃圾收集和并发功能,使它从其他语言中脱颖而出。
写出好的、可理解的代码是每个开发者所追求的,他们通过加入一个人们遵循某种风格的社区来学习这些模式、属性等。
正如 戴夫-切尼在他的文章中引用的 [围棋的禅意]:
说某个东西是成语,就是说它遵循了当时的风格。如果某件事情不是成语,它就没有遵循流行的风格。它是不合时宜的。
RESTful服务是整个软件行业最常用的一些做法。在本教程中,我们将看到如何用Go语言构建RESTful服务,并采用成语方法。我们将构建一个返回咖啡对象数据的API。
前提条件
读者应该具备以下条件才能继续学习。
- 在你的系统中安装Go。
- Go的基础知识。
- 熟悉Go的包,如
net/http和encoding/json。 - [REST]服务的基础知识。
文件夹的结构将按以下方式进行。
go_RESTful
├───data
| └───products.go
├───handlers
| └───product_handler.go
└───main.go
我们将首先创建一个产品结构,它将保存我们的咖啡产品的数据。在data文件夹下的products.go中,创建一个类型为struct 的结构Product ,如下图所示。
package data
import "time"
type Product struct {
ID int `json:"id"`
Name string `json:"name" validate:"required"`
Description string `json:"description"`
Price float32 `json:"price" validate:"gte=0"`
CreatedOn string `json:"-"`
UpdatedOn string `json:"-"`
DeletedOn string `json:"-"`
}
// productList is a list of Coffees
var productList = []*Product{
{
ID: 1,
Name: "Latte",
Description: "Made with espresso and steamed milk.",
Price: 2.99,
CreatedOn: time.Now().UTC().String(),
UpdatedOn: time.Now().UTC().String(),
},
{
ID: 2,
Name: "Mocaccino",
Description: "A chocolate-flavoured warm beverage that is a variant of a caffè latte",
Price: 1.99,
CreatedOn: time.Now().UTC().String(),
UpdatedOn: time.Now().UTC().String(),
},
}
Product 结构有上面提到的字段,分别用结构标签定义。结构标签功能允许我们为字段添加注释,并编写解析器来接收这些注释。
所以在这里,我们的字段名ID 在http响应中被重命名为id ,正如结构标签中定义的那样。我们还定义了productList ,这是一个Product 类型的数组,用于保存各种咖啡的数据。
我们将创建一个处理程序来处理API请求。在handlers文件夹下创建一个新文件product_handler.go。如下图所示,我们将定义一个名为Products 的新结构,其方法将满足Handler 的接口。
package handlers
// handlers here is the folder name
import (
"log"
"net/http"
)
type Products struct {
l *log.Logger
}
// a logger that references the Products struct
func NewProducts(l *log.Logger) *Products {
return &Products{l}
}
// function to handle the incoming requests
func (p *Products) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
}
并且根据文档,Handler 的语法是。
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}
记住这个一般规则,任何遵守httphandler 接口的东西都需要有ServeHTTP 方法。
我们使用标准的log 包来记录我们的服务器及其事件的基本信息,并将所有的日志写入os.Stdout 流。
日志也是一种很好的做法,在测试或调试代码的时候,它们是有益的。
NewProducts 方法将被用来作为参考,在main.go文件中创建一个处理程序。
在main.go 文件中;我们将为Handler 创建一个引用。
package main
import (
"log"
"net/http"
"os"
"go_RESTful/handlers"
)
func main() {
l := log.New(os.Stdout, "COFFEE-API", log.LstdFlags)
ph := handlers.NewProducts(l)
sm := http.NewServeMux()
sm.Handle("/", ph)
// register the handler with the server
http.ListenAndServe(":4200", sm)
}
ph是一个 ,它将从handlerproduct_handler.go中引用我们的结构 ,并为 方法服务。ProductsServeHTTPsm是 的一个实例,是处理所有API请求的ServeMux复用器。它将传入的HTTP请求与预定义的URL路径的查询进行比较,并在找到匹配的地方调用相应的处理程序。- 该方法
ListenAndServe在定义的端口上监听TCP连接,并调用handler来处理这些请求。
注意。
根据文档,ListenAndServe 的语法是。
func ListenAndServe(addr string, handler Handler) error
在我们的代码中,我们在这里传递一个ServeMux 的实例作为一个处理程序。它有一个定义的ServeHTTP 方法,因此它将满足Handler 接口。当它是nil ,它将在内部调用DefaultServeMux 。
到目前为止,我们已经有了产品列表和处理程序模板来服务于http请求。现在的问题是,我们如何将咖啡数据从Product 结构中传递给我们的ServeHTTP 函数。
为此,Go的标准库为我们提供了一个名为encoding/json 的包,用于对json数据进行编码/解码。
在基本结构准备就绪后,我们将在products.go文件中定义方法,以返回Product 数据。在products.go中定义这些数据模型函数背后的原因是为了创造抽象性。
现在,在data文件夹下的products.go 文件中,我们将需要创建一个函数,为我们返回Product 的列表,如图所示。
func GetProducts() []*Product {
return productList
}
与其这样做,我们不如创建一个叫做Products 的类型,一个Product 的列表,然后再添加方法。
package data
import (
"time"
"io"
"encoding/json"
)
type Product struct {
ID int `json:"id"`
Name string `json:"name" validate:"required"`
Description string `json:"description"`
Price float32 `json:"price" validate:"gte=0"`
CreatedOn string `json:"-"`
UpdatedOn string `json:"-"`
DeletedOn string `json:"-"`
}
type Products []*Product
// converts the Product fields to JSON
func (p *Products) ToJSON(w io.Writer) error {
e := json.NewEncoder(w)
return e.Encode(p)
}
// returns the list of products
func GetProducts() Products {
return productList
}
// productList is a list of products for this example data source
var productList = []*Product{
{
ID: 1,
Name: "Latte",
Description: "Made with espresso and steamed milk.",
Price: 2.99,
CreatedOn: time.Now().UTC().String(),
UpdatedOn: time.Now().UTC().String(),
},
{
ID: 2,
Name: "Mocaccino",
Description: "A chocolate-flavoured warm beverage that is a variant of a caffè latte",
Price: 1.99,
CreatedOn: time.Now().UTC().String(),
UpdatedOn: time.Now().UTC().String(),
},
}
ToJSON方法有来自encoding/json 包的函数NewEncoder ,它直接将数据写到io.Writer 。
最后一步是在product_handler.go文件中识别传入的HTTP请求的类型,并分别为该方法服务。
package handlers
import (
"log"
"net/http"
"go_RESTful/data"
)
type Products struct {
l *log.Logger
}
// a logger that references the Products struct
func NewProducts(l *log.Logger) *Products {
return &Products{l}
}
// function to handle the incoming requests
func (p *Products) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodGet {
p.getProducts(rw, r)
return
}
if r.Method == http.MethodPost {
p.addProduct(rw, r)
return
}
rw.WriteHeader(http.StatusMethodNotAllowed)
}
// function to handle GET requests
func (p *Products) getProducts(rw http.ResponseWriter, r *http.Request) {
p.l.Println("Handle GET Products")
lp := data.GetProducts()
err := lp.ToJSON(rw)
if err != nil {
http.Error(rw, "Unable to convert it to json", http.StatusInternalServerError)
}
}
// function to handle POST requests
func (p *Products) addProduct(rw http.ResponseWriter, r *http.Request) {
p.l.Println("Handle POST Product")
prod := &data.Product{}
err := prod.FromJSON(r.Body)
if err != nil {
http.Error(rw, "Unable to unmarshal json", http.StatusBadRequest)
}
data.AddProduct(prod)
}
我们已经添加了处理请求的方法。现在我们将在products.go中添加数据模型方法。
package data
import (
"time"
"io"
"encoding/json"
)
type Product struct {
ID int `json:"id"`
Name string `json:"name" validate:"required"`
Description string `json:"description"`
Price float32 `json:"price" validate:"gte=0"`
CreatedOn string `json:"-"`
UpdatedOn string `json:"-"`
DeletedOn string `json:"-"`
}
type Products []*Product
// converts the Product fields to JSON
func (p *Products) ToJSON(w io.Writer) error {
e := json.NewEncoder(w)
return e.Encode(p)
}
// converts incoming JSON data to Product fields
func (p *Product) FromJSON(r io.Reader) error {
e := json.NewDecoder(r)
return e.Decode(p)
}
// returns the list of products
func GetProducts() Products {
return productList
}
// adding a new Product item to productList
func AddProduct(p *Product) {
p.ID = getNextID()
productList = append(productList, p)
}
// returns a new ID based on the last item in productList
func getNextID() int {
lp := productList[len(productList)-1]
return lp.ID + 1
}
// productList is a list of products for this example data source
var productList = []*Product{
{
ID: 1,
Name: "Latte",
Description: "Made with espresso and steamed milk.",
Price: 2.99,
CreatedOn: time.Now().UTC().String(),
UpdatedOn: time.Now().UTC().String(),
},
{
ID: 2,
Name: "Mocaccino",
Description: "A chocolate-flavoured warm beverage that is a variant of a caffè latte",
Price: 1.99,
CreatedOn: time.Now().UTC().String(),
UpdatedOn: time.Now().UTC().String(),
},
}
从终端,在项目目录内,运行该命令。
go run main.go
这就完成了我们在Go中对RESTful服务的实现,下面附上了API调用的结果。
GET请求查询当前的咖啡项目列表。

POST请求插入一个新的咖啡项目。

- 为了验证新添加的项目,我们将再次调用
GET方法。

结论
最后,我们已经创建了处理程序来处理HTTP 请求。同时确保对Coffee数据列表的操作方法保持抽象化。最后,我们现在更好地理解了如何使用Go的标准包,如encoding/json,log, 和net/http 。