Routing and Bootstrapping

通过上面的两章,我们应该适应了创建API端点,后端的数据库用于保存相关的信息, 通过http请求进行路由匹配和输出数据.

对于上面最后一点,除了我们最基本的例子,我们需要一个库来处理URL复用。通常可以使用Gorilla web toollit. 这些库都非常神奇,但我们更应该知道如何在Go中直接处理请求,特别是创建强键的API端点,这涉及到条件和正则表达式。

虽然我们在之前提及到header信息对于web service客户端的重要性,这包括status code, 我们将开始挖掘这样这方面的价值,扩展我们的应用程序。

在这一章中,我们将学习以下的主题

  • 扩展Go的multiplexer,处理更复杂的请求
  • Gorilla中的高级请求
  • 介绍使用Gorilla实现RPC和web sockets
  • 处理我们应用和请求中的错误
  • 处理二进制数据

我们还将创建一系列的客户端友好的接口,这允许我们能够与social network的API请求进行交互, 这些请求包含PUT/POST/DELETE以及后面的OPTIONS.

在本章结束时,你应该能够在Go中写routers以及如何扩展它们来处理更加复杂的requests.

Writing custom routers in Go

到目前为此,我们都是使用Gorilla web tookit来处理URL路由和多路复用(multiplexers). 并且简单的介绍了Go内部的mux包

简单来说http.ServeMux的匹配模式必须是明确的,不允许通匹配符或者正则表达式。

让我们直接看看http.ServeMux的代码

// Find a handler on a handler map given a path string
// Most-specific (longest) pattern wins
func (mux *ServeMux) match(path string) (h Handler, pattern string) {
    var n = 0
    for k, v := range mux.m {
        if !pathMatch(k, path) {
        continue
    }
        if h == nil || len(k) > n {
            n = len(k)
            h = v.h
            pattern = v.pattern
        }
    }
    return
}

这里最关键的是!pathMatch函数,它匹配muxEntry map中的定义的路径

func pathMatch(pattern, path string) bool {
    if len(pattern) == 0 {
        // should not happen
        return false
    }
    n := len(pattern)
    if pattern[n-1] != '/' {
        return pattern == path
    }
    return len(path) >= n && path[0:n] == pattern
}

接下来需要扩展它

这里有两个方法,第一个是写你自己的包,来扩展这个包。第二个是直接修改src目录的中代码。但这会被后续的升级所取代。所以第二个选面会从根本上破坏Go语言。

因此我们使用第一个选项,那我们如何扩展http包呢?最简单的回答是,你不要直接进入源代码。你需要创建自己的代码,继承http struct中的主要方法,就能与http相连接。

从现在开始,我们创建一个新的包。这个新创建的包放置到你的golang src目录中特定域名下。

如果你曾经使用过go get获取第三方的包,你应该很清楚这个惯例,你可以看看go src目录的截图

我们在这里只是简单的创建一个指定域名的目录,用它来保存我们的包。另外,你也可以在代码仓库中创建你的项目,比如Github,然后通过go get导入。我们创建的这个域名是nathankozyra.com, 在它下面有个子目录httpex(http and regex),这个子目录用来扩展http.

为了节省空间,我们不会包含所有的http包的代码。我们只是复制ServeMux struct, method, and variables到httpex.go文件。代码如下

package httpex
import
(
    "net/http"
    "sync"
    "sync/atomic"
    "net/url"
    "path"
    "regexp"
)
type ServeMux struct {
    mu sync.RWMutex
    m map[string]muxEntry
    hosts bool // whether any patterns contain hostnames
}

我们最核心的改变是pathMatch()函数,之前的是字面量的最长匹配,没有使用到正则表达式,这里,我们将==改为正则表达式

// Does path match pattern?
func pathMatch(pattern, path string) bool {
    if len(pattern) == 0 {
    // should not happen
    return false
}
n := len(pattern)
if pattern[n-1] != '/' {
    match,_ := regexp.MatchString(pattern,path)
    return match
}
fullMatch,_ := regexp.MatchString(pattern,string(path[0:n]))
return len(path) >= n && fullMatch
}

感觉这一些都是在重新造轮子, 但Go核心的包只提供了基本的功能,你需要扩展它们,以符合你自己的需求。

这里还有一个临时应急的方法来扩展ServeMux路由器,即拦截所有的请求,并对这些请求运行正则表达式进行测试。好比最后一个例子那样。但这不是非常好ideal(效率不高). 但这可以用于紧急情况。以下是代码演示了这个想法。

import
(
    "fmt"
    "net/http"
    "regexp"
)

func main() {
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
    path := r.URL.Path
    message := "You have triggered nothing"
    testMatch,_ := regexp.MatchString("/testing[0-9]{3}",path);
    if (testMatch == true) {
        // helper functions
        message = "You hit the test!"
    }
    fmt.Fprintln(w,message)
    })
    http.ListenAndServe(":8080", nil)
}

这里,我们没有为每一个匹配指定一个handler, 而是在单个handler中进行测试testing[3 digits], 然后在做出相应的反应。

Using more advanced routers in Gorilla

我们只是简单的扩展了内置的多路复用,接下来看看Gorilla提供的多路复用。

除了简单的正则表达式外,我们可以接收URL参数,并且将从URL中接收到的参数,应用到之后的变量中。我们已经在之前的例子中应用过。但我们没有过多的对它的原理进行解释。

以下是一个例子,我们如何增强一个正则表达式,并且将它应用到一个httpHandler函数中的变量中

/api/users/3
/api/users/nkozyra

这两者都可以通过GET请求,检所我们users表中的用户信息。

mux := mux.NewRouter()
mux.HandleFunc("/api/users/[\w+\d+]", UserRetrieve)

可是我们需要保留最后的一部分。为此,Gorilla允许我们在一个map设置一个键值, 我们通过以下方式实现

mux.HandleFunc("/api/users/{key}", UserRetrieve)

这就允许我们在handler中,通过以下方式,获得变量的值.

variables := mux.Vars(r)
key := variables["key"]

在这里我们使用了"key"代替了正则表达式,但你也可以两者都保留,允许我们为一个键设置表达式。比如,如果我们的用户key变量是由字母,数字和-组成,我们可以如下设置

r.HandleFunc("/api/users/{key:[A-Za-z0-9\-]}",UserRetrieve

然后我们可以在UserRetrieve函数中,直接检索这个key.

func UserRetrieve(w http.ResponseWriter, r *http.Request) {
    urlParams := mux.Vars(r)
    key := vars["key"]
}

Using Gorilla for JSON-RPC

你可能会想起第二章中的RESTful服务,我们简短的提及到了RPC。在这里我们通过Gorilla toolkit快速的创建RPC服务.

以下这个例子,我们将接收一个字符串,并且通过RPC消息,返回这个字符串中的总的字符数量.

package main
import (
    "github.com/gorilla/rpc"
    "github.com/gorilla/rpc/json"
    "net/http"
    "fmt"
    "strconv"
    "unicode/utf8"
)
type RPCAPIArguments struct {
    Message string
}
type RPCAPIResponse struct {
    Message string
}
type StringService struct{}
func (h *StringService) Length(r *http.Request, arguments *RPCAPIArguments,
reply *RPCAPIResponse) error {
    reply.Message = "Your string is " + fmt.Sprintf("Your string is %d chars
    long", utf8.RuneCountInString(arguments.Message)) + " characters long"
    return nil
}
func main() {
    fmt.Println("Starting service")
    s := rpc.NewServer()
    s.RegisterCodec(json.NewCodec(), "application/json")
    s.RegisterService(new(StringService), "")
    http.Handle("/rpc", s)
    http.ListenAndServe(":10000", nil)
}

一个非常重要的是,RPC的方法都必须是可导出的,这意味着,方法的第一个字符必须是大写的。如果你们使用stringService,你会得到一个can't find service stringService错误。

Using services for API access

当我们在创建和测试我们的web service时,会遇到处理POST/PUT/DELETE,如何知道它的结果是什么。

这里有许多的方法,可以用于测试,第一个是使用cURL. 目前在你能想得的语言,都是支持cURL.

在Go语言中,没有内置的cURL. 你可以通过第三方的包来解决

但在这里,我们只需要在终端中简单的构建测试用的URL, 如下

curl http://localhost:8080/api/users --data
"name=nkozyra&[email protected]&first=nathan&last=nathan"

我们需要注意的是,数据库中必须是唯一的。但我们在创建时可能会发生错误。所以我们通过修改代码,来返回这个错误

type CreateResponse struct {
    Error string "json:error"
}

现在,我们调用它,如果从数据库中得到一个错误,我们必须在响应中包含这个错误。如果发返回的是空白的,则表示成功创建。

func UserCreate(w http.ResponseWriter, r *http.Request) {
    NewUser := User{}
    NewUser.Name = r.FormValue("user")
    NewUser.Email = r.FormValue("email")
    NewUser.First = r.FormValue("first")
    NewUser.Last = r.FormValue("last")
    output, err := json.Marshal(NewUser)
    fmt.Println(string(output))
    if err != nil {
        fmt.Println("Something went wrong!")
    }
    Response := CreateResponse{}
    sql := "INSERT INTO users SET user_nickname='" + NewUser.Name + "',
    user_first='" + NewUser.First + "', user_last='" + NewUser.Last + "',
    user_email='" + NewUser.Email + "'"
    q, err := database.Exec(sql)
    if err != nil {
        Response.Error = err.Error()
    }
    fmt.Println(q)
    createOutput,_ := json.Marshal(Response)
    fmt.Fprintln(w,string(createOutput))
}

现在我们发送想的数据,重复创建一个用户

> curl http://localhost:8080/api/users –data
"name=nkozyra&[email protected]&first=nathan&last=nathan"
{"Error": "Error 1062: Duplicate entry '' for key 'user nickname'"}

创建一个简单的接口,来访问API

<!DOCTYPE html>
<html lang="en">
    <head>
    <meta charset="utf-8">
    <title>API Interface</title>
    <script
    src="http://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js">
    </script>
    <link
    href="http://maxcdn.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap.min.css"
    rel="stylesheet">
    <script
    src="http://maxcdn.bootstrapcdn.com/bootstrap/3.2.0/js/bootstrap.min.js">
    </xscript>
    <link rel="stylesheet" href="style.css">
    <script src="script.js"></script>
    </head>
<body>
<div class="container">
<div class="row">
<div class="col-12-lg">
<h1>API Interface</h1>
<div class="alert alert-warning" id="api-messages" role="alert"></div>
<ul class="nav nav-tabs" role="tablist">
<li class="active"><a href="#create" role="tab" datatoggle="
tab">Create User</a></li>
</ul>
<div class="tab-content">
<div class="tab-pane active" id="create">
<div class="form-group">
<label for="createEmail">Email</label>
<input type="text" class="form-control" id="createEmail"
placeholder="Enter email">
</div>
<div class="form-group">
<label for="createUsername">Username</label>
<input type="text" class="form-control" id="createUsername"
placeholder="Enter username">
</div>
<div class="form-group">
<label for="createFirst">First Name</label>
<input type="text" class="form-control" id="createFirst"
placeholder="First Name">
</div>
<div class="form-group">
<label for="createLast">Last Name</label>
<input type="text" class="form-control" id="createLast"
placeholder="Last Name">
</div>
<button type="submit" onclick="userCreate();" class="btn btnsuccess">
Create</button>
</div>
</div>
</div>
</div>
</div>
<script>
function userCreate() {
    action = "http://localhost:8080/api/users";
    postData = {};
    postData.email = $('#createEmail').val();
    postData.user = $('#createUsername').val();
    postData.first = $('#createFirst').val();
    postData.last = $('#createLast').val();
    $.post(action,postData,function(data) {
    if (data.error) {
    $('.alert').html(data.error);
    $('.alert').alert();
    }
    },'jsonp');
}
$(document).ready(function() {
    $('.alert').alert('close');
});
</script>
</body>
</html>

为了支持跨域访问,你需要在API服务器中有相同的端口和域名。或者为每个请求中包含以下的头

w.Header().Set("Access-Control-Allow-Origin","http://localhost:9000")

现在http://localhost:9000表求请求的原始服务器,以下是整个网页的截图.

返回有价值的错误信息

在上面的例子中,我们只是简单的复制了数据库的错误信息,但这对于客户端来说是没有价值的。

MySQL自身是有简单明了的错误消息系统,但这只适用于MySQL,而不是我们的应用程序。

如果你的客户端不是很明白什么是"duplicate entry"?或者它们不会英文。你会为它们翻译这些信息嘛?

大多数的API都有自己的错误报告系统,最好是基于请求中的头部中的语言来返回错误信息。或者返回一个错误代码,然后在客户端自已决定,如何输出消息。

在这里最主要的错误是通过HTTP状态码进行返回。默认情况下,Go的http包,会对无效的资源请求,会返回404 not found消息。

以后我们会讲到很多 REST中指定的错误代码,但现在,我们先看看409错误。

Note 按照W3C的RFC 2616协议规范, 409表示一个冲突,以下是详细的描述

一个请求不能完成,因为与资源的当前状态发生冲突。这个代码的作用是,期望用户解决冲突,然后重新提交请求。在返回的信息中,应该包含足够的信息,使用户可以认清冲突发生的源。理想的,响应实体应当包含足够的信息,使用户和用户代理解决问题,但这不是必须的。

我们先解析MySQL错误。

func dbErrorParse(err string) (string, int64) {
    Parts := strings.Split(err, ":")
    errorMessage := Parts[1]
    Code := strings.Split(Parts[0],"Error ")
    errorCode,_ := strconv.ParseInt(Code[1],10,32)
    return errorMessage, errorCode
}

type CreateResponse struct {
    Error string "json:error"
    ErrorCode int "json:code"
}

我们在UserCreate函数中,修改以下代码

if err != nil {
    errorMessage, errorCode := dbErrorParse( err.Error() )
    fmt.Println(errorMessage)
    error, httpCode, msg := ErrorMessages(errorCode)
    Response.Error = msg
    Response.ErrorCode = error
    fmt.Println(httpCode)
}

我们通过dbErrorParse函数,解析数据库的错误,并且把结果传递给ErrorMessage函数,这个函数返回相应的错误信息,而不是数据库的错误信息.

type ErrMsg struct {
ErrCode int
StatusCode int
Msg string
} 
func ErrorMessages(err int64) (ErrMsg) {
    var em ErrMsg{}
    errorMessage := ""
    statusCode := 200;
    errorCode := 0
    switch (err) {
        case 1062:
        errorMessage = "Duplicate entry"
        errorCode = 10
        statusCode = 409
    }
    em.ErrCode = errorCode
    em.StatusCode = statusCode
    em.Msg = errorMsg
    return em
}

现在处误的处理还不够完善,它只能处理单个类型的错误,我们将在之后继续添加更多的错误处理和消息。

最后一步,我们需要处理相应的HTTP状态码。最简单的方式是通过http.Error()函数,设置HTTP状态。

http.Error(w, "Conflict", httpCode)

如果我们将它放置在一个条件语句中,我们将返回从ErrorMessage()函数中接收到的状态码.

if err != nil {
    errorMessage, errorCode := dbErrorParse( err.Error() )
    fmt.Println(errorMessage)
    error, httpCode, msg := ErrorMessages(errorCode)
    Response.Error = msg
    Response.ErrorCode = error
    http.Error(w, "Conflict", httpCode)
}

再次通过cURL和-v参数,我们可以获得额外的error信息,截图如下

Handling binary data

首先我们需要在MySQL中创建一个字段,用来保存图片数据。接下来,我们使用BLOB数据,它可以存储任意的二进制数据。

ALTER TABLE `users`
ADD COLUMN `user_image` MEDIUMBLOB NOT NULL AFTER `user_email`;

以下是html

<div class="form-group">
<label for="createLast">Image</label>
<input type="file" class="form-control" name="image" id="createImage"
placeholder="Image">
</div>

现在我们修改服务器的处理代码

f, _, err := r.FormFile("image1")
if err != nil {
    fmt.Println(err.Error())
}

接下来,我们读取整个文件,并且将它转换为一个字符串

fileData,_ := ioutil.ReadAll(f)
fileString := base64.StdEncoding.EncodeToString(fileData)
sql := "INSERT INTO users set user_image='" + fileString + "',
user_nickname='"