Designing APIs in Go

Designing our social network API

由于我们的应用程序是social network.我们不仅关注于用户信息,而需要连接它们,并且发送消息。我们需要确保新的用户可以与已经存在的群共享消息,创建和修改连接,以及处理身份验证。

Endpoints Method Description
/api/users GET Return a list of users with optional parameters
/api/users POST Create a user
/api/users/XXX PUT Update a user’s information
/api/users/XXX DELETE Delete a user
/api/connections GET Return a list of connections based on users
/api/connections POST Create a connection between users
/api/connections/XXX PUT Modify a connection
/api/connections/XXX DELETE Remove a connection between users
/api/statuses GET Get a list of statuses
/api/statuses POST Create a status
/api/statuses/XXX PUT Update a status
/api/statuses/XXX DELETE Delete a status
/api/comments GET Get list of comments
/api/comments POST Create a comment
/api/comments/XXX PUT Update a comment
/api/comments/XXX DELETE Delete a comment

XXX表示URL端点中的唯一标识符。

以上的端点直接反应了数据库对应的表,但有的API是通过OAuth:

Endpoint Method Description
/api/oauth/authorize GET Returns a list of users with optional parameters
/api/oauth/token POST Creates a user
/api/oauth/revoke PUT Updates a user’s information

如果你还不熟悉OAuth, 请不用担心,我们会有一章专门讲OAuth.

处理我们的API版本

如果你花了大量的时间在互联网上处理service和API, 你会发现有很多种服务来处理它们的API版本。

但有的方法会打破向前和向后的兼容性。你应该避免这种方式。默认情况下,使用版本作为URI:/api/v1.1/users. 你会发现这非常常见,比如Twitter的API处理请求. 这种方法也有优点和缺点,所以你应该考虑你的URI方法中潜在缺点。

明确定义API版本时,这意味着用户总是能够找到他要求的版本。这意味着任何API的升级都不会影响到之前的版本。最坏的是,用户可能不知道哪个版本是最新的,用来检查和校验描述性的API消息。

我们知道, Go不允许条件导入。即,我们不能有以下的代码

if version == 1 {
    import "v1"
} else if version == 2 {
    import "v2"
}

假设我们的文件结构如下

socialnetwork.go
/{GOPATH}/github.com/nkozyra/gowebservice/v1.go
/{GOPATH}/github.com/nkozyra/gowebservice/v2.go

我们可以通过以下方式进行导入

import "github.com/nkozyra/gowebservice/v1"
import "github.com/nkozyra/gowebservice/v2"

当然,我们必须在我们的应用程序中必须使用这些包,否则Go会触发一个编译错误。

以下是一个例子,操作多个版本

package main
import
(
"nathankozyra.com/api/v1"
"nathankozyra.com/api/v2"
)
func main() {
    v := 1
    if v == 1 {
        v1.API()
        // do stuff with API v1
    } else {
        v2.API()
        // do stuff with API v2
    }
}

但不幸的是,这打破了编程的一个核心原则:不能复制代码。虽然这不是硬性的规定,但复杂代码会导致功能的碎片和其它头痛的问题。我们可以把主要的方法跨版本方法,某种程度上可以解决这个问题。

在这个例子中,每一个我们的API版本都会导入我们标准的API,serving-and-routing文件,如下所示

package v2
import
(
    "nathankozyra.com/api/api"
)
type API struct {
}
func main() {
    api.Version = 1
    api.StartServer()
}

表面上看上去v2是一个不同的版本。本质上,它只是包装了我们的共享数据,比如数据库连接,数据编码等等。

为了详细说明,我们看看api.go的代码

import (
    "database/sql"
    "encoding/json"
    "fmt"
    _ "github.com/go-sql-driver/mysql"
    "github.com/gorilla/mux"
    "net/http"
    "log"
)
var Database *sql.DB
type Users struct {
    Users []User `json:"users"`
}
type User struct {
    ID int "json:id"
    Name string "json:username"
    Email string "json:email"
    First string "json:first"
    Last string "json:last"
}
func StartServer() {
    db, err := sql.Open("mysql", "root@/social_network")
    if err != nil {
    }
    Database = db
    routes := mux.NewRouter()
    http.Handle("/", routes)
    http.ListenAndServe(":8080", nil)
}

你可以使用第三方的包来处理基于JSON的RESTAPI, 它为JSON API Server(JAS). JAS是建立在HTTP之上,它会自动的处理请求到资源。 https://github.com/coocood/jas.

Concurrent WebSockets

在之前的章节提到过,一个WebSocket是保持客户端和服务器连接的一种方法,典型的应用是,替换浏览器和服务器之间的,多个http连接。

使用WebSockets建立API,可以减少客户端和服务器的延迟(每次TCP都需要三次握手),减少长轮询应用的复杂度.

为了说明它的优点,我们对比两张图,第一张是标准的HTTP请求

现在我们对比下WebSocket, 它也是建立在TCP之上,但它减少了多个handshake的开销和状态的控制

以下是一个WebSocket的例子,它接收一个消息,然后返回这个消息的长度

 package main
import (
    "fmt"
    "net/http"
    "code.google.com/p/go.net/websocket"
    "strconv"
)
var addr = ":12345"
func EchoLengthServer(ws *websocket.Conn) {
    var msg string
    for {
    websocket.Message.Receive(ws, &msg)
    fmt.Println("Got message",msg)
    length := len(msg)
    if err := websocket.Message.Send(ws, strconv.FormatInt(int64(length),
    10) ) ; err != nil {
        fmt.Println("Can't send message length")
        break
        }
    }

注意这里的循环,你的WebSocket只能是在客户端断开。

func websocketListen() {
    http.Handle("/length", websocket.Handler(EchoLengthServer))
    err := http.ListenAndServe(addr, nil)
    if err != nil {
        panic("ListenAndServe: " + err.Error())
    }
}

这是socket的主路由,我们将在端口12345上监听,并且计算消息的长度,并且返回。本质上讲,我们将http处理器映射到websocket处理器上。代码如下

 func main() {
    http.HandleFunc("/websocket", func(w http.ResponseWriter, r
    *http.Request) {
        http.ServeFile(w, r, "websocket.html")
    })
    websocketListen()
}

更多的你可以查看https://code.google.com/p/go/source/browse/websocket/websocket.go?repo=net.

分离我们的API逻辑

如我们在之前的版本章节中提到,为了跨不同的版本和格式,我们需要将API的逻辑进行分离。

我们之前看到过GetFormat()和SetFormat()函数,它们独立于所有的端点和版本。

扩展我们的error message

在上一章,我们简单的接触了通过HTTP状态码发送错误消息。我们使用409状态,表示在创建一个已存在于数据库的邮件地址冲突。

http包中的状态码并不能完全的表示REST中的消息,以下是RPC2616的建议

Error Number
StatusContinue 100
StatusSwitchingProtocols 101
StatusOK 200
StatusCreated 201
StatusAccepted 202
StatusNonAuthoritativeInfo 203
StatusNoContent 204
StatusResetContent 205
StatusPartialContent 206
StatusMultipleChoices 300
StatusMovedPermanently 301
StatusFound 302
StatusSeeOther 303
StatusNotModified 304
StatusUseProxy 305
StatusTemporaryRedirect 307
StatusBadRequest 400
StatusUnauthorized 401
StatusPaymentRequired 402
StatusForbidden 403
StatusNotFound 404
StatusMethodNotAllowed 405
StatusNotAcceptable 406
StatusProxyAuthRequired 407
StatusRequestTimeout 408
StatusConflict 409
StatusGone 410
StatusLengthRequired 411
StatusPreconditionFailed 412
StatusRequestEntityTooLarge 413
StatusRequestURITooLong 414
StatusUnsupportedMediaType 415
StatusRequestedRangeNotSatisfiable 416
StatusExpectationFailed 417
StatusTeapot 418
StatusInternalServerError 500
StatusNotImplemented 501
StatusBadGateway 502
StatusServiceUnavailable 503
StatusGatewayTimeout 504
StatusHTTPVersionNotSupported 505

你可能会回想起,我们之前确编码了这些错误信息。在api.go文件中,我们的ErrorMessage函数中,就明确的定义了,409 HTTP状态码错误。

func ErrorMessages(err int64) (int, int, string) {
    errorMessage := ""
    statusCode := 200;
    errorCode := 0
    switch (err) {
        case 1062:
        errorMessage = http.StatusText(409)
        errorCode = 10
        statusCode = http.StatusConflict
    }
    return errorCode, statusCode, errorMessage
}

在这里我们知道,1062是mysql错误,我们将它翻译成http错误,但我们还是需要保持原来的http错误信息,所以需要在switch中增加一个default.

default:
errorMessage = http.StatusText(err)
errorCode = 0
statusCode = err

通过web service更新用户

为了更新用户,我们增加以了一个/api/users/XXX的端点。

Routes.HandleFunc("/api/users/{id:[0-9]+}", UsersUpdate).Methods("PUT")

在UserUpdate函数中,我们首先确认用户的ID是否存在,如果它不存在,我们将返回一个404错误(a document not found error).它是最接近于一个资源记录没有找到。

如果用户存在,我们将通过查询,更新它们的e-mail ID,如果更新失败,我们将返回一个conflict message(或者其它的错误), 如果成功,我们返回200和JSON格式的一个成功的消息。以下是UserUpdates函数

func UsersUpdate(w http.ResponseWriter, r *http.Request) {
    Response := UpdateResponse{}
    params := mux.Vars(r)
    uid := params["id"]
    email := r.FormValue("email")
    var userCount int
    err := Database.QueryRow("SELECT COUNT(user_id) FROM users WHERE
    user_id=?", uid).Scan(&userCount)
    if userCount == 0 {
        error, httpCode, msg := ErrorMessages(404)
        log.Println(error)
        log.Println(w, msg, httpCode)
        Response.Error = msg
        Response.ErrorCode = httpCode
        http.Error(w, msg, httpCode)
    }else if err != nil {
        log.Println(error)
    } else {
    _,uperr := Database.Exec("UPDATE users SET user_email=?WHERE
    user_id=?",email,uid)
    if uperr != nil {
    _, errorCode := dbErrorParse( uperr.Error() )
    _, httpCode, msg := ErrorMessages(errorCode)
    Response.Error = msg
    Response.ErrorCode = httpCode
    http.Error(w, msg, httpCode)
} else {
    Response.Error = "success"
    Response.ErrorCode = 0
    output := SetFormat(Response)
    fmt.Fprintln(w,string(output))
}

我们将在之后在解释,现在,我们可以创建一个用户,返回多个用户例表和更新用户的e-mail地址

我们在这里介绍两个浏览器端的工具,一个是Postman, 一个是Poster, 它们让你可以直接在浏览器中处理REST端点。可以Google Postman inn Chrome和Poster in Firefox