這是一個建立於
的文章,其中的資訊可能已經有所發展或是發生改變。
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例子2POST /location{ "name": "Shanghai" }201 CreatedPOST /location{ "name": "Shanghai" }409 Conflicted### GET /location 返回資料庫中的所有城市例子3 GET /location200 OK[]例子4POST /location{ "name": "Shanghai" }201 CreatedPOST /location{ "name": "Beijing" }201 CreatedGET /location200 OK["Shanghai", "Beijing"]### GET /location/{name} 查詢openweathermap.com,返回結果,因為天氣資料更新不頻繁,可緩衝在資料庫中,保留1個小時不需要考慮查詢openweathermap.com返回錯誤的情況例子5GET /location/Shanghai200 OK{ "weather": [ { "description": "few clouds", "icon": "02d", "id": 801, "main": "Clouds" } ]}### DELETE /location/{name}例子6DELETE /location/Shanghai200 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/beegogo 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 mainimport ( _ "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 routersimport ( "github.com/cityweather/controllers" "github.com/astaxie/beego")func init() { beego.Router("/location/?:name", &controllers.CityWeatherController{})}
3. controller_cityweather.go
package controllersimport ( "encoding/json" "github.com/cityweather/models" "github.com/astaxie/beego")// Operations about cityweathertype 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 modelsimport ( "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 = 3600const 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的文盲到把以上這個駭客馬拉松基本跑完。
無論未來的前景是光明還是黑暗,筆者相信,自己一定有能力應對更多更強的挑戰!