Go创建REST服务
- 这两天,我用Go语言试着创建REST服务,做一下简单的总结。
起初,我是自己直接使用内置的http包写的;然后,利用了第三方包Gin快速创建了API。这两天收获很大,也让我对于框架带来的边界感到惊奇。我突然觉得Go的框架要比Python的更加灵活,或者说更适合我们写微服务。
一、自己创建REST API
1、API格式:
-
http://localhost:8000/websites; GET,POST方法
- http://localhost:8000/websites/{id} DELETE,PUT方法
API请求响应的数据格式都是json格式的,可通过curl测试。
2.使用的第三方模块:
-
"github.com/gorilla/mux" 提供了更灵活的路由,支持正则匹配。 _ "github.com/go-sql-driver/mysql" mysql的驱动
3.路由函数
直接看一下,server的主函数,定义了路由。
func main() {
router := mux.NewRouter()
//one struct ,包括drivername和datasource。
var dbclient DbClient
dbclient.Drivername,dbclient.Datasource = "mysql","root:root@tcp(localhost:3306)/app_dailyblog?charset=utf8"
dbclient.createdb()
//DbAPI 结构体,包括dbclient,表名,sql语句字段,这个主要是想针对不同的数据表提供不同的api。
var api DbAPI
//进行初始化操作,包括初始化sql.DB实例。
api.InitAPi(&dbclient,"blog_website",Website{})
//路由定义
router.HandleFunc("/websites",api.GetAllData).Methods("GET")
router.HandleFunc("/websites/{id}",api.GetSingleWebsite).Methods("GET")
/*
router.HandleFunc("/websites",dbclient.PostWebsite).Methods("POST")
router.HandleFunc("/websites/{id}",dbclient.UpdateWebsite).Methods("PUT")
router.HandleFunc("/websites/{website}",dbclient.DeleteWebsite).Methods("DELETE")
*/
log.Fatal(http.ListenAndServe(":8000",router))
}
我这里面比较重要的数据结构如下:
type DbClient struct {
Drivername string
Datasource string
ins *sql.DB
}
type DbAPI struct {
DbClient
datamap interface{}
tablename string
query string
update string
delete string
}
//对于每一个表,都会有对应的结构体,这是json数据转换的桥梁。
type Website struct {
Id int `json:"id"`
Website string `json:"website"`
Homepage string `json:"homepage"`
}
type Websites []Website
对于出现的错误,都有一个对应的错误吗。
type ErrCode struct {
Err_code int `json:"err_code"`
Desc string `json:"Desc"`
}
上面是整体逻辑,下面要做的就是实现具体的函数。看我上面的代码也能够发现函数都是结构体api的方法。我的初衷是希望它能够更加的OOP,希望能够根据不同的表,创建不同的实例,希望能够避免代码的重复编写,而不是对于每个不同的需求都重新定义函数。但是,我在实际编写的时候遇到了一些问题,目前为止我还找到解决的方法。现在贴出部分代码:
就拿一个获取所有对象的方法来说吧。
func (api *DbAPI) GetAllData(w http.ResponseWriter,r *http.Request) {
query_string := api.GetSql("query")
if query_string == "" {
panic("the query_string is empty")
}
fmt.Println(query_string,"query_string")
//获取所有数据
rows,err := api.ins.Query(query_string)
fmt.Println(rows,"rows",err)
if err != nil {
e := ErrCode{1,errcodes[1]}
EncodeResp(w,e)
return
} else {
defer rows.Close()
switch datamap := api.datamap.(type){
//这块我觉得这样写还是有问题,如果不定期的有新表,新需求,我也不能每次来修改代码,来添加新case啊。这完全不符合开放-关闭的设计原则啊。
case Website:
var dataarr []Website
fmt.Println(datamap,dataarr)
for rows.Next() {
//还有这个,我希望rows.Scan能够自动填充我对应结构体的字段指针,而不是这种需要具体创建,然后填充。我试着用reflect,但是没有解决问题啊。
var id int
var website,homepage string
err = rows.Scan(&id,&website,&homepage)
dataarr = append(dataarr,Website{id,website,homepage})
fmt.Println(dataarr)
}
EncodeResp(w,dataarr)
default:
var dataarr []Website
fmt.Println(datamap,dataarr)
}
}
}
我遇到的问题都写到了代码片段中,我希望后续在不断的经验积累下,能够解决这个问题。看看能不用类似自动绑定的一些方法。
通过自己动手写一遍REST API学到了很多,除了上面这些,还要记录一些:
- POST的json数据需要通过ioutil读取request.body中的数据,返回的是一个map,最后通过json模块反序列化,将json格式数据转换为map:
var req_body map[string]interface{}
body,_ := ioutil.ReadAll(r.Body)
json.Unmarshal(body,&req_body)
2、对于不确定类型的,可定义interface{}
二、使用Gin创建RESTFUL API
自己写API之后,再利用框架Gin,感觉像发现了新大陆。轮子是多么美妙的东西啊,它让敏捷开发成为可能,是开发者的福音。
我觉得我就不用具体描述用法了,因为github上要比我说的详细多了,我就把我自己写的贴上得了。
定义数据结构体,以及全局变量。我这里没使用数据库,想简单地做一下。
type User struct {
Id int `json:"Id"`
Name string `json:"Name"`
}
var users = []User{
{1,"haibo"},
{2,"lina"},
}
main入口函数:
func main() {
r := gin.Default()
v1 := r.Group("/api/v1")
{
v1.GET("/users",GetUsers)
v1.GET("/users/:id",GetOneUser)
v1.PUT("/users/:id",UpdateUser)
v1.DELETE("/users/:id",DeleteUser)
}
r.Run(":8000")
}
这是入口函数,用起来比较简单。它允许你定义url前缀,就像Django里面的url的include一样。如下:
- /api/v1/users/
- /api/v1/user1/1
此外,注意id等参数的写法,用的是冒号“:”,除了冒号,gin还提供了*
号处理参数,*
号能匹配的规则就更多。在对应函数中可以通过gin上下文获得对于对应的参数,如下函数:
func GetOneUser(c *gin.Context) {
//通过Params获得对应的资源id
para_id := c.Params.ByName("id")
fmt.Println(c.Params)
uid,err := strconv.Atoi(para_id)
if err != nil {
panic("id must be interger")
} else {
switch uid {
case 1:
user := User{1,"haibo"}
c.JSON(http.StatusOK,gin.H{"user":user})
case 2:
user := User{2,"lima"}
c.JSON(http.StatusOK,gin.H{"user":user})
default:
c.JSON(http.StatusNotFound,gin.H{"err":"no found"})
}
}
}
看到上面的用法,我为之感到奇妙,用法真的很棒。用c.JSON就可以响应JSON数据,该函数可以将map类型转换为JSON数据,还可以自己传递状态码。gin.H是一个map,元素类型是interface{},即任意类型。
对于POST或PUT数据,如果是x-www-form-urlencoded
或from-data的数据,可以直接使用c.POSTForm方法;如果是json数据,可以使用
BindJSON方法。我这个例子中的更新路由,就是这么用的。
func UpdateUser(c *gin.Context) {
para_id := c.Params.ByName("id")
fmt.Println(c.Params)
uid,err := strconv.Atoi(para_id)
if err != nil {
panic("id must be interger")
} else {
var user User
c.BindJSON(&user)
user.Id = uid
c.JSON(http.StatusOK,gin.H{"success":user})
}
}
上面是一个简单的例子,更丰富的应用需要后续实践中再具体研究。其实还要好多问题需要研究,比如权限的设置,可能这应该是应用层上的设置,比如对于那些url,我该设置什么样的权限。
微信分享/微信扫码阅读