分享
  1. 首页
  2. 文章

再次自我黑客马拉松--不用第三方库实现一个基于golang的web service

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

在上篇博客《自我黑客马拉松--从零开始创建一个基于Go语言的Web Service》中,笔者从零开始接触Go语言,挑战了一下自我,实现了一个web service. 不过这里有一个问题,在上次的实现中,用了一些第三方的库,比如beego框架和go-simplejson. 从工程的角度来说,利用框架等第三方库的优点是,很多地方的编码变得简单,代码量较少;但是缺点是:一、对golang本身built-in的库如net/http和encoding/json都还了解得很不够;二、一旦第三方的库出了问题,查找问题和修正bug都比较麻烦。所以这次,笔者打算再自我挑战一下,不用任何第三方库只用golang自带的库来把上次的4个API实现一遍,另外还要加上单元测试!


这次的目录结构比较简单,所有文件全放一个文件夹下了。鉴于上次的package叫做cityweather,那么这次的package就叫做cityweather2吧!(真是不会起名字啊)

对requirement不熟悉的朋友,还是请看上篇博客里的介绍吧。

这次一共有4个文件:main.go, controller.go, model.go 和 model_test.go,详情如下:

1. main.go

package main
import (
 "net/http"
)
func main() { 
 http.HandleFunc("/", topHandler)
 http.ListenAndServe(":8080", nil)
}

2. controller.go
package main
import (
 "io"
 "fmt"
 "net/http"
 "strings"
 "encoding/json"
 
 _ "github.com/mattn/go-sqlite3"
)
// POST /location
func postLocationHandler(w http.ResponseWriter, r *http.Request) {
 defer r.Body.Close()
 body := make([]byte, r.ContentLength)
 r.Body.Read(body)
 
 var city CityName
 err := json.Unmarshal(body, &city)
 if err != nil {
 w.WriteHeader(http.StatusInternalServerError)
 io.WriteString(w, err.Error())
 return
 }
 status, err := AddOneCity(city.Name)
 
 w.WriteHeader(status)
 if err != nil {
 io.WriteString(w, err.Error())
 }
}
// GET /location
func getAllLocationsHandler(w http.ResponseWriter, r *http.Request) {
 cities, respCode, err := GetAllCities()
 w.WriteHeader(respCode)
 
 if err == nil {
 citiesStr := "["
 for i, city := range cities {
 if i > 0 {
 citiesStr += (", " + city)
 } else {
 citiesStr += city
 }
 }
 citiesStr += "]"
 
 io.WriteString(w, citiesStr)
 } else {
 io.WriteString(w, err.Error())
 }
}
// DELETE /location/{name}
func deleteCityHandler(w http.ResponseWriter, r *http.Request, city string) {
 respCode, err := DeleteOneCity(city)
 
 w.WriteHeader(respCode)
 if err != nil {
 io.WriteString(w, err.Error())
 }
}
// GET /location/{name}
func getCityWeatherHandler(w http.ResponseWriter, r *http.Request, city string) {
 result, respCode, err := GetOneCityWeather(city)
 resp, err := json.Marshal(result)
 
 w.WriteHeader(respCode)
 if err == nil {
 w.Write(resp)
 } else {
 io.WriteString(w, err.Error())
 }
}
func topHandler(w http.ResponseWriter, r *http.Request) {
 items := strings.Split(r.URL.Path, "/")
 
 if (len(items) > 4 || len(items) <=1) {
 w.WriteHeader(http.StatusNotFound)
 fmt.Fprintf(w, "404 Not Found: %s", r.URL.Path)
 return
 }
 
 loc := "location"
 firstPlace := strings.ToLower(items[1])
 
 if firstPlace == loc {
 if (r.Method == http.MethodPost && len(items) == 2) { // POST /location
 postLocationHandler(w, r)
 
 } else if (r.Method == http.MethodGet && (len(items) == 2 || (len(items) == 3 && items[2] == ""))) { // GET /location
 getAllLocationsHandler(w, r)
 
 } else if (r.Method == http.MethodGet && (len(items) == 3 || (len(items) == 4 && items[3] == ""))) { // GET /location/{name}
 getCityWeatherHandler(w, r, items[2])
 
 } else if (r.Method == http.MethodDelete && len(items) == 3) { // DELETE /location/{name}
 deleteCityHandler(w, r, items[2])
 
 } else {
 w.WriteHeader(http.StatusNotFound)
 fmt.Fprintf(w, "404 Not Found: %s", r.URL.Path)
 }
 } else {
 w.WriteHeader(http.StatusNotFound)
 fmt.Fprintf(w, "404 Not Found: %s", r.URL.Path)
 }
}


3. model.go
package main 
import (
 "os"
 "fmt"
 "time"
 "regexp"
 "net/http"
 "io/ioutil"
 "database/sql"
 "encoding/json"
)
const weatherTable string = "city_weather"
const timeOutSeconds int64 = 3600
const OpenWeatherURL string = "http://api.openweathermap.org/data/2.5/weather"
const AppID string = "xxxxxxxxxxxxxxxxxxxxxxx"
var gopath string
var dbpath string
type CityName struct { // for Unmarshal HTTP Request Body
 Name string
}
type CityWeather struct { // for Database
 Id int64 // primary key, auto increment
 Name string // city name, UNIQUE
 Main string // main in weather
 Description string // description in weather
 Icon string // icon in weather
 Wid int64 // id in weather
 TimeStamp int64 // timestamp when updating
}
type WeatherReport struct {
 Id int64 `json:"id"`
 Main string `json:"main"`
 Description string `json:"description"`
 Icon string `json:"icon"`
}
type ReportResult struct { // for HTTP Response
 Weather []WeatherReport `json:"weather"`
}
func checkErr(err error) {
 if err != nil {
 panic(err)
 }
}
func init() {
 InitializeDatabase()
}
func InitializeDatabase() {
 gopath = os.Getenv("GOPATH")
 dbpath = gopath + "/bin/weather.db"
 
 db, err := sql.Open("sqlite3", dbpath)
 defer db.Close()
 checkErr(err)
 createTable := fmt.Sprintf("CREATE TABLE IF NOT EXISTS `%s` (`id` integer NOT NULL PRIMARY KEY AUTOINCREMENT, `name` varchar(255) NOT NULL DEFAULT '' UNIQUE, `main` varchar(255) NOT NULL DEFAULT '' , `description` varchar(255) NOT NULL DEFAULT '' , `icon` varchar(255) NOT NULL DEFAULT '' , `wid` integer NOT NULL DEFAULT 0 , `time_stamp` integer NOT NULL DEFAULT 0);", weatherTable)
 
 _, err = db.Exec(createTable)
 checkErr(err)
}
// For "POST /location"
func AddOneCity(city string) (respCode int, err error) {
 db, err := sql.Open("sqlite3", dbpath)
 defer db.Close()
 if err != nil {
 return http.StatusInternalServerError, err
 }
 queryStr := fmt.Sprintf("SELECT name FROM %s WHERE name=?", weatherTable) 
 tmpName := ""
 db.QueryRow(queryStr, city).Scan(&tmpName)
 
 if tmpName != "" { // result set is not empty
 respCode = http.StatusConflict // 409
 } else {
 insertStr := fmt.Sprintf("INSERT INTO %s (`name`, `wid`, `time_stamp`) values (?, ?, ?)", weatherTable)
 stmt, err := db.Prepare(insertStr)
 if err != nil {
 return http.StatusInternalServerError, err
 } 
 _, err = stmt.Exec(city, -1, 0)
 if err != nil {
 return http.StatusInternalServerError, err
 } 
 respCode = http.StatusCreated // 201
 }
 
 return respCode, err
}
// GET /location
func GetAllCities() (allCities []string, respCode int, err error) {
 allCities = []string{}
 
 db, err := sql.Open("sqlite3", dbpath)
 defer db.Close()
 if err != nil {
 return allCities, http.StatusInternalServerError, err
 }
 
 queryStr := fmt.Sprintf("SELECT name FROM %s", weatherTable)
 rows, err := db.Query(queryStr)
 if err != nil {
 return allCities, http.StatusInternalServerError, err
 }
 
 for rows.Next() {
 var cityName string
 err = rows.Scan(&cityName)
 if err != nil {
 return allCities, http.StatusInternalServerError, err
 }
 
 allCities = append(allCities, cityName)
 }
 
 return allCities, http.StatusOK, err
}
// DELETE /location/{name}
func DeleteOneCity(city string) (respCode int, err error) {
 db, err := sql.Open("sqlite3", dbpath)
 defer db.Close()
 if err != nil {
 return http.StatusInternalServerError, err
 } 
 
 execStr := fmt.Sprintf("DELETE FROM %s WHERE name=?", weatherTable)
 stmt, err := db.Prepare(execStr)
 if err != nil {
 return http.StatusInternalServerError, err
 } 
 _, err = stmt.Exec(city)
 if err != nil {
 return http.StatusInternalServerError, err
 } 
 
 return http.StatusOK, err
}
// GET /location/{name}
func GetOneCityWeather(city string) (result *ReportResult, respCode int, err error) {
 cw := new(CityWeather)
 result = new(ReportResult)
 db, err := sql.Open("sqlite3", dbpath)
 defer db.Close()
 if err != nil {
 return result, http.StatusInternalServerError, err
 }
 
 // Get data of the specified city from Database
 cw.Id = 0
 queryStr := fmt.Sprintf("SELECT id, name, main, description, icon, wid, time_stamp FROM %s WHERE name=?", weatherTable) 
 db.QueryRow(queryStr, city).Scan(&cw.Id, &cw.Name, &cw.Main, &cw.Description, &cw.Icon, &cw.Wid, &cw.TimeStamp)
 
 if cw.Id == 0 {
 return result, http.StatusNotFound, nil
 }
 
 currentTime := time.Now().UTC().UnixNano()
 passedSeconds := (currentTime - cw.TimeStamp) / 1e9
 
 if passedSeconds > timeOutSeconds { // If older than one hour or the first get, need to update database
 client := &http.Client{}
 url := fmt.Sprintf("%s?q=%s&APPID=%s", OpenWeatherURL, city, AppID)
 reqest, err := http.NewRequest("GET", url, nil)
 if err != nil {
 return result, http.StatusServiceUnavailable, err // 503
 }
 
 response, err := client.Do(reqest)
 defer response.Body.Close()
 
 if err != nil {
 return result, http.StatusServiceUnavailable, err // 503
 } else { // Get Response from openweather!!
 body, err := ioutil.ReadAll(response.Body)
 if err != nil {
 return result, http.StatusInternalServerError, err // 500
 }
 
 bodyStr := string(body)
 
 // get "weather" part as string
 reg := regexp.MustCompile(`"weather":(\[.+\])`)
 ws := (reg.FindStringSubmatch(bodyStr))[1]
 
 // convert "weather" string to bytes
 tmpBytes := make([]byte, len(ws))
 copy(tmpBytes[:], ws)
 
 // Unmarshal the bytes to ReportResult.Weather
 var rcds []WeatherReport
 json.Unmarshal(tmpBytes, &rcds)
 result.Weather = rcds
 
 // update cw
 cw.Wid = rcds[0].Id
 cw.Main = rcds[0].Main
 cw.Description = rcds[0].Description
 cw.Icon = rcds[0].Icon
 cw.TimeStamp = currentTime
 // Update Database
 updateStr := fmt.Sprintf("UPDATE %s SET wid=?, main=?, description=?, icon=?, time_stamp=? WHERE name=?", weatherTable)
 stmt, err := db.Prepare(updateStr)
 if err != nil {
 return result, http.StatusInternalServerError, err
 }
 _, err = stmt.Exec(cw.Wid, cw.Main, cw.Description, cw.Icon, cw.TimeStamp, city)
 if err != nil {
 return result, http.StatusInternalServerError, err
 }
 }
 } else { // If shorter than timeOutSeconds, get the data from Database
 var item WeatherReport
 item.Id = cw.Wid
 item.Main = cw.Main
 item.Icon = cw.Icon
 item.Description = cw.Description
 
 result.Weather = []WeatherReport{item}
 }
 
 return result, http.StatusOK, nil
}

4. model_test.go
package main
import (
 "testing"
 "net/http"
)
const sampleCityName string = "Shanghai"
func reportFailure(t *testing.T, respCode int, err error) {
 if respCode != http.StatusOK || err != nil {
 t.Errorf("Test Faield: respCode = %d, err = %v", respCode, err)
 }
}
func Test_DeleteOneCity(t *testing.T) {
 respCode, err := DeleteOneCity(sampleCityName)
 reportFailure(t, respCode, err)
}
func Test_AddOneCity(t *testing.T) {
 respCode, err := AddOneCity(sampleCityName)
 if respCode != http.StatusCreated || err != nil { // 201
 t.Errorf("Test Failed when adding %s for the first time: respCode = %d, err = %v", sampleCityName, respCode, err)
 }
 
 respCode, err = AddOneCity(sampleCityName)
 if respCode != http.StatusConflict || err != nil { // 409
 t.Errorf("Test Failed when adding %s for the second time: respCode = %d, err = %v", sampleCityName, respCode, err)
 }
}
func Test_GetAllCities(t *testing.T) {
 allCities, respCode, err := GetAllCities()
 reportFailure(t, respCode, err)
 
 found := false
 for _,v := range(allCities) {
 if v == sampleCityName {
 found = true
 break
 }
 }
 if found == false {
 t.Errorf("Test Faield due to no expected city")
 }
}
func Test_GetOneCityWeather(t *testing.T) {
 result, respCode, err := GetOneCityWeather(sampleCityName)
 reportFailure(t, respCode, err)
 
 if result == nil || result.Weather == nil || len(result.Weather) == 0 {
 t.Errorf("Test Failed: returned result = %v", result)
 }
}

对了,run test的时候只要在该文件夹下跑一句"go test -v"即可。当然,如果跑"go test -cover",那么就可以看到代码覆盖率了。

最后,笔者思考了一下有哪些不足之处,以遍日后改进,大约如下:

1. 在做model.go的单元测试时,没有去mock数据库的行为。那么应该怎么做呢?笔者没有仔细去研究了,大约是可以利用这个第三方的库吧:https://github.com/DATA-DOG/go-sqlmock

2. 没有写controller.go的单元测试。该怎么写呢?首先那么些个controller函数最后都是写到web上的,但其实它们调用的是一个接口 -- http.ResponseWriter,所以,我们只要fake几个http.Request作为输入参数,再mock这个http.ResponseWriter接口,将其原本写入到web的数据写入到另一个地方(文件或channel?),再从这个地方将数据取出来和期望值做对比,应该就可以实现了。

以上是笔者作为一个golang菜鸟的一些个人想法了。

(完)


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

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

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

用户登录

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

今日阅读排行

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

一周阅读排行

    加载中

关注我

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

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

给该专栏投稿 写篇新文章

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

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