自我駭客馬拉松 -- 從零開始建立一個基於Go語言的web service

來源:互聯網
上載者:User
這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。

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的文盲到把以上這個駭客馬拉松基本跑完。
無論未來的前景是光明還是黑暗,筆者相信,自己一定有能力應對更多更強的挑戰!

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.