您的位置:首页 > 编程语言 > Go语言

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

2016-04-04 21:19 585 查看
在上篇博客《自我黑客马拉松--从零开始创建一个基于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菜鸟的一些个人想法了。

(完)
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: