分享
  1. 首页
  2. 文章

自我黑客马拉松 -- 从零开始创建一个基于Go语言的web service

nirendao · · 2514 次点击 · · 开始浏览
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

20个小时的时间能干什么?也许浑浑噩噩就过去了,也许能看一些书、做一些工作、读几篇博客、再写个一两篇博客,等等。而黑客马拉松(HackAthon),其实是一种自我挑战--看看自己在有限的短时间内究竟能做出些什么。比如:让一个毫无某种语言经验的人用该种语言去实现4个如下的Restful API(假设此种语言为Go)。

* 语言 Go
* 框架 随意
* 后端数据库 Redis或者SQLite,只需要一种即可
## API 列表
* POST /location
* GET /location
* GET /location/{name}
* DELETE /location/{name}
### POST /location
增加支持的城市,如果已存在于数据库,返回409,否则返回201
例子1 
POST /location
{ "name": "Shanghai" }
201 Created
例子2
POST /location
{ "name": "Shanghai" }
201 Created
POST /location
{ "name": "Shanghai" }
409 Conflicted
### GET /location 返回数据库中的所有城市
例子3 
GET /location
200 OK
[]
例子4
POST /location
{ "name": "Shanghai" }
201 Created
POST /location
{ "name": "Beijing" }
201 Created
GET /location
200 OK
["Shanghai", "Beijing"]
### GET /location/{name} 查询openweathermap.com,返回结果,因为天气数据更新不频繁,可缓存在数据库中,保留1个小时
不需要考虑查询openweathermap.com返回错误的情况
例子5
GET /location/Shanghai
200 OK
{
 "weather": [
 {
 "description": "few clouds",
 "icon": "02d",
 "id": 801,
 "main": "Clouds"
 }
 ]
}
### DELETE /location/{name}
例子6
DELETE /location/Shanghai
200 OK


而根据以上描述,要调用openweathermap.com网站的Restful API,具体的调用方式如下:
curl "api.openweathermap.org/data/2.5/weather?q=Shanghai&APPID=xxxxxxxxxxxxxxxxxxxxxxxxxx"
{"coord":{"lon":121.46,"lat":31.22},"weather":[{"id":801,"main":"Clouds","description":"few clouds","icon":"02d"}],"base":"cmc stations","main":{"temp":286.15,"pressure":1019,"humidity":71,"temp_min":286.15,"temp_max":286.15},"wind":{"speed":7,"deg":140},"clouds":{"all":20},"dt":1458608400,"sys":{"type":1,"id":7452,"message":0.0091,"country":"CN","sunrise":1458597323,"sunset":1458641219},"id":1796236,"name":"Shanghai","cod":200}
## 参数
* q: 城市名
* APPID: xxxxxxxxxxxxxxxxxxxxxxxxxx 是预先申请的ID

以上所有,就是此次黑客马拉松的题目要求了。

对于一个毫无Go语言经验的人,该怎么去做呢?

好吧,虽然是毫无Go的经验,但总不能什么都不懂吧。开发Restful API的经验还是有的,尽管是Python以及Java的。但是以往所用的框架总是无法应用到Go上的吧。难道Go自己也有做web service的框架吗?查了一下,还真有,有一个很著名的框架的叫做beego,还是一个中国人主要开发的,连文档都有中文版的,真是省了不少事。

既然如此,那么在使用框架之前,总要学习一下Go语言吧。而学习Go语言之前又总要安装一下Go吧。很不好意思的是,笔者最近手头没有好用的Linux机器,只好装在Windows上了。安装之后,要先按照本机文档所述,配置好环境变量GOROOT和GOPATH,并将%GOROOT%/bin和%GOPATH%/bin加到%PATH%系统环境变量;接着看文档,大致了解了一下Go的文件目录结构以及一些命令,还是很有用的。比如,go get命令就是借(chao)鉴(xi)了apt-get或pip install,能够从github上下载库,并自动解决所有依赖,且自动build。这一点还是很赞的。

光看本机文档还是有点慢,再快一点呢,去看看一些简明教程吧。笔者找到3份比较好的简明教程如下:

http://www.vaikan.com/go/a-tour-of-go/
http://coolshell.cn/articles/8460.html
http://coolshell.cn/articles/8489.html

好了,花了2个半小时学习了以上Go的基础知识之后,竟然发现了原来Go原生的库 "net/http" 里面就自带了一个web server,请参见这篇文档"Wrting Web Applications":

https://golang.org/doc/articles/wiki/

好了,基本功打的差不多了,现在应该可以来尝试一下beego框架了吧。从哪儿开始呢?当然还是要先从文档开始。通过文档,了解到我们可以用bee new命令和bee api命令分别创建一个基本的web service和一个基于Restful API的web service. 啊哈,找到了,bee api,这就是我想要的。不过且慢,至少要先安装了框架才行吧。于是,还是根据文档,应该执行如下的命令:

go get github.com/astaxie/beego
go get github.com/beego/bee
第一个命令是用来安装beego的库的,即这个框架。第二个命令是用来安装beego的工具集的,即bee new命令和bee api命令等。
OK,框架的基本环境搭建好了,就写跑个小例子吧。这是自我学习最基本的步骤了。对于本次学习来说,就是运行bee api <project_name>命令了。这样的话,一个新的项目就新建成功了。深入代码去看,会发现这个sample project实现了关于object和user各一套的API。于是,运行以下命令进行build:
go install github.com/<project_name>
build完成之后,再在该工程目录下,运行以下命令让这个project中的web server跑起来!
bee run
接着,用Postman尝试着GET了几把,比如:GET http://localhost:8080/object 真的有JSON返回!太棒了!一个web service就跑起来了!

下面该干什么呢?没错,就是把人家这个sample给改了!改成我们需要实现的4个API!哈哈,好像离成功很近了,是不是?啊,少了点什么呢?

没错!后台的数据库用什么?仔细去看了下sample代码中model package的实现,根本就没有用任何数据库!好吧,那咱自己写。用哪个呢?根据题目要求,只能用redis或sqlite3,而再根据beego的文档,beego只支持Mysql,PostgreSql和sqlite3. 好吧,这样就没什么选择了,只能用sqlite3了。刚好是笔者又没用过的一个数据库。不过,笔者曾经看别人用过!虽然没写过代码,不过总算知道这东西用起来不难,就是个轻量级的文件数据库嘛。

根据以往用Cassandra和ElasticSearch的经验,sqlite3必然有Go语言的driver. 继续翻翻beego的文档和sqlite3的文档,知道了要运行以下的命令才能安装sqlite3的go语言的驱动。

go get github.com/mattn/go-sqlite3
好了,驱动也装好了。那干脆把这个数据库也装了吧。装了之后,才发现,这个真是轻量级啊。直接就一个单个目录,里面有sqlite3.exe,sqlite3.dll等寥寥几个文件而已。只要把该目录加到%PATH%中,就可以直接使用了。真是简单。果然数年前看一个象棋人工智能程序就是用的sqlite来做的开局库。于是,在命令行试了几把sqlite3,感觉蛮爽。

OK!一切前期工作都已经完成,开工吧!!

还是看代码。beego生成的Restful API项目是MVC架构的。

1. 在main.go里主要是装载一些必须的库,然后把HTTP server跑起来;

2. 在router.go里面,就是设置路由了。这个玩过Flask、Django等框架的应该都很熟悉。

3. 在controllers package里面,就是设置Controller了,也就是Router过后所到的第一层。

4. 具体和数据库打交道的,自然还是在models package里面。这部分的代码最难写。首先,原sample程序里根本没有;其次,要运用beego的ORM模块来做,又要去学习ORM模块的东西。

笔者吭哧吭哧,折腾了有10来个小时,终于连滚带爬,连文档带Bing(写代码这地儿Google被墙,试了几个VPN都不好使),终于给折腾出来了。

具体代码粘贴如下:

1. main.go

package main
import (
 _ "github.com/cityweather/docs"
 _ "github.com/cityweather/routers"
 _ "github.com/mattn/go-sqlite3"
 
 "time"
 "github.com/astaxie/beego"
 "github.com/astaxie/beego/orm"
)
func init() {
 orm.RegisterDataBase("default", "sqlite3", "./weather.db")
 orm.RunSyncdb("default", false, true)
 orm.DefaultTimeLoc = time.UTC
}
func main() {
 beego.Run()
}

2. router.go
package routers
import (
 "github.com/cityweather/controllers"
 "github.com/astaxie/beego"
)
func init() {
 beego.Router("/location/?:name", &controllers.CityWeatherController{})
}

3. controller_cityweather.go
package controllers
import (
 "encoding/json"
 
 "github.com/cityweather/models"
 "github.com/astaxie/beego"
)
// Operations about cityweather
type CityWeatherController struct {
 beego.Controller
}
// @router /location [post]
// Body: {"name": "SomeCity"}
// Return: "201 Created" or "409 Conflicted"
func (o *CityWeatherController) Post() {
 var cn models.CityName
 json.Unmarshal(o.Ctx.Input.RequestBody, &cn)
 responseCode := models.AddOneCity(&cn)
 
 o.Ctx.Output.Status = responseCode
}
// @router /location/?:name [get]
func (o *CityWeatherController) Get() {
 name := o.Ctx.Input.Param(":name")
 
 if name != "" {
 cw, err := models.GetOneCity(name)
 if err != nil {
 o.Data["json"] = err.Error()
 } else {
 o.Data["json"] = cw
 }
 } else { // name is empty, then Get all cities' names
 cities := models.GetAllCities()
 o.Data["json"] = cities
 }
 o.ServeJSON()
}
// @router /location/:name [delete]
// Return: always 200 OK 
func (o *CityWeatherController) Delete() {
 name := o.Ctx.Input.Param(":name")
 models.Delete(name)
 
 o.Data["json"] = "Delete success!"
 o.ServeJSON()
}

4. model_cityweather.go
package models
import (
 "fmt"
 "time"
 "net/http"
 "io/ioutil"
 
 "github.com/astaxie/beego/orm"
 "github.com/bitly/go-simplejson"
 
 _ "github.com/mattn/go-sqlite3"
)
const weatherTable string = "city_weather"
const timeoutSet int64 = 3600
const OpenWeatherURL string = "http://api.openweathermap.org/data/2.5/weather"
const AppID string = "xxxxxxxxxxxxxxxxxxxxxxxx"
type CityName struct {
 Name string
}
type CityWeather struct {
 Id int // primary key, auto increment
 Name string `orm:"unique;"` // city name
 Summary string // main in weather
 Description string // description in weather
 Icon string // icon in weather
 Wid int // id in weather
 TimeStamp int64 // timestamp when updating
}
func init() {
 orm.RegisterModel(new(CityWeather))
}
func AddOneCity(cn *CityName) (responseCode int) {
 cw := new(CityWeather)
 cw.Name = cn.Name
 cw.Wid = -1
 cw.TimeStamp = 0
 fmt.Println(cw)
 
 o := orm.NewOrm()
 o.Using("main")
 _, err := o.Insert(cw)
 
 responseCode = 201
 if err != nil {
 if err.Error() == "UNIQUE constraint failed: city_weather.name" {
 responseCode = 409 // conflicted
 } else {
 responseCode = 500 // server error
 }
 }
 
 return responseCode
}
func GetAllCities() []string {
 allCities := []string{} // dynamic array
 
 o := orm.NewOrm()
 o.Using("main")
 qs := o.QueryTable(weatherTable)
 
 var lists []orm.ParamsList
 num, err := qs.ValuesList(&lists, "name")
 if err == nil {
 fmt.Printf("Result Nums: %d\n", num)
 for _, row := range lists {
 fmt.Println(row[0])
 allCities = append(allCities, row[0].(string))
 }
 }
 
 return allCities
}
func GetOneCity(city string) (cw CityWeather, err error) {
 o := orm.NewOrm()
 o.Using("main")
 qs := o.QueryTable(weatherTable)
 
 err = qs.Filter("name", city).One(&cw)
 if err != nil {
 cw = CityWeather{Id: -1}
 return cw, err
 }
 
 currentTime := time.Now().UTC().UnixNano()
 diffSeconds := (currentTime - cw.TimeStamp) / 1e9
 
 fmt.Printf("Diff seconds = %d\n", diffSeconds)
 
 if diffSeconds > timeoutSet || cw.Wid == -1 { // Older than one hour or the first get, then need to update database
 client := &http.Client{}
 url := OpenWeatherURL + "?q=" + city + "&APPID=" + AppID
 reqest, err := http.NewRequest("GET", url, nil)
 if err != nil {
 panic(err)
 fmt.Println("Error happened when calling openweather")
 }
 
 response, respErr := client.Do(reqest)
 defer response.Body.Close()
 
 if respErr != nil {
 fmt.Printf("Response Error: %s\n", respErr)
 } else { // Get Response from openweather!!
 body, err := ioutil.ReadAll(response.Body)
 if err != nil {
 panic(err.Error())
 }
 
 js, err := simplejson.NewJson(body)
 if err != nil {
 panic(err.Error())
 }
 
 weather, ok := js.CheckGet("weather")
 if ok {
 fmt.Println(weather)
 desc, _ := weather.GetIndex(0).Get("description").String()
 icon, _ := weather.GetIndex(0).Get("icon").String()
 id, _ := weather.GetIndex(0).Get("id").Int()
 wtr, _ := weather.GetIndex(0).Get("main").String()
 
 num, err := qs.Filter("name", city).Update(orm.Params{
 "description": desc,
 "summary": wtr, // "main" field
 "wid": id,
 "icon": icon, 
 "time_stamp": currentTime, 
 })
 fmt.Printf("num = %d\n", num)
 if err != nil {
 fmt.Println(err)
 panic(err)
 }
 
 err = qs.Filter("name", city).One(&cw) // get cw after updating
 if err != nil {
 cw = CityWeather{Id: -1}
 return cw, err
 }
 }
 }
 } 
 
 return cw, err
}
func Delete(city string) {
 o := orm.NewOrm()
 o.Using("main")
 qs := o.QueryTable(weatherTable)
 
 num, err := qs.Filter("name", city).Delete()
 if err == nil {
 fmt.Println(num)
 } else {
 fmt.Println("Error happened in Delete()......")
 }
}

好了,打完收工!今晚洞房也没问题!噗~一口鲜血喷了出来。。。

20个小时能干些啥呢?20个小时可以从一个Golang的文盲到把以上这个黑客马拉松基本跑完。
无论未来的前景是光明还是黑暗,笔者相信,自己一定有能力应对更多更强的挑战!


有疑问加站长微信联系(非本文作者)

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

关注微信
2514 次点击
暂无回复
添加一条新回复 (您需要 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传

用户登录

没有账号?注册
(追記) (追記ここまで)

今日阅读排行

    加载中
(追記) (追記ここまで)

一周阅读排行

    加载中

关注我

  • 扫码关注领全套学习资料 关注微信公众号
  • 加入 QQ 群:
    • 192706294(已满)
    • 731990104(已满)
    • 798786647(已满)
    • 729884609(已满)
    • 977810755(已满)
    • 815126783(已满)
    • 812540095(已满)
    • 1006366459(已满)
    • 692541889

  • 关注微信公众号
  • 加入微信群:liuxiaoyan-s,备注入群
  • 也欢迎加入知识星球 Go粉丝们(免费)

给该专栏投稿 写篇新文章

每篇文章有总共有 5 次投稿机会

收入到我管理的专栏 新建专栏