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. 你可以通过第三方的包来解决
- go-curl, a binding by ShuYu Wang is available at https://github.com/andelf/go-curl.
- go-av, a simpler method with http bindings is available at https://github.com/goav/ 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='"