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学到了很多,除了上面这些,还要记录一些:

  1. 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-urlencodedfrom-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,我该设置什么样的权限。

 

 

--------EOF---------
微信分享/微信扫码阅读